@chit-run/cli 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/chit.js +913 -135
  2. package/package.json +1 -1
package/dist/chit.js CHANGED
@@ -1473,6 +1473,24 @@ var init_parse = __esm(() => {
1473
1473
  TEMPLATE_REF_RE = /\{\{\s*([\w.]+)\s*\}\}/g;
1474
1474
  });
1475
1475
  // ../../packages/core/src/show.ts
1476
+ function participantPermissionDisplay(p) {
1477
+ if (p.permissions.filesystem === "read_only") {
1478
+ return {
1479
+ filesystem: "read_only",
1480
+ readOnlyEnforcement: p.enforcesReadOnly ? "enforced" : "NOT ENFORCED",
1481
+ readOnlyEnforcementClass: p.enforcesReadOnly ? "ok" : "warn"
1482
+ };
1483
+ }
1484
+ return {
1485
+ filesystem: "write",
1486
+ readOnlyEnforcement: p.enforcesReadOnly ? "not requested (adapter supports)" : "not requested (adapter cannot enforce)",
1487
+ readOnlyEnforcementClass: "info"
1488
+ };
1489
+ }
1490
+ function participantPermissionText(p) {
1491
+ const display = participantPermissionDisplay(p);
1492
+ return `filesystem=${display.filesystem} read_only_enforcement=${display.readOnlyEnforcement}`;
1493
+ }
1476
1494
  function configPairs(c) {
1477
1495
  const pairs = [
1478
1496
  ["model", c.model ?? "default"],
@@ -1547,8 +1565,7 @@ function renderAscii(m) {
1547
1565
  out.push("");
1548
1566
  out.push("participants:");
1549
1567
  for (const [pid, p] of Object.entries(m.participants)) {
1550
- const enforces = p.enforcesReadOnly ? "enforces=yes" : "enforces=NO";
1551
- out.push(` ${pid} agent=${p.agentId} session=${p.session} permissions=${p.permissions.filesystem} adapter=${p.adapter} ${enforces}`);
1568
+ out.push(` ${pid} agent=${p.agentId} session=${p.session} ${participantPermissionText(p)} adapter=${p.adapter}`);
1552
1569
  if (p.adapter === "unknown") {
1553
1570
  out.push(" config unresolved (unknown agent)");
1554
1571
  } else {
@@ -1637,7 +1654,7 @@ function renderHtml(m) {
1637
1654
  levelColumns.push(renderLevelColumn(m, level));
1638
1655
  }
1639
1656
  const participantsSection = Object.entries(m.participants).map(([pid, p]) => {
1640
- const enforceBadge = p.enforcesReadOnly ? '<span class="badge ok">enforces read-only</span>' : '<span class="badge warn">does not enforce</span>';
1657
+ const permission = participantPermissionDisplay(p);
1641
1658
  const configBadges = p.adapter === "unknown" ? '<span class="badge warn">config: unresolved (unknown agent)</span>' : configPairs(p.config).map(([k, v]) => `<span class="badge info">${escapeHtml(k)}: ${escapeHtml(v)}</span>`).join(`
1642
1659
  `);
1643
1660
  return `<div class="participant">
@@ -1646,8 +1663,8 @@ function renderHtml(m) {
1646
1663
  <span class="badge info">agent: ${escapeHtml(p.agentId)}</span>
1647
1664
  <span class="badge info">adapter: ${escapeHtml(p.adapter)}</span>
1648
1665
  <span class="badge info">session: ${escapeHtml(p.session)}</span>
1649
- <span class="badge info">filesystem: ${escapeHtml(p.permissions.filesystem)}</span>
1650
- ${enforceBadge}
1666
+ <span class="badge info">filesystem: ${escapeHtml(permission.filesystem)}</span>
1667
+ <span class="badge ${permission.readOnlyEnforcementClass}">read_only enforcement: ${escapeHtml(permission.readOnlyEnforcement)}</span>
1651
1668
  </div>
1652
1669
  <div class="participant-config">
1653
1670
  ${configBadges}
@@ -9967,12 +9984,12 @@ var init_dist = __esm(() => {
9967
9984
  });
9968
9985
 
9969
9986
  // ../studio/src/server/audit.ts
9970
- import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
9971
- import { homedir as homedir7 } from "os";
9972
- import { join as join10 } from "path";
9987
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
9988
+ import { homedir as homedir9 } from "os";
9989
+ import { join as join12 } from "path";
9973
9990
  function defaultAuditDir2() {
9974
- const xdg = process.env.XDG_STATE_HOME || join10(homedir7(), ".local", "state");
9975
- return join10(xdg, "chit", "audit");
9991
+ const xdg = process.env.XDG_STATE_HOME || join12(homedir9(), ".local", "state");
9992
+ return join12(xdg, "chit", "audit");
9976
9993
  }
9977
9994
  function blobRefs(e) {
9978
9995
  switch (e.type) {
@@ -9991,13 +10008,13 @@ function blobRefs(e) {
9991
10008
  function readAuditRun(auditDir, runId, includeBlobs) {
9992
10009
  if (!SAFE_RUN_ID2.test(runId))
9993
10010
  return { kind: "invalid-id" };
9994
- const runDir = join10(auditDir, "runs", runId);
9995
- const eventsPath = join10(runDir, "events.jsonl");
9996
- if (!existsSync9(eventsPath))
10011
+ const runDir = join12(auditDir, "runs", runId);
10012
+ const eventsPath = join12(runDir, "events.jsonl");
10013
+ if (!existsSync11(eventsPath))
9997
10014
  return { kind: "not-found" };
9998
10015
  let events2;
9999
10016
  try {
10000
- events2 = parseAuditLog(readFileSync12(eventsPath, "utf-8"));
10017
+ events2 = parseAuditLog(readFileSync13(eventsPath, "utf-8"));
10001
10018
  } catch (e) {
10002
10019
  if (e instanceof AuditEventError)
10003
10020
  return { kind: "invalid-log", message: e.message };
@@ -10005,15 +10022,15 @@ function readAuditRun(auditDir, runId, includeBlobs) {
10005
10022
  }
10006
10023
  if (!includeBlobs)
10007
10024
  return { kind: "ok", events: events2 };
10008
- const blobsDir = join10(runDir, "blobs");
10025
+ const blobsDir = join12(runDir, "blobs");
10009
10026
  const blobs = {};
10010
10027
  for (const e of events2) {
10011
10028
  for (const ref of blobRefs(e)) {
10012
10029
  if (!SHA256_HEX2.test(ref) || ref in blobs)
10013
10030
  continue;
10014
- const blobPath = join10(blobsDir, ref);
10015
- if (existsSync9(blobPath))
10016
- blobs[ref] = readFileSync12(blobPath, "utf-8");
10031
+ const blobPath = join12(blobsDir, ref);
10032
+ if (existsSync11(blobPath))
10033
+ blobs[ref] = readFileSync13(blobPath, "utf-8");
10017
10034
  }
10018
10035
  }
10019
10036
  return { kind: "ok", events: events2, blobs };
@@ -10071,12 +10088,12 @@ var init_auth = __esm(() => {
10071
10088
  });
10072
10089
 
10073
10090
  // ../studio/src/server/paths.ts
10074
- import { existsSync as existsSync10, statSync as statSync3 } from "fs";
10091
+ import { existsSync as existsSync12, statSync as statSync3 } from "fs";
10075
10092
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "path";
10076
10093
  function resolveExplicitPath(userPath, cwd) {
10077
10094
  const candidate = isAbsolute4(userPath) ? userPath : resolve6(cwd, userPath);
10078
10095
  const canonical = resolve6(candidate);
10079
- if (!existsSync10(canonical)) {
10096
+ if (!existsSync12(canonical)) {
10080
10097
  throw new PathError("not-found", `path "${userPath}" does not exist`);
10081
10098
  }
10082
10099
  if (!statSync3(canonical).isFile()) {
@@ -10097,11 +10114,11 @@ var init_paths = __esm(() => {
10097
10114
  });
10098
10115
 
10099
10116
  // ../studio/src/server/discovery.ts
10100
- import { readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
10101
- import { basename, join as join11, relative } from "path";
10117
+ import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
10118
+ import { basename, join as join13, relative } from "path";
10102
10119
  function safeParseChit(absolutePath) {
10103
10120
  try {
10104
- const raw2 = JSON.parse(readFileSync13(absolutePath, "utf-8"));
10121
+ const raw2 = JSON.parse(readFileSync14(absolutePath, "utf-8"));
10105
10122
  parseManifest(raw2);
10106
10123
  return true;
10107
10124
  } catch {
@@ -10121,14 +10138,14 @@ function discover(opts) {
10121
10138
  relPath: relPathFromCwd(absolutePath, opts.cwd)
10122
10139
  };
10123
10140
  }
10124
- const entries = readdirSync4(opts.cwd, { withFileTypes: true });
10141
+ const entries = readdirSync5(opts.cwd, { withFileTypes: true });
10125
10142
  const candidates = [];
10126
10143
  for (const entry of entries) {
10127
10144
  if (!entry.isFile())
10128
10145
  continue;
10129
10146
  if (!entry.name.endsWith(".json"))
10130
10147
  continue;
10131
- const absolutePath = join11(opts.cwd, entry.name);
10148
+ const absolutePath = join13(opts.cwd, entry.name);
10132
10149
  if (!safeParseChit(absolutePath))
10133
10150
  continue;
10134
10151
  candidates.push({
@@ -10154,7 +10171,7 @@ var init_discovery = __esm(() => {
10154
10171
 
10155
10172
  // ../studio/src/server/docs.ts
10156
10173
  import { createHash as createHash6 } from "crypto";
10157
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
10174
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
10158
10175
  import { basename as basename2, relative as relative2 } from "path";
10159
10176
  function canonicalize(draft) {
10160
10177
  return JSON.stringify(draft, null, "\t");
@@ -10187,7 +10204,7 @@ class DocStore {
10187
10204
  if (!entry)
10188
10205
  return null;
10189
10206
  try {
10190
- return hashRaw(readFileSync14(entry.absolutePath, "utf-8"));
10207
+ return hashRaw(readFileSync15(entry.absolutePath, "utf-8"));
10191
10208
  } catch {
10192
10209
  return null;
10193
10210
  }
@@ -10229,7 +10246,7 @@ class DocStore {
10229
10246
  return null;
10230
10247
  let raw2;
10231
10248
  try {
10232
- raw2 = readFileSync14(entry.absolutePath, "utf-8");
10249
+ raw2 = readFileSync15(entry.absolutePath, "utf-8");
10233
10250
  } catch (e) {
10234
10251
  const errorDoc = {
10235
10252
  id: docId,
@@ -10285,7 +10302,7 @@ class DocStore {
10285
10302
  return { kind: "not-found" };
10286
10303
  let currentRaw;
10287
10304
  try {
10288
- currentRaw = readFileSync14(entry.absolutePath, "utf-8");
10305
+ currentRaw = readFileSync15(entry.absolutePath, "utf-8");
10289
10306
  } catch {
10290
10307
  return { kind: "not-found" };
10291
10308
  }
@@ -10297,7 +10314,7 @@ class DocStore {
10297
10314
  const manifest = parseManifest(draft);
10298
10315
  const graphModel = buildGraphModel(manifest, this.registry, surface);
10299
10316
  const canonicalRaw = canonicalize(draft);
10300
- writeFileSync7(entry.absolutePath, canonicalRaw, "utf-8");
10317
+ writeFileSync8(entry.absolutePath, canonicalRaw, "utf-8");
10301
10318
  const newHash = hashRaw(canonicalRaw);
10302
10319
  return {
10303
10320
  kind: "saved",
@@ -10366,8 +10383,8 @@ var init_docs = __esm(() => {
10366
10383
  });
10367
10384
 
10368
10385
  // ../studio/src/server/loops.ts
10369
- import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync15 } from "fs";
10370
- import { join as join12 } from "path";
10386
+ import { existsSync as existsSync13, readdirSync as readdirSync6, readFileSync as readFileSync16 } from "fs";
10387
+ import { join as join14 } from "path";
10371
10388
  function summarize(loopId, records) {
10372
10389
  const header = records[0];
10373
10390
  if (header?.type !== "loop")
@@ -10385,11 +10402,11 @@ function summarize(loopId, records) {
10385
10402
  };
10386
10403
  }
10387
10404
  function readLoopFrom(dir, loopId) {
10388
- const path = join12(dir, `${loopId}.jsonl`);
10389
- if (!existsSync11(path))
10405
+ const path = join14(dir, `${loopId}.jsonl`);
10406
+ if (!existsSync13(path))
10390
10407
  return { kind: "not-found" };
10391
10408
  try {
10392
- const records = validateLoopLog(parseLoopLog(readFileSync15(path, "utf-8")));
10409
+ const records = validateLoopLog(parseLoopLog(readFileSync16(path, "utf-8")));
10393
10410
  const header = records[0];
10394
10411
  if (header?.type !== "loop" || header.loopId !== loopId) {
10395
10412
  return { kind: "invalid-log", message: "header loopId does not match the file name" };
@@ -10402,10 +10419,10 @@ function readLoopFrom(dir, loopId) {
10402
10419
  }
10403
10420
  }
10404
10421
  function listLoops(loopsDir) {
10405
- if (!loopsDir || !existsSync11(loopsDir))
10422
+ if (!loopsDir || !existsSync13(loopsDir))
10406
10423
  return [];
10407
10424
  const summaries = [];
10408
- for (const name of readdirSync5(loopsDir)) {
10425
+ for (const name of readdirSync6(loopsDir)) {
10409
10426
  if (!name.endsWith(".jsonl"))
10410
10427
  continue;
10411
10428
  const loopId = name.slice(0, -".jsonl".length);
@@ -10462,8 +10479,8 @@ __export(exports_server, {
10462
10479
  buildApp: () => buildApp,
10463
10480
  PathError: () => PathError
10464
10481
  });
10465
- import { existsSync as existsSync12 } from "fs";
10466
- import { join as join13 } from "path";
10482
+ import { existsSync as existsSync14 } from "fs";
10483
+ import { join as join15 } from "path";
10467
10484
  async function startStudio(opts) {
10468
10485
  const hostname3 = opts.hostname ?? "127.0.0.1";
10469
10486
  const requestedPort = opts.port ?? 0;
@@ -10516,8 +10533,8 @@ function buildApp(opts) {
10516
10533
  const asset = c.req.param("asset");
10517
10534
  if (!CLIENT_ASSETS.has(asset))
10518
10535
  return c.text("not found", 404);
10519
- const path = join13(opts.clientDistDir, asset);
10520
- if (!existsSync12(path)) {
10536
+ const path = join15(opts.clientDistDir, asset);
10537
+ if (!existsSync14(path)) {
10521
10538
  return c.text(`client bundle missing at ${path}. Run: bun run studio:build`, 503);
10522
10539
  }
10523
10540
  return new Response(Bun.file(path));
@@ -10692,15 +10709,15 @@ var init_server = __esm(() => {
10692
10709
  init_loops();
10693
10710
  init_token();
10694
10711
  init_paths();
10695
- CLIENT_DIST = join13(import.meta.dir, "..", "..", "dist", "client");
10712
+ CLIENT_DIST = join15(import.meta.dir, "..", "..", "dist", "client");
10696
10713
  CLIENT_ASSETS = new Set(["index.js", "index.css"]);
10697
10714
  });
10698
10715
 
10699
10716
  // src/cli/run.ts
10700
10717
  init_src();
10701
- import { readFileSync as readFileSync16 } from "fs";
10702
- import { homedir as homedir8 } from "os";
10703
- import { basename as basename3, dirname as dirname2, join as join14 } from "path";
10718
+ import { readFileSync as readFileSync17 } from "fs";
10719
+ import { homedir as homedir10 } from "os";
10720
+ import { basename as basename3, dirname as dirname3, join as join16 } from "path";
10704
10721
 
10705
10722
  // src/adapters/sanitize.ts
10706
10723
  var SENSITIVE_KEY = /key|token|secret|password|auth/i;
@@ -13372,7 +13389,7 @@ function uninstall(parentDir, name) {
13372
13389
 
13373
13390
  // src/surfaces/mcp/server.ts
13374
13391
  import { spawn } from "child_process";
13375
- import { readFileSync as readFileSync11 } from "fs";
13392
+ import { readFileSync as readFileSync12 } from "fs";
13376
13393
  import { isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
13377
13394
 
13378
13395
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v3/helpers/util.js
@@ -36010,6 +36027,7 @@ class StdioServerTransport {
36010
36027
  }
36011
36028
 
36012
36029
  // src/audit/reader.ts
36030
+ init_src();
36013
36031
  var USAGE_KEYS2 = [
36014
36032
  "inputTokens",
36015
36033
  "outputTokens",
@@ -36107,6 +36125,30 @@ function listAudit(store, limit) {
36107
36125
  summaries.sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
36108
36126
  return limit !== undefined ? summaries.slice(0, limit) : summaries;
36109
36127
  }
36128
+ function receiptTimelineEvent(e) {
36129
+ if (e.type !== "run.started")
36130
+ return e;
36131
+ const { participants: _participants, ...rest } = e;
36132
+ return rest;
36133
+ }
36134
+ function participantReceipts(snapshots) {
36135
+ if (snapshots === undefined)
36136
+ return;
36137
+ return Object.fromEntries(Object.entries(snapshots).map(([pid, p]) => {
36138
+ const permission = participantPermissionDisplay(p);
36139
+ return [
36140
+ pid,
36141
+ {
36142
+ agentId: p.agentId,
36143
+ adapter: p.adapter,
36144
+ session: p.session,
36145
+ filesystem: permission.filesystem,
36146
+ readOnlyEnforcement: permission.readOnlyEnforcement,
36147
+ config: p.config
36148
+ }
36149
+ ];
36150
+ }));
36151
+ }
36110
36152
  function readBody(store, runId, ref) {
36111
36153
  try {
36112
36154
  return store.readBlob(runId, ref);
@@ -36123,21 +36165,22 @@ function hiddenAdapterEventCount(events2) {
36123
36165
  function auditTimeline(store, runId, events2, opts) {
36124
36166
  const rows = opts.verbose ? events2 : events2.filter(isReceiptEvent);
36125
36167
  return rows.map((e) => {
36168
+ const row = receiptTimelineEvent(e);
36126
36169
  if (!opts.includeBodies)
36127
- return e;
36170
+ return row;
36128
36171
  if (e.type === "adapter.call.started") {
36129
- return { ...e, input: readBody(store, runId, e.inputBlob) };
36172
+ return { ...row, input: readBody(store, runId, e.inputBlob) };
36130
36173
  }
36131
36174
  if (e.type === "adapter.event" && e.rawBlob !== undefined) {
36132
- return { ...e, raw: readBody(store, runId, e.rawBlob) };
36175
+ return { ...row, raw: readBody(store, runId, e.rawBlob) };
36133
36176
  }
36134
36177
  if (e.type === "adapter.call.completed") {
36135
- return { ...e, output: readBody(store, runId, e.outputBlob) };
36178
+ return { ...row, output: readBody(store, runId, e.outputBlob) };
36136
36179
  }
36137
36180
  if (e.type === "step.completed" && e.outputBlob !== undefined) {
36138
- return { ...e, output: readBody(store, runId, e.outputBlob) };
36181
+ return { ...row, output: readBody(store, runId, e.outputBlob) };
36139
36182
  }
36140
- return e;
36183
+ return row;
36141
36184
  });
36142
36185
  }
36143
36186
  function showAudit(store, runId, opts) {
@@ -36151,7 +36194,7 @@ function showAudit(store, runId, opts) {
36151
36194
  out.incompleteReason = describeIncomplete(summary, events2);
36152
36195
  const started = events2.find((e) => e.type === "run.started");
36153
36196
  if (started?.type === "run.started" && started.participants !== undefined) {
36154
- out.participants = started.participants;
36197
+ out.participants = participantReceipts(started.participants);
36155
36198
  }
36156
36199
  if (!opts.verbose) {
36157
36200
  const hidden = hiddenAdapterEventCount(events2);
@@ -36162,6 +36205,579 @@ function showAudit(store, runId, opts) {
36162
36205
  return out;
36163
36206
  }
36164
36207
 
36208
+ // src/campaigns/plan.ts
36209
+ class PlanError extends Error {
36210
+ }
36211
+ var SAFE_TASK_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
36212
+ function normalizeClaim(claim, taskId) {
36213
+ const raw = claim.trim();
36214
+ if (raw === "")
36215
+ throw new PlanError(`task ${JSON.stringify(taskId)}: a claimedPath is empty`);
36216
+ if (raw.startsWith("/")) {
36217
+ throw new PlanError(`task ${JSON.stringify(taskId)}: claimedPath must be repo-relative, got ${JSON.stringify(claim)}`);
36218
+ }
36219
+ const dirGlob = raw.endsWith("/**");
36220
+ const dirSlash = !dirGlob && raw.endsWith("/");
36221
+ const body = dirGlob ? raw.slice(0, -3) : dirSlash ? raw.slice(0, -1) : raw;
36222
+ const segments = [];
36223
+ for (const seg of body.split("/")) {
36224
+ if (seg === "" || seg === ".")
36225
+ continue;
36226
+ if (seg === "..") {
36227
+ throw new PlanError(`task ${JSON.stringify(taskId)}: claimedPath may not contain "..": ${JSON.stringify(claim)}`);
36228
+ }
36229
+ segments.push(seg);
36230
+ }
36231
+ if (segments.length === 0) {
36232
+ throw new PlanError(`task ${JSON.stringify(taskId)}: claimedPath is empty after normalization: ${JSON.stringify(claim)}`);
36233
+ }
36234
+ const base = segments.join("/");
36235
+ return dirGlob ? `${base}/**` : dirSlash ? `${base}/` : base;
36236
+ }
36237
+ function planTasks(inputs) {
36238
+ if (inputs.length === 0)
36239
+ throw new PlanError("a campaign needs at least one task");
36240
+ const ids = new Set;
36241
+ for (const t of inputs) {
36242
+ if (!SAFE_TASK_ID.test(t.id)) {
36243
+ throw new PlanError(`invalid task id ${JSON.stringify(t.id)} (use [A-Za-z0-9][A-Za-z0-9_-]*)`);
36244
+ }
36245
+ if (ids.has(t.id))
36246
+ throw new PlanError(`duplicate task id ${JSON.stringify(t.id)}`);
36247
+ ids.add(t.id);
36248
+ if (!t.title?.trim())
36249
+ throw new PlanError(`task ${JSON.stringify(t.id)}: title is required`);
36250
+ if (!t.body?.trim())
36251
+ throw new PlanError(`task ${JSON.stringify(t.id)}: body is required`);
36252
+ }
36253
+ for (const t of inputs) {
36254
+ for (const dep of t.dependencies ?? []) {
36255
+ if (!ids.has(dep)) {
36256
+ throw new PlanError(`task ${JSON.stringify(t.id)} depends on unknown task ${JSON.stringify(dep)}`);
36257
+ }
36258
+ if (dep === t.id)
36259
+ throw new PlanError(`task ${JSON.stringify(t.id)} depends on itself`);
36260
+ }
36261
+ const claims = t.claimedPaths ?? [];
36262
+ if (claims.length === 0 && !t.allowPathOverlap) {
36263
+ throw new PlanError(`task ${JSON.stringify(t.id)}: claimedPaths is required (declare the paths it will touch), ` + "or set allowPathOverlap to run it without a declared footprint (it will run alone)");
36264
+ }
36265
+ }
36266
+ assertAcyclic(inputs);
36267
+ return inputs.map((t) => {
36268
+ const task = {
36269
+ id: t.id,
36270
+ title: t.title,
36271
+ body: t.body,
36272
+ status: "pending",
36273
+ dependencies: [...t.dependencies ?? []],
36274
+ claimedPaths: (t.claimedPaths ?? []).map((c) => normalizeClaim(c, t.id))
36275
+ };
36276
+ if (t.allowPathOverlap)
36277
+ task.allowPathOverlap = true;
36278
+ if (t.manifestPath !== undefined)
36279
+ task.manifestPath = t.manifestPath;
36280
+ return task;
36281
+ });
36282
+ }
36283
+ function assertAcyclic(inputs) {
36284
+ const deps = new Map(inputs.map((t) => [t.id, t.dependencies ?? []]));
36285
+ const state = new Map;
36286
+ const visit = (id, stack) => {
36287
+ const s = state.get(id);
36288
+ if (s === "done")
36289
+ return;
36290
+ if (s === "visiting") {
36291
+ const cycle = [...stack.slice(stack.indexOf(id)), id].join(" -> ");
36292
+ throw new PlanError(`dependency cycle: ${cycle}`);
36293
+ }
36294
+ state.set(id, "visiting");
36295
+ for (const dep of deps.get(id) ?? [])
36296
+ visit(dep, [...stack, id]);
36297
+ state.set(id, "done");
36298
+ };
36299
+ for (const t of inputs)
36300
+ visit(t.id, []);
36301
+ }
36302
+ function resolveManifestPath(task, campaignDefault) {
36303
+ return task.manifestPath ?? campaignDefault;
36304
+ }
36305
+
36306
+ // src/campaigns/overlap.ts
36307
+ function shape(claim) {
36308
+ if (claim.endsWith("/**"))
36309
+ return { base: claim.slice(0, -3), isDir: true };
36310
+ if (claim.endsWith("/"))
36311
+ return { base: claim.slice(0, -1), isDir: true };
36312
+ return { base: claim, isDir: false };
36313
+ }
36314
+ function pathsOverlap(a, b) {
36315
+ const sa = shape(a);
36316
+ const sb = shape(b);
36317
+ if (sa.base === sb.base)
36318
+ return true;
36319
+ if (sa.isDir && (sb.base === sa.base || sb.base.startsWith(`${sa.base}/`)))
36320
+ return true;
36321
+ if (sb.isDir && (sa.base === sb.base || sa.base.startsWith(`${sb.base}/`)))
36322
+ return true;
36323
+ return false;
36324
+ }
36325
+ function tasksClaimsOverlap(a, b) {
36326
+ if (a.allowPathOverlap || b.allowPathOverlap)
36327
+ return true;
36328
+ return a.claimedPaths.some((pa) => b.claimedPaths.some((pb) => pathsOverlap(pa, pb)));
36329
+ }
36330
+
36331
+ // src/campaigns/types.ts
36332
+ var ACTIVE_TASK_STATUSES = new Set(["running"]);
36333
+ var DEPENDENCY_SATISFIED_STATUSES = new Set([
36334
+ "review_ready"
36335
+ ]);
36336
+ var MAX_PARALLEL_CAP = 4;
36337
+
36338
+ // src/campaigns/schedule.ts
36339
+ function byId(tasks) {
36340
+ return new Map(tasks.map((t) => [t.id, t]));
36341
+ }
36342
+ function depsSatisfied(task, index) {
36343
+ return task.dependencies.every((dep) => {
36344
+ const d = index.get(dep);
36345
+ return d !== undefined && DEPENDENCY_SATISFIED_STATUSES.has(d.status);
36346
+ });
36347
+ }
36348
+ function isBlocked(task, campaign) {
36349
+ if (task.status !== "pending")
36350
+ return false;
36351
+ const index = byId(campaign.tasks);
36352
+ return task.dependencies.some((dep) => {
36353
+ const d = index.get(dep);
36354
+ return d !== undefined && (d.status === "failed" || d.status === "cancelled");
36355
+ });
36356
+ }
36357
+ function isStartable(task, campaign) {
36358
+ return task.status === "pending" && depsSatisfied(task, byId(campaign.tasks));
36359
+ }
36360
+ function selectRunnable(campaign) {
36361
+ const index = byId(campaign.tasks);
36362
+ const active = campaign.tasks.filter((t) => ACTIVE_TASK_STATUSES.has(t.status));
36363
+ let freeSlots = Math.max(0, campaign.maxParallel - active.length);
36364
+ if (freeSlots === 0)
36365
+ return [];
36366
+ const selected = [];
36367
+ const blockers = [...active];
36368
+ for (const task of campaign.tasks) {
36369
+ if (freeSlots === 0)
36370
+ break;
36371
+ if (task.status !== "pending")
36372
+ continue;
36373
+ if (!depsSatisfied(task, index))
36374
+ continue;
36375
+ if (blockers.some((b) => tasksClaimsOverlap(task, b)))
36376
+ continue;
36377
+ selected.push(task);
36378
+ blockers.push(task);
36379
+ freeSlots--;
36380
+ }
36381
+ return selected;
36382
+ }
36383
+ function deriveCampaignStatus(campaign) {
36384
+ const tasks = campaign.tasks;
36385
+ if (tasks.length === 0)
36386
+ return "ready_for_review";
36387
+ const anyActive = tasks.some((t) => ACTIVE_TASK_STATUSES.has(t.status));
36388
+ const anyStartable = tasks.some((t) => isStartable(t, campaign));
36389
+ if (anyActive || anyStartable)
36390
+ return "running";
36391
+ const stuckPending = tasks.some((t) => t.status === "pending");
36392
+ const anyReviewReady = tasks.some((t) => t.status === "review_ready");
36393
+ const anyFailed = tasks.some((t) => t.status === "failed");
36394
+ if (stuckPending)
36395
+ return "needs_human";
36396
+ if (anyReviewReady)
36397
+ return "ready_for_review";
36398
+ if (anyFailed)
36399
+ return "failed";
36400
+ return "ready_for_review";
36401
+ }
36402
+
36403
+ // src/campaigns/worktree.ts
36404
+ import { execFileSync as execFileSync2 } from "child_process";
36405
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
36406
+ import { homedir as homedir7 } from "os";
36407
+ import { dirname as dirname2, join as join9 } from "path";
36408
+
36409
+ class WorktreeError extends Error {
36410
+ }
36411
+ var realGit = (args, cwd) => {
36412
+ try {
36413
+ const stdout = execFileSync2("git", args, {
36414
+ cwd,
36415
+ encoding: "utf-8",
36416
+ stdio: ["ignore", "pipe", "pipe"]
36417
+ });
36418
+ return { code: 0, stdout, stderr: "" };
36419
+ } catch (e) {
36420
+ const err = e;
36421
+ return {
36422
+ code: typeof err.status === "number" ? err.status : 1,
36423
+ stdout: String(err.stdout ?? ""),
36424
+ stderr: String(err.stderr ?? "")
36425
+ };
36426
+ }
36427
+ };
36428
+ function gitErr(r) {
36429
+ return (r.stderr || r.stdout || `exit ${r.code}`).trim();
36430
+ }
36431
+ function taskWorktree(campaignId, taskId) {
36432
+ return {
36433
+ worktreePath: join9(homedir7(), "worktrees", "chit", campaignId, taskId),
36434
+ branch: `chit-campaign/${campaignId}/${taskId}`
36435
+ };
36436
+ }
36437
+ function resolveBaseSha(git, repo, ref) {
36438
+ const r = git(["rev-parse", ref], repo);
36439
+ if (r.code !== 0) {
36440
+ throw new WorktreeError(`cannot resolve base ref ${JSON.stringify(ref)}: ${gitErr(r)}`);
36441
+ }
36442
+ return r.stdout.trim();
36443
+ }
36444
+ function repoToplevel(git, cwd) {
36445
+ const r = git(["rev-parse", "--show-toplevel"], cwd);
36446
+ if (r.code !== 0) {
36447
+ throw new WorktreeError(`not a git repository at ${JSON.stringify(cwd)}: ${gitErr(r)}`);
36448
+ }
36449
+ return r.stdout.trim();
36450
+ }
36451
+ function createTaskWorktree(git, repo, campaignId, taskId, baseSha) {
36452
+ const { worktreePath, branch } = taskWorktree(campaignId, taskId);
36453
+ if (git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], repo).code === 0) {
36454
+ throw new WorktreeError(`branch ${JSON.stringify(branch)} already exists`);
36455
+ }
36456
+ if (existsSync9(worktreePath)) {
36457
+ throw new WorktreeError(`worktree path already exists: ${worktreePath}`);
36458
+ }
36459
+ mkdirSync6(dirname2(worktreePath), { recursive: true });
36460
+ const r = git(["worktree", "add", "-b", branch, worktreePath, baseSha], repo);
36461
+ if (r.code !== 0) {
36462
+ throw new WorktreeError(`git worktree add failed: ${gitErr(r)}`);
36463
+ }
36464
+ return { worktreePath, branch };
36465
+ }
36466
+
36467
+ // src/campaigns/engine.ts
36468
+ class CampaignEngineError extends Error {
36469
+ }
36470
+ var DEFAULT_MAX_ITERATIONS = 3;
36471
+ function iso3(ms) {
36472
+ return new Date(ms).toISOString();
36473
+ }
36474
+ function startCampaign(store, deps, opts) {
36475
+ const tasks = planTasks(opts.tasks);
36476
+ const repo = repoToplevel(deps.git, opts.cwd);
36477
+ const baseBranch = opts.baseBranch ?? "HEAD";
36478
+ const baseSha = resolveBaseSha(deps.git, repo, baseBranch);
36479
+ const maxParallel = Math.max(1, Math.min(opts.maxParallel, MAX_PARALLEL_CAP));
36480
+ const maxIterations = opts.maxIterations ?? DEFAULT_MAX_ITERATIONS;
36481
+ const now = iso3(deps.now());
36482
+ const campaign = {
36483
+ schema: 1,
36484
+ id: opts.id,
36485
+ repo,
36486
+ repoKey: repoKey(opts.cwd),
36487
+ baseBranch,
36488
+ baseSha,
36489
+ maxParallel,
36490
+ ...opts.manifestPath !== undefined && { manifestPath: opts.manifestPath },
36491
+ status: "planning",
36492
+ tasks,
36493
+ createdAt: now,
36494
+ updatedAt: now
36495
+ };
36496
+ store.create(campaign);
36497
+ return store.update(opts.id, (c) => launchWave(c, deps, maxIterations));
36498
+ }
36499
+ function advanceCampaign(store, deps, id, maxIterations = DEFAULT_MAX_ITERATIONS) {
36500
+ const existing = store.get(id);
36501
+ if (!existing)
36502
+ throw new CampaignEngineError(`no campaign ${JSON.stringify(id)}`);
36503
+ if (existing.status === "cancelled")
36504
+ return existing;
36505
+ return store.update(id, (c) => launchWave(reconcile(c, deps), deps, maxIterations));
36506
+ }
36507
+ function cancelCampaign(store, deps, id) {
36508
+ const existing = store.get(id);
36509
+ if (!existing)
36510
+ throw new CampaignEngineError(`no campaign ${JSON.stringify(id)}`);
36511
+ return store.update(id, (c) => {
36512
+ for (const t of c.tasks) {
36513
+ if (t.status === "running" && t.jobId) {
36514
+ try {
36515
+ deps.cancelJob(t.jobId);
36516
+ } catch {}
36517
+ t.status = "cancelled";
36518
+ } else if (t.status === "pending") {
36519
+ t.status = "cancelled";
36520
+ }
36521
+ }
36522
+ c.status = "cancelled";
36523
+ c.updatedAt = iso3(deps.now());
36524
+ return c;
36525
+ });
36526
+ }
36527
+ function reconcile(c, deps) {
36528
+ for (const t of c.tasks) {
36529
+ if (t.status !== "running" || !t.jobId)
36530
+ continue;
36531
+ const job = deps.getJob(t.jobId);
36532
+ if (!job) {
36533
+ settleTask(t, "failed", deps, { failure: "job record not found" });
36534
+ continue;
36535
+ }
36536
+ const inFlight = job.state === "queued" || job.state === "running";
36537
+ if (inFlight) {
36538
+ if (deps.isStale(job)) {
36539
+ settleTask(t, "failed", deps, { job, failure: "worker appears dead (stale job)" });
36540
+ }
36541
+ continue;
36542
+ }
36543
+ if (job.state === "completed" && job.stopStatus === "converged") {
36544
+ settleTask(t, "review_ready", deps, { job });
36545
+ } else if (job.state === "cancelled") {
36546
+ settleTask(t, "cancelled", deps, { job });
36547
+ } else {
36548
+ settleTask(t, "failed", deps, {
36549
+ job,
36550
+ failure: job.failure ?? `did not converge (${job.stopStatus ?? "failed"})`
36551
+ });
36552
+ }
36553
+ }
36554
+ return c;
36555
+ }
36556
+ function jobIsSettleable(job, deps) {
36557
+ if (job.state === "queued" || job.state === "running")
36558
+ return deps.isStale(job);
36559
+ return true;
36560
+ }
36561
+ function settleTask(t, status, deps, extra) {
36562
+ t.status = status;
36563
+ const detail = t.worktreePath && t.jobId ? deps.loopDetail(t.worktreePath, jobLoopId(t, extra.job)) : undefined;
36564
+ const result = {
36565
+ iterations: extra.job?.iterationsCompleted ?? 0,
36566
+ changedFiles: detail?.changedFiles ?? [],
36567
+ workspaceWarnings: detail?.workspaceWarnings ?? [],
36568
+ auditRefs: extra.job?.auditRefs ?? []
36569
+ };
36570
+ if (extra.job?.stopStatus !== undefined)
36571
+ result.stopStatus = extra.job.stopStatus;
36572
+ if (extra.job?.lastVerdict !== undefined)
36573
+ result.lastVerdict = extra.job.lastVerdict;
36574
+ t.result = result;
36575
+ if (status === "failed" && extra.failure !== undefined)
36576
+ t.error = extra.failure;
36577
+ }
36578
+ function jobLoopId(t, job) {
36579
+ return job?.loopId ?? t.id;
36580
+ }
36581
+ function launchWave(c, deps, maxIterations) {
36582
+ if (c.status === "cancelled")
36583
+ return c;
36584
+ const runnable = selectRunnable(c);
36585
+ for (const task of runnable) {
36586
+ const t = c.tasks.find((x) => x.id === task.id);
36587
+ if (!t)
36588
+ continue;
36589
+ try {
36590
+ const { worktreePath, branch } = deps.createWorktree(c.repo, c.id, t.id, c.baseSha);
36591
+ const loopId = `${c.id}-${t.id}`;
36592
+ const { jobId } = deps.launchJob({
36593
+ cwd: worktreePath,
36594
+ scope: `campaign-${c.id}-${t.id}`,
36595
+ task: t.body,
36596
+ loopId,
36597
+ ...resolveManifestPath(t, c.manifestPath) !== undefined && {
36598
+ manifestPath: resolveManifestPath(t, c.manifestPath)
36599
+ },
36600
+ maxIterations
36601
+ });
36602
+ t.worktreePath = worktreePath;
36603
+ t.branch = branch;
36604
+ t.jobId = jobId;
36605
+ t.status = "running";
36606
+ } catch (e) {
36607
+ t.status = "failed";
36608
+ t.error = e instanceof WorktreeError ? e.message : e.message;
36609
+ }
36610
+ }
36611
+ c.status = deriveCampaignStatus(c);
36612
+ c.updatedAt = iso3(deps.now());
36613
+ return c;
36614
+ }
36615
+ function describeCampaign(c, deps) {
36616
+ const tasks = c.tasks.map((t) => {
36617
+ const view = {
36618
+ id: t.id,
36619
+ title: t.title,
36620
+ status: t.status,
36621
+ dependencies: t.dependencies,
36622
+ ...t.branch !== undefined && { branch: t.branch },
36623
+ ...t.worktreePath !== undefined && { worktreePath: t.worktreePath },
36624
+ ...t.jobId !== undefined && { jobId: t.jobId }
36625
+ };
36626
+ if (t.status === "running" && t.jobId) {
36627
+ const job = deps.getJob(t.jobId);
36628
+ if (job) {
36629
+ view.jobState = job.state === "running" && deps.isStale(job) ? "stale" : job.state;
36630
+ if (job.phase !== undefined)
36631
+ view.phase = job.phase;
36632
+ }
36633
+ }
36634
+ if (t.result) {
36635
+ if (t.result.stopStatus !== undefined)
36636
+ view.stopStatus = t.result.stopStatus;
36637
+ if (t.result.lastVerdict !== undefined)
36638
+ view.lastVerdict = t.result.lastVerdict;
36639
+ view.changedFiles = t.result.changedFiles;
36640
+ view.workspaceWarnings = t.result.workspaceWarnings;
36641
+ view.auditRefs = t.result.auditRefs;
36642
+ }
36643
+ if (t.error !== undefined)
36644
+ view.error = t.error;
36645
+ return view;
36646
+ });
36647
+ const runnable = selectRunnable(c);
36648
+ const anyReconcilable = c.tasks.some((t) => {
36649
+ if (t.status !== "running" || !t.jobId)
36650
+ return false;
36651
+ const job = deps.getJob(t.jobId);
36652
+ return job !== undefined && jobIsSettleable(job, deps);
36653
+ });
36654
+ const startableBlocked = c.tasks.filter((t) => isStartable(t, c)).length;
36655
+ const blocked = c.tasks.filter((t) => isBlocked(t, c)).length;
36656
+ let nextAction;
36657
+ if (c.status === "cancelled") {
36658
+ nextAction = "campaign cancelled";
36659
+ } else if (c.status === "ready_for_review") {
36660
+ nextAction = "all tasks terminal; review the task worktrees (chit_campaign_status lists them)";
36661
+ } else if (c.status === "needs_human") {
36662
+ nextAction = `${blocked} task(s) blocked by a failed/cancelled dependency; inspect and start a fresh campaign for them`;
36663
+ } else if (runnable.length > 0 || anyReconcilable) {
36664
+ const n = runnable.length;
36665
+ nextAction = anyReconcilable && n === 0 ? "a job finished; call chit_campaign_advance to reconcile and launch newly runnable task(s)" : `call chit_campaign_advance to launch ${n} runnable task(s)`;
36666
+ } else {
36667
+ nextAction = "tasks in flight; poll chit_campaign_status, or chit_campaign_cancel to stop";
36668
+ }
36669
+ return {
36670
+ id: c.id,
36671
+ repo: c.repo,
36672
+ baseBranch: c.baseBranch,
36673
+ baseSha: c.baseSha,
36674
+ maxParallel: c.maxParallel,
36675
+ status: c.status,
36676
+ tasks,
36677
+ runnableCount: runnable.length,
36678
+ nextAction,
36679
+ createdAt: c.createdAt,
36680
+ updatedAt: c.updatedAt
36681
+ };
36682
+ }
36683
+
36684
+ // src/campaigns/store.ts
36685
+ import { randomUUID as randomUUID5 } from "crypto";
36686
+ import {
36687
+ existsSync as existsSync10,
36688
+ mkdirSync as mkdirSync7,
36689
+ readdirSync as readdirSync4,
36690
+ readFileSync as readFileSync11,
36691
+ renameSync as renameSync4,
36692
+ rmSync as rmSync7,
36693
+ writeFileSync as writeFileSync6
36694
+ } from "fs";
36695
+ import { homedir as homedir8 } from "os";
36696
+ import { join as join10 } from "path";
36697
+ class CampaignStoreError extends Error {
36698
+ }
36699
+ var SAFE_CAMPAIGN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
36700
+ function campaignsDir(cwd) {
36701
+ const xdg = process.env.XDG_STATE_HOME || join10(homedir8(), ".local", "state");
36702
+ return join10(xdg, "chit", "campaigns", repoKey(cwd));
36703
+ }
36704
+
36705
+ class CampaignStore {
36706
+ cwd;
36707
+ constructor(cwd) {
36708
+ this.cwd = cwd;
36709
+ }
36710
+ dir() {
36711
+ return campaignsDir(this.cwd);
36712
+ }
36713
+ path(id) {
36714
+ if (!SAFE_CAMPAIGN_ID.test(id)) {
36715
+ throw new CampaignStoreError(`invalid campaign id ${JSON.stringify(id)}`);
36716
+ }
36717
+ return join10(this.dir(), `${id}.json`);
36718
+ }
36719
+ lockPath(id) {
36720
+ return `${this.path(id)}.lock`;
36721
+ }
36722
+ create(campaign) {
36723
+ mkdirSync7(this.dir(), { recursive: true });
36724
+ const path = this.path(campaign.id);
36725
+ withFileLock(this.lockPath(campaign.id), () => {
36726
+ if (existsSync10(path)) {
36727
+ throw new CampaignStoreError(`campaign ${JSON.stringify(campaign.id)} already exists`);
36728
+ }
36729
+ writeAtomic2(path, campaign);
36730
+ });
36731
+ }
36732
+ get(id) {
36733
+ const path = this.path(id);
36734
+ if (!existsSync10(path))
36735
+ return;
36736
+ try {
36737
+ return JSON.parse(readFileSync11(path, "utf-8"));
36738
+ } catch {
36739
+ return;
36740
+ }
36741
+ }
36742
+ update(id, mutate) {
36743
+ const path = this.path(id);
36744
+ mkdirSync7(this.dir(), { recursive: true });
36745
+ return withFileLock(this.lockPath(id), () => {
36746
+ if (!existsSync10(path))
36747
+ throw new CampaignStoreError(`no campaign ${JSON.stringify(id)}`);
36748
+ const current = JSON.parse(readFileSync11(path, "utf-8"));
36749
+ const next = mutate(current);
36750
+ writeAtomic2(path, next);
36751
+ return next;
36752
+ });
36753
+ }
36754
+ list() {
36755
+ const dir = this.dir();
36756
+ if (!existsSync10(dir))
36757
+ return [];
36758
+ const out = [];
36759
+ for (const name of readdirSync4(dir)) {
36760
+ if (!name.endsWith(".json"))
36761
+ continue;
36762
+ try {
36763
+ out.push(JSON.parse(readFileSync11(join10(dir, name), "utf-8")));
36764
+ } catch {}
36765
+ }
36766
+ out.sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
36767
+ return out;
36768
+ }
36769
+ }
36770
+ function writeAtomic2(path, campaign) {
36771
+ const tmp = `${path}.${randomUUID5()}.tmp`;
36772
+ writeFileSync6(tmp, JSON.stringify(campaign, null, 2));
36773
+ try {
36774
+ renameSync4(tmp, path);
36775
+ } catch (err) {
36776
+ rmSync7(tmp, { force: true });
36777
+ throw err;
36778
+ }
36779
+ }
36780
+
36165
36781
  // src/jobs/health.ts
36166
36782
  var STALE_AFTER_MS = 60000;
36167
36783
  function pidAlive(pid) {
@@ -36799,7 +37415,7 @@ server.registerTool("chit_run_start", {
36799
37415
  const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(process.cwd(), manifest_path);
36800
37416
  let raw;
36801
37417
  try {
36802
- raw = JSON.parse(readFileSync11(path, "utf-8"));
37418
+ raw = JSON.parse(readFileSync12(path, "utf-8"));
36803
37419
  } catch (e) {
36804
37420
  return errorResult(`could not read manifest at ${path}: ${e.message}`);
36805
37421
  }
@@ -36951,7 +37567,7 @@ server.registerTool("chit_converge_start", {
36951
37567
  if (manifest_path) {
36952
37568
  const path = isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path);
36953
37569
  try {
36954
- raw = JSON.parse(readFileSync11(path, "utf-8"));
37570
+ raw = JSON.parse(readFileSync12(path, "utf-8"));
36955
37571
  } catch (e) {
36956
37572
  return errorResult(`could not read manifest at ${path}: ${e.message}`);
36957
37573
  }
@@ -37121,64 +37737,54 @@ function spawnJobWorker(jobId, cwd) {
37121
37737
  });
37122
37738
  child.unref();
37123
37739
  }
37124
- server.registerTool("chit_converge_run", {
37125
- description: "Start an autonomous converge loop as a BACKGROUND job (a detached worker advances it; you keep chatting). Returns immediately with a job_id and loop_id. Inspect with chit_job_status / chit_status, stop with chit_job_cancel. Use the foreground chit_converge_start/next instead when you want to checkpoint each iteration. v1 starts a NEW loop only: an existing loop_id is refused (use chit_converge_next to continue a foreground loop, or force=true / a new loop_id).",
37126
- inputSchema: {
37127
- task: exports_external.string().describe("The slice to converge on"),
37128
- scope: exports_external.string().describe("Session scope id; both agents keep their thread across iterations"),
37129
- cwd: exports_external.string().optional().describe("Repo to run in (defaults to the server cwd)"),
37130
- manifest_path: exports_external.string().optional().describe("Converge manifest path (absolute or relative to cwd). Default: the built-in."),
37131
- max_iterations: exports_external.number().int().min(1).default(3).describe("Iteration budget. Default 3."),
37132
- loop_id: exports_external.string().optional().describe("Seed a loop id. Default: generated."),
37133
- force: exports_external.boolean().default(false).describe("Overwrite an existing loop log at this loop_id rather than refusing."),
37134
- allow_unenforced_permissions: exports_external.boolean().default(false).describe("Run even when a declared permission cannot be enforced (emits warnings).")
37135
- }
37136
- }, async ({
37137
- task,
37138
- scope,
37139
- cwd,
37140
- manifest_path,
37141
- max_iterations,
37142
- loop_id,
37143
- force,
37144
- allow_unenforced_permissions
37145
- }) => {
37146
- const runCwd = resolve4(cwd ?? process.cwd());
37740
+ function launchConvergeJob(p) {
37147
37741
  let raw;
37148
37742
  let manifestAbs;
37149
- if (manifest_path) {
37150
- manifestAbs = isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path);
37743
+ if (p.manifestPath) {
37744
+ manifestAbs = isAbsolute3(p.manifestPath) ? p.manifestPath : resolve4(p.cwd, p.manifestPath);
37151
37745
  try {
37152
- raw = JSON.parse(readFileSync11(manifestAbs, "utf-8"));
37746
+ raw = JSON.parse(readFileSync12(manifestAbs, "utf-8"));
37153
37747
  } catch (e) {
37154
- return errorResult(`could not read manifest at ${manifestAbs}: ${e.message}`);
37748
+ return {
37749
+ ok: false,
37750
+ error: `could not read manifest at ${manifestAbs}: ${e.message}`
37751
+ };
37155
37752
  }
37156
37753
  } else {
37157
37754
  raw = DEFAULT_CONVERGE_MANIFEST;
37158
37755
  }
37159
- const prep = prepareConvergeExecute(raw, getRegistry(), scope, runCwd, allow_unenforced_permissions);
37756
+ const prep = prepareConvergeExecute(raw, getRegistry(), p.scope, p.cwd, p.allowUnenforced);
37160
37757
  if (!prep.ok)
37161
- return errorResult(prep.error);
37162
- const loopId = loop_id ?? crypto.randomUUID();
37758
+ return { ok: false, error: prep.error };
37759
+ const loopId = p.loopId ?? crypto.randomUUID();
37163
37760
  try {
37164
- startLoop(runCwd, { scope, task, maxIterations: max_iterations, loopId, force });
37761
+ startLoop(p.cwd, {
37762
+ scope: p.scope,
37763
+ task: p.task,
37764
+ maxIterations: p.maxIterations,
37765
+ loopId,
37766
+ force: p.force
37767
+ });
37165
37768
  } catch (e) {
37166
37769
  if (e instanceof LoopStoreError) {
37167
- return 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.`);
37770
+ return {
37771
+ ok: false,
37772
+ error: `${e.message}. Use chit_converge_next to continue a foreground loop, or start a background job with force=true or a new loop_id.`
37773
+ };
37168
37774
  }
37169
- return errorResult(e.message);
37775
+ return { ok: false, error: e.message };
37170
37776
  }
37171
37777
  const jobId = crypto.randomUUID();
37172
37778
  const job = {
37173
37779
  jobId,
37174
37780
  loopId,
37175
- repoKey: repoKey(runCwd),
37176
- cwd: runCwd,
37177
- scope,
37178
- task,
37781
+ repoKey: repoKey(p.cwd),
37782
+ cwd: p.cwd,
37783
+ scope: p.scope,
37784
+ task: p.task,
37179
37785
  ...manifestAbs !== undefined && { manifestPath: manifestAbs },
37180
- maxIterations: max_iterations,
37181
- allowUnenforced: allow_unenforced_permissions,
37786
+ maxIterations: p.maxIterations,
37787
+ allowUnenforced: p.allowUnenforced,
37182
37788
  state: "queued",
37183
37789
  createdAt: new Date().toISOString(),
37184
37790
  iterationsCompleted: 0,
@@ -37187,11 +37793,11 @@ server.registerTool("chit_converge_run", {
37187
37793
  try {
37188
37794
  jobStore.create(job);
37189
37795
  } catch (e) {
37190
- stopLoop(runCwd, loopId, { status: "blocked", reason: "could not create job record" });
37191
- return errorResult(e.message);
37796
+ stopLoop(p.cwd, loopId, { status: "blocked", reason: "could not create job record" });
37797
+ return { ok: false, error: e.message };
37192
37798
  }
37193
37799
  try {
37194
- spawnJobWorker(jobId, runCwd);
37800
+ spawnJobWorker(jobId, p.cwd);
37195
37801
  } catch (e) {
37196
37802
  jobStore.update(jobId, (c) => ({
37197
37803
  ...c,
@@ -37199,16 +37805,53 @@ server.registerTool("chit_converge_run", {
37199
37805
  failure: `could not spawn worker: ${e.message}`,
37200
37806
  endedAt: new Date().toISOString()
37201
37807
  }));
37202
- stopLoop(runCwd, loopId, { status: "blocked", reason: "worker spawn failed" });
37203
- return errorResult(`could not spawn background worker: ${e.message}`);
37808
+ stopLoop(p.cwd, loopId, { status: "blocked", reason: "worker spawn failed" });
37809
+ return { ok: false, error: `could not spawn background worker: ${e.message}` };
37810
+ }
37811
+ return { ok: true, jobId, loopId, warnings: prep.warnings };
37812
+ }
37813
+ server.registerTool("chit_converge_run", {
37814
+ description: "Start an autonomous converge loop as a BACKGROUND job (a detached worker advances it; you keep chatting). Returns immediately with a job_id and loop_id. Inspect with chit_job_status / chit_status, stop with chit_job_cancel. Use the foreground chit_converge_start/next instead when you want to checkpoint each iteration. v1 starts a NEW loop only: an existing loop_id is refused (use chit_converge_next to continue a foreground loop, or force=true / a new loop_id).",
37815
+ inputSchema: {
37816
+ task: exports_external.string().describe("The slice to converge on"),
37817
+ scope: exports_external.string().describe("Session scope id; both agents keep their thread across iterations"),
37818
+ cwd: exports_external.string().optional().describe("Repo to run in (defaults to the server cwd)"),
37819
+ manifest_path: exports_external.string().optional().describe("Converge manifest path (absolute or relative to cwd). Default: the built-in."),
37820
+ max_iterations: exports_external.number().int().min(1).default(3).describe("Iteration budget. Default 3."),
37821
+ loop_id: exports_external.string().optional().describe("Seed a loop id. Default: generated."),
37822
+ force: exports_external.boolean().default(false).describe("Overwrite an existing loop log at this loop_id rather than refusing."),
37823
+ allow_unenforced_permissions: exports_external.boolean().default(false).describe("Run even when a declared permission cannot be enforced (emits warnings).")
37204
37824
  }
37825
+ }, async ({
37826
+ task,
37827
+ scope,
37828
+ cwd,
37829
+ manifest_path,
37830
+ max_iterations,
37831
+ loop_id,
37832
+ force,
37833
+ allow_unenforced_permissions
37834
+ }) => {
37835
+ const runCwd = resolve4(cwd ?? process.cwd());
37836
+ const r = launchConvergeJob({
37837
+ task,
37838
+ scope,
37839
+ cwd: runCwd,
37840
+ ...manifest_path !== undefined && { manifestPath: manifest_path },
37841
+ maxIterations: max_iterations,
37842
+ ...loop_id !== undefined && { loopId: loop_id },
37843
+ force,
37844
+ allowUnenforced: allow_unenforced_permissions
37845
+ });
37846
+ if (!r.ok)
37847
+ return errorResult(r.error);
37205
37848
  return jsonResult({
37206
- jobId,
37207
- loopId,
37849
+ jobId: r.jobId,
37850
+ loopId: r.loopId,
37208
37851
  repo: repoRoot(runCwd),
37209
37852
  state: "queued",
37210
- nextAction: `running in the background; poll chit_job_status "${jobId}" (or chit_status), cancel with chit_job_cancel "${jobId}"`,
37211
- ...prep.warnings.length > 0 && { warnings: prep.warnings }
37853
+ nextAction: `running in the background; poll chit_job_status "${r.jobId}" (or chit_status), cancel with chit_job_cancel "${r.jobId}"`,
37854
+ ...r.warnings.length > 0 && { warnings: r.warnings }
37212
37855
  });
37213
37856
  });
37214
37857
  function describeJob(job) {
@@ -37266,6 +37909,27 @@ function describeJob(job) {
37266
37909
  nextAction
37267
37910
  };
37268
37911
  }
37912
+ function requestJobCancel(jobId) {
37913
+ const job = jobStore.get(jobId);
37914
+ if (!job)
37915
+ return { status: "missing" };
37916
+ if (job.state !== "queued" && job.state !== "running") {
37917
+ return { status: "terminal", state: job.state };
37918
+ }
37919
+ const updated = jobStore.update(jobId, (c) => ({
37920
+ ...c,
37921
+ cancelRequestedAt: new Date().toISOString(),
37922
+ ...c.state === "running" && { phase: "cancelling" }
37923
+ }));
37924
+ let signaled = false;
37925
+ if (!isStale(updated, Date.now()) && updated.pgid !== undefined && pidAlive(updated.pid)) {
37926
+ try {
37927
+ process.kill(-updated.pgid, "SIGTERM");
37928
+ signaled = true;
37929
+ } catch {}
37930
+ }
37931
+ return { status: "requested", state: updated.state, signaled };
37932
+ }
37269
37933
  server.registerTool("chit_job_status", {
37270
37934
  description: "Show one background job: state (queued/running/completed/cancelled/failed, or derived `stale` when the worker is gone), current phase, timing fields (elapsedMs, lastHeartbeatAgeMs, phaseElapsedMs), loop id, iterations, last verdict, audit refs, and the latest iteration's changed files / workspace warnings / usage. Read-only.",
37271
37935
  inputSchema: { job_id: exports_external.string() }
@@ -37279,37 +37943,152 @@ server.registerTool("chit_job_cancel", {
37279
37943
  description: "Cancel a background job from any turn. Persists the cancel intent FIRST (so it survives a worker restart), then signals the worker's process group. A queued job is cancelled before it starts; a running job stops at the next safe point and records a clean `cancelled` stop. A job that already finished is reported back unchanged.",
37280
37944
  inputSchema: { job_id: exports_external.string() }
37281
37945
  }, async ({ job_id }) => {
37282
- const job = jobStore.get(job_id);
37283
- if (!job)
37946
+ const r = requestJobCancel(job_id);
37947
+ if (r.status === "missing")
37284
37948
  return errorResult(`unknown job_id ${job_id}`);
37285
- if (job.state !== "queued" && job.state !== "running") {
37949
+ if (r.status === "terminal") {
37286
37950
  return jsonResult({
37287
37951
  jobId: job_id,
37288
- state: job.state,
37952
+ state: r.state,
37289
37953
  cancelled: false,
37290
- note: `job already ${job.state}`
37954
+ note: `job already ${r.state}`
37291
37955
  });
37292
37956
  }
37293
- const updated = jobStore.update(job_id, (c) => ({
37294
- ...c,
37295
- cancelRequestedAt: new Date().toISOString(),
37296
- ...c.state === "running" && { phase: "cancelling" }
37297
- }));
37298
- let signaled = false;
37299
- if (!isStale(updated, Date.now()) && updated.pgid !== undefined && pidAlive(updated.pid)) {
37300
- try {
37301
- process.kill(-updated.pgid, "SIGTERM");
37302
- signaled = true;
37303
- } catch {}
37304
- }
37305
37957
  return jsonResult({
37306
37958
  jobId: job_id,
37307
- state: updated.state,
37959
+ state: r.state,
37308
37960
  cancelRequested: true,
37309
- signaled,
37961
+ signaled: r.signaled,
37310
37962
  note: "cancellation requested; the worker stops at the next safe point and records a clean cancelled stop"
37311
37963
  });
37312
37964
  });
37965
+ var campaignDeps = {
37966
+ git: realGit,
37967
+ createWorktree: (repo, cid, tid, sha) => createTaskWorktree(realGit, repo, cid, tid, sha),
37968
+ launchJob: (p) => {
37969
+ const r = launchConvergeJob({
37970
+ task: p.task,
37971
+ scope: p.scope,
37972
+ cwd: p.cwd,
37973
+ ...p.manifestPath !== undefined && { manifestPath: p.manifestPath },
37974
+ maxIterations: p.maxIterations,
37975
+ loopId: p.loopId,
37976
+ allowUnenforced: false
37977
+ });
37978
+ if (!r.ok)
37979
+ throw new Error(r.error);
37980
+ return { jobId: r.jobId, loopId: r.loopId };
37981
+ },
37982
+ getJob: (id) => jobStore.get(id),
37983
+ cancelJob: (id) => {
37984
+ requestJobCancel(id);
37985
+ },
37986
+ isStale: (job) => isStale(job, Date.now()),
37987
+ loopDetail: (worktreePath, loopId) => {
37988
+ try {
37989
+ const iters = readLoop(worktreePath, loopId).filter((r) => r.type === "iteration");
37990
+ const last = iters.at(-1);
37991
+ if (last && last.type === "iteration") {
37992
+ return { changedFiles: last.changedFiles, workspaceWarnings: last.workspaceWarnings ?? [] };
37993
+ }
37994
+ } catch {}
37995
+ return { changedFiles: [], workspaceWarnings: [] };
37996
+ },
37997
+ now: () => Date.now()
37998
+ };
37999
+ var campaignTaskSchema = exports_external.object({
38000
+ id: exports_external.string().describe("Unique task id within the campaign (a safe slug)"),
38001
+ title: exports_external.string().describe("Short task title"),
38002
+ body: exports_external.string().describe("The task brief handed to the converge implementer"),
38003
+ dependencies: exports_external.array(exports_external.string()).optional().describe("Task ids that must reach review_ready before this task runs"),
38004
+ claimedPaths: exports_external.array(exports_external.string()).optional().describe("Paths this task will touch (globs: dir/**, dir/, or a file). Required unless allowPathOverlap; tasks with overlapping claims never run concurrently."),
38005
+ allowPathOverlap: exports_external.boolean().optional().describe("Opt-in to running with no/overlapping claims; the task then runs alone."),
38006
+ manifestPath: exports_external.string().optional().describe("Per-task converge manifest override (absolute or relative to cwd).")
38007
+ });
38008
+ function campaignError(e) {
38009
+ if (e instanceof PlanError || e instanceof WorktreeError || e instanceof CampaignStoreError) {
38010
+ return errorResult(e.message);
38011
+ }
38012
+ return errorResult(e.message);
38013
+ }
38014
+ server.registerTool("chit_campaign_start", {
38015
+ description: "Start a campaign: run several converge tasks in parallel, each in its own git worktree, as background jobs. Plans the task graph, launches the initial runnable wave (no-dependency tasks, up to max_parallel), and returns immediately. Then poll chit_campaign_status and call chit_campaign_advance to launch the next wave as jobs finish. No auto-merge: the output is reviewable worktree branches. Manifest resolution per task: task.manifestPath > campaign manifest_path > the bundled default converge manifest.",
38016
+ inputSchema: {
38017
+ tasks: exports_external.array(campaignTaskSchema).min(1).describe("The task graph (an explicit, reviewed list)"),
38018
+ cwd: exports_external.string().optional().describe("Any path in the target repo (defaults to the server cwd)"),
38019
+ max_parallel: exports_external.number().int().min(1).default(2).describe("Max concurrent tasks. Default 2."),
38020
+ base_branch: exports_external.string().optional().describe("Ref task worktrees branch from. Default: HEAD."),
38021
+ manifest_path: exports_external.string().optional().describe("Campaign-level default converge manifest (absolute or relative to cwd)."),
38022
+ max_iterations: exports_external.number().int().min(1).default(3).describe("Per-task iteration budget. Default 3.")
38023
+ }
38024
+ }, async ({ tasks, cwd, max_parallel, base_branch, manifest_path, max_iterations }) => {
38025
+ const runCwd = resolve4(cwd ?? process.cwd());
38026
+ const campaignManifest = manifest_path !== undefined ? isAbsolute3(manifest_path) ? manifest_path : resolve4(runCwd, manifest_path) : undefined;
38027
+ const planned = tasks.map((t) => ({
38028
+ ...t,
38029
+ ...t.manifestPath !== undefined && {
38030
+ manifestPath: isAbsolute3(t.manifestPath) ? t.manifestPath : resolve4(runCwd, t.manifestPath)
38031
+ }
38032
+ }));
38033
+ const store = new CampaignStore(runCwd);
38034
+ try {
38035
+ const campaign = startCampaign(store, campaignDeps, {
38036
+ id: crypto.randomUUID(),
38037
+ cwd: runCwd,
38038
+ tasks: planned,
38039
+ maxParallel: max_parallel,
38040
+ ...base_branch !== undefined && { baseBranch: base_branch },
38041
+ ...campaignManifest !== undefined && { manifestPath: campaignManifest },
38042
+ maxIterations: max_iterations
38043
+ });
38044
+ return jsonResult(describeCampaign(campaign, campaignDeps));
38045
+ } catch (e) {
38046
+ return campaignError(e);
38047
+ }
38048
+ });
38049
+ server.registerTool("chit_campaign_status", {
38050
+ description: "Read-only campaign overview: each task's status, live job state/phase, branch/worktree, changed files, audit refs, plus how many tasks are runnable now and the next action. Inspection is safe: this NEVER launches jobs, creates worktrees, or mutates state (use chit_campaign_advance to make progress).",
38051
+ inputSchema: {
38052
+ campaign_id: exports_external.string(),
38053
+ cwd: exports_external.string().optional().describe("Any path in the target repo (defaults to the server cwd)")
38054
+ }
38055
+ }, async ({ campaign_id, cwd }) => {
38056
+ const store = new CampaignStore(resolve4(cwd ?? process.cwd()));
38057
+ const campaign = store.get(campaign_id);
38058
+ if (!campaign)
38059
+ return errorResult(`unknown campaign_id ${campaign_id}`);
38060
+ return jsonResult(describeCampaign(campaign, campaignDeps));
38061
+ });
38062
+ server.registerTool("chit_campaign_advance", {
38063
+ description: "Advance a campaign: reconcile finished jobs into task state (converged -> review_ready; blocked/max-iterations/failed/stale -> failed; dependents proceed only past a review_ready task), then launch the next runnable wave. The only progression trigger besides start. Call it when chit_campaign_status reports runnable tasks or a finished job.",
38064
+ inputSchema: {
38065
+ campaign_id: exports_external.string(),
38066
+ cwd: exports_external.string().optional().describe("Any path in the target repo (defaults to the server cwd)")
38067
+ }
38068
+ }, async ({ campaign_id, cwd }) => {
38069
+ const store = new CampaignStore(resolve4(cwd ?? process.cwd()));
38070
+ try {
38071
+ const campaign = advanceCampaign(store, campaignDeps, campaign_id);
38072
+ return jsonResult(describeCampaign(campaign, campaignDeps));
38073
+ } catch (e) {
38074
+ return campaignError(e);
38075
+ }
38076
+ });
38077
+ server.registerTool("chit_campaign_cancel", {
38078
+ description: "Cancel a campaign: request cancellation of every active task job (intent-first, the same safety as chit_job_cancel) and mark pending tasks cancelled. Running jobs settle cleanly in the background. Worktrees are left in place for inspection.",
38079
+ inputSchema: {
38080
+ campaign_id: exports_external.string(),
38081
+ cwd: exports_external.string().optional().describe("Any path in the target repo (defaults to the server cwd)")
38082
+ }
38083
+ }, async ({ campaign_id, cwd }) => {
38084
+ const store = new CampaignStore(resolve4(cwd ?? process.cwd()));
38085
+ try {
38086
+ const campaign = cancelCampaign(store, campaignDeps, campaign_id);
38087
+ return jsonResult(describeCampaign(campaign, campaignDeps));
38088
+ } catch (e) {
38089
+ return campaignError(e);
38090
+ }
38091
+ });
37313
38092
  async function startMcpServer() {
37314
38093
  await server.connect(new StdioServerTransport);
37315
38094
  }
@@ -37459,8 +38238,7 @@ participants (recorded config):
37459
38238
  `);
37460
38239
  } else {
37461
38240
  for (const [pid, p] of Object.entries(snapshots)) {
37462
- const enforces = p.enforcesReadOnly ? "enforces=yes" : "enforces=NO";
37463
- io.out(` ${pid} agent=${p.agentId} session=${p.session} permissions=${p.permissions.filesystem} adapter=${p.adapter} ${enforces}
38241
+ io.out(` ${pid} agent=${p.agentId} session=${p.session} ${participantPermissionText(p)} adapter=${p.adapter}
37464
38242
  `);
37465
38243
  const pairs = p.adapter === "unknown" ? "unresolved (unknown agent)" : configPairs(p.config).map(([k, v]) => `${k}=${v}`).join(" ");
37466
38244
  io.out(` config ${pairs}
@@ -37538,9 +38316,9 @@ ${AUDIT_HELP}`);
37538
38316
  }
37539
38317
 
37540
38318
  // src/cli/doctor.ts
37541
- import { randomUUID as randomUUID5 } from "crypto";
37542
- import { mkdirSync as mkdirSync6, rmSync as rmSync7, writeFileSync as writeFileSync6 } from "fs";
37543
- import { join as join9 } from "path";
38319
+ import { randomUUID as randomUUID6 } from "crypto";
38320
+ import { mkdirSync as mkdirSync8, rmSync as rmSync8, writeFileSync as writeFileSync7 } from "fs";
38321
+ import { join as join11 } from "path";
37544
38322
  var defaultIO3 = {
37545
38323
  out: (s) => process.stdout.write(s),
37546
38324
  err: (s) => process.stderr.write(s)
@@ -37608,10 +38386,10 @@ function checkGitRepo(deps) {
37608
38386
  }
37609
38387
  function checkAuditDir(deps) {
37610
38388
  try {
37611
- mkdirSync6(deps.auditDir, { recursive: true });
37612
- const probeFile = join9(deps.auditDir, `.doctor-${randomUUID5()}`);
37613
- writeFileSync6(probeFile, "ok");
37614
- rmSync7(probeFile);
38389
+ mkdirSync8(deps.auditDir, { recursive: true });
38390
+ const probeFile = join11(deps.auditDir, `.doctor-${randomUUID6()}`);
38391
+ writeFileSync7(probeFile, "ok");
38392
+ rmSync8(probeFile);
37615
38393
  return { name: "audit dir", status: "pass", detail: `writable (${deps.auditDir})` };
37616
38394
  } catch (e) {
37617
38395
  return {
@@ -38284,7 +39062,7 @@ ${HELP}`);
38284
39062
  }
38285
39063
  let manifestRaw;
38286
39064
  try {
38287
- manifestRaw = JSON.parse(readFileSync16(args.manifestPath, "utf-8"));
39065
+ manifestRaw = JSON.parse(readFileSync17(args.manifestPath, "utf-8"));
38288
39066
  } catch (e) {
38289
39067
  process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
38290
39068
  `);
@@ -38455,7 +39233,7 @@ Pass --allow-unenforced-permissions to run anyway (emits a warning each run).
38455
39233
  return 1;
38456
39234
  }
38457
39235
  function defaultRuntimePath() {
38458
- return dirname2(dirname2(import.meta.dir));
39236
+ return dirname3(dirname3(import.meta.dir));
38459
39237
  }
38460
39238
  var TRACE_PREVIEW_CHARS = 280;
38461
39239
  function tracePreview(label, text) {
@@ -38495,7 +39273,7 @@ ${HELP}`);
38495
39273
  `);
38496
39274
  return 2;
38497
39275
  }
38498
- const outputDir = args.outputDir ?? join14(homedir8(), ".claude", "skills");
39276
+ const outputDir = args.outputDir ?? join16(homedir10(), ".claude", "skills");
38499
39277
  const runtimePath = args.runtimePath ?? defaultRuntimePath();
38500
39278
  try {
38501
39279
  const result = installClaudeSkill({
@@ -38538,7 +39316,7 @@ ${HELP}`);
38538
39316
  }
38539
39317
  let raw2;
38540
39318
  try {
38541
- raw2 = JSON.parse(readFileSync16(args.manifestPath, "utf-8"));
39319
+ raw2 = JSON.parse(readFileSync17(args.manifestPath, "utf-8"));
38542
39320
  } catch (e) {
38543
39321
  process.stderr.write(`chit: failed to read manifest ${args.manifestPath}: ${e.message}
38544
39322
  `);