@chit-run/cli 0.5.0 → 0.7.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.
Files changed (2) hide show
  1. package/dist/chit.js +2057 -1374
  2. package/package.json +1 -1
package/dist/chit.js CHANGED
@@ -970,6 +970,7 @@ function validateLoopRecord(raw) {
970
970
  scope: str2(o, "scope", ctx),
971
971
  task: str2(o, "task", ctx),
972
972
  repo: str2(o, "repo", ctx),
973
+ repoKey: str2(o, "repoKey", ctx),
973
974
  startedAt: str2(o, "startedAt", ctx),
974
975
  maxIterations: int2(o, "maxIterations", ctx, 1)
975
976
  };
@@ -988,8 +989,11 @@ function validateLoopRecord(raw) {
988
989
  checkDurationMs: int2(o, "checkDurationMs", ctx, 0),
989
990
  at: str2(o, "at", ctx)
990
991
  };
991
- if (o.detailsRef !== undefined)
992
- rec.detailsRef = str2(o, "detailsRef", ctx);
992
+ if (o.workspaceWarnings !== undefined) {
993
+ rec.workspaceWarnings = stringArray2(o, "workspaceWarnings", ctx);
994
+ }
995
+ if (o.auditRef !== undefined)
996
+ rec.auditRef = str2(o, "auditRef", ctx);
993
997
  const usage = optUsage2(o, ctx);
994
998
  if (usage !== undefined)
995
999
  rec.usage = usage;
@@ -4675,7 +4679,7 @@ var require_compile = __commonJS((exports) => {
4675
4679
  const schOrFunc = root.refs[ref];
4676
4680
  if (schOrFunc)
4677
4681
  return schOrFunc;
4678
- let _sch = resolve2.call(this, root, ref);
4682
+ let _sch = resolve4.call(this, root, ref);
4679
4683
  if (_sch === undefined) {
4680
4684
  const schema = (_a3 = root.localRefs) === null || _a3 === undefined ? undefined : _a3[ref];
4681
4685
  const { schemaId } = this.opts;
@@ -4702,7 +4706,7 @@ var require_compile = __commonJS((exports) => {
4702
4706
  function sameSchemaEnv(s1, s2) {
4703
4707
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
4704
4708
  }
4705
- function resolve2(root, ref) {
4709
+ function resolve4(root, ref) {
4706
4710
  let sch;
4707
4711
  while (typeof (sch = this.refs[ref]) == "string")
4708
4712
  ref = sch;
@@ -5288,7 +5292,7 @@ var require_fast_uri = __commonJS((exports, module) => {
5288
5292
  }
5289
5293
  return uri;
5290
5294
  }
5291
- function resolve2(baseURI, relativeURI, options) {
5295
+ function resolve4(baseURI, relativeURI, options) {
5292
5296
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
5293
5297
  const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
5294
5298
  schemelessOptions.skipEscape = true;
@@ -5547,7 +5551,7 @@ var require_fast_uri = __commonJS((exports, module) => {
5547
5551
  var fastUri = {
5548
5552
  SCHEMES,
5549
5553
  normalize,
5550
- resolve: resolve2,
5554
+ resolve: resolve4,
5551
5555
  resolveComponent,
5552
5556
  equal,
5553
5557
  serialize,
@@ -9963,12 +9967,12 @@ var init_dist = __esm(() => {
9963
9967
  });
9964
9968
 
9965
9969
  // ../studio/src/server/audit.ts
9966
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
9967
- import { homedir as homedir5 } from "os";
9968
- import { join as join8 } from "path";
9970
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
9971
+ import { homedir as homedir7 } from "os";
9972
+ import { join as join10 } from "path";
9969
9973
  function defaultAuditDir2() {
9970
- const xdg = process.env.XDG_STATE_HOME || join8(homedir5(), ".local", "state");
9971
- return join8(xdg, "chit", "audit");
9974
+ const xdg = process.env.XDG_STATE_HOME || join10(homedir7(), ".local", "state");
9975
+ return join10(xdg, "chit", "audit");
9972
9976
  }
9973
9977
  function blobRefs(e) {
9974
9978
  switch (e.type) {
@@ -9987,13 +9991,13 @@ function blobRefs(e) {
9987
9991
  function readAuditRun(auditDir, runId, includeBlobs) {
9988
9992
  if (!SAFE_RUN_ID2.test(runId))
9989
9993
  return { kind: "invalid-id" };
9990
- const runDir = join8(auditDir, "runs", runId);
9991
- const eventsPath = join8(runDir, "events.jsonl");
9992
- if (!existsSync8(eventsPath))
9994
+ const runDir = join10(auditDir, "runs", runId);
9995
+ const eventsPath = join10(runDir, "events.jsonl");
9996
+ if (!existsSync9(eventsPath))
9993
9997
  return { kind: "not-found" };
9994
9998
  let events2;
9995
9999
  try {
9996
- events2 = parseAuditLog(readFileSync9(eventsPath, "utf-8"));
10000
+ events2 = parseAuditLog(readFileSync12(eventsPath, "utf-8"));
9997
10001
  } catch (e) {
9998
10002
  if (e instanceof AuditEventError)
9999
10003
  return { kind: "invalid-log", message: e.message };
@@ -10001,15 +10005,15 @@ function readAuditRun(auditDir, runId, includeBlobs) {
10001
10005
  }
10002
10006
  if (!includeBlobs)
10003
10007
  return { kind: "ok", events: events2 };
10004
- const blobsDir = join8(runDir, "blobs");
10008
+ const blobsDir = join10(runDir, "blobs");
10005
10009
  const blobs = {};
10006
10010
  for (const e of events2) {
10007
10011
  for (const ref of blobRefs(e)) {
10008
10012
  if (!SHA256_HEX2.test(ref) || ref in blobs)
10009
10013
  continue;
10010
- const blobPath = join8(blobsDir, ref);
10011
- if (existsSync8(blobPath))
10012
- blobs[ref] = readFileSync9(blobPath, "utf-8");
10014
+ const blobPath = join10(blobsDir, ref);
10015
+ if (existsSync9(blobPath))
10016
+ blobs[ref] = readFileSync12(blobPath, "utf-8");
10013
10017
  }
10014
10018
  }
10015
10019
  return { kind: "ok", events: events2, blobs };
@@ -10067,12 +10071,12 @@ var init_auth = __esm(() => {
10067
10071
  });
10068
10072
 
10069
10073
  // ../studio/src/server/paths.ts
10070
- import { existsSync as existsSync9, statSync as statSync3 } from "fs";
10071
- import { isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
10074
+ import { existsSync as existsSync10, statSync as statSync3 } from "fs";
10075
+ import { isAbsolute as isAbsolute4, resolve as resolve6 } from "path";
10072
10076
  function resolveExplicitPath(userPath, cwd) {
10073
- const candidate = isAbsolute3(userPath) ? userPath : resolve5(cwd, userPath);
10074
- const canonical = resolve5(candidate);
10075
- if (!existsSync9(canonical)) {
10077
+ const candidate = isAbsolute4(userPath) ? userPath : resolve6(cwd, userPath);
10078
+ const canonical = resolve6(candidate);
10079
+ if (!existsSync10(canonical)) {
10076
10080
  throw new PathError("not-found", `path "${userPath}" does not exist`);
10077
10081
  }
10078
10082
  if (!statSync3(canonical).isFile()) {
@@ -10093,11 +10097,11 @@ var init_paths = __esm(() => {
10093
10097
  });
10094
10098
 
10095
10099
  // ../studio/src/server/discovery.ts
10096
- import { readdirSync as readdirSync3, readFileSync as readFileSync10 } from "fs";
10097
- import { basename, join as join9, relative } from "path";
10100
+ import { readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
10101
+ import { basename, join as join11, relative } from "path";
10098
10102
  function safeParseChit(absolutePath) {
10099
10103
  try {
10100
- const raw2 = JSON.parse(readFileSync10(absolutePath, "utf-8"));
10104
+ const raw2 = JSON.parse(readFileSync13(absolutePath, "utf-8"));
10101
10105
  parseManifest(raw2);
10102
10106
  return true;
10103
10107
  } catch {
@@ -10117,14 +10121,14 @@ function discover(opts) {
10117
10121
  relPath: relPathFromCwd(absolutePath, opts.cwd)
10118
10122
  };
10119
10123
  }
10120
- const entries = readdirSync3(opts.cwd, { withFileTypes: true });
10124
+ const entries = readdirSync4(opts.cwd, { withFileTypes: true });
10121
10125
  const candidates = [];
10122
10126
  for (const entry of entries) {
10123
10127
  if (!entry.isFile())
10124
10128
  continue;
10125
10129
  if (!entry.name.endsWith(".json"))
10126
10130
  continue;
10127
- const absolutePath = join9(opts.cwd, entry.name);
10131
+ const absolutePath = join11(opts.cwd, entry.name);
10128
10132
  if (!safeParseChit(absolutePath))
10129
10133
  continue;
10130
10134
  candidates.push({
@@ -10149,14 +10153,14 @@ var init_discovery = __esm(() => {
10149
10153
  });
10150
10154
 
10151
10155
  // ../studio/src/server/docs.ts
10152
- import { createHash as createHash5 } from "crypto";
10153
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
10156
+ import { createHash as createHash6 } from "crypto";
10157
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
10154
10158
  import { basename as basename2, relative as relative2 } from "path";
10155
10159
  function canonicalize(draft) {
10156
10160
  return JSON.stringify(draft, null, "\t");
10157
10161
  }
10158
10162
  function hashRaw(raw2) {
10159
- return createHash5("sha256").update(raw2, "utf-8").digest("hex");
10163
+ return createHash6("sha256").update(raw2, "utf-8").digest("hex");
10160
10164
  }
10161
10165
 
10162
10166
  class DocStore {
@@ -10183,7 +10187,7 @@ class DocStore {
10183
10187
  if (!entry)
10184
10188
  return null;
10185
10189
  try {
10186
- return hashRaw(readFileSync11(entry.absolutePath, "utf-8"));
10190
+ return hashRaw(readFileSync14(entry.absolutePath, "utf-8"));
10187
10191
  } catch {
10188
10192
  return null;
10189
10193
  }
@@ -10225,7 +10229,7 @@ class DocStore {
10225
10229
  return null;
10226
10230
  let raw2;
10227
10231
  try {
10228
- raw2 = readFileSync11(entry.absolutePath, "utf-8");
10232
+ raw2 = readFileSync14(entry.absolutePath, "utf-8");
10229
10233
  } catch (e) {
10230
10234
  const errorDoc = {
10231
10235
  id: docId,
@@ -10281,7 +10285,7 @@ class DocStore {
10281
10285
  return { kind: "not-found" };
10282
10286
  let currentRaw;
10283
10287
  try {
10284
- currentRaw = readFileSync11(entry.absolutePath, "utf-8");
10288
+ currentRaw = readFileSync14(entry.absolutePath, "utf-8");
10285
10289
  } catch {
10286
10290
  return { kind: "not-found" };
10287
10291
  }
@@ -10293,7 +10297,7 @@ class DocStore {
10293
10297
  const manifest = parseManifest(draft);
10294
10298
  const graphModel = buildGraphModel(manifest, this.registry, surface);
10295
10299
  const canonicalRaw = canonicalize(draft);
10296
- writeFileSync6(entry.absolutePath, canonicalRaw, "utf-8");
10300
+ writeFileSync7(entry.absolutePath, canonicalRaw, "utf-8");
10297
10301
  const newHash = hashRaw(canonicalRaw);
10298
10302
  return {
10299
10303
  kind: "saved",
@@ -10362,11 +10366,8 @@ var init_docs = __esm(() => {
10362
10366
  });
10363
10367
 
10364
10368
  // ../studio/src/server/loops.ts
10365
- import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs";
10366
- import { join as join10 } from "path";
10367
- function loopsDir2(cwd) {
10368
- return join10(cwd, ".chit", "loops");
10369
- }
10369
+ import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync15 } from "fs";
10370
+ import { join as join12 } from "path";
10370
10371
  function summarize(loopId, records) {
10371
10372
  const header = records[0];
10372
10373
  if (header?.type !== "loop")
@@ -10383,40 +10384,46 @@ function summarize(loopId, records) {
10383
10384
  startedAt: header.startedAt
10384
10385
  };
10385
10386
  }
10386
- function listLoops(cwd) {
10387
- const dir = loopsDir2(cwd);
10388
- if (!existsSync10(dir))
10387
+ function readLoopFrom(dir, loopId) {
10388
+ const path = join12(dir, `${loopId}.jsonl`);
10389
+ if (!existsSync11(path))
10390
+ return { kind: "not-found" };
10391
+ try {
10392
+ const records = validateLoopLog(parseLoopLog(readFileSync15(path, "utf-8")));
10393
+ const header = records[0];
10394
+ if (header?.type !== "loop" || header.loopId !== loopId) {
10395
+ return { kind: "invalid-log", message: "header loopId does not match the file name" };
10396
+ }
10397
+ return { kind: "ok", records };
10398
+ } catch (e) {
10399
+ if (e instanceof LoopLogError)
10400
+ return { kind: "invalid-log", message: e.message };
10401
+ throw e;
10402
+ }
10403
+ }
10404
+ function listLoops(loopsDir) {
10405
+ if (!loopsDir || !existsSync11(loopsDir))
10389
10406
  return [];
10390
10407
  const summaries = [];
10391
- for (const name of readdirSync4(dir)) {
10408
+ for (const name of readdirSync5(loopsDir)) {
10392
10409
  if (!name.endsWith(".jsonl"))
10393
10410
  continue;
10394
10411
  const loopId = name.slice(0, -".jsonl".length);
10395
- const result = readLoop2(cwd, loopId);
10412
+ if (!SAFE_LOOP_ID2.test(loopId))
10413
+ continue;
10414
+ const result = readLoopFrom(loopsDir, loopId);
10396
10415
  if (result.kind === "ok")
10397
10416
  summaries.push(summarize(loopId, result.records));
10398
10417
  }
10399
10418
  summaries.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
10400
10419
  return summaries;
10401
10420
  }
10402
- function readLoop2(cwd, loopId) {
10421
+ function readLoop2(loopsDir, loopId) {
10403
10422
  if (!SAFE_LOOP_ID2.test(loopId))
10404
10423
  return { kind: "invalid-id" };
10405
- const path = join10(loopsDir2(cwd), `${loopId}.jsonl`);
10406
- if (!existsSync10(path))
10424
+ if (!loopsDir)
10407
10425
  return { kind: "not-found" };
10408
- try {
10409
- const records = validateLoopLog(parseLoopLog(readFileSync12(path, "utf-8")));
10410
- const header = records[0];
10411
- if (header?.type !== "loop" || header.loopId !== loopId) {
10412
- return { kind: "invalid-log", message: "header loopId does not match the file name" };
10413
- }
10414
- return { kind: "ok", records };
10415
- } catch (e) {
10416
- if (e instanceof LoopLogError)
10417
- return { kind: "invalid-log", message: e.message };
10418
- throw e;
10419
- }
10426
+ return readLoopFrom(loopsDir, loopId);
10420
10427
  }
10421
10428
  var SAFE_LOOP_ID2;
10422
10429
  var init_loops = __esm(() => {
@@ -10455,8 +10462,8 @@ __export(exports_server, {
10455
10462
  buildApp: () => buildApp,
10456
10463
  PathError: () => PathError
10457
10464
  });
10458
- import { existsSync as existsSync11 } from "fs";
10459
- import { join as join11 } from "path";
10465
+ import { existsSync as existsSync12 } from "fs";
10466
+ import { join as join13 } from "path";
10460
10467
  async function startStudio(opts) {
10461
10468
  const hostname3 = opts.hostname ?? "127.0.0.1";
10462
10469
  const requestedPort = opts.port ?? 0;
@@ -10474,7 +10481,8 @@ async function startStudio(opts) {
10474
10481
  store,
10475
10482
  allowedHosts,
10476
10483
  clientDistDir,
10477
- lifecycle: opts.lifecycle
10484
+ lifecycle: opts.lifecycle,
10485
+ loopsDir: opts.loopsDir
10478
10486
  });
10479
10487
  const server2 = Bun.serve({
10480
10488
  port: requestedPort,
@@ -10508,8 +10516,8 @@ function buildApp(opts) {
10508
10516
  const asset = c.req.param("asset");
10509
10517
  if (!CLIENT_ASSETS.has(asset))
10510
10518
  return c.text("not found", 404);
10511
- const path = join11(opts.clientDistDir, asset);
10512
- if (!existsSync11(path)) {
10519
+ const path = join13(opts.clientDistDir, asset);
10520
+ if (!existsSync12(path)) {
10513
10521
  return c.text(`client bundle missing at ${path}. Run: bun run studio:build`, 503);
10514
10522
  }
10515
10523
  return new Response(Bun.file(path));
@@ -10589,10 +10597,10 @@ function buildApp(opts) {
10589
10597
  return c.json(result);
10590
10598
  });
10591
10599
  app.get("/api/loops", (c) => {
10592
- return c.json(listLoops(opts.cwd));
10600
+ return c.json(listLoops(opts.loopsDir));
10593
10601
  });
10594
10602
  app.get("/api/loops/:loopId", (c) => {
10595
- const result = readLoop2(opts.cwd, c.req.param("loopId"));
10603
+ const result = readLoop2(opts.loopsDir, c.req.param("loopId"));
10596
10604
  if (result.kind === "not-found")
10597
10605
  return c.text("not found", 404);
10598
10606
  if (result.kind === "invalid-id")
@@ -10684,15 +10692,15 @@ var init_server = __esm(() => {
10684
10692
  init_loops();
10685
10693
  init_token();
10686
10694
  init_paths();
10687
- CLIENT_DIST = join11(import.meta.dir, "..", "..", "dist", "client");
10695
+ CLIENT_DIST = join13(import.meta.dir, "..", "..", "dist", "client");
10688
10696
  CLIENT_ASSETS = new Set(["index.js", "index.css"]);
10689
10697
  });
10690
10698
 
10691
10699
  // src/cli/run.ts
10692
10700
  init_src();
10693
- import { readFileSync as readFileSync13 } from "fs";
10694
- import { homedir as homedir6 } from "os";
10695
- import { basename as basename3, dirname as dirname2, join as join12 } from "path";
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";
10696
10704
 
10697
10705
  // src/adapters/sanitize.ts
10698
10706
  var SENSITIVE_KEY = /key|token|secret|password|auth/i;
@@ -11596,8 +11604,317 @@ function wrapAdaptersWithAudit(adapters, recorder) {
11596
11604
  return out;
11597
11605
  }
11598
11606
 
11607
+ // src/jobs/store.ts
11608
+ import { randomUUID as randomUUID3 } from "crypto";
11609
+ import {
11610
+ existsSync as existsSync3,
11611
+ mkdirSync as mkdirSync2,
11612
+ readdirSync as readdirSync2,
11613
+ readFileSync as readFileSync4,
11614
+ renameSync as renameSync2,
11615
+ rmSync as rmSync3,
11616
+ writeFileSync as writeFileSync2
11617
+ } from "fs";
11618
+ import { homedir as homedir3 } from "os";
11619
+ import { join as join3 } from "path";
11620
+
11621
+ // src/jobs/lock.ts
11622
+ import { randomUUID as randomUUID2 } from "crypto";
11623
+ import { closeSync, openSync, readFileSync as readFileSync3, rmSync as rmSync2, writeSync } from "fs";
11624
+
11625
+ class LockError extends Error {
11626
+ }
11627
+ function sleepSync(ms) {
11628
+ if (ms <= 0)
11629
+ return;
11630
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
11631
+ }
11632
+ function acquireLock(lockPath, opts = {}) {
11633
+ const retryMs = opts.retryMs ?? 50;
11634
+ const maxAttempts = opts.maxAttempts ?? 200;
11635
+ const token = randomUUID2();
11636
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
11637
+ try {
11638
+ const fd = openSync(lockPath, "wx");
11639
+ try {
11640
+ writeSync(fd, token);
11641
+ } finally {
11642
+ closeSync(fd);
11643
+ }
11644
+ return { path: lockPath, token };
11645
+ } catch (err) {
11646
+ if (err.code !== "EEXIST")
11647
+ throw err;
11648
+ sleepSync(retryMs);
11649
+ }
11650
+ }
11651
+ 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)}`);
11652
+ }
11653
+ function releaseLock(lock) {
11654
+ try {
11655
+ if (readFileSync3(lock.path, "utf-8") === lock.token)
11656
+ rmSync2(lock.path, { force: true });
11657
+ } catch {}
11658
+ }
11659
+ function withFileLock(lockPath, fn, opts) {
11660
+ const lock = acquireLock(lockPath, opts);
11661
+ try {
11662
+ return fn();
11663
+ } finally {
11664
+ releaseLock(lock);
11665
+ }
11666
+ }
11667
+
11668
+ // src/jobs/store.ts
11669
+ class JobStoreError extends Error {
11670
+ }
11671
+ var SAFE_JOB_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
11672
+ function defaultJobsDir() {
11673
+ const xdg = process.env.XDG_STATE_HOME || join3(homedir3(), ".local", "state");
11674
+ return join3(xdg, "chit", "jobs");
11675
+ }
11676
+
11677
+ class JobStore {
11678
+ baseDir;
11679
+ constructor(baseDir = defaultJobsDir()) {
11680
+ this.baseDir = baseDir;
11681
+ }
11682
+ path(jobId) {
11683
+ if (!SAFE_JOB_ID.test(jobId))
11684
+ throw new JobStoreError(`invalid job id ${JSON.stringify(jobId)}`);
11685
+ return join3(this.baseDir, `${jobId}.json`);
11686
+ }
11687
+ lockPath(jobId) {
11688
+ return `${this.path(jobId)}.lock`;
11689
+ }
11690
+ loopLockPath(loopId) {
11691
+ if (!SAFE_JOB_ID.test(loopId))
11692
+ throw new JobStoreError(`invalid loop id ${JSON.stringify(loopId)}`);
11693
+ mkdirSync2(join3(this.baseDir, "locks"), { recursive: true });
11694
+ return join3(this.baseDir, "locks", `${loopId}.lock`);
11695
+ }
11696
+ create(record) {
11697
+ mkdirSync2(this.baseDir, { recursive: true });
11698
+ const path = this.path(record.jobId);
11699
+ withFileLock(this.lockPath(record.jobId), () => {
11700
+ if (existsSync3(path))
11701
+ throw new JobStoreError(`job ${JSON.stringify(record.jobId)} already exists`);
11702
+ writeAtomic(path, record);
11703
+ });
11704
+ }
11705
+ get(jobId) {
11706
+ const path = this.path(jobId);
11707
+ if (!existsSync3(path))
11708
+ return;
11709
+ try {
11710
+ return JSON.parse(readFileSync4(path, "utf-8"));
11711
+ } catch {
11712
+ return;
11713
+ }
11714
+ }
11715
+ update(jobId, mutate) {
11716
+ const path = this.path(jobId);
11717
+ return withFileLock(this.lockPath(jobId), () => {
11718
+ if (!existsSync3(path))
11719
+ throw new JobStoreError(`no job ${JSON.stringify(jobId)}`);
11720
+ const current = JSON.parse(readFileSync4(path, "utf-8"));
11721
+ const next = mutate(current);
11722
+ writeAtomic(path, next);
11723
+ return next;
11724
+ });
11725
+ }
11726
+ list() {
11727
+ if (!existsSync3(this.baseDir))
11728
+ return [];
11729
+ const jobs = [];
11730
+ for (const name of readdirSync2(this.baseDir)) {
11731
+ if (!name.endsWith(".json"))
11732
+ continue;
11733
+ try {
11734
+ jobs.push(JSON.parse(readFileSync4(join3(this.baseDir, name), "utf-8")));
11735
+ } catch {}
11736
+ }
11737
+ jobs.sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
11738
+ return jobs;
11739
+ }
11740
+ }
11741
+ function writeAtomic(path, record) {
11742
+ const tmp = `${path}.${randomUUID3()}.tmp`;
11743
+ writeFileSync2(tmp, JSON.stringify(record, null, 2));
11744
+ try {
11745
+ renameSync2(tmp, path);
11746
+ } catch (err) {
11747
+ rmSync3(tmp, { force: true });
11748
+ throw err;
11749
+ }
11750
+ }
11751
+
11752
+ // src/jobs/worker.ts
11753
+ import { readFileSync as readFileSync8 } from "fs";
11754
+ import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
11755
+
11756
+ // src/cli/converge.ts
11757
+ init_src();
11758
+ import { execFileSync } from "child_process";
11759
+ import { readFileSync as readFileSync7 } from "fs";
11760
+ import { resolve as resolve2 } from "path";
11761
+
11762
+ // src/loops/log-store.ts
11763
+ init_src();
11764
+ import { appendFileSync as appendFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
11765
+ import { join as join5 } from "path";
11766
+
11767
+ // src/loops/location.ts
11768
+ import { spawnSync } from "child_process";
11769
+ import { createHash as createHash2 } from "crypto";
11770
+ import { realpathSync } from "fs";
11771
+ import { homedir as homedir4 } from "os";
11772
+ import { join as join4 } from "path";
11773
+ function gitTopLevel(cwd) {
11774
+ try {
11775
+ const out = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
11776
+ encoding: "utf-8",
11777
+ stdio: ["ignore", "pipe", "ignore"]
11778
+ });
11779
+ if (out.status !== 0)
11780
+ return;
11781
+ const top = out.stdout.trim();
11782
+ return top || undefined;
11783
+ } catch {
11784
+ return;
11785
+ }
11786
+ }
11787
+ function repoRoot(cwd) {
11788
+ const base = gitTopLevel(cwd) ?? cwd;
11789
+ try {
11790
+ return realpathSync(base);
11791
+ } catch {
11792
+ return base;
11793
+ }
11794
+ }
11795
+ function repoKey(cwd) {
11796
+ return createHash2("sha256").update(repoRoot(cwd)).digest("hex").slice(0, 16);
11797
+ }
11798
+ function loopStateDir() {
11799
+ const xdg = process.env.XDG_STATE_HOME || join4(homedir4(), ".local", "state");
11800
+ return join4(xdg, "chit", "loops");
11801
+ }
11802
+ function loopLogDir(cwd) {
11803
+ return join4(loopStateDir(), repoKey(cwd));
11804
+ }
11805
+
11806
+ // src/loops/log-store.ts
11807
+ class LoopStoreError extends Error {
11808
+ }
11809
+ var SAFE_LOOP_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
11810
+ var realClock2 = () => Date.now();
11811
+ function iso(ms) {
11812
+ return new Date(ms).toISOString();
11813
+ }
11814
+ function safeId(loopId) {
11815
+ if (!SAFE_LOOP_ID.test(loopId)) {
11816
+ throw new LoopStoreError(`invalid loop id ${JSON.stringify(loopId)}`);
11817
+ }
11818
+ return loopId;
11819
+ }
11820
+ function loopPath(cwd, loopId) {
11821
+ return join5(loopLogDir(cwd), `${safeId(loopId)}.jsonl`);
11822
+ }
11823
+ function readRecords(path, loopId) {
11824
+ if (!existsSync4(path)) {
11825
+ throw new LoopStoreError(`no loop log for ${JSON.stringify(loopId)} at ${path}`);
11826
+ }
11827
+ const records = validateLoopLog(parseLoopLog(readFileSync5(path, "utf-8")));
11828
+ const header = records[0];
11829
+ if (header.loopId !== loopId) {
11830
+ throw new LoopStoreError(`loop log at ${path} declares loopId ${JSON.stringify(header.loopId)}, expected ${JSON.stringify(loopId)}`);
11831
+ }
11832
+ return records;
11833
+ }
11834
+ function startLoop(cwd, opts) {
11835
+ const loopId = opts.loopId ?? crypto.randomUUID();
11836
+ const path = loopPath(cwd, loopId);
11837
+ if (existsSync4(path) && !opts.force) {
11838
+ throw new LoopStoreError(`loop log already exists at ${path} (pass force to overwrite)`);
11839
+ }
11840
+ mkdirSync3(loopLogDir(cwd), { recursive: true });
11841
+ const header = {
11842
+ type: "loop",
11843
+ schema: 1,
11844
+ loopId,
11845
+ scope: opts.scope,
11846
+ task: opts.task,
11847
+ repo: repoRoot(cwd),
11848
+ repoKey: repoKey(cwd),
11849
+ startedAt: iso((opts.clock ?? realClock2)()),
11850
+ maxIterations: opts.maxIterations
11851
+ };
11852
+ writeFileSync3(path, `${serializeLoopRecord(header)}
11853
+ `);
11854
+ return { loopId, path };
11855
+ }
11856
+ function appendIteration(cwd, loopId, opts) {
11857
+ const path = loopPath(cwd, loopId);
11858
+ const records = readRecords(path, loopId);
11859
+ if (records.some((r) => r.type === "stop")) {
11860
+ throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is already stopped; cannot append`);
11861
+ }
11862
+ const header = records[0];
11863
+ const n = records.filter((r) => r.type === "iteration").length + 1;
11864
+ if (n > header.maxIterations) {
11865
+ throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is at its iteration budget (maxIterations=${header.maxIterations}); cannot append iteration ${n}`);
11866
+ }
11867
+ const rec = {
11868
+ type: "iteration",
11869
+ n,
11870
+ implementSummary: opts.implementSummary,
11871
+ changedFiles: opts.changedFiles,
11872
+ checksRun: opts.checksRun,
11873
+ verdict: opts.verdict,
11874
+ findingCount: opts.findingCount,
11875
+ decision: opts.decision,
11876
+ checkDurationMs: opts.checkDurationMs,
11877
+ at: iso((opts.clock ?? realClock2)())
11878
+ };
11879
+ if (opts.workspaceWarnings !== undefined && opts.workspaceWarnings.length > 0) {
11880
+ rec.workspaceWarnings = opts.workspaceWarnings;
11881
+ }
11882
+ if (opts.auditRef !== undefined)
11883
+ rec.auditRef = opts.auditRef;
11884
+ if (opts.usage !== undefined)
11885
+ rec.usage = opts.usage;
11886
+ appendFileSync2(path, `${serializeLoopRecord(rec)}
11887
+ `);
11888
+ return { n, path };
11889
+ }
11890
+ function stopLoop(cwd, loopId, opts) {
11891
+ const path = loopPath(cwd, loopId);
11892
+ const records = readRecords(path, loopId);
11893
+ if (records.some((r) => r.type === "stop")) {
11894
+ throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is already stopped`);
11895
+ }
11896
+ const header = records[0];
11897
+ const iterations = records.filter((r) => r.type === "iteration").length;
11898
+ const nowMs = (opts.clock ?? realClock2)();
11899
+ const totalElapsedMs = Math.max(0, nowMs - Date.parse(header.startedAt));
11900
+ const rec = {
11901
+ type: "stop",
11902
+ status: opts.status,
11903
+ reason: opts.reason,
11904
+ iterations,
11905
+ totalElapsedMs,
11906
+ endedAt: iso(nowMs)
11907
+ };
11908
+ appendFileSync2(path, `${serializeLoopRecord(rec)}
11909
+ `);
11910
+ return { iterations, totalElapsedMs, path };
11911
+ }
11912
+ function readLoop(cwd, loopId) {
11913
+ return readRecords(loopPath(cwd, loopId), loopId);
11914
+ }
11915
+
11599
11916
  // src/runtime/render.ts
11600
- import { existsSync as existsSync3 } from "fs";
11917
+ import { existsSync as existsSync5 } from "fs";
11601
11918
  import { isAbsolute, resolve } from "path";
11602
11919
 
11603
11920
  class RuntimeError extends Error {
@@ -11645,7 +11962,7 @@ function renderFilePaths(paths, invocationCwd, inputName) {
11645
11962
  const resolved = [];
11646
11963
  for (const p of paths) {
11647
11964
  const abs = isAbsolute(p) ? p : resolve(invocationCwd, p);
11648
- if (!existsSync3(abs)) {
11965
+ if (!existsSync5(abs)) {
11649
11966
  throw new RuntimeError(`input "${inputName}" references missing file: ${p}`);
11650
11967
  }
11651
11968
  resolved.push(abs);
@@ -11818,7 +12135,7 @@ async function executeManifest(manifest, options) {
11818
12135
  }
11819
12136
 
11820
12137
  // src/sessions/fingerprint.ts
11821
- import { createHash as createHash2 } from "crypto";
12138
+ import { createHash as createHash3 } from "crypto";
11822
12139
  function computeFingerprint(input) {
11823
12140
  const { agent, participant } = input;
11824
12141
  const baseUrl = agent.env?.ANTHROPIC_BASE_URL ?? agent.env?.OPENAI_BASE_URL ?? agent.env?.OLLAMA_HOST ?? "";
@@ -11834,7 +12151,7 @@ function computeFingerprint(input) {
11834
12151
  session: participant.session,
11835
12152
  permissions: participant.permissions
11836
12153
  });
11837
- return createHash2("sha256").update(material).digest("hex").slice(0, 16);
12154
+ return createHash3("sha256").update(material).digest("hex").slice(0, 16);
11838
12155
  }
11839
12156
 
11840
12157
  // src/sessions/coordinator.ts
@@ -11886,20 +12203,20 @@ function buildSessionAdapter(inner, manifestId, scope, trackedFingerprints, stor
11886
12203
  }
11887
12204
 
11888
12205
  // src/sessions/store.ts
11889
- import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
12206
+ import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
11890
12207
  import {
11891
- closeSync,
11892
- existsSync as existsSync4,
11893
- mkdirSync as mkdirSync2,
11894
- openSync,
11895
- readFileSync as readFileSync3,
11896
- renameSync as renameSync2,
11897
- rmSync as rmSync2,
11898
- writeFileSync as writeFileSync2,
11899
- writeSync
12208
+ closeSync as closeSync2,
12209
+ existsSync as existsSync6,
12210
+ mkdirSync as mkdirSync4,
12211
+ openSync as openSync2,
12212
+ readFileSync as readFileSync6,
12213
+ renameSync as renameSync3,
12214
+ rmSync as rmSync4,
12215
+ writeFileSync as writeFileSync4,
12216
+ writeSync as writeSync2
11900
12217
  } from "fs";
11901
- import { homedir as homedir3 } from "os";
11902
- import { dirname, join as join3 } from "path";
12218
+ import { homedir as homedir5 } from "os";
12219
+ import { dirname, join as join6 } from "path";
11903
12220
  function isObject4(v) {
11904
12221
  return v !== null && typeof v === "object" && !Array.isArray(v);
11905
12222
  }
@@ -11910,14 +12227,14 @@ function entryKey(participantId, fingerprint) {
11910
12227
  return `${participantId}--${fingerprint}`;
11911
12228
  }
11912
12229
  var HASH_SEP = String.fromCharCode(0);
11913
- function sleepSync(ms) {
12230
+ function sleepSync2(ms) {
11914
12231
  if (ms <= 0)
11915
12232
  return;
11916
12233
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
11917
12234
  }
11918
12235
  function defaultSessionDir() {
11919
- const xdg = process.env.XDG_STATE_HOME || join3(homedir3(), ".local", "state");
11920
- return join3(xdg, "chit", "sessions");
12236
+ const xdg = process.env.XDG_STATE_HOME || join6(homedir5(), ".local", "state");
12237
+ return join6(xdg, "chit", "sessions");
11921
12238
  }
11922
12239
 
11923
12240
  class FileSessionStore {
@@ -11931,11 +12248,11 @@ class FileSessionStore {
11931
12248
  }
11932
12249
  load(key) {
11933
12250
  const path = this.filePath(key);
11934
- if (!existsSync4(path))
12251
+ if (!existsSync6(path))
11935
12252
  return;
11936
12253
  let raw;
11937
12254
  try {
11938
- raw = JSON.parse(readFileSync3(path, "utf-8"));
12255
+ raw = JSON.parse(readFileSync6(path, "utf-8"));
11939
12256
  } catch {
11940
12257
  return;
11941
12258
  }
@@ -11945,13 +12262,13 @@ class FileSessionStore {
11945
12262
  }
11946
12263
  save(key, payload) {
11947
12264
  const path = this.filePath(key);
11948
- mkdirSync2(dirname(path), { recursive: true });
12265
+ mkdirSync4(dirname(path), { recursive: true });
11949
12266
  const lock = this.acquireLock(path);
11950
12267
  try {
11951
12268
  let data = {};
11952
- if (existsSync4(path)) {
12269
+ if (existsSync6(path)) {
11953
12270
  try {
11954
- const raw = JSON.parse(readFileSync3(path, "utf-8"));
12271
+ const raw = JSON.parse(readFileSync6(path, "utf-8"));
11955
12272
  if (isObject4(raw))
11956
12273
  data = raw;
11957
12274
  } catch {
@@ -11959,13 +12276,13 @@ class FileSessionStore {
11959
12276
  }
11960
12277
  }
11961
12278
  data[entryKey(key.participantId, key.fingerprint)] = payload;
11962
- const tmpPath = `${path}.${randomUUID2()}.tmp`;
11963
- writeFileSync2(tmpPath, JSON.stringify(data, null, 2));
12279
+ const tmpPath = `${path}.${randomUUID4()}.tmp`;
12280
+ writeFileSync4(tmpPath, JSON.stringify(data, null, 2));
11964
12281
  try {
11965
12282
  this.assertOwned(lock);
11966
- renameSync2(tmpPath, path);
12283
+ renameSync3(tmpPath, path);
11967
12284
  } catch (err) {
11968
- rmSync2(tmpPath, { force: true });
12285
+ rmSync4(tmpPath, { force: true });
11969
12286
  throw err;
11970
12287
  }
11971
12288
  } finally {
@@ -11974,20 +12291,20 @@ class FileSessionStore {
11974
12291
  }
11975
12292
  acquireLock(path) {
11976
12293
  const lockPath = `${path}.lock`;
11977
- const token = randomUUID2();
12294
+ const token = randomUUID4();
11978
12295
  for (let attempt = 0;attempt < this.lockMaxAttempts; attempt++) {
11979
12296
  try {
11980
- const fd = openSync(lockPath, "wx");
12297
+ const fd = openSync2(lockPath, "wx");
11981
12298
  try {
11982
- writeSync(fd, token);
12299
+ writeSync2(fd, token);
11983
12300
  } finally {
11984
- closeSync(fd);
12301
+ closeSync2(fd);
11985
12302
  }
11986
12303
  return { path: lockPath, token };
11987
12304
  } catch (err) {
11988
12305
  if (err.code !== "EEXIST")
11989
12306
  throw err;
11990
- sleepSync(this.lockRetryMs);
12307
+ sleepSync2(this.lockRetryMs);
11991
12308
  }
11992
12309
  }
11993
12310
  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)}`);
@@ -11995,7 +12312,7 @@ class FileSessionStore {
11995
12312
  assertOwned(lock) {
11996
12313
  let current;
11997
12314
  try {
11998
- current = readFileSync3(lock.path, "utf-8");
12315
+ current = readFileSync6(lock.path, "utf-8");
11999
12316
  } catch {
12000
12317
  current = undefined;
12001
12318
  }
@@ -12005,265 +12322,999 @@ class FileSessionStore {
12005
12322
  }
12006
12323
  releaseLock(lock) {
12007
12324
  try {
12008
- if (readFileSync3(lock.path, "utf-8") === lock.token) {
12009
- rmSync2(lock.path, { force: true });
12325
+ if (readFileSync6(lock.path, "utf-8") === lock.token) {
12326
+ rmSync4(lock.path, { force: true });
12010
12327
  }
12011
12328
  } catch {}
12012
12329
  }
12013
12330
  filePath(key) {
12014
12331
  const readable = `${safeSegment(key.scope)}--${safeSegment(key.manifestId)}`;
12015
- const hash = createHash3("sha256").update(`${key.scope}${HASH_SEP}${key.manifestId}`).digest("hex").slice(0, 12);
12016
- return join3(this.baseDir, `${readable}--${hash}.json`);
12332
+ const hash = createHash4("sha256").update(`${key.scope}${HASH_SEP}${key.manifestId}`).digest("hex").slice(0, 12);
12333
+ return join6(this.baseDir, `${readable}--${hash}.json`);
12017
12334
  }
12018
12335
  }
12019
12336
 
12020
- // src/surfaces/claude-skill.ts
12021
- init_src();
12022
- import { createHash as createHash4, randomBytes } from "crypto";
12023
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
12024
- import { join as join4, resolve as resolvePath } from "path";
12025
- class SurfaceInstallError extends Error {
12026
- constructor(message) {
12027
- super(message);
12028
- this.name = "SurfaceInstallError";
12029
- }
12030
- }
12031
- var CLAUDE_SKILL_CAPABILITIES = new Set([
12032
- "can_show_markdown",
12033
- "can_provide_stable_scope"
12034
- ]);
12035
- function installClaudeSkill(opts) {
12036
- if (opts.overrideName !== undefined && !VALID_INSTALL_NAME_RE.test(opts.overrideName)) {
12037
- 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.`);
12038
- }
12039
- const outputDir = resolvePath(opts.outputDir);
12040
- const runtimePath = resolvePath(opts.runtimePath);
12041
- const allowUnenforced = opts.allowUnenforcedPermissions === true;
12042
- let rawJson;
12043
- try {
12044
- rawJson = JSON.parse(readFileSync4(opts.manifestPath, "utf-8"));
12045
- } catch (e) {
12046
- throw new SurfaceInstallError(`failed to read manifest ${opts.manifestPath}: ${e.message}`);
12047
- }
12048
- let manifest;
12049
- try {
12050
- manifest = parseManifest(rawJson);
12051
- } catch (e) {
12052
- throw new SurfaceInstallError(`invalid manifest at ${opts.manifestPath}: ${e.message}`);
12053
- }
12054
- const missingCaps = findMissingCapabilities(manifest, CLAUDE_SKILL_CAPABILITIES);
12055
- if (missingCaps.length > 0) {
12056
- throw new SurfaceInstallError(`claude-skill surface does not provide capabilities required by "${manifest.id}": ${missingCaps.join(", ")}`);
12057
- }
12058
- const inputNames = Object.keys(manifest.inputs);
12059
- const primaryInput = inputNames[0];
12060
- if (!primaryInput) {
12061
- throw new SurfaceInstallError(`manifest "${manifest.id}" has no inputs; nothing to wire from $ARGUMENTS`);
12062
- }
12063
- const primaryInputSchema = manifest.inputs[primaryInput];
12064
- if (primaryInputSchema?.type !== "string") {
12065
- throw new SurfaceInstallError(`manifest "${manifest.id}": claude-skill surface only supports a string-typed primary input ` + `(got "${primaryInput}": ${primaryInputSchema?.type ?? "missing"})`);
12066
- }
12067
- if (inputNames.length > 1) {
12068
- throw new SurfaceInstallError(`manifest "${manifest.id}" declares multiple inputs (${inputNames.join(", ")}); ` + `claude-skill surface in PR6 supports exactly one string input`);
12069
- }
12070
- const registry2 = opts.registry ?? loadRegistry();
12071
- const unknownAgents = findUnknownAgents(manifest, registry2);
12072
- if (unknownAgents.length > 0) {
12073
- const lines = unknownAgents.map((u) => ` - participant "${u.participantId}" references unknown agent "${u.agentId}"`).join(`
12074
- `);
12075
- throw new SurfaceInstallError(`manifest "${manifest.id}" references agents that are not in the registry:
12076
- ${lines}`);
12077
- }
12078
- const gaps = findEnforcementGaps(manifest, registry2);
12079
- if (gaps.length > 0 && !allowUnenforced) {
12080
- throw new SurfaceInstallError(`cannot enforce required permissions for "${manifest.id}":
12081
- ${formatEnforcementGaps(gaps)}
12337
+ // src/cli/default-converge-manifest.ts
12338
+ var DEFAULT_CONVERGE_MANIFEST = {
12339
+ schema: 1,
12340
+ id: "converge",
12341
+ 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.",
12342
+ inputs: {
12343
+ task: { type: "string" },
12344
+ prior_review: { type: "string", optional: true }
12345
+ },
12346
+ requires: {
12347
+ can_show_markdown: true
12348
+ },
12349
+ participants: {
12350
+ implementer: {
12351
+ agent: "claude",
12352
+ 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.",
12353
+ session: "per_scope",
12354
+ permissions: { filesystem: "write" }
12355
+ },
12356
+ reviewer: {
12357
+ agent: "codex",
12358
+ 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.",
12359
+ session: "per_scope",
12360
+ permissions: { filesystem: "read_only" }
12361
+ }
12362
+ },
12363
+ steps: {
12364
+ implement: {
12365
+ call: "implementer",
12366
+ prompt: `Task:
12367
+ {{ inputs.task }}
12082
12368
 
12083
- Pass allowUnenforcedPermissions=true to install anyway; the generated skill will warn on every run.`);
12084
- }
12085
- const installName = opts.overrideName ?? manifest.id;
12086
- const skillDir = join4(outputDir, installName);
12087
- if (existsSync5(skillDir)) {
12088
- if (!opts.force) {
12089
- throw new SurfaceInstallError(`skill directory already exists: ${skillDir}
12369
+ Prior review to address (empty on the first iteration):
12370
+ {{ inputs.prior_review }}
12090
12371
 
12091
- 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).`);
12092
- }
12093
- rmSync3(skillDir, { recursive: true, force: true });
12094
- }
12095
- mkdirSync3(skillDir, { recursive: true });
12096
- const skillMdPath = join4(skillDir, "SKILL.md");
12097
- const manifestPath = join4(skillDir, "manifest.json");
12098
- const markerPath = join4(skillDir, INSTALL_MARKER_FILENAME);
12099
- const manifestJson = `${JSON.stringify(rawJson, null, 2)}
12100
- `;
12101
- writeFileSync3(manifestPath, manifestJson);
12102
- writeFileSync3(skillMdPath, buildSkillMd({
12103
- manifest,
12104
- runtimePath,
12105
- primaryInputName: primaryInput,
12106
- allowUnenforced: gaps.length > 0,
12107
- trace: opts.trace === true,
12108
- heredocDelimiter: generateHeredocDelimiter(),
12109
- installName
12110
- }));
12111
- const marker = {
12112
- schema: 1,
12113
- surface: "claude-skill",
12114
- installName,
12115
- manifestId: manifest.id,
12116
- runtimePath,
12117
- installedAt: new Date().toISOString(),
12118
- manifestHash: createHash4("sha256").update(manifestJson).digest("hex")
12119
- };
12120
- writeFileSync3(markerPath, `${JSON.stringify(marker, null, 2)}
12121
- `);
12122
- return { skillDir, skillMdPath, manifestPath, markerPath, enforcementGaps: gaps };
12123
- }
12124
- function generateHeredocDelimiter() {
12125
- return `CHIT_INPUT_${randomBytes(8).toString("hex").toUpperCase()}_EOF`;
12126
- }
12127
- function buildSkillMd(opts) {
12128
- const {
12129
- manifest,
12130
- runtimePath,
12131
- primaryInputName,
12132
- allowUnenforced,
12133
- trace,
12134
- heredocDelimiter,
12135
- installName
12136
- } = opts;
12137
- const allowFlag = allowUnenforced ? `\\
12138
- --allow-unenforced-permissions ` : "";
12139
- const traceFlag = trace ? `\\
12140
- --trace ` : "";
12141
- return `---
12142
- name: ${installName}
12143
- description: ${escapeFrontmatter(manifest.description)}
12144
- argument-hint: <${primaryInputName}>
12145
- disable-model-invocation: true
12146
- ---
12372
+ 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.`
12373
+ },
12374
+ review: {
12375
+ call: "reviewer",
12376
+ prompt: `Task under review:
12377
+ {{ inputs.task }}
12147
12378
 
12148
- # /${installName}
12379
+ Claude's summary of what it just implemented:
12380
+ {{ steps.implement.output }}
12149
12381
 
12150
- \`\`\`!
12151
- {
12152
- # Claude Code's \`! \` preprocessor substitutes \${CLAUDE_SESSION_ID} (bare
12153
- # brace form) BEFORE bash runs. The :- default form is NOT substituted, so
12154
- # we use the bare form: when running inside Claude Code, this line gets
12155
- # the real session id baked in (always non-empty); when running outside,
12156
- # bash evaluates the env var (typically empty, so we fail fast).
12157
- if [ -z "\${CLAUDE_SESSION_ID}" ]; then
12158
- echo "chit: CLAUDE_SESSION_ID is required; this skill must run inside Claude Code" >&2
12159
- exit 2
12160
- fi
12161
- WORKTREE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
12162
- SCOPE_HASH=$(printf '%s' "$WORKTREE" | (shasum -a 256 2>/dev/null || sha256sum) | cut -c1-12)
12163
- SCOPE="\${CLAUDE_SESSION_ID}-\${SCOPE_HASH}"
12382
+ 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:
12383
+ 1. Verdict: proceed / revise / block.
12384
+ 2. Findings ordered by severity, with file:line.
12385
+ 3. What Claude should fix next if the verdict is revise.
12386
+ 4. Remaining risk if proceeding.
12164
12387
 
12165
- bun "${runtimePath}/src/cli/run.ts" run \\
12166
- "\${CLAUDE_SKILL_DIR}/manifest.json" \\
12167
- --scope "$SCOPE" \\
12168
- --invocation-cwd "$WORKTREE" ${allowFlag}${traceFlag}\\
12169
- --input-stdin ${primaryInputName} <<'${heredocDelimiter}'
12170
- $ARGUMENTS
12171
- ${heredocDelimiter}
12172
- } 2>&1
12388
+ 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:
12389
+ \`\`\`json
12390
+ {"verdict": "proceed | revise | block", "findingCount": 0, "checksRun": "the non-mutating checks you ran, or 'none'", "risk": "remaining risk if proceeding"}
12173
12391
  \`\`\`
12392
+ findingCount is the integer number of findings; checksRun is a short human string.`
12393
+ },
12394
+ out: {
12395
+ format: `## Converge iteration
12174
12396
 
12175
- 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.
12176
- `;
12177
- }
12178
- function escapeFrontmatter(s) {
12179
- return s.replace(/\r?\n/g, " ").replace(/"/g, "'");
12180
- }
12397
+ ### Implementer (Claude)
12398
+ {{ steps.implement.output }}
12181
12399
 
12182
- // src/surfaces/lifecycle.ts
12183
- init_src();
12184
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync2 } from "fs";
12185
- import { homedir as homedir4 } from "os";
12186
- import { join as join5 } from "path";
12187
- function defaultSkillsDir() {
12188
- return join5(homedir4(), ".claude", "skills");
12189
- }
12400
+ ### Reviewer (Codex)
12401
+ {{ steps.review.output }}`
12402
+ }
12403
+ },
12404
+ output: "out"
12405
+ };
12190
12406
 
12191
- class LifecycleError extends Error {
12192
- constructor(message) {
12193
- super(message);
12194
- this.name = "LifecycleError";
12195
- }
12407
+ // src/cli/workspace.ts
12408
+ function isChitOwned(path) {
12409
+ return path === ".chit" || path.startsWith(".chit/");
12196
12410
  }
12197
- function listInstalled(parentDir) {
12198
- if (!existsSync6(parentDir))
12199
- return [];
12200
- const out = [];
12201
- const entries = readdirSync2(parentDir);
12202
- for (const name of entries) {
12203
- const skillDir = join5(parentDir, name);
12204
- let stat;
12205
- try {
12206
- stat = statSync2(skillDir);
12207
- } catch {
12208
- continue;
12209
- }
12210
- if (!stat.isDirectory())
12211
- continue;
12212
- const markerPath = join5(skillDir, INSTALL_MARKER_FILENAME);
12213
- if (!existsSync6(markerPath))
12214
- continue;
12215
- let raw;
12216
- try {
12217
- raw = JSON.parse(readFileSync5(markerPath, "utf-8"));
12218
- } catch {
12219
- continue;
12220
- }
12221
- try {
12222
- const marker = parseInstallMarker(raw, markerPath);
12223
- out.push({ skillDir, markerPath, marker });
12224
- } catch {}
12225
- }
12226
- return out.sort((a, b) => a.marker.installName.localeCompare(b.marker.installName));
12411
+ function isGeneratedArtifact(path) {
12412
+ const base = path.slice(path.lastIndexOf("/") + 1);
12413
+ return path === "__pycache__" || path.startsWith("__pycache__/") || path.includes("/__pycache__/") || path.endsWith(".pyc") || path.endsWith(".pyo") || base === ".DS_Store";
12227
12414
  }
12228
- function uninstall(parentDir, name) {
12229
- if (!VALID_INSTALL_NAME_RE.test(name)) {
12230
- 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.`);
12231
- }
12232
- const skillDir = join5(parentDir, name);
12233
- if (!existsSync6(skillDir)) {
12234
- throw new LifecycleError(`no install at ${skillDir}`);
12235
- }
12236
- if (!statSync2(skillDir).isDirectory()) {
12237
- throw new LifecycleError(`${skillDir} is not a directory`);
12415
+ function classifyWorkspace(snap) {
12416
+ const changed = new Set;
12417
+ for (const f of snap.tracked) {
12418
+ if (!isChitOwned(f))
12419
+ changed.add(f);
12238
12420
  }
12239
- const markerPath = join5(skillDir, INSTALL_MARKER_FILENAME);
12240
- if (!existsSync6(markerPath)) {
12241
- 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.`);
12421
+ const workspaceWarnings = [];
12422
+ for (const f of snap.untracked) {
12423
+ if (isChitOwned(f))
12424
+ continue;
12425
+ if (isGeneratedArtifact(f))
12426
+ workspaceWarnings.push(`untracked generated artifact: ${f}`);
12427
+ else
12428
+ changed.add(f);
12242
12429
  }
12243
- let raw;
12244
- try {
12245
- raw = JSON.parse(readFileSync5(markerPath, "utf-8"));
12246
- } catch (e) {
12247
- throw new LifecycleError(`refusing to uninstall ${skillDir}: ${INSTALL_MARKER_FILENAME} is not valid JSON (${e.message})`);
12430
+ return { changedFiles: [...changed], workspaceWarnings };
12431
+ }
12432
+
12433
+ // src/cli/converge.ts
12434
+ var defaultIO = {
12435
+ out: (s) => process.stdout.write(s),
12436
+ err: (s) => process.stderr.write(s)
12437
+ };
12438
+ var JSON_BLOCK_RE = /```json\s*([\s\S]*?)```/gi;
12439
+ var VERDICTS3 = new Set(["proceed", "revise", "block"]);
12440
+ var IMPLEMENT_STEP_ID = "implement";
12441
+ var REVIEW_STEP_ID = "review";
12442
+ var IMPLEMENT_SUMMARY_CAP = 2000;
12443
+ var CHECKS_RUN_FALLBACK = "unreported";
12444
+ function extractReviewJson(reviewText) {
12445
+ let last;
12446
+ for (const m of reviewText.matchAll(JSON_BLOCK_RE)) {
12447
+ if (m[1] !== undefined)
12448
+ last = m[1];
12248
12449
  }
12249
- let marker;
12450
+ if (last === undefined)
12451
+ return null;
12250
12452
  try {
12251
- marker = parseInstallMarker(raw, markerPath);
12252
- } catch (e) {
12253
- if (e instanceof MarkerError) {
12254
- throw new LifecycleError(`refusing to uninstall ${skillDir}: ${e.message}`);
12453
+ const parsed = JSON.parse(last);
12454
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
12455
+ return parsed;
12255
12456
  }
12256
- throw e;
12257
- }
12258
- const record = { skillDir, markerPath, marker };
12259
- rmSync4(skillDir, { recursive: true, force: true });
12260
- return record;
12457
+ } catch {}
12458
+ return null;
12261
12459
  }
12262
-
12263
- // src/surfaces/mcp/server.ts
12460
+ function parseReview(reviewText) {
12461
+ const block = extractReviewJson(reviewText);
12462
+ const rawVerdict = typeof block?.verdict === "string" ? block.verdict.toLowerCase() : undefined;
12463
+ if (block && rawVerdict && VERDICTS3.has(rawVerdict)) {
12464
+ const fc = block.findingCount;
12465
+ const cr = block.checksRun;
12466
+ return {
12467
+ verdict: rawVerdict,
12468
+ findingCount: typeof fc === "number" && Number.isInteger(fc) && fc >= 0 ? fc : 0,
12469
+ checksRun: typeof cr === "string" && cr.trim() !== "" ? cr.trim() : CHECKS_RUN_FALLBACK
12470
+ };
12471
+ }
12472
+ return { verdict: "block", findingCount: 0, checksRun: CHECKS_RUN_FALLBACK };
12473
+ }
12474
+ function reviewDurationMs(trace) {
12475
+ for (const e of trace) {
12476
+ if (e.type === "step.completed" && e.stepId === REVIEW_STEP_ID)
12477
+ return e.durationMs;
12478
+ }
12479
+ return 0;
12480
+ }
12481
+ var USAGE_KEYS = [
12482
+ "inputTokens",
12483
+ "outputTokens",
12484
+ "totalTokens",
12485
+ "cachedInputTokens",
12486
+ "reasoningTokens",
12487
+ "estimatedCostUsd"
12488
+ ];
12489
+ function sumTraceUsage(trace) {
12490
+ const usage = {};
12491
+ let any = false;
12492
+ for (const e of trace) {
12493
+ if (e.type !== "step.completed" || !e.usage)
12494
+ continue;
12495
+ for (const k of USAGE_KEYS) {
12496
+ const v = e.usage[k];
12497
+ if (typeof v === "number") {
12498
+ usage[k] = (usage[k] ?? 0) + v;
12499
+ any = true;
12500
+ }
12501
+ }
12502
+ }
12503
+ return any ? usage : undefined;
12504
+ }
12505
+ function capSummary(text) {
12506
+ if (text === "")
12507
+ return "(no summary)";
12508
+ if (text.length <= IMPLEMENT_SUMMARY_CAP)
12509
+ return text;
12510
+ return `${text.slice(0, IMPLEMENT_SUMMARY_CAP)}\u2026 (truncated, ${text.length} chars)`;
12511
+ }
12512
+ function gitLines(cwd, args) {
12513
+ try {
12514
+ const out = execFileSync("git", args, {
12515
+ cwd,
12516
+ encoding: "utf-8",
12517
+ stdio: ["ignore", "pipe", "ignore"]
12518
+ });
12519
+ return out.split(`
12520
+ `).map((s) => s.trim()).filter(Boolean);
12521
+ } catch {
12522
+ return [];
12523
+ }
12524
+ }
12525
+ function gitWorkspace(cwd) {
12526
+ return classifyWorkspace({
12527
+ tracked: [
12528
+ ...gitLines(cwd, ["diff", "--name-only"]),
12529
+ ...gitLines(cwd, ["diff", "--cached", "--name-only"])
12530
+ ],
12531
+ untracked: gitLines(cwd, ["ls-files", "--others", "--exclude-standard"])
12532
+ });
12533
+ }
12534
+
12535
+ class ConvergeExecuteError extends Error {
12536
+ executeError;
12537
+ constructor(executeError) {
12538
+ super(executeError instanceof Error ? executeError.message : String(executeError));
12539
+ this.executeError = executeError;
12540
+ }
12541
+ }
12542
+ async function runConvergeIteration(ctx) {
12543
+ let result;
12544
+ try {
12545
+ result = await ctx.execute({ task: ctx.task, prior_review: ctx.prior_review }, {
12546
+ loopId: ctx.loopId,
12547
+ iteration: ctx.iteration,
12548
+ ...ctx.signal && { signal: ctx.signal },
12549
+ ...ctx.onTrace && { onTrace: ctx.onTrace }
12550
+ });
12551
+ } catch (e) {
12552
+ throw new ConvergeExecuteError(e);
12553
+ }
12554
+ if (!result.ok) {
12555
+ return {
12556
+ ok: false,
12557
+ failure: `manifest run failed at step "${result.failedStep}": ${result.error}`
12558
+ };
12559
+ }
12560
+ const reviewText = result.outputs.review ?? "";
12561
+ const review = parseReview(reviewText);
12562
+ const usage = sumTraceUsage(result.trace);
12563
+ const { changedFiles, workspaceWarnings } = gitWorkspace(ctx.cwd);
12564
+ appendIteration(ctx.cwd, ctx.loopId, {
12565
+ implementSummary: capSummary(result.outputs.implement ?? ""),
12566
+ changedFiles,
12567
+ workspaceWarnings,
12568
+ checksRun: review.checksRun,
12569
+ verdict: review.verdict,
12570
+ findingCount: review.findingCount,
12571
+ decision: review.verdict,
12572
+ checkDurationMs: reviewDurationMs(result.trace),
12573
+ ...usage && { usage },
12574
+ ...result.auditRunId && { auditRef: result.auditRunId }
12575
+ });
12576
+ const stopStatus = review.verdict === "proceed" ? "converged" : review.verdict === "block" ? "blocked" : undefined;
12577
+ return {
12578
+ ok: true,
12579
+ verdict: review.verdict,
12580
+ findingCount: review.findingCount,
12581
+ checksRun: review.checksRun,
12582
+ decision: review.verdict,
12583
+ changedFiles,
12584
+ workspaceWarnings,
12585
+ ...usage && { usage },
12586
+ ...result.auditRunId && { auditRunId: result.auditRunId },
12587
+ reviewText,
12588
+ ...stopStatus && { stopStatus }
12589
+ };
12590
+ }
12591
+ async function convergeLoop(opts) {
12592
+ const { loopId } = startLoop(opts.cwd, {
12593
+ scope: opts.scope,
12594
+ task: opts.task,
12595
+ maxIterations: opts.maxIterations,
12596
+ loopId: opts.loopId,
12597
+ force: opts.force
12598
+ });
12599
+ let priorReview = "";
12600
+ let iterations = 0;
12601
+ let status;
12602
+ for (let i = 1;i <= opts.maxIterations; i++) {
12603
+ let iter;
12604
+ try {
12605
+ iter = await runConvergeIteration({
12606
+ cwd: opts.cwd,
12607
+ loopId,
12608
+ iteration: i,
12609
+ task: opts.task,
12610
+ prior_review: priorReview,
12611
+ execute: opts.execute
12612
+ });
12613
+ } catch (e) {
12614
+ if (e instanceof ConvergeExecuteError) {
12615
+ stopLoop(opts.cwd, loopId, {
12616
+ status: "blocked",
12617
+ reason: `manifest run threw: ${e.message}`
12618
+ });
12619
+ throw e.executeError;
12620
+ }
12621
+ throw e;
12622
+ }
12623
+ if (!iter.ok) {
12624
+ status = "blocked";
12625
+ stopLoop(opts.cwd, loopId, { status, reason: iter.failure });
12626
+ return { loopId, iterations, status, failure: iter.failure };
12627
+ }
12628
+ iterations++;
12629
+ if (iter.stopStatus !== undefined) {
12630
+ status = iter.stopStatus;
12631
+ break;
12632
+ }
12633
+ priorReview = iter.reviewText;
12634
+ }
12635
+ if (status === undefined)
12636
+ status = "max-iterations";
12637
+ const reason = status === "converged" ? "reviewer returned proceed" : status === "blocked" ? "reviewer returned block" : `reached max iterations (${opts.maxIterations}) without converging`;
12638
+ stopLoop(opts.cwd, loopId, { status, reason });
12639
+ return { loopId, iterations, status };
12640
+ }
12641
+ function buildExecute(manifest, registry2, scope, cwd) {
12642
+ const baseAdapters = {};
12643
+ for (const p of Object.values(manifest.participants)) {
12644
+ if (!(p.agent in baseAdapters)) {
12645
+ const agent = registry2.agents[p.agent];
12646
+ if (!agent)
12647
+ continue;
12648
+ baseAdapters[p.agent] = buildAdapter(agent);
12649
+ }
12650
+ }
12651
+ return makeAuditedExecute(manifest, baseAdapters, registry2, scope, cwd, new FileSessionStore(defaultSessionDir()), new AuditStore);
12652
+ }
12653
+ function prepareConvergeExecute(raw, registry2, scope, cwd, allowUnenforced) {
12654
+ let manifest;
12655
+ try {
12656
+ manifest = parseManifest(raw);
12657
+ } catch (e) {
12658
+ return { ok: false, error: e.message };
12659
+ }
12660
+ const shapeError = validateConvergeManifest(manifest);
12661
+ if (shapeError)
12662
+ return { ok: false, error: shapeError };
12663
+ const unknown = findUnknownAgents(manifest, registry2);
12664
+ if (unknown.length > 0) {
12665
+ return {
12666
+ ok: false,
12667
+ error: `unknown agent(s): ${unknown.map((u) => `${u.agentId} (participant "${u.participantId}")`).join(", ")}`
12668
+ };
12669
+ }
12670
+ const gaps = findEnforcementGaps(manifest, registry2);
12671
+ if (gaps.length > 0 && !allowUnenforced) {
12672
+ return {
12673
+ ok: false,
12674
+ error: `cannot enforce required permissions:
12675
+ ${formatEnforcementGaps(gaps)}
12676
+ Pass allow_unenforced_permissions=true to run anyway.`
12677
+ };
12678
+ }
12679
+ const warnings = gaps.map((g) => `unenforced permission: participant "${g.participantId}" requires ${g.permission}`);
12680
+ return { ok: true, execute: buildExecute(manifest, registry2, scope, cwd), warnings };
12681
+ }
12682
+ function makeAuditedExecute(manifest, baseAdapters, registry2, scope, cwd, sessionStore, auditStore) {
12683
+ return async (inputs, ctx) => {
12684
+ const runId = crypto.randomUUID();
12685
+ const recorder = new AuditRecorder(auditStore, runId, {
12686
+ manifestId: manifest.id,
12687
+ cwd,
12688
+ surface: "converge",
12689
+ scope,
12690
+ ...ctx?.loopId !== undefined && { loopId: ctx.loopId },
12691
+ ...ctx?.iteration !== undefined && { iteration: ctx.iteration },
12692
+ participants: resolveParticipantSnapshots(manifest, registry2)
12693
+ });
12694
+ recorder.runStarted();
12695
+ const adapters = wrapAdaptersWithSessions(wrapAdaptersWithAudit(baseAdapters, recorder), manifest, registry2, scope, sessionStore);
12696
+ const startedAt = Date.now();
12697
+ try {
12698
+ const result = await executeManifest(manifest, {
12699
+ inputs,
12700
+ adapters,
12701
+ invocationCwd: cwd,
12702
+ onTrace: (e) => {
12703
+ recorder.fromTrace(e);
12704
+ ctx?.onTrace?.(e);
12705
+ },
12706
+ ...ctx?.signal && { signal: ctx.signal }
12707
+ });
12708
+ recorder.runCompleted(result.ok ? "ok" : "failed", Date.now() - startedAt);
12709
+ recorder.prune();
12710
+ return recorder.lastError === undefined ? { ...result, auditRunId: runId } : result;
12711
+ } catch (e) {
12712
+ recorder.runCompleted("failed", Date.now() - startedAt);
12713
+ recorder.prune();
12714
+ throw e;
12715
+ }
12716
+ };
12717
+ }
12718
+ function validateConvergeManifest(manifest) {
12719
+ for (const id of [IMPLEMENT_STEP_ID, REVIEW_STEP_ID]) {
12720
+ const step = manifest.steps[id];
12721
+ if (!step) {
12722
+ return `manifest "${manifest.id}" is not converge-shaped: missing call step "${id}" (converge needs call steps named "implement" and "review")`;
12723
+ }
12724
+ if (step.kind !== "call") {
12725
+ return `manifest "${manifest.id}" is not converge-shaped: step "${id}" must be a call step, not ${step.kind}`;
12726
+ }
12727
+ }
12728
+ return null;
12729
+ }
12730
+ var CONVERGE_HELP = `chit converge --task <text> --scope <id> [options]
12731
+
12732
+ --task <text> Required. The slice to converge on.
12733
+ --scope <id> Required. Session scope; both agents keep their thread.
12734
+ --cwd <dir> Repo to run in. Default: current directory.
12735
+ --manifest <path> Convergence manifest. Default: the built-in converge manifest.
12736
+ --max-iterations <n> Iteration budget. Default: 3.
12737
+ --loop-id <id> Reuse/seed a loop id. Default: generated.
12738
+ --allow-unenforced-permissions
12739
+ Run even when the manifest declares permissions its
12740
+ adapter cannot enforce (emits a warning each run).
12741
+ Default off: such a manifest is refused before running.
12742
+
12743
+ Runs the implement/check loop to convergence and records it under chit's
12744
+ state dir (keyed by repo, not in the worktree). Stops at the reviewer's verdict: proceed ->
12745
+ converged, block -> blocked, else revise and retry up to the budget. An
12746
+ unparseable verdict is treated as block (never an implicit proceed).
12747
+ `;
12748
+
12749
+ class UsageError extends Error {
12750
+ }
12751
+ function parseConvergeArgs(argv) {
12752
+ let task;
12753
+ let scope;
12754
+ let cwd;
12755
+ let manifestPath;
12756
+ let maxIterations = 3;
12757
+ let loopId;
12758
+ let allowUnenforcedPermissions = false;
12759
+ for (let i = 0;i < argv.length; i++) {
12760
+ const a = argv[i];
12761
+ const need = (key) => {
12762
+ const v = argv[++i];
12763
+ if (v === undefined)
12764
+ throw new UsageError(`${key} requires a value`);
12765
+ return v;
12766
+ };
12767
+ if (a === "--task")
12768
+ task = need("--task");
12769
+ else if (a === "--scope")
12770
+ scope = need("--scope");
12771
+ else if (a === "--cwd")
12772
+ cwd = need("--cwd");
12773
+ else if (a === "--manifest")
12774
+ manifestPath = need("--manifest");
12775
+ else if (a === "--loop-id")
12776
+ loopId = need("--loop-id");
12777
+ else if (a === "--allow-unenforced-permissions")
12778
+ allowUnenforcedPermissions = true;
12779
+ else if (a === "--max-iterations") {
12780
+ const raw = need("--max-iterations");
12781
+ const n = Number(raw);
12782
+ if (!Number.isInteger(n) || n < 1) {
12783
+ throw new UsageError(`--max-iterations must be a positive integer (got ${JSON.stringify(raw)})`);
12784
+ }
12785
+ maxIterations = n;
12786
+ } else {
12787
+ throw new UsageError(`unknown flag ${JSON.stringify(a)}`);
12788
+ }
12789
+ }
12790
+ if (task === undefined)
12791
+ throw new UsageError("--task is required");
12792
+ if (scope === undefined)
12793
+ throw new UsageError("--scope is required");
12794
+ return {
12795
+ task,
12796
+ scope,
12797
+ cwd: resolve2(cwd ?? process.cwd()),
12798
+ manifestPath,
12799
+ maxIterations,
12800
+ loopId,
12801
+ allowUnenforcedPermissions
12802
+ };
12803
+ }
12804
+ async function runConverge(argv, io = defaultIO) {
12805
+ if (argv[0] === "-h" || argv[0] === "--help") {
12806
+ io.out(CONVERGE_HELP);
12807
+ return 0;
12808
+ }
12809
+ let parsed;
12810
+ try {
12811
+ parsed = parseConvergeArgs(argv);
12812
+ } catch (e) {
12813
+ if (e instanceof UsageError) {
12814
+ io.err(`chit converge: ${e.message}
12815
+
12816
+ ${CONVERGE_HELP}`);
12817
+ return 2;
12818
+ }
12819
+ throw e;
12820
+ }
12821
+ let manifest;
12822
+ try {
12823
+ const raw = parsed.manifestPath !== undefined ? JSON.parse(readFileSync7(parsed.manifestPath, "utf-8")) : DEFAULT_CONVERGE_MANIFEST;
12824
+ manifest = parseManifest(raw);
12825
+ } catch (e) {
12826
+ io.err(`chit converge: failed to load manifest ${parsed.manifestPath ?? "(built-in default)"}: ${e.message}
12827
+ `);
12828
+ return 2;
12829
+ }
12830
+ const shapeError = validateConvergeManifest(manifest);
12831
+ if (shapeError) {
12832
+ io.err(`chit converge: ${shapeError}
12833
+ `);
12834
+ return 1;
12835
+ }
12836
+ let registry2;
12837
+ try {
12838
+ registry2 = loadRegistry();
12839
+ } catch (e) {
12840
+ io.err(`chit converge: ${e.message}
12841
+ `);
12842
+ return 1;
12843
+ }
12844
+ const unknown = findUnknownAgents(manifest, registry2);
12845
+ if (unknown.length > 0) {
12846
+ for (const u of unknown) {
12847
+ io.err(`chit converge: unknown agent "${u.agentId}" in registry (participant "${u.participantId}")
12848
+ `);
12849
+ }
12850
+ return 2;
12851
+ }
12852
+ const gaps = findEnforcementGaps(manifest, registry2);
12853
+ if (gaps.length > 0 && !parsed.allowUnenforcedPermissions) {
12854
+ io.err(`chit converge: cannot enforce required permissions for "${manifest.id}":
12855
+ `);
12856
+ io.err(`${formatEnforcementGaps(gaps)}
12857
+ `);
12858
+ io.err(`
12859
+ Pass --allow-unenforced-permissions to run anyway (emits a warning each run).
12860
+ `);
12861
+ return 1;
12862
+ }
12863
+ for (const g of gaps) {
12864
+ io.err(`chit converge: WARNING -- unenforced permission: participant "${g.participantId}" requires ${g.permission}
12865
+ `);
12866
+ }
12867
+ let result;
12868
+ try {
12869
+ result = await convergeLoop({
12870
+ cwd: parsed.cwd,
12871
+ scope: parsed.scope,
12872
+ task: parsed.task,
12873
+ maxIterations: parsed.maxIterations,
12874
+ loopId: parsed.loopId,
12875
+ execute: buildExecute(manifest, registry2, parsed.scope, parsed.cwd)
12876
+ });
12877
+ } catch (e) {
12878
+ io.err(`chit converge: ${e.message}
12879
+ `);
12880
+ return 1;
12881
+ }
12882
+ if (result.failure !== undefined) {
12883
+ io.err(`chit converge: ${result.failure}
12884
+ `);
12885
+ return 1;
12886
+ }
12887
+ io.out(`chit converge: ${result.loopId}
12888
+ `);
12889
+ io.out(` iterations: ${result.iterations}
12890
+ `);
12891
+ io.out(` status: ${result.status}
12892
+ `);
12893
+ return 0;
12894
+ }
12895
+
12896
+ // src/jobs/worker.ts
12897
+ function iso2(ms) {
12898
+ return new Date(ms).toISOString();
12899
+ }
12900
+ function defaultResolveExecute(job) {
12901
+ let raw;
12902
+ if (job.manifestPath) {
12903
+ const path = isAbsolute2(job.manifestPath) ? job.manifestPath : resolve3(job.cwd, job.manifestPath);
12904
+ try {
12905
+ raw = JSON.parse(readFileSync8(path, "utf-8"));
12906
+ } catch (e) {
12907
+ return { ok: false, error: `could not read manifest at ${path}: ${e.message}` };
12908
+ }
12909
+ } else {
12910
+ raw = DEFAULT_CONVERGE_MANIFEST;
12911
+ }
12912
+ const prep = prepareConvergeExecute(raw, loadRegistry(), job.scope, job.cwd, job.allowUnenforced);
12913
+ return prep.ok ? { ok: true, execute: prep.execute } : { ok: false, error: prep.error };
12914
+ }
12915
+ async function runJobWorker(jobId, deps) {
12916
+ const store = deps.jobStore;
12917
+ const now = deps.now ?? Date.now;
12918
+ const heartbeatMs = deps.heartbeatMs ?? 1e4;
12919
+ const resolveExecute = deps.resolveExecute ?? defaultResolveExecute;
12920
+ const job = store.get(jobId);
12921
+ if (job?.state !== "queued")
12922
+ return;
12923
+ const workerToken = crypto.randomUUID();
12924
+ const setPhase = (phase) => {
12925
+ try {
12926
+ store.update(jobId, (c) => ({ ...c, phase, lastHeartbeatAt: iso2(now()) }));
12927
+ } catch {}
12928
+ };
12929
+ const controller = new AbortController;
12930
+ const onSignal = () => {
12931
+ setPhase("cancelling");
12932
+ controller.abort();
12933
+ };
12934
+ if (deps.installSignalHandlers !== false) {
12935
+ process.once("SIGTERM", onSignal);
12936
+ process.once("SIGINT", onSignal);
12937
+ }
12938
+ let loopLock;
12939
+ let heartbeat;
12940
+ try {
12941
+ store.update(jobId, (c) => ({
12942
+ ...c,
12943
+ state: "running",
12944
+ startedAt: iso2(now()),
12945
+ pid: process.pid,
12946
+ pgid: process.pid,
12947
+ workerToken,
12948
+ lastHeartbeatAt: iso2(now()),
12949
+ phase: "starting"
12950
+ }));
12951
+ const resolved = resolveExecute(job);
12952
+ if (!resolved.ok) {
12953
+ stopLoopSafely(job, "blocked", `could not prepare converge execute: ${resolved.error}`);
12954
+ finish(store, jobId, now, "failed", { failure: resolved.error });
12955
+ return;
12956
+ }
12957
+ try {
12958
+ loopLock = acquireLock(store.loopLockPath(job.loopId), deps.loopLockOpts);
12959
+ } catch (e) {
12960
+ if (e instanceof LockError) {
12961
+ finish(store, jobId, now, "failed", {
12962
+ failure: `loop "${job.loopId}" is locked by another advancer; not started`
12963
+ });
12964
+ return;
12965
+ }
12966
+ throw e;
12967
+ }
12968
+ heartbeat = setInterval(() => {
12969
+ try {
12970
+ store.update(jobId, (c) => ({ ...c, lastHeartbeatAt: iso2(now()) }));
12971
+ } catch {}
12972
+ }, heartbeatMs);
12973
+ let priorReview = "";
12974
+ for (let i = 1;i <= job.maxIterations; i++) {
12975
+ if (store.get(jobId)?.cancelRequestedAt || controller.signal.aborted) {
12976
+ stopLoopSafely(job, "cancelled", "cancelled via chit_job_cancel");
12977
+ finish(store, jobId, now, "cancelled", { stopStatus: "cancelled" });
12978
+ return;
12979
+ }
12980
+ store.update(jobId, (c) => ({
12981
+ ...c,
12982
+ iteration: i,
12983
+ phase: "implementing",
12984
+ lastHeartbeatAt: iso2(now())
12985
+ }));
12986
+ let iter;
12987
+ try {
12988
+ iter = await runConvergeIteration({
12989
+ cwd: job.cwd,
12990
+ loopId: job.loopId,
12991
+ iteration: i,
12992
+ task: job.task,
12993
+ prior_review: priorReview,
12994
+ execute: resolved.execute,
12995
+ signal: controller.signal,
12996
+ onTrace: (e) => {
12997
+ if (e.type === "step.started" && e.stepId === "implement")
12998
+ setPhase("implementing");
12999
+ else if (e.type === "step.started" && e.stepId === "review")
13000
+ setPhase("reviewing");
13001
+ }
13002
+ });
13003
+ } catch (e) {
13004
+ if (e instanceof ConvergeExecuteError) {
13005
+ stopLoopSafely(job, "blocked", `manifest run threw: ${e.message}`);
13006
+ finish(store, jobId, now, "failed", { failure: e.message });
13007
+ } else {
13008
+ finish(store, jobId, now, "failed", { failure: e.message });
13009
+ }
13010
+ return;
13011
+ }
13012
+ if (!iter.ok) {
13013
+ if (controller.signal.aborted) {
13014
+ stopLoopSafely(job, "cancelled", "cancelled mid-iteration (signal)");
13015
+ finish(store, jobId, now, "cancelled", { stopStatus: "cancelled" });
13016
+ } else {
13017
+ stopLoopSafely(job, "blocked", iter.failure);
13018
+ finish(store, jobId, now, "failed", { failure: iter.failure });
13019
+ }
13020
+ return;
13021
+ }
13022
+ store.update(jobId, (c) => ({
13023
+ ...c,
13024
+ phase: "recording",
13025
+ iterationsCompleted: i,
13026
+ lastVerdict: iter.verdict,
13027
+ auditRefs: iter.auditRunId ? [...c.auditRefs, iter.auditRunId] : c.auditRefs,
13028
+ lastHeartbeatAt: iso2(now())
13029
+ }));
13030
+ if (iter.stopStatus) {
13031
+ const reason = iter.stopStatus === "converged" ? "reviewer returned proceed" : "reviewer returned block";
13032
+ stopLoopSafely(job, iter.stopStatus, reason);
13033
+ finish(store, jobId, now, "completed", { stopStatus: iter.stopStatus });
13034
+ return;
13035
+ }
13036
+ priorReview = iter.reviewText;
13037
+ if (i >= job.maxIterations) {
13038
+ stopLoopSafely(job, "max-iterations", `reached max iterations (${job.maxIterations}) without converging`);
13039
+ finish(store, jobId, now, "completed", { stopStatus: "max-iterations" });
13040
+ return;
13041
+ }
13042
+ }
13043
+ } finally {
13044
+ if (heartbeat)
13045
+ clearInterval(heartbeat);
13046
+ if (deps.installSignalHandlers !== false) {
13047
+ process.removeListener("SIGTERM", onSignal);
13048
+ process.removeListener("SIGINT", onSignal);
13049
+ }
13050
+ if (loopLock)
13051
+ releaseLock(loopLock);
13052
+ }
13053
+ }
13054
+ function stopLoopSafely(job, status, reason) {
13055
+ try {
13056
+ stopLoop(job.cwd, job.loopId, { status, reason });
13057
+ } catch {}
13058
+ }
13059
+ function finish(store, jobId, now, state, extra) {
13060
+ store.update(jobId, (c) => ({
13061
+ ...c,
13062
+ state,
13063
+ endedAt: iso2(now()),
13064
+ phase: undefined,
13065
+ lastHeartbeatAt: iso2(now()),
13066
+ ...extra.stopStatus !== undefined && { stopStatus: extra.stopStatus },
13067
+ ...extra.failure !== undefined && { failure: extra.failure }
13068
+ }));
13069
+ }
13070
+
13071
+ // src/surfaces/claude-skill.ts
12264
13072
  init_src();
12265
- import { readFileSync as readFileSync8 } from "fs";
12266
- import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
13073
+ import { createHash as createHash5, randomBytes } from "crypto";
13074
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
13075
+ import { join as join7, resolve as resolvePath } from "path";
13076
+ class SurfaceInstallError extends Error {
13077
+ constructor(message) {
13078
+ super(message);
13079
+ this.name = "SurfaceInstallError";
13080
+ }
13081
+ }
13082
+ var CLAUDE_SKILL_CAPABILITIES = new Set([
13083
+ "can_show_markdown",
13084
+ "can_provide_stable_scope"
13085
+ ]);
13086
+ function installClaudeSkill(opts) {
13087
+ if (opts.overrideName !== undefined && !VALID_INSTALL_NAME_RE.test(opts.overrideName)) {
13088
+ 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.`);
13089
+ }
13090
+ const outputDir = resolvePath(opts.outputDir);
13091
+ const runtimePath = resolvePath(opts.runtimePath);
13092
+ const allowUnenforced = opts.allowUnenforcedPermissions === true;
13093
+ let rawJson;
13094
+ try {
13095
+ rawJson = JSON.parse(readFileSync9(opts.manifestPath, "utf-8"));
13096
+ } catch (e) {
13097
+ throw new SurfaceInstallError(`failed to read manifest ${opts.manifestPath}: ${e.message}`);
13098
+ }
13099
+ let manifest;
13100
+ try {
13101
+ manifest = parseManifest(rawJson);
13102
+ } catch (e) {
13103
+ throw new SurfaceInstallError(`invalid manifest at ${opts.manifestPath}: ${e.message}`);
13104
+ }
13105
+ const missingCaps = findMissingCapabilities(manifest, CLAUDE_SKILL_CAPABILITIES);
13106
+ if (missingCaps.length > 0) {
13107
+ throw new SurfaceInstallError(`claude-skill surface does not provide capabilities required by "${manifest.id}": ${missingCaps.join(", ")}`);
13108
+ }
13109
+ const inputNames = Object.keys(manifest.inputs);
13110
+ const primaryInput = inputNames[0];
13111
+ if (!primaryInput) {
13112
+ throw new SurfaceInstallError(`manifest "${manifest.id}" has no inputs; nothing to wire from $ARGUMENTS`);
13113
+ }
13114
+ const primaryInputSchema = manifest.inputs[primaryInput];
13115
+ if (primaryInputSchema?.type !== "string") {
13116
+ throw new SurfaceInstallError(`manifest "${manifest.id}": claude-skill surface only supports a string-typed primary input ` + `(got "${primaryInput}": ${primaryInputSchema?.type ?? "missing"})`);
13117
+ }
13118
+ if (inputNames.length > 1) {
13119
+ throw new SurfaceInstallError(`manifest "${manifest.id}" declares multiple inputs (${inputNames.join(", ")}); ` + `claude-skill surface in PR6 supports exactly one string input`);
13120
+ }
13121
+ const registry2 = opts.registry ?? loadRegistry();
13122
+ const unknownAgents = findUnknownAgents(manifest, registry2);
13123
+ if (unknownAgents.length > 0) {
13124
+ const lines = unknownAgents.map((u) => ` - participant "${u.participantId}" references unknown agent "${u.agentId}"`).join(`
13125
+ `);
13126
+ throw new SurfaceInstallError(`manifest "${manifest.id}" references agents that are not in the registry:
13127
+ ${lines}`);
13128
+ }
13129
+ const gaps = findEnforcementGaps(manifest, registry2);
13130
+ if (gaps.length > 0 && !allowUnenforced) {
13131
+ throw new SurfaceInstallError(`cannot enforce required permissions for "${manifest.id}":
13132
+ ${formatEnforcementGaps(gaps)}
13133
+
13134
+ Pass allowUnenforcedPermissions=true to install anyway; the generated skill will warn on every run.`);
13135
+ }
13136
+ const installName = opts.overrideName ?? manifest.id;
13137
+ const skillDir = join7(outputDir, installName);
13138
+ if (existsSync7(skillDir)) {
13139
+ if (!opts.force) {
13140
+ throw new SurfaceInstallError(`skill directory already exists: ${skillDir}
13141
+
13142
+ 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).`);
13143
+ }
13144
+ rmSync5(skillDir, { recursive: true, force: true });
13145
+ }
13146
+ mkdirSync5(skillDir, { recursive: true });
13147
+ const skillMdPath = join7(skillDir, "SKILL.md");
13148
+ const manifestPath = join7(skillDir, "manifest.json");
13149
+ const markerPath = join7(skillDir, INSTALL_MARKER_FILENAME);
13150
+ const manifestJson = `${JSON.stringify(rawJson, null, 2)}
13151
+ `;
13152
+ writeFileSync5(manifestPath, manifestJson);
13153
+ writeFileSync5(skillMdPath, buildSkillMd({
13154
+ manifest,
13155
+ runtimePath,
13156
+ primaryInputName: primaryInput,
13157
+ allowUnenforced: gaps.length > 0,
13158
+ trace: opts.trace === true,
13159
+ heredocDelimiter: generateHeredocDelimiter(),
13160
+ installName
13161
+ }));
13162
+ const marker = {
13163
+ schema: 1,
13164
+ surface: "claude-skill",
13165
+ installName,
13166
+ manifestId: manifest.id,
13167
+ runtimePath,
13168
+ installedAt: new Date().toISOString(),
13169
+ manifestHash: createHash5("sha256").update(manifestJson).digest("hex")
13170
+ };
13171
+ writeFileSync5(markerPath, `${JSON.stringify(marker, null, 2)}
13172
+ `);
13173
+ return { skillDir, skillMdPath, manifestPath, markerPath, enforcementGaps: gaps };
13174
+ }
13175
+ function generateHeredocDelimiter() {
13176
+ return `CHIT_INPUT_${randomBytes(8).toString("hex").toUpperCase()}_EOF`;
13177
+ }
13178
+ function buildSkillMd(opts) {
13179
+ const {
13180
+ manifest,
13181
+ runtimePath,
13182
+ primaryInputName,
13183
+ allowUnenforced,
13184
+ trace,
13185
+ heredocDelimiter,
13186
+ installName
13187
+ } = opts;
13188
+ const allowFlag = allowUnenforced ? `\\
13189
+ --allow-unenforced-permissions ` : "";
13190
+ const traceFlag = trace ? `\\
13191
+ --trace ` : "";
13192
+ return `---
13193
+ name: ${installName}
13194
+ description: ${escapeFrontmatter(manifest.description)}
13195
+ argument-hint: <${primaryInputName}>
13196
+ disable-model-invocation: true
13197
+ ---
13198
+
13199
+ # /${installName}
13200
+
13201
+ \`\`\`!
13202
+ {
13203
+ # Claude Code's \`! \` preprocessor substitutes \${CLAUDE_SESSION_ID} (bare
13204
+ # brace form) BEFORE bash runs. The :- default form is NOT substituted, so
13205
+ # we use the bare form: when running inside Claude Code, this line gets
13206
+ # the real session id baked in (always non-empty); when running outside,
13207
+ # bash evaluates the env var (typically empty, so we fail fast).
13208
+ if [ -z "\${CLAUDE_SESSION_ID}" ]; then
13209
+ echo "chit: CLAUDE_SESSION_ID is required; this skill must run inside Claude Code" >&2
13210
+ exit 2
13211
+ fi
13212
+ WORKTREE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
13213
+ SCOPE_HASH=$(printf '%s' "$WORKTREE" | (shasum -a 256 2>/dev/null || sha256sum) | cut -c1-12)
13214
+ SCOPE="\${CLAUDE_SESSION_ID}-\${SCOPE_HASH}"
13215
+
13216
+ bun "${runtimePath}/src/cli/run.ts" run \\
13217
+ "\${CLAUDE_SKILL_DIR}/manifest.json" \\
13218
+ --scope "$SCOPE" \\
13219
+ --invocation-cwd "$WORKTREE" ${allowFlag}${traceFlag}\\
13220
+ --input-stdin ${primaryInputName} <<'${heredocDelimiter}'
13221
+ $ARGUMENTS
13222
+ ${heredocDelimiter}
13223
+ } 2>&1
13224
+ \`\`\`
13225
+
13226
+ 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.
13227
+ `;
13228
+ }
13229
+ function escapeFrontmatter(s) {
13230
+ return s.replace(/\r?\n/g, " ").replace(/"/g, "'");
13231
+ }
13232
+
13233
+ // src/surfaces/lifecycle.ts
13234
+ init_src();
13235
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync10, rmSync as rmSync6, statSync as statSync2 } from "fs";
13236
+ import { homedir as homedir6 } from "os";
13237
+ import { join as join8 } from "path";
13238
+ function defaultSkillsDir() {
13239
+ return join8(homedir6(), ".claude", "skills");
13240
+ }
13241
+
13242
+ class LifecycleError extends Error {
13243
+ constructor(message) {
13244
+ super(message);
13245
+ this.name = "LifecycleError";
13246
+ }
13247
+ }
13248
+ function listInstalled(parentDir) {
13249
+ if (!existsSync8(parentDir))
13250
+ return [];
13251
+ const out = [];
13252
+ const entries = readdirSync3(parentDir);
13253
+ for (const name of entries) {
13254
+ const skillDir = join8(parentDir, name);
13255
+ let stat;
13256
+ try {
13257
+ stat = statSync2(skillDir);
13258
+ } catch {
13259
+ continue;
13260
+ }
13261
+ if (!stat.isDirectory())
13262
+ continue;
13263
+ const markerPath = join8(skillDir, INSTALL_MARKER_FILENAME);
13264
+ if (!existsSync8(markerPath))
13265
+ continue;
13266
+ let raw;
13267
+ try {
13268
+ raw = JSON.parse(readFileSync10(markerPath, "utf-8"));
13269
+ } catch {
13270
+ continue;
13271
+ }
13272
+ try {
13273
+ const marker = parseInstallMarker(raw, markerPath);
13274
+ out.push({ skillDir, markerPath, marker });
13275
+ } catch {}
13276
+ }
13277
+ return out.sort((a, b) => a.marker.installName.localeCompare(b.marker.installName));
13278
+ }
13279
+ function uninstall(parentDir, name) {
13280
+ if (!VALID_INSTALL_NAME_RE.test(name)) {
13281
+ 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.`);
13282
+ }
13283
+ const skillDir = join8(parentDir, name);
13284
+ if (!existsSync8(skillDir)) {
13285
+ throw new LifecycleError(`no install at ${skillDir}`);
13286
+ }
13287
+ if (!statSync2(skillDir).isDirectory()) {
13288
+ throw new LifecycleError(`${skillDir} is not a directory`);
13289
+ }
13290
+ const markerPath = join8(skillDir, INSTALL_MARKER_FILENAME);
13291
+ if (!existsSync8(markerPath)) {
13292
+ 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.`);
13293
+ }
13294
+ let raw;
13295
+ try {
13296
+ raw = JSON.parse(readFileSync10(markerPath, "utf-8"));
13297
+ } catch (e) {
13298
+ throw new LifecycleError(`refusing to uninstall ${skillDir}: ${INSTALL_MARKER_FILENAME} is not valid JSON (${e.message})`);
13299
+ }
13300
+ let marker;
13301
+ try {
13302
+ marker = parseInstallMarker(raw, markerPath);
13303
+ } catch (e) {
13304
+ if (e instanceof MarkerError) {
13305
+ throw new LifecycleError(`refusing to uninstall ${skillDir}: ${e.message}`);
13306
+ }
13307
+ throw e;
13308
+ }
13309
+ const record = { skillDir, markerPath, marker };
13310
+ rmSync6(skillDir, { recursive: true, force: true });
13311
+ return record;
13312
+ }
13313
+
13314
+ // src/surfaces/mcp/server.ts
13315
+ import { spawn } from "child_process";
13316
+ import { readFileSync as readFileSync11 } from "fs";
13317
+ import { isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
12267
13318
 
12268
13319
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v3/helpers/util.js
12269
13320
  var util;
@@ -33131,7 +34182,7 @@ class Protocol {
33131
34182
  return;
33132
34183
  }
33133
34184
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
33134
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
34185
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
33135
34186
  options?.signal?.throwIfAborted();
33136
34187
  }
33137
34188
  } catch (error51) {
@@ -33143,7 +34194,7 @@ class Protocol {
33143
34194
  }
33144
34195
  request(request, resultSchema, options) {
33145
34196
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
33146
- return new Promise((resolve2, reject) => {
34197
+ return new Promise((resolve4, reject) => {
33147
34198
  const earlyReject = (error51) => {
33148
34199
  reject(error51);
33149
34200
  };
@@ -33221,7 +34272,7 @@ class Protocol {
33221
34272
  if (!parseResult.success) {
33222
34273
  reject(parseResult.error);
33223
34274
  } else {
33224
- resolve2(parseResult.data);
34275
+ resolve4(parseResult.data);
33225
34276
  }
33226
34277
  } catch (error51) {
33227
34278
  reject(error51);
@@ -33412,12 +34463,12 @@ class Protocol {
33412
34463
  interval = task.pollInterval;
33413
34464
  }
33414
34465
  } catch {}
33415
- return new Promise((resolve2, reject) => {
34466
+ return new Promise((resolve4, reject) => {
33416
34467
  if (signal.aborted) {
33417
34468
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
33418
34469
  return;
33419
34470
  }
33420
- const timeoutId = setTimeout(resolve2, interval);
34471
+ const timeoutId = setTimeout(resolve4, interval);
33421
34472
  signal.addEventListener("abort", () => {
33422
34473
  clearTimeout(timeoutId);
33423
34474
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -34270,7 +35321,7 @@ class McpServer {
34270
35321
  let task = createTaskResult.task;
34271
35322
  const pollInterval = task.pollInterval ?? 5000;
34272
35323
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
34273
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
35324
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
34274
35325
  const updatedTask = await extra.taskStore.getTask(taskId);
34275
35326
  if (!updatedTask) {
34276
35327
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -34647,1022 +35698,433 @@ class McpServer {
34647
35698
  this.sendToolListChanged();
34648
35699
  return registeredTool;
34649
35700
  }
34650
- tool(name, ...rest) {
34651
- if (this._registeredTools[name]) {
34652
- throw new Error(`Tool ${name} is already registered`);
34653
- }
34654
- let description;
34655
- let inputSchema;
34656
- let outputSchema;
34657
- let annotations;
34658
- if (typeof rest[0] === "string") {
34659
- description = rest.shift();
34660
- }
34661
- if (rest.length > 1) {
34662
- const firstArg = rest[0];
34663
- if (isZodRawShapeCompat(firstArg)) {
34664
- inputSchema = rest.shift();
34665
- if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !isZodRawShapeCompat(rest[0])) {
34666
- annotations = rest.shift();
34667
- }
34668
- } else if (typeof firstArg === "object" && firstArg !== null) {
34669
- if (Object.values(firstArg).some((v) => typeof v === "object" && v !== null)) {
34670
- throw new Error(`Tool ${name} expected a Zod schema or ToolAnnotations, but received an unrecognized object`);
34671
- }
34672
- annotations = rest.shift();
34673
- }
34674
- }
34675
- const callback = rest[0];
34676
- return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, { taskSupport: "forbidden" }, undefined, callback);
34677
- }
34678
- registerTool(name, config2, cb) {
34679
- if (this._registeredTools[name]) {
34680
- throw new Error(`Tool ${name} is already registered`);
34681
- }
34682
- const { title, description, inputSchema, outputSchema, annotations, _meta } = config2;
34683
- return this._createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, { taskSupport: "forbidden" }, _meta, cb);
34684
- }
34685
- prompt(name, ...rest) {
34686
- if (this._registeredPrompts[name]) {
34687
- throw new Error(`Prompt ${name} is already registered`);
34688
- }
34689
- let description;
34690
- if (typeof rest[0] === "string") {
34691
- description = rest.shift();
34692
- }
34693
- let argsSchema;
34694
- if (rest.length > 1) {
34695
- argsSchema = rest.shift();
34696
- }
34697
- const cb = rest[0];
34698
- const registeredPrompt = this._createRegisteredPrompt(name, undefined, description, argsSchema, cb);
34699
- this.setPromptRequestHandlers();
34700
- this.sendPromptListChanged();
34701
- return registeredPrompt;
34702
- }
34703
- registerPrompt(name, config2, cb) {
34704
- if (this._registeredPrompts[name]) {
34705
- throw new Error(`Prompt ${name} is already registered`);
34706
- }
34707
- const { title, description, argsSchema } = config2;
34708
- const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema, cb);
34709
- this.setPromptRequestHandlers();
34710
- this.sendPromptListChanged();
34711
- return registeredPrompt;
34712
- }
34713
- isConnected() {
34714
- return this.server.transport !== undefined;
34715
- }
34716
- async sendLoggingMessage(params, sessionId) {
34717
- return this.server.sendLoggingMessage(params, sessionId);
34718
- }
34719
- sendResourceListChanged() {
34720
- if (this.isConnected()) {
34721
- this.server.sendResourceListChanged();
34722
- }
34723
- }
34724
- sendToolListChanged() {
34725
- if (this.isConnected()) {
34726
- this.server.sendToolListChanged();
34727
- }
34728
- }
34729
- sendPromptListChanged() {
34730
- if (this.isConnected()) {
34731
- this.server.sendPromptListChanged();
34732
- }
34733
- }
34734
- }
34735
- var EMPTY_OBJECT_JSON_SCHEMA = {
34736
- type: "object",
34737
- properties: {}
34738
- };
34739
- function isZodTypeLike(value) {
34740
- return value !== null && typeof value === "object" && "parse" in value && typeof value.parse === "function" && "safeParse" in value && typeof value.safeParse === "function";
34741
- }
34742
- function isZodSchemaInstance(obj3) {
34743
- return "_def" in obj3 || "_zod" in obj3 || isZodTypeLike(obj3);
34744
- }
34745
- function isZodRawShapeCompat(obj3) {
34746
- if (typeof obj3 !== "object" || obj3 === null) {
34747
- return false;
34748
- }
34749
- if (isZodSchemaInstance(obj3)) {
34750
- return false;
34751
- }
34752
- if (Object.keys(obj3).length === 0) {
34753
- return true;
34754
- }
34755
- return Object.values(obj3).some(isZodTypeLike);
34756
- }
34757
- function getZodSchemaObject(schema) {
34758
- if (!schema) {
34759
- return;
34760
- }
34761
- if (isZodRawShapeCompat(schema)) {
34762
- return objectFromShape(schema);
34763
- }
34764
- if (!isZodSchemaInstance(schema)) {
34765
- throw new Error("inputSchema must be a Zod schema or raw shape, received an unrecognized object");
34766
- }
34767
- return schema;
34768
- }
34769
- function promptArgumentsFromSchema(schema) {
34770
- const shape = getObjectShape(schema);
34771
- if (!shape)
34772
- return [];
34773
- return Object.entries(shape).map(([name, field]) => {
34774
- const description = getSchemaDescription(field);
34775
- const isOptional = isSchemaOptional(field);
34776
- return {
34777
- name,
34778
- description,
34779
- required: !isOptional
34780
- };
34781
- });
34782
- }
34783
- function getMethodValue(schema) {
34784
- const shape = getObjectShape(schema);
34785
- const methodSchema = shape?.method;
34786
- if (!methodSchema) {
34787
- throw new Error("Schema is missing a method literal");
34788
- }
34789
- const value = getLiteralValue(methodSchema);
34790
- if (typeof value === "string") {
34791
- return value;
34792
- }
34793
- throw new Error("Schema method literal must be a string");
34794
- }
34795
- function createCompletionResult(suggestions) {
34796
- return {
34797
- completion: {
34798
- values: suggestions.slice(0, 100),
34799
- total: suggestions.length,
34800
- hasMore: suggestions.length > 100
34801
- }
34802
- };
34803
- }
34804
- var EMPTY_COMPLETION_RESULT = {
34805
- completion: {
34806
- values: [],
34807
- hasMore: false
34808
- }
34809
- };
34810
-
34811
- // ../../node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
34812
- import process3 from "process";
34813
-
34814
- // ../../node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
34815
- class ReadBuffer {
34816
- append(chunk) {
34817
- this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
34818
- }
34819
- readMessage() {
34820
- if (!this._buffer) {
34821
- return null;
34822
- }
34823
- const index = this._buffer.indexOf(`
34824
- `);
34825
- if (index === -1) {
34826
- return null;
34827
- }
34828
- const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
34829
- this._buffer = this._buffer.subarray(index + 1);
34830
- return deserializeMessage(line);
34831
- }
34832
- clear() {
34833
- this._buffer = undefined;
34834
- }
34835
- }
34836
- function deserializeMessage(line) {
34837
- return JSONRPCMessageSchema.parse(JSON.parse(line));
34838
- }
34839
- function serializeMessage(message) {
34840
- return JSON.stringify(message) + `
34841
- `;
34842
- }
34843
-
34844
- // ../../node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
34845
- class StdioServerTransport {
34846
- constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
34847
- this._stdin = _stdin;
34848
- this._stdout = _stdout;
34849
- this._readBuffer = new ReadBuffer;
34850
- this._started = false;
34851
- this._ondata = (chunk) => {
34852
- this._readBuffer.append(chunk);
34853
- this.processReadBuffer();
34854
- };
34855
- this._onerror = (error51) => {
34856
- this.onerror?.(error51);
34857
- };
34858
- }
34859
- async start() {
34860
- if (this._started) {
34861
- throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
34862
- }
34863
- this._started = true;
34864
- this._stdin.on("data", this._ondata);
34865
- this._stdin.on("error", this._onerror);
34866
- }
34867
- processReadBuffer() {
34868
- while (true) {
34869
- try {
34870
- const message = this._readBuffer.readMessage();
34871
- if (message === null) {
34872
- break;
34873
- }
34874
- this.onmessage?.(message);
34875
- } catch (error51) {
34876
- this.onerror?.(error51);
34877
- }
34878
- }
34879
- }
34880
- async close() {
34881
- this._stdin.off("data", this._ondata);
34882
- this._stdin.off("error", this._onerror);
34883
- const remainingDataListeners = this._stdin.listenerCount("data");
34884
- if (remainingDataListeners === 0) {
34885
- this._stdin.pause();
34886
- }
34887
- this._readBuffer.clear();
34888
- this.onclose?.();
34889
- }
34890
- send(message) {
34891
- return new Promise((resolve2) => {
34892
- const json2 = serializeMessage(message);
34893
- if (this._stdout.write(json2)) {
34894
- resolve2();
34895
- } else {
34896
- this._stdout.once("drain", resolve2);
34897
- }
34898
- });
34899
- }
34900
- }
34901
-
34902
- // src/audit/reader.ts
34903
- var USAGE_KEYS = [
34904
- "inputTokens",
34905
- "outputTokens",
34906
- "totalTokens",
34907
- "cachedInputTokens",
34908
- "reasoningTokens",
34909
- "estimatedCostUsd"
34910
- ];
34911
- function sumUsage(events2) {
34912
- const usage = {};
34913
- let any3 = false;
34914
- for (const e of events2) {
34915
- if (e.type !== "adapter.call.completed" || !e.usage)
34916
- continue;
34917
- for (const k of USAGE_KEYS) {
34918
- const v = e.usage[k];
34919
- if (typeof v === "number") {
34920
- usage[k] = (usage[k] ?? 0) + v;
34921
- any3 = true;
34922
- }
34923
- }
34924
- }
34925
- return any3 ? usage : undefined;
34926
- }
34927
- function findOpenCall(events2) {
34928
- const open = new Map;
34929
- for (const e of events2) {
34930
- if (e.type === "adapter.call.started") {
34931
- open.set(e.stepId, {
34932
- stepId: e.stepId,
34933
- participantId: e.participantId,
34934
- agentId: e.agentId,
34935
- since: e.ts
34936
- });
34937
- } else if (e.type === "adapter.call.completed") {
34938
- open.delete(e.stepId);
34939
- }
34940
- }
34941
- let latest;
34942
- for (const c of open.values()) {
34943
- if (latest === undefined || c.since > latest.since)
34944
- latest = c;
34945
- }
34946
- return latest;
34947
- }
34948
- function describeIncomplete(s, events2) {
34949
- if (s.openCall) {
34950
- const c = s.openCall;
34951
- return `open call: ${c.stepId} ${c.participantId}/${c.agentId} since ${c.since}; no adapter.call.completed`;
34952
- }
34953
- const failed = events2.find((e) => e.type === "step.failed");
34954
- if (failed?.type === "step.failed") {
34955
- const err = failed.error.replace(/\s+/g, " ").trim();
34956
- const clipped = err.length > 200 ? `${err.slice(0, 200)}...` : err;
34957
- return `failed step: ${failed.stepId}: ${clipped}`;
34958
- }
34959
- return "abandoned before terminal run.completed";
34960
- }
34961
- function summarizeRun(runId, events2) {
34962
- const started = events2.find((e) => e.type === "run.started");
34963
- const completed = events2.find((e) => e.type === "run.completed");
34964
- const summary = {
34965
- runId,
34966
- manifestId: started?.type === "run.started" ? started.manifestId : "?",
34967
- surface: started?.type === "run.started" ? started.surface : "?",
34968
- status: completed?.type === "run.completed" ? completed.status : "incomplete",
34969
- stepCount: events2.filter((e) => e.type === "step.completed").length
34970
- };
34971
- if (started?.type === "run.started") {
34972
- summary.startedAt = started.ts;
34973
- if (started.scope !== undefined)
34974
- summary.scope = started.scope;
34975
- if (started.loopId !== undefined)
34976
- summary.loopId = started.loopId;
34977
- if (started.iteration !== undefined)
34978
- summary.iteration = started.iteration;
34979
- }
34980
- const usage = sumUsage(events2);
34981
- if (usage !== undefined)
34982
- summary.usage = usage;
34983
- const openCall = findOpenCall(events2);
34984
- if (openCall !== undefined)
34985
- summary.openCall = openCall;
34986
- return summary;
34987
- }
34988
- function safeReadEvents(store, runId) {
34989
- try {
34990
- return store.readEvents(runId);
34991
- } catch {
34992
- return [];
34993
- }
34994
- }
34995
- function listAudit(store, limit) {
34996
- const summaries = store.listRuns().map((id) => summarizeRun(id, safeReadEvents(store, id)));
34997
- summaries.sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
34998
- return limit !== undefined ? summaries.slice(0, limit) : summaries;
34999
- }
35000
- function readBody(store, runId, ref) {
35001
- try {
35002
- return store.readBlob(runId, ref);
35003
- } catch (err) {
35004
- return `<blob unavailable: ${err.message}>`;
35005
- }
35006
- }
35007
- function isReceiptEvent(e) {
35008
- return e.type !== "adapter.event";
35009
- }
35010
- function hiddenAdapterEventCount(events2) {
35011
- return events2.reduce((n, e) => e.type === "adapter.event" ? n + 1 : n, 0);
35012
- }
35013
- function auditTimeline(store, runId, events2, opts) {
35014
- const rows = opts.verbose ? events2 : events2.filter(isReceiptEvent);
35015
- return rows.map((e) => {
35016
- if (!opts.includeBodies)
35017
- return e;
35018
- if (e.type === "adapter.call.started") {
35019
- return { ...e, input: readBody(store, runId, e.inputBlob) };
35020
- }
35021
- if (e.type === "adapter.event" && e.rawBlob !== undefined) {
35022
- return { ...e, raw: readBody(store, runId, e.rawBlob) };
35701
+ tool(name, ...rest) {
35702
+ if (this._registeredTools[name]) {
35703
+ throw new Error(`Tool ${name} is already registered`);
35023
35704
  }
35024
- if (e.type === "adapter.call.completed") {
35025
- return { ...e, output: readBody(store, runId, e.outputBlob) };
35705
+ let description;
35706
+ let inputSchema;
35707
+ let outputSchema;
35708
+ let annotations;
35709
+ if (typeof rest[0] === "string") {
35710
+ description = rest.shift();
35026
35711
  }
35027
- if (e.type === "step.completed" && e.outputBlob !== undefined) {
35028
- return { ...e, output: readBody(store, runId, e.outputBlob) };
35712
+ if (rest.length > 1) {
35713
+ const firstArg = rest[0];
35714
+ if (isZodRawShapeCompat(firstArg)) {
35715
+ inputSchema = rest.shift();
35716
+ if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !isZodRawShapeCompat(rest[0])) {
35717
+ annotations = rest.shift();
35718
+ }
35719
+ } else if (typeof firstArg === "object" && firstArg !== null) {
35720
+ if (Object.values(firstArg).some((v) => typeof v === "object" && v !== null)) {
35721
+ throw new Error(`Tool ${name} expected a Zod schema or ToolAnnotations, but received an unrecognized object`);
35722
+ }
35723
+ annotations = rest.shift();
35724
+ }
35029
35725
  }
35030
- return e;
35031
- });
35032
- }
35033
- function showAudit(store, runId, opts) {
35034
- const events2 = store.readEvents(runId);
35035
- const summary = summarizeRun(runId, events2);
35036
- const out = {
35037
- summary,
35038
- timeline: auditTimeline(store, runId, events2, opts)
35039
- };
35040
- if (summary.status === "incomplete")
35041
- out.incompleteReason = describeIncomplete(summary, events2);
35042
- const started = events2.find((e) => e.type === "run.started");
35043
- if (started?.type === "run.started" && started.participants !== undefined) {
35044
- out.participants = started.participants;
35726
+ const callback = rest[0];
35727
+ return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, { taskSupport: "forbidden" }, undefined, callback);
35045
35728
  }
35046
- if (!opts.verbose) {
35047
- const hidden = hiddenAdapterEventCount(events2);
35048
- if (hidden > 0) {
35049
- out.note = `${hidden} raw adapter events hidden; pass verbose to include them, include_bodies to show blob bodies.`;
35729
+ registerTool(name, config2, cb) {
35730
+ if (this._registeredTools[name]) {
35731
+ throw new Error(`Tool ${name} is already registered`);
35050
35732
  }
35733
+ const { title, description, inputSchema, outputSchema, annotations, _meta } = config2;
35734
+ return this._createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, { taskSupport: "forbidden" }, _meta, cb);
35051
35735
  }
35052
- return out;
35053
- }
35054
-
35055
- // src/cli/converge.ts
35056
- init_src();
35057
- import { execFileSync } from "child_process";
35058
- import { readFileSync as readFileSync7 } from "fs";
35059
- import { resolve as resolve2 } from "path";
35060
-
35061
- // src/loops/log-store.ts
35062
- init_src();
35063
- import { appendFileSync as appendFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
35064
- import { join as join6 } from "path";
35065
-
35066
- class LoopStoreError extends Error {
35067
- }
35068
- var SAFE_LOOP_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
35069
- var realClock2 = () => Date.now();
35070
- function iso(ms) {
35071
- return new Date(ms).toISOString();
35072
- }
35073
- function loopsDir(cwd) {
35074
- return join6(cwd, ".chit", "loops");
35075
- }
35076
- function loopPath(cwd, loopId) {
35077
- if (!SAFE_LOOP_ID.test(loopId)) {
35078
- throw new LoopStoreError(`invalid loop id ${JSON.stringify(loopId)}`);
35079
- }
35080
- return join6(loopsDir(cwd), `${loopId}.jsonl`);
35081
- }
35082
- function readRecords(path, loopId) {
35083
- if (!existsSync7(path)) {
35084
- throw new LoopStoreError(`no loop log for ${JSON.stringify(loopId)} at ${path}`);
35085
- }
35086
- const records = validateLoopLog(parseLoopLog(readFileSync6(path, "utf-8")));
35087
- const header = records[0];
35088
- if (header.loopId !== loopId) {
35089
- throw new LoopStoreError(`loop log at ${path} declares loopId ${JSON.stringify(header.loopId)}, expected ${JSON.stringify(loopId)}`);
35090
- }
35091
- return records;
35092
- }
35093
- function startLoop(cwd, opts) {
35094
- const loopId = opts.loopId ?? crypto.randomUUID();
35095
- const path = loopPath(cwd, loopId);
35096
- if (existsSync7(path) && !opts.force) {
35097
- throw new LoopStoreError(`loop log already exists at ${path} (pass force to overwrite)`);
35736
+ prompt(name, ...rest) {
35737
+ if (this._registeredPrompts[name]) {
35738
+ throw new Error(`Prompt ${name} is already registered`);
35739
+ }
35740
+ let description;
35741
+ if (typeof rest[0] === "string") {
35742
+ description = rest.shift();
35743
+ }
35744
+ let argsSchema;
35745
+ if (rest.length > 1) {
35746
+ argsSchema = rest.shift();
35747
+ }
35748
+ const cb = rest[0];
35749
+ const registeredPrompt = this._createRegisteredPrompt(name, undefined, description, argsSchema, cb);
35750
+ this.setPromptRequestHandlers();
35751
+ this.sendPromptListChanged();
35752
+ return registeredPrompt;
35098
35753
  }
35099
- mkdirSync4(loopsDir(cwd), { recursive: true });
35100
- const header = {
35101
- type: "loop",
35102
- schema: 1,
35103
- loopId,
35104
- scope: opts.scope,
35105
- task: opts.task,
35106
- repo: cwd,
35107
- startedAt: iso((opts.clock ?? realClock2)()),
35108
- maxIterations: opts.maxIterations
35109
- };
35110
- writeFileSync4(path, `${serializeLoopRecord(header)}
35111
- `);
35112
- return { loopId, path };
35113
- }
35114
- function appendIteration(cwd, loopId, opts) {
35115
- const path = loopPath(cwd, loopId);
35116
- const records = readRecords(path, loopId);
35117
- if (records.some((r) => r.type === "stop")) {
35118
- throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is already stopped; cannot append`);
35754
+ registerPrompt(name, config2, cb) {
35755
+ if (this._registeredPrompts[name]) {
35756
+ throw new Error(`Prompt ${name} is already registered`);
35757
+ }
35758
+ const { title, description, argsSchema } = config2;
35759
+ const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema, cb);
35760
+ this.setPromptRequestHandlers();
35761
+ this.sendPromptListChanged();
35762
+ return registeredPrompt;
35119
35763
  }
35120
- const header = records[0];
35121
- const n = records.filter((r) => r.type === "iteration").length + 1;
35122
- if (n > header.maxIterations) {
35123
- throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is at its iteration budget (maxIterations=${header.maxIterations}); cannot append iteration ${n}`);
35764
+ isConnected() {
35765
+ return this.server.transport !== undefined;
35124
35766
  }
35125
- const rec = {
35126
- type: "iteration",
35127
- n,
35128
- implementSummary: opts.implementSummary,
35129
- changedFiles: opts.changedFiles,
35130
- checksRun: opts.checksRun,
35131
- verdict: opts.verdict,
35132
- findingCount: opts.findingCount,
35133
- decision: opts.decision,
35134
- checkDurationMs: opts.checkDurationMs,
35135
- at: iso((opts.clock ?? realClock2)())
35136
- };
35137
- if (opts.detailsRef !== undefined)
35138
- rec.detailsRef = opts.detailsRef;
35139
- if (opts.usage !== undefined)
35140
- rec.usage = opts.usage;
35141
- appendFileSync2(path, `${serializeLoopRecord(rec)}
35142
- `);
35143
- return { n, path };
35144
- }
35145
- function stopLoop(cwd, loopId, opts) {
35146
- const path = loopPath(cwd, loopId);
35147
- const records = readRecords(path, loopId);
35148
- if (records.some((r) => r.type === "stop")) {
35149
- throw new LoopStoreError(`loop ${JSON.stringify(loopId)} is already stopped`);
35767
+ async sendLoggingMessage(params, sessionId) {
35768
+ return this.server.sendLoggingMessage(params, sessionId);
35150
35769
  }
35151
- const header = records[0];
35152
- const iterations = records.filter((r) => r.type === "iteration").length;
35153
- const nowMs = (opts.clock ?? realClock2)();
35154
- const totalElapsedMs = Math.max(0, nowMs - Date.parse(header.startedAt));
35155
- const rec = {
35156
- type: "stop",
35157
- status: opts.status,
35158
- reason: opts.reason,
35159
- iterations,
35160
- totalElapsedMs,
35161
- endedAt: iso(nowMs)
35162
- };
35163
- appendFileSync2(path, `${serializeLoopRecord(rec)}
35164
- `);
35165
- return { iterations, totalElapsedMs, path };
35166
- }
35167
- function readLoop(cwd, loopId) {
35168
- return readRecords(loopPath(cwd, loopId), loopId);
35169
- }
35170
-
35171
- // src/cli/default-converge-manifest.ts
35172
- var DEFAULT_CONVERGE_MANIFEST = {
35173
- schema: 1,
35174
- id: "converge",
35175
- 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_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.",
35176
- inputs: {
35177
- task: { type: "string" },
35178
- prior_review: { type: "string", optional: true }
35179
- },
35180
- requires: {
35181
- can_show_markdown: true
35182
- },
35183
- participants: {
35184
- implementer: {
35185
- agent: "claude",
35186
- 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.",
35187
- session: "per_scope",
35188
- permissions: { filesystem: "write" }
35189
- },
35190
- reviewer: {
35191
- agent: "codex",
35192
- 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, verify the work against the task, and 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.",
35193
- session: "per_scope",
35194
- permissions: { filesystem: "read_only" }
35195
- }
35196
- },
35197
- steps: {
35198
- implement: {
35199
- call: "implementer",
35200
- prompt: `Task:
35201
- {{ inputs.task }}
35202
-
35203
- Prior review to address (empty on the first iteration):
35204
- {{ inputs.prior_review }}
35205
-
35206
- 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.`
35207
- },
35208
- review: {
35209
- call: "reviewer",
35210
- prompt: `Task under review:
35211
- {{ inputs.task }}
35212
-
35213
- Claude's summary of what it just implemented:
35214
- {{ steps.implement.output }}
35215
-
35216
- 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:
35217
- 1. Verdict: proceed / revise / block.
35218
- 2. Findings ordered by severity, with file:line.
35219
- 3. What Claude should fix next if the verdict is revise.
35220
- 4. Remaining risk if proceeding.
35221
-
35222
- 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:
35223
- \`\`\`json
35224
- {"verdict": "proceed | revise | block", "findingCount": 0, "checksRun": "the non-mutating checks you ran, or 'none'", "risk": "remaining risk if proceeding"}
35225
- \`\`\`
35226
- findingCount is the integer number of findings; checksRun is a short human string.`
35227
- },
35228
- out: {
35229
- format: `## Converge iteration
35230
-
35231
- ### Implementer (Claude)
35232
- {{ steps.implement.output }}
35233
-
35234
- ### Reviewer (Codex)
35235
- {{ steps.review.output }}`
35770
+ sendResourceListChanged() {
35771
+ if (this.isConnected()) {
35772
+ this.server.sendResourceListChanged();
35236
35773
  }
35237
- },
35238
- output: "out"
35239
- };
35240
-
35241
- // src/cli/converge.ts
35242
- var defaultIO = {
35243
- out: (s) => process.stdout.write(s),
35244
- err: (s) => process.stderr.write(s)
35245
- };
35246
- var JSON_BLOCK_RE = /```json\s*([\s\S]*?)```/gi;
35247
- var VERDICTS3 = new Set(["proceed", "revise", "block"]);
35248
- var IMPLEMENT_STEP_ID = "implement";
35249
- var REVIEW_STEP_ID = "review";
35250
- var IMPLEMENT_SUMMARY_CAP = 2000;
35251
- var CHECKS_RUN_FALLBACK = "unreported";
35252
- function extractReviewJson(reviewText) {
35253
- let last;
35254
- for (const m of reviewText.matchAll(JSON_BLOCK_RE)) {
35255
- if (m[1] !== undefined)
35256
- last = m[1];
35257
35774
  }
35258
- if (last === undefined)
35259
- return null;
35260
- try {
35261
- const parsed = JSON.parse(last);
35262
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
35263
- return parsed;
35775
+ sendToolListChanged() {
35776
+ if (this.isConnected()) {
35777
+ this.server.sendToolListChanged();
35264
35778
  }
35265
- } catch {}
35266
- return null;
35267
- }
35268
- function parseReview(reviewText) {
35269
- const block = extractReviewJson(reviewText);
35270
- const rawVerdict = typeof block?.verdict === "string" ? block.verdict.toLowerCase() : undefined;
35271
- if (block && rawVerdict && VERDICTS3.has(rawVerdict)) {
35272
- const fc = block.findingCount;
35273
- const cr = block.checksRun;
35274
- return {
35275
- verdict: rawVerdict,
35276
- findingCount: typeof fc === "number" && Number.isInteger(fc) && fc >= 0 ? fc : 0,
35277
- checksRun: typeof cr === "string" && cr.trim() !== "" ? cr.trim() : CHECKS_RUN_FALLBACK
35278
- };
35279
- }
35280
- return { verdict: "block", findingCount: 0, checksRun: CHECKS_RUN_FALLBACK };
35281
- }
35282
- function reviewDurationMs(trace) {
35283
- for (const e of trace) {
35284
- if (e.type === "step.completed" && e.stepId === REVIEW_STEP_ID)
35285
- return e.durationMs;
35286
35779
  }
35287
- return 0;
35288
- }
35289
- var USAGE_KEYS2 = [
35290
- "inputTokens",
35291
- "outputTokens",
35292
- "totalTokens",
35293
- "cachedInputTokens",
35294
- "reasoningTokens",
35295
- "estimatedCostUsd"
35296
- ];
35297
- function sumTraceUsage(trace) {
35298
- const usage = {};
35299
- let any3 = false;
35300
- for (const e of trace) {
35301
- if (e.type !== "step.completed" || !e.usage)
35302
- continue;
35303
- for (const k of USAGE_KEYS2) {
35304
- const v = e.usage[k];
35305
- if (typeof v === "number") {
35306
- usage[k] = (usage[k] ?? 0) + v;
35307
- any3 = true;
35308
- }
35780
+ sendPromptListChanged() {
35781
+ if (this.isConnected()) {
35782
+ this.server.sendPromptListChanged();
35309
35783
  }
35310
35784
  }
35311
- return any3 ? usage : undefined;
35312
- }
35313
- function capSummary(text) {
35314
- if (text === "")
35315
- return "(no summary)";
35316
- if (text.length <= IMPLEMENT_SUMMARY_CAP)
35317
- return text;
35318
- return `${text.slice(0, IMPLEMENT_SUMMARY_CAP)}\u2026 (truncated, ${text.length} chars)`;
35319
35785
  }
35320
- function gitLines(cwd, args) {
35321
- try {
35322
- const out = execFileSync("git", args, {
35323
- cwd,
35324
- encoding: "utf-8",
35325
- stdio: ["ignore", "pipe", "ignore"]
35326
- });
35327
- return out.split(`
35328
- `).map((s) => s.trim()).filter(Boolean);
35329
- } catch {
35330
- return [];
35331
- }
35786
+ var EMPTY_OBJECT_JSON_SCHEMA = {
35787
+ type: "object",
35788
+ properties: {}
35789
+ };
35790
+ function isZodTypeLike(value) {
35791
+ return value !== null && typeof value === "object" && "parse" in value && typeof value.parse === "function" && "safeParse" in value && typeof value.safeParse === "function";
35332
35792
  }
35333
- function gitChangedFiles(cwd) {
35334
- const all = [
35335
- ...gitLines(cwd, ["diff", "--name-only"]),
35336
- ...gitLines(cwd, ["diff", "--cached", "--name-only"]),
35337
- ...gitLines(cwd, ["ls-files", "--others", "--exclude-standard"])
35338
- ];
35339
- return [...new Set(all)];
35793
+ function isZodSchemaInstance(obj3) {
35794
+ return "_def" in obj3 || "_zod" in obj3 || isZodTypeLike(obj3);
35340
35795
  }
35341
-
35342
- class ConvergeExecuteError extends Error {
35343
- executeError;
35344
- constructor(executeError) {
35345
- super(executeError instanceof Error ? executeError.message : String(executeError));
35346
- this.executeError = executeError;
35796
+ function isZodRawShapeCompat(obj3) {
35797
+ if (typeof obj3 !== "object" || obj3 === null) {
35798
+ return false;
35347
35799
  }
35348
- }
35349
- async function runConvergeIteration(ctx) {
35350
- let result;
35351
- try {
35352
- result = await ctx.execute({ task: ctx.task, prior_review: ctx.prior_review }, {
35353
- loopId: ctx.loopId,
35354
- iteration: ctx.iteration,
35355
- ...ctx.signal && { signal: ctx.signal }
35356
- });
35357
- } catch (e) {
35358
- throw new ConvergeExecuteError(e);
35800
+ if (isZodSchemaInstance(obj3)) {
35801
+ return false;
35359
35802
  }
35360
- if (!result.ok) {
35361
- return {
35362
- ok: false,
35363
- failure: `manifest run failed at step "${result.failedStep}": ${result.error}`
35364
- };
35803
+ if (Object.keys(obj3).length === 0) {
35804
+ return true;
35365
35805
  }
35366
- const reviewText = result.outputs.review ?? "";
35367
- const review = parseReview(reviewText);
35368
- const usage = sumTraceUsage(result.trace);
35369
- const changedFiles = gitChangedFiles(ctx.cwd);
35370
- appendIteration(ctx.cwd, ctx.loopId, {
35371
- implementSummary: capSummary(result.outputs.implement ?? ""),
35372
- changedFiles,
35373
- checksRun: review.checksRun,
35374
- verdict: review.verdict,
35375
- findingCount: review.findingCount,
35376
- decision: review.verdict,
35377
- checkDurationMs: reviewDurationMs(result.trace),
35378
- ...usage && { usage },
35379
- ...result.auditRunId && { detailsRef: `audit:${result.auditRunId}` }
35380
- });
35381
- const stopStatus = review.verdict === "proceed" ? "converged" : review.verdict === "block" ? "blocked" : undefined;
35382
- return {
35383
- ok: true,
35384
- verdict: review.verdict,
35385
- findingCount: review.findingCount,
35386
- checksRun: review.checksRun,
35387
- decision: review.verdict,
35388
- changedFiles,
35389
- ...usage && { usage },
35390
- ...result.auditRunId && { auditRunId: result.auditRunId },
35391
- reviewText,
35392
- ...stopStatus && { stopStatus }
35393
- };
35806
+ return Object.values(obj3).some(isZodTypeLike);
35394
35807
  }
35395
- async function convergeLoop(opts) {
35396
- const { loopId } = startLoop(opts.cwd, {
35397
- scope: opts.scope,
35398
- task: opts.task,
35399
- maxIterations: opts.maxIterations,
35400
- loopId: opts.loopId,
35401
- force: opts.force
35402
- });
35403
- let priorReview = "";
35404
- let iterations = 0;
35405
- let status;
35406
- for (let i = 1;i <= opts.maxIterations; i++) {
35407
- let iter;
35408
- try {
35409
- iter = await runConvergeIteration({
35410
- cwd: opts.cwd,
35411
- loopId,
35412
- iteration: i,
35413
- task: opts.task,
35414
- prior_review: priorReview,
35415
- execute: opts.execute
35416
- });
35417
- } catch (e) {
35418
- if (e instanceof ConvergeExecuteError) {
35419
- stopLoop(opts.cwd, loopId, {
35420
- status: "blocked",
35421
- reason: `manifest run threw: ${e.message}`
35422
- });
35423
- throw e.executeError;
35424
- }
35425
- throw e;
35426
- }
35427
- if (!iter.ok) {
35428
- status = "blocked";
35429
- stopLoop(opts.cwd, loopId, { status, reason: iter.failure });
35430
- return { loopId, iterations, status, failure: iter.failure };
35431
- }
35432
- iterations++;
35433
- if (iter.stopStatus !== undefined) {
35434
- status = iter.stopStatus;
35435
- break;
35436
- }
35437
- priorReview = iter.reviewText;
35808
+ function getZodSchemaObject(schema) {
35809
+ if (!schema) {
35810
+ return;
35438
35811
  }
35439
- if (status === undefined)
35440
- status = "max-iterations";
35441
- const reason = status === "converged" ? "reviewer returned proceed" : status === "blocked" ? "reviewer returned block" : `reached max iterations (${opts.maxIterations}) without converging`;
35442
- stopLoop(opts.cwd, loopId, { status, reason });
35443
- return { loopId, iterations, status };
35444
- }
35445
- function buildExecute(manifest, registry3, scope, cwd) {
35446
- const baseAdapters = {};
35447
- for (const p of Object.values(manifest.participants)) {
35448
- if (!(p.agent in baseAdapters)) {
35449
- const agent = registry3.agents[p.agent];
35450
- if (!agent)
35451
- continue;
35452
- baseAdapters[p.agent] = buildAdapter(agent);
35453
- }
35812
+ if (isZodRawShapeCompat(schema)) {
35813
+ return objectFromShape(schema);
35814
+ }
35815
+ if (!isZodSchemaInstance(schema)) {
35816
+ throw new Error("inputSchema must be a Zod schema or raw shape, received an unrecognized object");
35454
35817
  }
35455
- return makeAuditedExecute(manifest, baseAdapters, registry3, scope, cwd, new FileSessionStore(defaultSessionDir()), new AuditStore);
35818
+ return schema;
35456
35819
  }
35457
- function makeAuditedExecute(manifest, baseAdapters, registry3, scope, cwd, sessionStore, auditStore) {
35458
- return async (inputs, ctx) => {
35459
- const runId = crypto.randomUUID();
35460
- const recorder = new AuditRecorder(auditStore, runId, {
35461
- manifestId: manifest.id,
35462
- cwd,
35463
- surface: "converge",
35464
- scope,
35465
- ...ctx?.loopId !== undefined && { loopId: ctx.loopId },
35466
- ...ctx?.iteration !== undefined && { iteration: ctx.iteration },
35467
- participants: resolveParticipantSnapshots(manifest, registry3)
35468
- });
35469
- recorder.runStarted();
35470
- const adapters = wrapAdaptersWithSessions(wrapAdaptersWithAudit(baseAdapters, recorder), manifest, registry3, scope, sessionStore);
35471
- const startedAt = Date.now();
35472
- try {
35473
- const result = await executeManifest(manifest, {
35474
- inputs,
35475
- adapters,
35476
- invocationCwd: cwd,
35477
- onTrace: (e) => recorder.fromTrace(e),
35478
- ...ctx?.signal && { signal: ctx.signal }
35479
- });
35480
- recorder.runCompleted(result.ok ? "ok" : "failed", Date.now() - startedAt);
35481
- recorder.prune();
35482
- return recorder.lastError === undefined ? { ...result, auditRunId: runId } : result;
35483
- } catch (e) {
35484
- recorder.runCompleted("failed", Date.now() - startedAt);
35485
- recorder.prune();
35486
- throw e;
35820
+ function promptArgumentsFromSchema(schema) {
35821
+ const shape = getObjectShape(schema);
35822
+ if (!shape)
35823
+ return [];
35824
+ return Object.entries(shape).map(([name, field]) => {
35825
+ const description = getSchemaDescription(field);
35826
+ const isOptional = isSchemaOptional(field);
35827
+ return {
35828
+ name,
35829
+ description,
35830
+ required: !isOptional
35831
+ };
35832
+ });
35833
+ }
35834
+ function getMethodValue(schema) {
35835
+ const shape = getObjectShape(schema);
35836
+ const methodSchema = shape?.method;
35837
+ if (!methodSchema) {
35838
+ throw new Error("Schema is missing a method literal");
35839
+ }
35840
+ const value = getLiteralValue(methodSchema);
35841
+ if (typeof value === "string") {
35842
+ return value;
35843
+ }
35844
+ throw new Error("Schema method literal must be a string");
35845
+ }
35846
+ function createCompletionResult(suggestions) {
35847
+ return {
35848
+ completion: {
35849
+ values: suggestions.slice(0, 100),
35850
+ total: suggestions.length,
35851
+ hasMore: suggestions.length > 100
35487
35852
  }
35488
35853
  };
35489
35854
  }
35490
- function validateConvergeManifest(manifest) {
35491
- for (const id of [IMPLEMENT_STEP_ID, REVIEW_STEP_ID]) {
35492
- const step = manifest.steps[id];
35493
- if (!step) {
35494
- return `manifest "${manifest.id}" is not converge-shaped: missing call step "${id}" (converge needs call steps named "implement" and "review")`;
35855
+ var EMPTY_COMPLETION_RESULT = {
35856
+ completion: {
35857
+ values: [],
35858
+ hasMore: false
35859
+ }
35860
+ };
35861
+
35862
+ // ../../node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
35863
+ import process3 from "process";
35864
+
35865
+ // ../../node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
35866
+ class ReadBuffer {
35867
+ append(chunk) {
35868
+ this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
35869
+ }
35870
+ readMessage() {
35871
+ if (!this._buffer) {
35872
+ return null;
35495
35873
  }
35496
- if (step.kind !== "call") {
35497
- return `manifest "${manifest.id}" is not converge-shaped: step "${id}" must be a call step, not ${step.kind}`;
35874
+ const index = this._buffer.indexOf(`
35875
+ `);
35876
+ if (index === -1) {
35877
+ return null;
35498
35878
  }
35879
+ const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
35880
+ this._buffer = this._buffer.subarray(index + 1);
35881
+ return deserializeMessage(line);
35882
+ }
35883
+ clear() {
35884
+ this._buffer = undefined;
35499
35885
  }
35500
- return null;
35501
35886
  }
35502
- var CONVERGE_HELP = `chit converge --task <text> --scope <id> [options]
35503
-
35504
- --task <text> Required. The slice to converge on.
35505
- --scope <id> Required. Session scope; both agents keep their thread.
35506
- --cwd <dir> Repo to run in. Default: current directory.
35507
- --manifest <path> Convergence manifest. Default: the built-in converge manifest.
35508
- --max-iterations <n> Iteration budget. Default: 3.
35509
- --loop-id <id> Reuse/seed a loop id. Default: generated.
35510
- --allow-unenforced-permissions
35511
- Run even when the manifest declares permissions its
35512
- adapter cannot enforce (emits a warning each run).
35513
- Default off: such a manifest is refused before running.
35514
-
35515
- Runs the implement/check loop to convergence and records it under
35516
- .chit/loops/<loopId>.jsonl. Stops at the reviewer's verdict: proceed ->
35517
- converged, block -> blocked, else revise and retry up to the budget. An
35518
- unparseable verdict is treated as block (never an implicit proceed).
35887
+ function deserializeMessage(line) {
35888
+ return JSONRPCMessageSchema.parse(JSON.parse(line));
35889
+ }
35890
+ function serializeMessage(message) {
35891
+ return JSON.stringify(message) + `
35519
35892
  `;
35520
-
35521
- class UsageError extends Error {
35522
35893
  }
35523
- function parseConvergeArgs(argv) {
35524
- let task;
35525
- let scope;
35526
- let cwd;
35527
- let manifestPath;
35528
- let maxIterations = 3;
35529
- let loopId;
35530
- let allowUnenforcedPermissions = false;
35531
- for (let i = 0;i < argv.length; i++) {
35532
- const a = argv[i];
35533
- const need = (key) => {
35534
- const v = argv[++i];
35535
- if (v === undefined)
35536
- throw new UsageError(`${key} requires a value`);
35537
- return v;
35894
+
35895
+ // ../../node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
35896
+ class StdioServerTransport {
35897
+ constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
35898
+ this._stdin = _stdin;
35899
+ this._stdout = _stdout;
35900
+ this._readBuffer = new ReadBuffer;
35901
+ this._started = false;
35902
+ this._ondata = (chunk) => {
35903
+ this._readBuffer.append(chunk);
35904
+ this.processReadBuffer();
35538
35905
  };
35539
- if (a === "--task")
35540
- task = need("--task");
35541
- else if (a === "--scope")
35542
- scope = need("--scope");
35543
- else if (a === "--cwd")
35544
- cwd = need("--cwd");
35545
- else if (a === "--manifest")
35546
- manifestPath = need("--manifest");
35547
- else if (a === "--loop-id")
35548
- loopId = need("--loop-id");
35549
- else if (a === "--allow-unenforced-permissions")
35550
- allowUnenforcedPermissions = true;
35551
- else if (a === "--max-iterations") {
35552
- const raw = need("--max-iterations");
35553
- const n = Number(raw);
35554
- if (!Number.isInteger(n) || n < 1) {
35555
- throw new UsageError(`--max-iterations must be a positive integer (got ${JSON.stringify(raw)})`);
35906
+ this._onerror = (error51) => {
35907
+ this.onerror?.(error51);
35908
+ };
35909
+ }
35910
+ async start() {
35911
+ if (this._started) {
35912
+ throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
35913
+ }
35914
+ this._started = true;
35915
+ this._stdin.on("data", this._ondata);
35916
+ this._stdin.on("error", this._onerror);
35917
+ }
35918
+ processReadBuffer() {
35919
+ while (true) {
35920
+ try {
35921
+ const message = this._readBuffer.readMessage();
35922
+ if (message === null) {
35923
+ break;
35924
+ }
35925
+ this.onmessage?.(message);
35926
+ } catch (error51) {
35927
+ this.onerror?.(error51);
35556
35928
  }
35557
- maxIterations = n;
35558
- } else {
35559
- throw new UsageError(`unknown flag ${JSON.stringify(a)}`);
35560
35929
  }
35561
35930
  }
35562
- if (task === undefined)
35563
- throw new UsageError("--task is required");
35564
- if (scope === undefined)
35565
- throw new UsageError("--scope is required");
35566
- return {
35567
- task,
35568
- scope,
35569
- cwd: resolve2(cwd ?? process.cwd()),
35570
- manifestPath,
35571
- maxIterations,
35572
- loopId,
35573
- allowUnenforcedPermissions
35574
- };
35575
- }
35576
- async function runConverge(argv, io = defaultIO) {
35577
- if (argv[0] === "-h" || argv[0] === "--help") {
35578
- io.out(CONVERGE_HELP);
35579
- return 0;
35931
+ async close() {
35932
+ this._stdin.off("data", this._ondata);
35933
+ this._stdin.off("error", this._onerror);
35934
+ const remainingDataListeners = this._stdin.listenerCount("data");
35935
+ if (remainingDataListeners === 0) {
35936
+ this._stdin.pause();
35937
+ }
35938
+ this._readBuffer.clear();
35939
+ this.onclose?.();
35580
35940
  }
35581
- let parsed;
35582
- try {
35583
- parsed = parseConvergeArgs(argv);
35584
- } catch (e) {
35585
- if (e instanceof UsageError) {
35586
- io.err(`chit converge: ${e.message}
35941
+ send(message) {
35942
+ return new Promise((resolve4) => {
35943
+ const json2 = serializeMessage(message);
35944
+ if (this._stdout.write(json2)) {
35945
+ resolve4();
35946
+ } else {
35947
+ this._stdout.once("drain", resolve4);
35948
+ }
35949
+ });
35950
+ }
35951
+ }
35587
35952
 
35588
- ${CONVERGE_HELP}`);
35589
- return 2;
35953
+ // src/audit/reader.ts
35954
+ var USAGE_KEYS2 = [
35955
+ "inputTokens",
35956
+ "outputTokens",
35957
+ "totalTokens",
35958
+ "cachedInputTokens",
35959
+ "reasoningTokens",
35960
+ "estimatedCostUsd"
35961
+ ];
35962
+ function sumUsage(events2) {
35963
+ const usage = {};
35964
+ let any3 = false;
35965
+ for (const e of events2) {
35966
+ if (e.type !== "adapter.call.completed" || !e.usage)
35967
+ continue;
35968
+ for (const k of USAGE_KEYS2) {
35969
+ const v = e.usage[k];
35970
+ if (typeof v === "number") {
35971
+ usage[k] = (usage[k] ?? 0) + v;
35972
+ any3 = true;
35973
+ }
35590
35974
  }
35591
- throw e;
35592
35975
  }
35593
- let manifest;
35594
- try {
35595
- const raw = parsed.manifestPath !== undefined ? JSON.parse(readFileSync7(parsed.manifestPath, "utf-8")) : DEFAULT_CONVERGE_MANIFEST;
35596
- manifest = parseManifest(raw);
35597
- } catch (e) {
35598
- io.err(`chit converge: failed to load manifest ${parsed.manifestPath ?? "(built-in default)"}: ${e.message}
35599
- `);
35600
- return 2;
35976
+ return any3 ? usage : undefined;
35977
+ }
35978
+ function findOpenCall(events2) {
35979
+ const open = new Map;
35980
+ for (const e of events2) {
35981
+ if (e.type === "adapter.call.started") {
35982
+ open.set(e.stepId, {
35983
+ stepId: e.stepId,
35984
+ participantId: e.participantId,
35985
+ agentId: e.agentId,
35986
+ since: e.ts
35987
+ });
35988
+ } else if (e.type === "adapter.call.completed") {
35989
+ open.delete(e.stepId);
35990
+ }
35601
35991
  }
35602
- const shapeError = validateConvergeManifest(manifest);
35603
- if (shapeError) {
35604
- io.err(`chit converge: ${shapeError}
35605
- `);
35606
- return 1;
35992
+ let latest;
35993
+ for (const c of open.values()) {
35994
+ if (latest === undefined || c.since > latest.since)
35995
+ latest = c;
35607
35996
  }
35608
- let registry3;
35997
+ return latest;
35998
+ }
35999
+ function describeIncomplete(s, events2) {
36000
+ if (s.openCall) {
36001
+ const c = s.openCall;
36002
+ return `open call: ${c.stepId} ${c.participantId}/${c.agentId} since ${c.since}; no adapter.call.completed`;
36003
+ }
36004
+ const failed = events2.find((e) => e.type === "step.failed");
36005
+ if (failed?.type === "step.failed") {
36006
+ const err = failed.error.replace(/\s+/g, " ").trim();
36007
+ const clipped = err.length > 200 ? `${err.slice(0, 200)}...` : err;
36008
+ return `failed step: ${failed.stepId}: ${clipped}`;
36009
+ }
36010
+ return "abandoned before terminal run.completed";
36011
+ }
36012
+ function summarizeRun(runId, events2) {
36013
+ const started = events2.find((e) => e.type === "run.started");
36014
+ const completed = events2.find((e) => e.type === "run.completed");
36015
+ const summary = {
36016
+ runId,
36017
+ manifestId: started?.type === "run.started" ? started.manifestId : "?",
36018
+ surface: started?.type === "run.started" ? started.surface : "?",
36019
+ status: completed?.type === "run.completed" ? completed.status : "incomplete",
36020
+ stepCount: events2.filter((e) => e.type === "step.completed").length
36021
+ };
36022
+ if (started?.type === "run.started") {
36023
+ summary.startedAt = started.ts;
36024
+ if (started.scope !== undefined)
36025
+ summary.scope = started.scope;
36026
+ if (started.loopId !== undefined)
36027
+ summary.loopId = started.loopId;
36028
+ if (started.iteration !== undefined)
36029
+ summary.iteration = started.iteration;
36030
+ }
36031
+ const usage = sumUsage(events2);
36032
+ if (usage !== undefined)
36033
+ summary.usage = usage;
36034
+ const openCall = findOpenCall(events2);
36035
+ if (openCall !== undefined)
36036
+ summary.openCall = openCall;
36037
+ return summary;
36038
+ }
36039
+ function safeReadEvents(store, runId) {
35609
36040
  try {
35610
- registry3 = loadRegistry();
35611
- } catch (e) {
35612
- io.err(`chit converge: ${e.message}
35613
- `);
35614
- return 1;
36041
+ return store.readEvents(runId);
36042
+ } catch {
36043
+ return [];
35615
36044
  }
35616
- const unknown3 = findUnknownAgents(manifest, registry3);
35617
- if (unknown3.length > 0) {
35618
- for (const u of unknown3) {
35619
- io.err(`chit converge: unknown agent "${u.agentId}" in registry (participant "${u.participantId}")
35620
- `);
35621
- }
35622
- return 2;
36045
+ }
36046
+ function listAudit(store, limit) {
36047
+ const summaries = store.listRuns().map((id) => summarizeRun(id, safeReadEvents(store, id)));
36048
+ summaries.sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
36049
+ return limit !== undefined ? summaries.slice(0, limit) : summaries;
36050
+ }
36051
+ function readBody(store, runId, ref) {
36052
+ try {
36053
+ return store.readBlob(runId, ref);
36054
+ } catch (err) {
36055
+ return `<blob unavailable: ${err.message}>`;
35623
36056
  }
35624
- const gaps = findEnforcementGaps(manifest, registry3);
35625
- if (gaps.length > 0 && !parsed.allowUnenforcedPermissions) {
35626
- io.err(`chit converge: cannot enforce required permissions for "${manifest.id}":
35627
- `);
35628
- io.err(`${formatEnforcementGaps(gaps)}
35629
- `);
35630
- io.err(`
35631
- Pass --allow-unenforced-permissions to run anyway (emits a warning each run).
35632
- `);
35633
- return 1;
36057
+ }
36058
+ function isReceiptEvent(e) {
36059
+ return e.type !== "adapter.event";
36060
+ }
36061
+ function hiddenAdapterEventCount(events2) {
36062
+ return events2.reduce((n, e) => e.type === "adapter.event" ? n + 1 : n, 0);
36063
+ }
36064
+ function auditTimeline(store, runId, events2, opts) {
36065
+ const rows = opts.verbose ? events2 : events2.filter(isReceiptEvent);
36066
+ return rows.map((e) => {
36067
+ if (!opts.includeBodies)
36068
+ return e;
36069
+ if (e.type === "adapter.call.started") {
36070
+ return { ...e, input: readBody(store, runId, e.inputBlob) };
36071
+ }
36072
+ if (e.type === "adapter.event" && e.rawBlob !== undefined) {
36073
+ return { ...e, raw: readBody(store, runId, e.rawBlob) };
36074
+ }
36075
+ if (e.type === "adapter.call.completed") {
36076
+ return { ...e, output: readBody(store, runId, e.outputBlob) };
36077
+ }
36078
+ if (e.type === "step.completed" && e.outputBlob !== undefined) {
36079
+ return { ...e, output: readBody(store, runId, e.outputBlob) };
36080
+ }
36081
+ return e;
36082
+ });
36083
+ }
36084
+ function showAudit(store, runId, opts) {
36085
+ const events2 = store.readEvents(runId);
36086
+ const summary = summarizeRun(runId, events2);
36087
+ const out = {
36088
+ summary,
36089
+ timeline: auditTimeline(store, runId, events2, opts)
36090
+ };
36091
+ if (summary.status === "incomplete")
36092
+ out.incompleteReason = describeIncomplete(summary, events2);
36093
+ const started = events2.find((e) => e.type === "run.started");
36094
+ if (started?.type === "run.started" && started.participants !== undefined) {
36095
+ out.participants = started.participants;
35634
36096
  }
35635
- for (const g of gaps) {
35636
- io.err(`chit converge: WARNING -- unenforced permission: participant "${g.participantId}" requires ${g.permission}
35637
- `);
36097
+ if (!opts.verbose) {
36098
+ const hidden = hiddenAdapterEventCount(events2);
36099
+ if (hidden > 0) {
36100
+ out.note = `${hidden} raw adapter events hidden; pass verbose to include them, include_bodies to show blob bodies.`;
36101
+ }
35638
36102
  }
35639
- let result;
36103
+ return out;
36104
+ }
36105
+
36106
+ // src/jobs/health.ts
36107
+ var STALE_AFTER_MS = 60000;
36108
+ function pidAlive(pid) {
36109
+ if (pid === undefined)
36110
+ return false;
35640
36111
  try {
35641
- result = await convergeLoop({
35642
- cwd: parsed.cwd,
35643
- scope: parsed.scope,
35644
- task: parsed.task,
35645
- maxIterations: parsed.maxIterations,
35646
- loopId: parsed.loopId,
35647
- execute: buildExecute(manifest, registry3, parsed.scope, parsed.cwd)
35648
- });
36112
+ process.kill(pid, 0);
36113
+ return true;
35649
36114
  } catch (e) {
35650
- io.err(`chit converge: ${e.message}
35651
- `);
35652
- return 1;
36115
+ return e.code === "EPERM";
35653
36116
  }
35654
- if (result.failure !== undefined) {
35655
- io.err(`chit converge: ${result.failure}
35656
- `);
35657
- return 1;
36117
+ }
36118
+ function isStale(job, nowMs, staleAfterMs = STALE_AFTER_MS) {
36119
+ if (job.state === "queued") {
36120
+ const created = Date.parse(job.createdAt);
36121
+ return Number.isFinite(created) && nowMs - created > staleAfterMs;
35658
36122
  }
35659
- io.out(`chit converge: ${result.loopId}
35660
- `);
35661
- io.out(` iterations: ${result.iterations}
35662
- `);
35663
- io.out(` status: ${result.status}
35664
- `);
35665
- return 0;
36123
+ if (job.state !== "running")
36124
+ return false;
36125
+ const beat = job.lastHeartbeatAt ? Date.parse(job.lastHeartbeatAt) : 0;
36126
+ const heartbeatOld = !Number.isFinite(beat) || nowMs - beat > staleAfterMs;
36127
+ return heartbeatOld || !pidAlive(job.pid);
35666
36128
  }
35667
36129
 
35668
36130
  // src/surfaces/mcp/converge-engine.ts
@@ -35764,6 +36226,7 @@ async function runNextIteration(session, signal) {
35764
36226
  findingCount: iter.findingCount,
35765
36227
  checksRun: iter.checksRun,
35766
36228
  changedFiles: iter.changedFiles,
36229
+ workspaceWarnings: iter.workspaceWarnings,
35767
36230
  ...iter.usage !== undefined && { usage: iter.usage },
35768
36231
  ...iter.auditRunId !== undefined && { auditRunId: iter.auditRunId },
35769
36232
  ...session.terminalStatus !== undefined && { stopStatus: session.terminalStatus }
@@ -35878,7 +36341,7 @@ function startRun(runId, opts) {
35878
36341
  }
35879
36342
  const needsScope = Object.values(manifest.participants).some((p) => p.session === "per_scope");
35880
36343
  if (needsScope && opts.scope === undefined) {
35881
- throw new RuntimeError(`manifest "${manifest.id}" has per_scope participant(s); a scope is required (pass scope to chit_start)`);
36344
+ throw new RuntimeError(`manifest "${manifest.id}" has per_scope participant(s); a scope is required (pass scope to chit_run_start)`);
35882
36345
  }
35883
36346
  const baseAdapters = {};
35884
36347
  for (const p of Object.values(manifest.participants)) {
@@ -36115,6 +36578,25 @@ function summarizeRunForStatus(run) {
36115
36578
  audited: run.recorder !== undefined && run.recorder.lastError === undefined
36116
36579
  };
36117
36580
  }
36581
+ function summarizeJobForStatus(job, nowMs) {
36582
+ const stale = isStale(job, nowMs);
36583
+ const display = stale ? "stale" : job.state;
36584
+ const nextAction = display === "running" ? `in progress${job.phase ? ` (${job.phase})` : ""}; chit_job_status / chit_job_cancel "${job.jobId}"` : 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}" or chit_audit_show <ref>`;
36585
+ return {
36586
+ jobId: job.jobId,
36587
+ loopId: job.loopId,
36588
+ scope: job.scope,
36589
+ task: job.task,
36590
+ display,
36591
+ ...job.phase !== undefined && { phase: job.phase },
36592
+ iterationsCompleted: job.iterationsCompleted,
36593
+ ...job.lastVerdict !== undefined && { lastVerdict: job.lastVerdict },
36594
+ ...job.stopStatus !== undefined && { stopStatus: job.stopStatus },
36595
+ auditRefs: job.auditRefs,
36596
+ createdAt: job.createdAt,
36597
+ nextAction
36598
+ };
36599
+ }
36118
36600
  function byNewest(items) {
36119
36601
  return [...items].sort((a, b) => b.startedAtMs - a.startedAtMs);
36120
36602
  }
@@ -36127,21 +36609,34 @@ function recentRuns(auditStore, recentLimit) {
36127
36609
  return [];
36128
36610
  }
36129
36611
  }
36130
- function buildStatus(runs, convergeSessions, auditStore, recentLimit) {
36612
+ function buildStatus(runs, convergeSessions, auditStore, jobStore, recentLimit, nowMs) {
36131
36613
  return {
36132
36614
  active: {
36133
36615
  runs: byNewest(runs.list()).map(summarizeRunForStatus),
36134
36616
  loops: byNewest(convergeSessions.list()).map(describeConverge)
36135
36617
  },
36618
+ jobs: jobsForStatus(jobStore, recentLimit, nowMs),
36136
36619
  recent: recentRuns(auditStore, recentLimit)
36137
36620
  };
36138
36621
  }
36622
+ function jobsForStatus(jobStore, recentLimit, nowMs) {
36623
+ let all;
36624
+ try {
36625
+ all = jobStore.list();
36626
+ } catch {
36627
+ return [];
36628
+ }
36629
+ const inFlight = all.filter((j) => j.state === "queued" || j.state === "running");
36630
+ const terminal = all.filter((j) => j.state !== "queued" && j.state !== "running").slice(0, recentLimit);
36631
+ return [...inFlight, ...terminal].map((j) => summarizeJobForStatus(j, nowMs));
36632
+ }
36139
36633
 
36140
36634
  // src/surfaces/mcp/server.ts
36141
36635
  var runs = new RunStore;
36142
36636
  var controllers = new Map;
36143
36637
  var convergeSessions = new ConvergeStore;
36144
36638
  var auditStore = new AuditStore;
36639
+ var jobStore = new JobStore;
36145
36640
  var registryCache;
36146
36641
  function getRegistry() {
36147
36642
  registryCache ??= loadRegistry();
@@ -36179,7 +36674,7 @@ function describeRun(run) {
36179
36674
  audit: run.recorder && run.recorder.lastError === undefined ? { runId: run.runId } : undefined
36180
36675
  };
36181
36676
  }
36182
- server.registerTool("chit_start", {
36677
+ server.registerTool("chit_run_start", {
36183
36678
  description: "Start a stepwise run of a chit manifest. Returns a run_id and the steps ready to run. chit owns the declared order; only ready steps can be run. Then call chit_run_step for each ready step.",
36184
36679
  inputSchema: {
36185
36680
  manifest_path: exports_external.string().describe("Path to the manifest .json (absolute, or relative to cwd)"),
@@ -36191,10 +36686,10 @@ server.registerTool("chit_start", {
36191
36686
  }
36192
36687
  }, async ({ manifest_path, inputs, scope, cwd, allow_unenforced_permissions, audit }) => {
36193
36688
  runs.sweep(Date.now());
36194
- const path = isAbsolute2(manifest_path) ? manifest_path : resolve3(process.cwd(), manifest_path);
36689
+ const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(process.cwd(), manifest_path);
36195
36690
  let raw;
36196
36691
  try {
36197
- raw = JSON.parse(readFileSync8(path, "utf-8"));
36692
+ raw = JSON.parse(readFileSync11(path, "utf-8"));
36198
36693
  } catch (e) {
36199
36694
  return errorResult(`could not read manifest at ${path}: ${e.message}`);
36200
36695
  }
@@ -36215,7 +36710,7 @@ server.registerTool("chit_start", {
36215
36710
  runs.add(run, Date.now());
36216
36711
  return jsonResult(describeRun(run));
36217
36712
  });
36218
- server.registerTool("chit_next", {
36713
+ server.registerTool("chit_run_next", {
36219
36714
  description: "List the steps ready to run next for a run, or report that the run is complete.",
36220
36715
  inputSchema: { run_id: exports_external.string() }
36221
36716
  }, async ({ run_id }) => {
@@ -36274,7 +36769,7 @@ server.registerTool("chit_run_step", {
36274
36769
  runs.touch(run_id, Date.now());
36275
36770
  }
36276
36771
  });
36277
- server.registerTool("chit_cancel", {
36772
+ server.registerTool("chit_run_cancel", {
36278
36773
  description: "Cancel a step that is currently running: aborts its controller, which kills the agent's child process and settles the step as cancelled (terminal, blocks dependents). Returns cancelled:true if it stopped a running step, or a reason (already_done | not_running) otherwise. Use after interrupting a long step.",
36279
36774
  inputSchema: { run_id: exports_external.string(), step_id: exports_external.string() }
36280
36775
  }, async ({ run_id, step_id }) => {
@@ -36291,7 +36786,7 @@ server.registerTool("chit_cancel", {
36291
36786
  ...describeRun(run)
36292
36787
  });
36293
36788
  });
36294
- server.registerTool("chit_trace", {
36789
+ server.registerTool("chit_run_trace", {
36295
36790
  description: "Return the transcript of a run so far: each step's status, participant, agent, elapsed, and output.",
36296
36791
  inputSchema: { run_id: exports_external.string() }
36297
36792
  }, async ({ run_id }) => {
@@ -36318,38 +36813,8 @@ server.registerTool("chit_trace", {
36318
36813
  trace
36319
36814
  });
36320
36815
  });
36321
- function prepareConvergeExecute(raw, scope, cwd, allowUnenforced) {
36322
- let manifest;
36323
- try {
36324
- manifest = parseManifest(raw);
36325
- } catch (e) {
36326
- return { ok: false, error: e.message };
36327
- }
36328
- const shapeError = validateConvergeManifest(manifest);
36329
- if (shapeError)
36330
- return { ok: false, error: shapeError };
36331
- const registry3 = getRegistry();
36332
- const unknown3 = findUnknownAgents(manifest, registry3);
36333
- if (unknown3.length > 0) {
36334
- return {
36335
- ok: false,
36336
- error: `unknown agent(s): ${unknown3.map((u) => `${u.agentId} (participant "${u.participantId}")`).join(", ")}`
36337
- };
36338
- }
36339
- const gaps = findEnforcementGaps(manifest, registry3);
36340
- if (gaps.length > 0 && !allowUnenforced) {
36341
- return {
36342
- ok: false,
36343
- error: `cannot enforce required permissions:
36344
- ${formatEnforcementGaps(gaps)}
36345
- Pass allow_unenforced_permissions=true to run anyway.`
36346
- };
36347
- }
36348
- const warnings = gaps.map((g) => `unenforced permission: participant "${g.participantId}" requires ${g.permission}`);
36349
- return { ok: true, execute: buildExecute(manifest, registry3, scope, cwd), warnings };
36350
- }
36351
36816
  server.registerTool("chit_converge_start", {
36352
- 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 to .chit/loops/<loop_id>.jsonl, identical to `chit converge`.",
36817
+ 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`.",
36353
36818
  inputSchema: {
36354
36819
  task: exports_external.string().describe("The slice to converge on"),
36355
36820
  scope: exports_external.string().describe("Session scope id; both agents keep their thread across iterations"),
@@ -36371,19 +36836,19 @@ server.registerTool("chit_converge_start", {
36371
36836
  allow_unenforced_permissions
36372
36837
  }) => {
36373
36838
  convergeSessions.sweep(Date.now());
36374
- const runCwd = resolve3(cwd ?? process.cwd());
36839
+ const runCwd = resolve4(cwd ?? process.cwd());
36375
36840
  let raw;
36376
36841
  if (manifest_path) {
36377
- const path = isAbsolute2(manifest_path) ? manifest_path : resolve3(runCwd, manifest_path);
36842
+ const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path);
36378
36843
  try {
36379
- raw = JSON.parse(readFileSync8(path, "utf-8"));
36844
+ raw = JSON.parse(readFileSync11(path, "utf-8"));
36380
36845
  } catch (e) {
36381
36846
  return errorResult(`could not read manifest at ${path}: ${e.message}`);
36382
36847
  }
36383
36848
  } else {
36384
36849
  raw = DEFAULT_CONVERGE_MANIFEST;
36385
36850
  }
36386
- const prep = prepareConvergeExecute(raw, scope, runCwd, allow_unenforced_permissions);
36851
+ const prep = prepareConvergeExecute(raw, getRegistry(), scope, runCwd, allow_unenforced_permissions);
36387
36852
  if (!prep.ok)
36388
36853
  return errorResult(prep.error);
36389
36854
  let session;
@@ -36425,6 +36890,15 @@ server.registerTool("chit_converge_next", {
36425
36890
  }
36426
36891
  server.sendLoggingMessage({ level: "info", data: message, logger: "chit" }).catch(() => {});
36427
36892
  };
36893
+ let loopLock;
36894
+ try {
36895
+ loopLock = acquireLock(jobStore.loopLockPath(loop_id), { retryMs: 50, maxAttempts: 4 });
36896
+ } catch (e) {
36897
+ if (e instanceof LockError) {
36898
+ return errorResult(`loop "${loop_id}" is being advanced by a background job; cancel it with chit_job_cancel or wait, then retry`);
36899
+ }
36900
+ throw e;
36901
+ }
36428
36902
  const iterationNo = session.iteration + 1;
36429
36903
  heartbeat(`${loop_id} \xB7 iteration ${iterationNo} \xB7 starting`);
36430
36904
  try {
@@ -36454,6 +36928,7 @@ server.registerTool("chit_converge_next", {
36454
36928
  findingCount: result.findingCount,
36455
36929
  checksRun: result.checksRun,
36456
36930
  changedFiles: result.changedFiles,
36931
+ workspaceWarnings: result.workspaceWarnings,
36457
36932
  ...result.usage && { usage: result.usage },
36458
36933
  ...result.auditRunId && { auditRunId: result.auditRunId },
36459
36934
  ...result.stopStatus && { stopStatus: result.stopStatus },
@@ -36462,6 +36937,7 @@ server.registerTool("chit_converge_next", {
36462
36937
  } catch (e) {
36463
36938
  return errorResult(e.message);
36464
36939
  } finally {
36940
+ releaseLock(loopLock);
36465
36941
  convergeSessions.touch(loop_id, Date.now());
36466
36942
  }
36467
36943
  });
@@ -36520,12 +36996,197 @@ server.registerTool("chit_audit_show", {
36520
36996
  }
36521
36997
  });
36522
36998
  server.registerTool("chit_status", {
36523
- description: "Operator overview: the stepwise runs and converge loops live in THIS server right now (each loop with its status and next action), plus a compact list of recently audited runs (newest first). Read-only; answers 'what is active and what should I do next?'. Active state is per-session (a new session starts empty, and idle runs are evicted); recent state is durable. Drill into one item with chit_converge_status/chit_trace, or chit_audit_show for a run's receipt.",
36999
+ description: "Operator overview: the stepwise runs and converge loops live in THIS server right now (each loop with its status and next action), plus a compact list of recently audited runs (newest first). Read-only; answers 'what is active and what should I do next?'. Active state is per-session (a new session starts empty, and idle runs are evicted); recent state is durable. Drill into one item with chit_converge_status/chit_run_trace, or chit_audit_show for a run's receipt.",
36524
37000
  inputSchema: {
36525
37001
  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.")
36526
37002
  }
36527
37003
  }, async ({ recent_limit }) => {
36528
- return jsonResult(buildStatus(runs, convergeSessions, auditStore, recent_limit));
37004
+ return jsonResult(buildStatus(runs, convergeSessions, auditStore, jobStore, recent_limit, Date.now()));
37005
+ });
37006
+ function spawnJobWorker(jobId, cwd) {
37007
+ const child = spawn(String(process.argv[0]), [String(process.argv[1]), "job-run", jobId], {
37008
+ cwd,
37009
+ detached: true,
37010
+ stdio: "ignore"
37011
+ });
37012
+ child.unref();
37013
+ }
37014
+ server.registerTool("chit_converge_run", {
37015
+ 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).",
37016
+ inputSchema: {
37017
+ task: exports_external.string().describe("The slice to converge on"),
37018
+ scope: exports_external.string().describe("Session scope id; both agents keep their thread across iterations"),
37019
+ cwd: exports_external.string().optional().describe("Repo to run in (defaults to the server cwd)"),
37020
+ manifest_path: exports_external.string().optional().describe("Converge manifest path (absolute or relative to cwd). Default: the built-in."),
37021
+ max_iterations: exports_external.number().int().min(1).default(3).describe("Iteration budget. Default 3."),
37022
+ loop_id: exports_external.string().optional().describe("Seed a loop id. Default: generated."),
37023
+ force: exports_external.boolean().default(false).describe("Overwrite an existing loop log at this loop_id rather than refusing."),
37024
+ allow_unenforced_permissions: exports_external.boolean().default(false).describe("Run even when a declared permission cannot be enforced (emits warnings).")
37025
+ }
37026
+ }, async ({
37027
+ task,
37028
+ scope,
37029
+ cwd,
37030
+ manifest_path,
37031
+ max_iterations,
37032
+ loop_id,
37033
+ force,
37034
+ allow_unenforced_permissions
37035
+ }) => {
37036
+ const runCwd = resolve4(cwd ?? process.cwd());
37037
+ let raw;
37038
+ let manifestAbs;
37039
+ if (manifest_path) {
37040
+ manifestAbs = isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path);
37041
+ try {
37042
+ raw = JSON.parse(readFileSync11(manifestAbs, "utf-8"));
37043
+ } catch (e) {
37044
+ return errorResult(`could not read manifest at ${manifestAbs}: ${e.message}`);
37045
+ }
37046
+ } else {
37047
+ raw = DEFAULT_CONVERGE_MANIFEST;
37048
+ }
37049
+ const prep = prepareConvergeExecute(raw, getRegistry(), scope, runCwd, allow_unenforced_permissions);
37050
+ if (!prep.ok)
37051
+ return errorResult(prep.error);
37052
+ const loopId = loop_id ?? crypto.randomUUID();
37053
+ try {
37054
+ startLoop(runCwd, { scope, task, maxIterations: max_iterations, loopId, force });
37055
+ } catch (e) {
37056
+ if (e instanceof LoopStoreError) {
37057
+ 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.`);
37058
+ }
37059
+ return errorResult(e.message);
37060
+ }
37061
+ const jobId = crypto.randomUUID();
37062
+ const job = {
37063
+ jobId,
37064
+ loopId,
37065
+ repoKey: repoKey(runCwd),
37066
+ cwd: runCwd,
37067
+ scope,
37068
+ task,
37069
+ ...manifestAbs !== undefined && { manifestPath: manifestAbs },
37070
+ maxIterations: max_iterations,
37071
+ allowUnenforced: allow_unenforced_permissions,
37072
+ state: "queued",
37073
+ createdAt: new Date().toISOString(),
37074
+ iterationsCompleted: 0,
37075
+ auditRefs: []
37076
+ };
37077
+ try {
37078
+ jobStore.create(job);
37079
+ } catch (e) {
37080
+ stopLoop(runCwd, loopId, { status: "blocked", reason: "could not create job record" });
37081
+ return errorResult(e.message);
37082
+ }
37083
+ try {
37084
+ spawnJobWorker(jobId, runCwd);
37085
+ } catch (e) {
37086
+ jobStore.update(jobId, (c) => ({
37087
+ ...c,
37088
+ state: "failed",
37089
+ failure: `could not spawn worker: ${e.message}`,
37090
+ endedAt: new Date().toISOString()
37091
+ }));
37092
+ stopLoop(runCwd, loopId, { status: "blocked", reason: "worker spawn failed" });
37093
+ return errorResult(`could not spawn background worker: ${e.message}`);
37094
+ }
37095
+ return jsonResult({
37096
+ jobId,
37097
+ loopId,
37098
+ repo: repoRoot(runCwd),
37099
+ state: "queued",
37100
+ nextAction: `running in the background; poll chit_job_status "${jobId}" (or chit_status), cancel with chit_job_cancel "${jobId}"`,
37101
+ ...prep.warnings.length > 0 && { warnings: prep.warnings }
37102
+ });
37103
+ });
37104
+ function describeJob(job) {
37105
+ const now = Date.now();
37106
+ const stale = isStale(job, now);
37107
+ const display = stale ? "stale" : job.state;
37108
+ let latest;
37109
+ try {
37110
+ const iters = readLoop(job.cwd, job.loopId).filter((r) => r.type === "iteration");
37111
+ const last = iters.at(-1);
37112
+ if (last && last.type === "iteration") {
37113
+ latest = {
37114
+ iteration: last.n,
37115
+ changedFiles: last.changedFiles,
37116
+ workspaceWarnings: last.workspaceWarnings ?? [],
37117
+ ...last.usage !== undefined && { usage: last.usage }
37118
+ };
37119
+ }
37120
+ } catch {}
37121
+ const nextAction = display === "running" ? "in progress; 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 (and chit_audit_show <auditRef> for transcripts), then start a fresh job" : `${display}${job.stopStatus ? ` (${job.stopStatus})` : ""}; open a transcript with chit_audit_show <auditRef>`;
37122
+ return {
37123
+ jobId: job.jobId,
37124
+ loopId: job.loopId,
37125
+ scope: job.scope,
37126
+ task: job.task,
37127
+ state: job.state,
37128
+ display,
37129
+ stale,
37130
+ alive: pidAlive(job.pid),
37131
+ ...job.phase !== undefined && { phase: job.phase },
37132
+ ...job.iteration !== undefined && { iteration: job.iteration },
37133
+ iterationsCompleted: job.iterationsCompleted,
37134
+ ...job.lastVerdict !== undefined && { lastVerdict: job.lastVerdict },
37135
+ ...job.stopStatus !== undefined && { stopStatus: job.stopStatus },
37136
+ ...job.failure !== undefined && { failure: job.failure },
37137
+ ...job.cancelRequestedAt !== undefined && { cancelRequestedAt: job.cancelRequestedAt },
37138
+ auditRefs: job.auditRefs,
37139
+ createdAt: job.createdAt,
37140
+ ...job.startedAt !== undefined && { startedAt: job.startedAt },
37141
+ ...job.endedAt !== undefined && { endedAt: job.endedAt },
37142
+ ...job.lastHeartbeatAt !== undefined && { lastHeartbeatAt: job.lastHeartbeatAt },
37143
+ ...latest !== undefined && { latest },
37144
+ nextAction
37145
+ };
37146
+ }
37147
+ server.registerTool("chit_job_status", {
37148
+ description: "Show one background job: state (queued/running/completed/cancelled/failed, or derived `stale` when the worker is gone), current phase, loop id, iterations, last verdict, audit refs, and the latest iteration's changed files / workspace warnings / usage. Read-only.",
37149
+ inputSchema: { job_id: exports_external.string() }
37150
+ }, async ({ job_id }) => {
37151
+ const job = jobStore.get(job_id);
37152
+ if (!job)
37153
+ return errorResult(`unknown job_id ${job_id}`);
37154
+ return jsonResult(describeJob(job));
37155
+ });
37156
+ server.registerTool("chit_job_cancel", {
37157
+ 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.",
37158
+ inputSchema: { job_id: exports_external.string() }
37159
+ }, async ({ job_id }) => {
37160
+ const job = jobStore.get(job_id);
37161
+ if (!job)
37162
+ return errorResult(`unknown job_id ${job_id}`);
37163
+ if (job.state !== "queued" && job.state !== "running") {
37164
+ return jsonResult({
37165
+ jobId: job_id,
37166
+ state: job.state,
37167
+ cancelled: false,
37168
+ note: `job already ${job.state}`
37169
+ });
37170
+ }
37171
+ const updated = jobStore.update(job_id, (c) => ({
37172
+ ...c,
37173
+ cancelRequestedAt: new Date().toISOString(),
37174
+ ...c.state === "running" && { phase: "cancelling" }
37175
+ }));
37176
+ let signaled = false;
37177
+ if (!isStale(updated, Date.now()) && updated.pgid !== undefined && pidAlive(updated.pid)) {
37178
+ try {
37179
+ process.kill(-updated.pgid, "SIGTERM");
37180
+ signaled = true;
37181
+ } catch {}
37182
+ }
37183
+ return jsonResult({
37184
+ jobId: job_id,
37185
+ state: updated.state,
37186
+ cancelRequested: true,
37187
+ signaled,
37188
+ note: "cancellation requested; the worker stops at the next safe point and records a clean cancelled stop"
37189
+ });
36529
37190
  });
36530
37191
  async function startMcpServer() {
36531
37192
  await server.connect(new StdioServerTransport);
@@ -36755,9 +37416,9 @@ ${AUDIT_HELP}`);
36755
37416
  }
36756
37417
 
36757
37418
  // src/cli/doctor.ts
36758
- import { randomUUID as randomUUID3 } from "crypto";
36759
- import { mkdirSync as mkdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
36760
- import { join as join7 } from "path";
37419
+ import { randomUUID as randomUUID5 } from "crypto";
37420
+ import { mkdirSync as mkdirSync6, rmSync as rmSync7, writeFileSync as writeFileSync6 } from "fs";
37421
+ import { join as join9 } from "path";
36761
37422
  var defaultIO3 = {
36762
37423
  out: (s) => process.stdout.write(s),
36763
37424
  err: (s) => process.stderr.write(s)
@@ -36825,10 +37486,10 @@ function checkGitRepo(deps) {
36825
37486
  }
36826
37487
  function checkAuditDir(deps) {
36827
37488
  try {
36828
- mkdirSync5(deps.auditDir, { recursive: true });
36829
- const probeFile = join7(deps.auditDir, `.doctor-${randomUUID3()}`);
36830
- writeFileSync5(probeFile, "ok");
36831
- rmSync5(probeFile);
37489
+ mkdirSync6(deps.auditDir, { recursive: true });
37490
+ const probeFile = join9(deps.auditDir, `.doctor-${randomUUID5()}`);
37491
+ writeFileSync6(probeFile, "ok");
37492
+ rmSync7(probeFile);
36832
37493
  return { name: "audit dir", status: "pass", detail: `writable (${deps.auditDir})` };
36833
37494
  } catch (e) {
36834
37495
  return {
@@ -36946,7 +37607,7 @@ ${DOCTOR_HELP}`);
36946
37607
 
36947
37608
  // src/cli/loop-log.ts
36948
37609
  init_src();
36949
- import { resolve as resolve4 } from "path";
37610
+ import { resolve as resolve5 } from "path";
36950
37611
  var defaultIO4 = {
36951
37612
  out: (s) => process.stdout.write(s),
36952
37613
  err: (s) => process.stderr.write(s)
@@ -36959,12 +37620,13 @@ var ALLOWED = {
36959
37620
  "loop-id",
36960
37621
  "summary",
36961
37622
  "changed-files",
37623
+ "workspace-warnings",
36962
37624
  "checks-run",
36963
37625
  "verdict",
36964
37626
  "finding-count",
36965
37627
  "decision",
36966
37628
  "duration-ms",
36967
- "details-ref"
37629
+ "audit-ref"
36968
37630
  ],
36969
37631
  bools: []
36970
37632
  },
@@ -36976,7 +37638,8 @@ var LOOP_LOG_HELP = `chit loop-log <start|append|stop|show> [flags]
36976
37638
  start --scope <s> --task <t> --max-iterations <n> [--cwd <dir>] [--loop-id <id>] [--force]
36977
37639
  append --loop-id <id> --summary <t> --changed-files <json> --checks-run <t>
36978
37640
  --verdict <proceed|revise|block> --finding-count <n>
36979
- --decision <proceed|revise|block> --duration-ms <n> [--details-ref <r>] [--cwd <dir>]
37641
+ --decision <proceed|revise|block> --duration-ms <n>
37642
+ [--workspace-warnings <json>] [--audit-ref <r>] [--cwd <dir>]
36980
37643
  stop --loop-id <id> --status <converged|blocked|max-iterations|needs-decision> --reason <t> [--cwd <dir>]
36981
37644
  show --loop-id <id> [--json] [--cwd <dir>]
36982
37645
 
@@ -37026,15 +37689,15 @@ function intFlag(p, key, verb) {
37026
37689
  }
37027
37690
  return n;
37028
37691
  }
37029
- function parseChangedFiles(raw) {
37692
+ function parseStringArray(raw, flag) {
37030
37693
  let parsed;
37031
37694
  try {
37032
37695
  parsed = JSON.parse(raw);
37033
37696
  } catch {
37034
- throw new UsageError3("--changed-files must be a JSON array of strings");
37697
+ throw new UsageError3(`--${flag} must be a JSON array of strings`);
37035
37698
  }
37036
37699
  if (!Array.isArray(parsed) || parsed.some((e) => typeof e !== "string")) {
37037
- throw new UsageError3("--changed-files must be a JSON array of strings");
37700
+ throw new UsageError3(`--${flag} must be a JSON array of strings`);
37038
37701
  }
37039
37702
  return parsed;
37040
37703
  }
@@ -37053,6 +37716,11 @@ function renderLoop(records) {
37053
37716
  lines.push(` ${r.n}. ${r.implementSummary}`);
37054
37717
  lines.push(` ${r.changedFiles.length} files \xB7 check ${r.verdict.toUpperCase()} \xB7 ` + `${Math.round(r.checkDurationMs / 1000)}s \xB7 ${r.findingCount} findings`);
37055
37718
  lines.push(` decide ${r.decision}`);
37719
+ if (r.workspaceWarnings && r.workspaceWarnings.length > 0) {
37720
+ lines.push(` workspace: ${r.workspaceWarnings.length} warning(s)`);
37721
+ for (const w of r.workspaceWarnings)
37722
+ lines.push(` - ${w}`);
37723
+ }
37056
37724
  } else if (r.type === "stop") {
37057
37725
  lines.push("");
37058
37726
  lines.push(` stopped: ${r.status} (${r.reason}) \xB7 ${r.iterations} iters \xB7 ` + `${Math.round(r.totalElapsedMs / 1000)}s`);
@@ -37070,7 +37738,7 @@ function runLoopLog(argv, io = defaultIO4) {
37070
37738
  }
37071
37739
  try {
37072
37740
  const p = parseFlags(verb, argv.slice(1));
37073
- const cwd = resolve4(p.flags.cwd ?? ".");
37741
+ const cwd = resolve5(p.flags.cwd ?? ".");
37074
37742
  if (verb === "start") {
37075
37743
  const res = startLoop(cwd, {
37076
37744
  scope: req(p, "scope", verb),
@@ -37084,15 +37752,19 @@ function runLoopLog(argv, io = defaultIO4) {
37084
37752
  return 0;
37085
37753
  }
37086
37754
  if (verb === "append") {
37755
+ const warningsRaw = p.flags["workspace-warnings"];
37087
37756
  const res = appendIteration(cwd, req(p, "loop-id", verb), {
37088
37757
  implementSummary: req(p, "summary", verb),
37089
- changedFiles: parseChangedFiles(req(p, "changed-files", verb)),
37758
+ changedFiles: parseStringArray(req(p, "changed-files", verb), "changed-files"),
37759
+ ...warningsRaw !== undefined && {
37760
+ workspaceWarnings: parseStringArray(warningsRaw, "workspace-warnings")
37761
+ },
37090
37762
  checksRun: req(p, "checks-run", verb),
37091
37763
  verdict: req(p, "verdict", verb),
37092
37764
  findingCount: intFlag(p, "finding-count", verb),
37093
37765
  decision: req(p, "decision", verb),
37094
37766
  checkDurationMs: intFlag(p, "duration-ms", verb),
37095
- detailsRef: p.flags["details-ref"]
37767
+ auditRef: p.flags["audit-ref"]
37096
37768
  });
37097
37769
  io.out(`${JSON.stringify(res)}
37098
37770
  `);
@@ -37439,6 +38111,16 @@ async function runMain(argv) {
37439
38111
  return runAudit(argv.slice(1));
37440
38112
  if (argv[0] === "doctor")
37441
38113
  return runDoctor(argv.slice(1));
38114
+ if (argv[0] === "job-run") {
38115
+ const jobId = argv[1];
38116
+ if (!jobId) {
38117
+ process.stderr.write(`chit job-run: requires a <jobId>
38118
+ `);
38119
+ return 2;
38120
+ }
38121
+ await runJobWorker(jobId, { jobStore: new JobStore });
38122
+ return 0;
38123
+ }
37442
38124
  let args;
37443
38125
  try {
37444
38126
  args = parseArgs(argv);
@@ -37479,7 +38161,7 @@ ${HELP}`);
37479
38161
  }
37480
38162
  let manifestRaw;
37481
38163
  try {
37482
- manifestRaw = JSON.parse(readFileSync13(args.manifestPath, "utf-8"));
38164
+ manifestRaw = JSON.parse(readFileSync16(args.manifestPath, "utf-8"));
37483
38165
  } catch (e) {
37484
38166
  process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
37485
38167
  `);
@@ -37690,7 +38372,7 @@ ${HELP}`);
37690
38372
  `);
37691
38373
  return 2;
37692
38374
  }
37693
- const outputDir = args.outputDir ?? join12(homedir6(), ".claude", "skills");
38375
+ const outputDir = args.outputDir ?? join14(homedir8(), ".claude", "skills");
37694
38376
  const runtimePath = args.runtimePath ?? defaultRuntimePath();
37695
38377
  try {
37696
38378
  const result = installClaudeSkill({
@@ -37733,7 +38415,7 @@ ${HELP}`);
37733
38415
  }
37734
38416
  let raw2;
37735
38417
  try {
37736
- raw2 = JSON.parse(readFileSync13(args.manifestPath, "utf-8"));
38418
+ raw2 = JSON.parse(readFileSync16(args.manifestPath, "utf-8"));
37737
38419
  } catch (e) {
37738
38420
  process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
37739
38421
  `);
@@ -37857,7 +38539,8 @@ async function runStudio(args) {
37857
38539
  cwd: process.cwd(),
37858
38540
  explicitPath: args.manifestPath,
37859
38541
  registry: registry3,
37860
- lifecycle: buildStudioLifecycle()
38542
+ lifecycle: buildStudioLifecycle(),
38543
+ loopsDir: loopLogDir(process.cwd())
37861
38544
  });
37862
38545
  } catch (e) {
37863
38546
  if (e instanceof PathError2) {
@@ -37871,13 +38554,13 @@ async function runStudio(args) {
37871
38554
  `);
37872
38555
  process.stdout.write(`Press Ctrl-C to stop.
37873
38556
  `);
37874
- await new Promise((resolve6) => {
38557
+ await new Promise((resolve7) => {
37875
38558
  process.on("SIGINT", () => {
37876
38559
  process.stdout.write(`
37877
38560
  chit studio: stopped
37878
38561
  `);
37879
38562
  handle.stop();
37880
- resolve6();
38563
+ resolve7();
37881
38564
  });
37882
38565
  });
37883
38566
  return 0;