@coreplane/switchboard 1.201.0 → 1.202.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 (28) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/deploy/cloudflare-resident/refresh.ts +238 -0
  3. package/dist/assets/deploy/cloudflare-resident/shared.ts +80 -0
  4. package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
  5. package/dist/assets/deploy/cloudflare-resident/worker.ts +67 -285
  6. package/dist/assets/deploy/cloudflare-resident/wrangler.template.jsonc +2 -1
  7. package/dist/assets/package-lock.json +480 -3
  8. package/dist/assets/package.json +1 -1
  9. package/dist/assets/project.json +1 -1
  10. package/dist/assets/source.json +3 -3
  11. package/dist/assets/src/core/authz/policy.ts +3 -0
  12. package/dist/assets/src/execution/residentRefresh.ts +29 -0
  13. package/dist/assets/web/dist/.vite/manifest.json +43 -43
  14. package/dist/assets/web/dist/assets/{AppShell-CjDyTiip.js → AppShell-CJLPO_aI.js} +1 -1
  15. package/dist/assets/web/dist/assets/{CostsPage-CMAh3G74.js → CostsPage-C6ZvnAmH.js} +1 -1
  16. package/dist/assets/web/dist/assets/{NotFoundPage-CbEKm0tY.js → NotFoundPage-BdLcY60r.js} +1 -1
  17. package/dist/assets/web/dist/assets/{ResidentDetailPage-CcfmOAIP.js → ResidentDetailPage-hXWrr8T6.js} +1 -1
  18. package/dist/assets/web/dist/assets/{ResidentsIndexPage-9Z5jcteM.js → ResidentsIndexPage-YLu4JwxJ.js} +1 -1
  19. package/dist/assets/web/dist/assets/{RunRoutePage-Cdimh4e0.js → RunRoutePage-BDhHRJVO.js} +1 -1
  20. package/dist/assets/web/dist/assets/{RunsIndexPage-CvgDXrO4.js → RunsIndexPage-D7zZ5a0l.js} +1 -1
  21. package/dist/assets/web/dist/assets/{RunsTabs-rEjVQG6H.js → RunsTabs-BOUlSa2W.js} +1 -1
  22. package/dist/assets/web/dist/assets/{ScheduledPage-C5bG9qvd.js → ScheduledPage-BciOQcC-.js} +1 -1
  23. package/dist/assets/web/dist/assets/{StatusDot-D_IPK3WP.js → StatusDot-CTDLBv92.js} +1 -1
  24. package/dist/assets/web/dist/assets/{Tooltip-JlZp3OVC.js → Tooltip-DD2v9Gxx.js} +1 -1
  25. package/dist/assets/web/dist/assets/{favicon-DZDjc8ab.js → favicon-C4Q8cRsP.js} +1 -1
  26. package/dist/assets/web/dist/assets/{main-BKKzFAnK.js → main-DrSlUcMg.js} +2 -2
  27. package/dist/cli.js +629 -207
  28. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -544,6 +544,9 @@ var init_policy = __esm({
544
544
  { action: "deploy:write", resource: "command", when: [grant("deploy:write")] },
545
545
  { action: "env:write", resource: "command", when: [grant("env:write")] },
546
546
  { action: "setup:write", resource: "command", when: [grant("setup:write")] },
547
+ // `contract render` reads host paths the caller names: CLI-only, the read
548
+ // grant by name — no chat baseline holds it.
549
+ { action: "contract:read", resource: "command", when: [grant("contract:read")] },
547
550
  // ── mcp (external MCP servers live in the three config tiers) ────────────
548
551
  { action: "mcp:read", resource: "command", when: [grant("mcp:read")] },
549
552
  { action: "mcp:write", resource: "command", when: [grant("mcp:write")] },
@@ -1181,6 +1184,301 @@ var init_schedules = __esm({
1181
1184
  }
1182
1185
  });
1183
1186
 
1187
+ // ../../src/core/ship/contract.ts
1188
+ function parsePlanUnit(planMarkdown, unitId) {
1189
+ const lines = planMarkdown.split("\n");
1190
+ const start2 = lines.findIndex((l) => {
1191
+ const m = UNIT_HEADING.exec(l);
1192
+ return m !== null && m[1] === unitId;
1193
+ });
1194
+ if (start2 < 0) return void 0;
1195
+ let end = lines.findIndex((l, i) => i > start2 && ANY_HEADING.test(l));
1196
+ if (end < 0) end = lines.length;
1197
+ const body = lines.slice(start2, end);
1198
+ while (body.length > 1 && /^\s*$/.test(body[body.length - 1])) body.pop();
1199
+ if (body.length > 1 && /^---\s*$/.test(body[body.length - 1])) body.pop();
1200
+ while (body.length > 1 && /^\s*$/.test(body[body.length - 1])) body.pop();
1201
+ const heading = UNIT_HEADING.exec(body[0]);
1202
+ return { id: heading[1], title: heading[2], section: body.join("\n"), bullets: parseBullets(body.slice(1)) };
1203
+ }
1204
+ function planUnitIds(planMarkdown) {
1205
+ return planMarkdown.split("\n").map((l) => UNIT_HEADING.exec(l)?.[1]).filter((id) => id !== void 0);
1206
+ }
1207
+ function bulletSpans(sectionLines) {
1208
+ const spans = [];
1209
+ for (let i = 1; i < sectionLines.length; i++) {
1210
+ const m = BULLET.exec(sectionLines[i]);
1211
+ if (!m) continue;
1212
+ if (spans.length > 0) spans[spans.length - 1].end = i;
1213
+ spans.push({ key: m[1], start: i, end: sectionLines.length });
1214
+ }
1215
+ return spans;
1216
+ }
1217
+ function bulletBody(lines) {
1218
+ const first = BULLET.exec(lines[0]);
1219
+ const head = first[2] ?? "";
1220
+ const rest = lines.slice(1);
1221
+ while (rest.length > 0 && /^\s*$/.test(rest[rest.length - 1])) rest.pop();
1222
+ if (rest.length === 0) return head.trim();
1223
+ const indent = Math.min(...rest.filter((l) => l.trim() !== "").map((l) => /^\s*/.exec(l)[0].length));
1224
+ const dedented = rest.map((l) => l.slice(Math.min(indent, /^\s*/.exec(l)[0].length)));
1225
+ return [head.trim(), ...dedented].filter((l, i) => i > 0 || l !== "").join("\n");
1226
+ }
1227
+ function parseBullets(bodyLines) {
1228
+ const withHeading = ["", ...bodyLines];
1229
+ const out = {};
1230
+ for (const span of bulletSpans(withHeading)) out[span.key] = bulletBody(withHeading.slice(span.start, span.end));
1231
+ return out;
1232
+ }
1233
+ function expandItemList(list) {
1234
+ const out = [];
1235
+ const ranged = /(\d+)\s*(?:–|-|to)\s*(\d+)/g;
1236
+ let rest = list;
1237
+ for (const m of list.matchAll(ranged)) {
1238
+ const from = Number(m[1]);
1239
+ const to = Number(m[2]);
1240
+ if (to >= from && to - from <= 100) for (let n2 = from; n2 <= to; n2++) out.push(n2);
1241
+ rest = rest.replace(m[0], " ");
1242
+ }
1243
+ for (const m of rest.matchAll(/\d+/g)) out.push(Number(m[0]));
1244
+ return out;
1245
+ }
1246
+ function specNameOf(mention) {
1247
+ const m = /^(?:docs\/reference\/specs\/)?([a-z0-9-]+\.md)$/.exec(mention);
1248
+ return m?.[1];
1249
+ }
1250
+ function specItemRefs(section) {
1251
+ const seen = /* @__PURE__ */ new Set();
1252
+ const refs = [];
1253
+ const add = (spec, item) => {
1254
+ const key = `${spec}#${item}`;
1255
+ if (seen.has(key)) return;
1256
+ seen.add(key);
1257
+ refs.push({ spec, item });
1258
+ };
1259
+ const named = [];
1260
+ for (const m of section.matchAll(NAMED_REF)) {
1261
+ const spec = m[1] ?? m[2] ?? m[3];
1262
+ named.push({ index: m.index, end: m.index + m[0].length, spec, items: expandItemList(m[4]) });
1263
+ }
1264
+ const mentions = [...section.matchAll(ANY_MD_MENTION)].map((m) => ({ index: m.index, spec: specNameOf(m[0]) }));
1265
+ const bare = [];
1266
+ for (const m of section.matchAll(ITEM_LIST)) {
1267
+ if (named.some((n2) => m.index >= n2.index && m.index < n2.end)) continue;
1268
+ bare.push({ index: m.index, items: expandItemList(m[1]) });
1269
+ }
1270
+ const all = [
1271
+ ...named.map((n2) => ({ index: n2.index, spec: n2.spec, items: n2.items })),
1272
+ ...bare.map((b) => ({
1273
+ index: b.index,
1274
+ spec: mentions.filter((x) => x.index < b.index).at(-1)?.spec,
1275
+ items: b.items
1276
+ }))
1277
+ ].sort((a, b) => a.index - b.index);
1278
+ for (const ref of all) if (ref.spec !== void 0) for (const item of ref.items) add(ref.spec, item);
1279
+ return refs;
1280
+ }
1281
+ function parseSpecItem(specMarkdown, item) {
1282
+ const lines = specMarkdown.split("\n");
1283
+ const start2 = lines.findIndex((l) => Number(SPEC_ITEM.exec(l)?.[1]) === item);
1284
+ if (start2 < 0) return void 0;
1285
+ let end = lines.findIndex((l, i) => i > start2 && (SPEC_ITEM.test(l) || /^#{1,3} /.test(l)));
1286
+ if (end < 0) end = lines.length;
1287
+ const body = lines.slice(start2, end);
1288
+ while (body.length > 1 && /^\s*$/.test(body[body.length - 1])) body.pop();
1289
+ return body.join("\n");
1290
+ }
1291
+ function parseValidationRows(specMarkdown) {
1292
+ const rows = [];
1293
+ for (const line2 of specMarkdown.split("\n")) {
1294
+ if (!line2.startsWith("|")) continue;
1295
+ const cells = line2.trim().replace(/^\||\|$/g, "").split(/(?<!\\)\|/).map((c) => c.trim());
1296
+ if (cells.length < 2) continue;
1297
+ if (/^-+$/.test(cells[0]) || cells[0] === "Criterion" && cells[1] === "Proof") continue;
1298
+ rows.push({ criterion: cells[0], proof: cells.slice(1).join(" | ") });
1299
+ }
1300
+ return rows;
1301
+ }
1302
+ function itemNumbersNamed(criterion) {
1303
+ const out = /* @__PURE__ */ new Set();
1304
+ const lead = /^(\d+(?:\s*[,/]\s*\d+)*)\s*:/.exec(criterion);
1305
+ if (lead) for (const n2 of expandItemList(lead[1])) out.add(n2);
1306
+ for (const m of criterion.matchAll(ITEM_LIST)) for (const n2 of expandItemList(m[1])) out.add(n2);
1307
+ return out;
1308
+ }
1309
+ function resolveSpecRow(ref, specMarkdown) {
1310
+ if (specMarkdown === void 0) return { ...ref, specRead: false, text: void 0, validation: [] };
1311
+ return {
1312
+ ...ref,
1313
+ specRead: true,
1314
+ text: parseSpecItem(specMarkdown, ref.item),
1315
+ validation: parseValidationRows(specMarkdown).filter((row) => itemNumbersNamed(row.criterion).has(ref.item))
1316
+ };
1317
+ }
1318
+ function contractFromPlan(input) {
1319
+ const unit = parsePlanUnit(input.planMarkdown, input.unitId);
1320
+ if (!unit) {
1321
+ const ids = planUnitIds(input.planMarkdown);
1322
+ throw new Error(
1323
+ `the plan has no unit ${input.unitId} (its units: ${ids.length > 0 ? ids.join(", ") : "none \u2014 no `### U<n>.` heading"})`
1324
+ );
1325
+ }
1326
+ return {
1327
+ unit,
1328
+ specRows: specItemRefs(unit.section).map((ref) => resolveSpecRow(ref, input.readSpec(ref.spec))),
1329
+ agentRules: input.agentRules,
1330
+ guards: GUARDS,
1331
+ rebase: { branch: input.rebase?.branch, onto: input.rebase?.onto }
1332
+ };
1333
+ }
1334
+ function unitBody(unit, dropApproach) {
1335
+ const lines = unit.section.split("\n");
1336
+ if (dropApproach) {
1337
+ const span = bulletSpans(lines).find((s) => s.key === "Approach");
1338
+ if (span) lines.splice(span.start, span.end - span.start, APPROACH_DROPPED);
1339
+ }
1340
+ return lines.slice(1).join("\n").trim();
1341
+ }
1342
+ function hasApproach(unit) {
1343
+ return "Approach" in unit.bullets;
1344
+ }
1345
+ function renderFirstInstruction(rebase) {
1346
+ const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
1347
+ const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
1348
+ return `Rebase ${branch} onto ${onto} before any other work \u2014 the parent unit has merged and the base has moved; the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop.`;
1349
+ }
1350
+ function renderSpecRow(row, dropText) {
1351
+ const head = `#### ${row.spec} item ${row.item}`;
1352
+ if (!row.specRead) return `${head}
1353
+
1354
+ (not found: no spec ${row.spec} was readable)`;
1355
+ if (row.text === void 0) return `${head}
1356
+
1357
+ (not found: ${row.spec} has no item ${row.item})`;
1358
+ const text = dropText ? `(text dropped \u2014 read item ${row.item} in the spec)` : row.text;
1359
+ const rows = row.validation.length === 0 ? `(no validation row names item ${row.item})` : ["| Criterion | Proof |", "|---|---|", ...row.validation.map((r) => `| ${r.criterion} | ${r.proof} |`)].join(
1360
+ "\n"
1361
+ );
1362
+ return `${head}
1363
+
1364
+ ${text}
1365
+
1366
+ ${rows}`;
1367
+ }
1368
+ function renderSpecRows(rows, dropped) {
1369
+ if (rows.length === 0)
1370
+ return "(none \u2014 the unit names no spec rows; until decided, a unit naming none receives none, not the covering specs' rows by path)";
1371
+ if (dropped.rows) return rows.map((r) => `- ${r.spec} item ${r.item}`).join("\n");
1372
+ return rows.map((r) => renderSpecRow(r, dropped.text)).join("\n\n");
1373
+ }
1374
+ function renderAgentRules(rules, dropped) {
1375
+ if (!rules) return "(none \u2014 the repository has neither AGENTS.md nor CLAUDE.md)";
1376
+ if (dropped)
1377
+ return `Source: ${rules.file}
1378
+
1379
+ (dropped to fit the contract's budget \u2014 read ${rules.file} at the head)`;
1380
+ return `Source: ${rules.file}
1381
+
1382
+ ${rules.text.trim()}`;
1383
+ }
1384
+ function renderGuards(guards) {
1385
+ return [
1386
+ "Never weaken a guard: each refuses one class of change, and the review runs them over your range.",
1387
+ ...guards.map((g) => `- \`${g.name}\` \u2014 ${g.refuses}`)
1388
+ ].join("\n");
1389
+ }
1390
+ function renderContract(contract, opts) {
1391
+ const maxChars = opts.maxChars ?? DEFAULT_CONTRACT_MAX_CHARS;
1392
+ const hasSpecText = contract.specRows.some((r) => r.text !== void 0);
1393
+ const ladder = [
1394
+ { part: "approach", present: hasApproach(contract.unit) },
1395
+ { part: "agentRules", present: contract.agentRules !== void 0 },
1396
+ { part: "specText", present: hasSpecText },
1397
+ { part: "specRows", present: contract.specRows.length > 0 }
1398
+ ];
1399
+ const compose = (dropped2) => {
1400
+ const has = (p) => dropped2.includes(p);
1401
+ const parts = [
1402
+ CONTRACT_HEADING,
1403
+ `${CONTRACT_SECTION_HEADINGS.firstInstruction}
1404
+
1405
+ ${renderFirstInstruction(contract.rebase)}`,
1406
+ `${CONTRACT_SECTION_HEADINGS.unit} ${contract.unit.id} \u2014 ${contract.unit.title}
1407
+
1408
+ ${unitBody(contract.unit, has("approach"))}`,
1409
+ `${CONTRACT_SECTION_HEADINGS.specRows}
1410
+
1411
+ ${renderSpecRows(contract.specRows, { text: has("specText"), rows: has("specRows") })}`,
1412
+ `${CONTRACT_SECTION_HEADINGS.agentRules}
1413
+
1414
+ ${renderAgentRules(contract.agentRules, has("agentRules"))}`,
1415
+ `${CONTRACT_SECTION_HEADINGS.guards}
1416
+
1417
+ ${renderGuards(contract.guards)}`
1418
+ ];
1419
+ if (dropped2.length > 0)
1420
+ parts.push(`Cut to fit ${maxChars} characters: ${dropped2.map((p) => DROP_NOTE[p]).join("; ")}.`);
1421
+ return parts.join("\n\n");
1422
+ };
1423
+ const dropped = [];
1424
+ let text = compose(dropped);
1425
+ for (const step of ladder) {
1426
+ if (text.length <= maxChars) break;
1427
+ if (!step.present) continue;
1428
+ dropped.push(step.part);
1429
+ text = compose(dropped);
1430
+ }
1431
+ return { text, chars: text.length, dropped, overBudget: text.length > maxChars };
1432
+ }
1433
+ var GUARDS, CONTRACT_HEADING, CONTRACT_SECTION_HEADINGS, DEFAULT_CONTRACT_MAX_CHARS, UNIT_HEADING, ANY_HEADING, BULLET, SPEC_MENTION, ITEM_LIST, NAMED_REF, ANY_MD_MENTION, SPEC_ITEM, APPROACH_DROPPED, DROP_NOTE;
1434
+ var init_contract = __esm({
1435
+ "../../src/core/ship/contract.ts"() {
1436
+ "use strict";
1437
+ GUARDS = [
1438
+ {
1439
+ name: "specs:check",
1440
+ refuses: "a spec whose `file::describe::it` proof names a test that does not exist, or whose Code/Tests header path is gone \u2014 a test cannot be renamed or deleted out from under its spec row"
1441
+ },
1442
+ {
1443
+ name: "specs:coverage --test-guard",
1444
+ refuses: "a deleted test file, a removed test title or an added skip marker in a test file unless a spec covering that file changes in the same range; a lower assertion count or a rename is printed for the reviewer to dispose of"
1445
+ },
1446
+ {
1447
+ name: "hygiene:check",
1448
+ refuses: "a new imprint in the public tree (a company, a person, a tracker reference, a plan id, a platform id, a date); the recorded list only shrinks"
1449
+ },
1450
+ {
1451
+ name: "decisions:check",
1452
+ refuses: "a record under docs/decisions or docs/plans without a valid status, a superseded one that names no successor, and any edit to an accepted record's body"
1453
+ }
1454
+ ];
1455
+ CONTRACT_HEADING = "## Contract";
1456
+ CONTRACT_SECTION_HEADINGS = {
1457
+ firstInstruction: "### First instruction",
1458
+ unit: "### Unit",
1459
+ specRows: "### Spec rows",
1460
+ agentRules: "### Agent rules",
1461
+ guards: "### Guards"
1462
+ };
1463
+ DEFAULT_CONTRACT_MAX_CHARS = 48e3;
1464
+ UNIT_HEADING = /^### (U\d+)\. (.+?)\s*$/;
1465
+ ANY_HEADING = /^#{2,3} /;
1466
+ BULLET = /^- \*\*([^*]+)\*\*:(?:\s?(.*))?$/;
1467
+ SPEC_MENTION = /`(?:docs\/reference\/specs\/)?([a-z0-9-]+\.md)`|\[([a-z0-9-]+\.md)\]\([^)]*\)|(?<![\w/.-])(?:docs\/reference\/specs\/)?([a-z0-9-]+\.md)(?![\w/])/g;
1468
+ ITEM_LIST = /\b[Ii]tems?\s+(\d+(?:\s*(?:,|and|&|–|-|to|\/)\s*\d+)*)/g;
1469
+ NAMED_REF = new RegExp(`(?:${SPEC_MENTION.source})\\s+(?:\\([^)]*\\)\\s+)?${ITEM_LIST.source}`, "g");
1470
+ ANY_MD_MENTION = /(?<![\w/.-])(?:[\w.-]+\/)*([\w.-]+\.md)(?![\w/])/g;
1471
+ SPEC_ITEM = /^(\d+)\. /;
1472
+ APPROACH_DROPPED = "- **Approach**: (dropped to fit the contract's budget \u2014 read it in the plan)";
1473
+ DROP_NOTE = {
1474
+ approach: "the unit's Approach text was dropped (read it in the plan)",
1475
+ agentRules: "the agent rules were dropped (read the file at the head)",
1476
+ specText: "the spec items' text was dropped (read them in the specs; their proof rows stay)",
1477
+ specRows: "the spec rows' proof rows were dropped too (their names stay above)"
1478
+ };
1479
+ }
1480
+ });
1481
+
1184
1482
  // ../../src/agents/registry.ts
1185
1483
  function getAgent(name) {
1186
1484
  const a = AGENTS[name];
@@ -1189,10 +1487,11 @@ function getAgent(name) {
1189
1487
  }
1190
1488
  return a;
1191
1489
  }
1192
- var PR_DESCRIPTION_TEMPLATE, NEVER_MERGE, CODING_SYSTEM, CODING_SYSTEM_RESIDENT, REVIEW_VERDICT_INSTRUCTION, REVIEW_SPEC_CHECK, REVIEW_WHOLE_CHANGE, REVIEW_SYSTEM, REVIEW_SYSTEM_RESIDENT, RESEARCH_SYSTEM, GENERAL_SYSTEM, AGENTS;
1490
+ var PR_DESCRIPTION_TEMPLATE, NEVER_MERGE, CONTRACT_HEADINGS_LIST, UNIT_CONTRACT, CODING_SYSTEM, CODING_SYSTEM_RESIDENT, REVIEW_VERDICT_INSTRUCTION, REVIEW_SPEC_CHECK, REVIEW_UNIT_CONTRACT, REVIEW_WHOLE_CHANGE, REVIEW_SYSTEM, REVIEW_SYSTEM_RESIDENT, RESEARCH_SYSTEM, GENERAL_SYSTEM, AGENTS;
1193
1491
  var init_registry = __esm({
1194
1492
  "../../src/agents/registry.ts"() {
1195
1493
  "use strict";
1494
+ init_contract();
1196
1495
  PR_DESCRIPTION_TEMPLATE = `PR description \u2014 submit it with the submit_pr_description tool for EVERY PR (this is the default, not something to wait to be asked for). Switchboard renders the GitHub body from the object you submit, so never author PR-body markdown yourself. Content contract per field (each renders as its own section): prose is unwrapped \u2014 no hard line breaks inside a paragraph. Always hyperlink the triggering issue/request. Never fabricate validation \u2014 state exactly what you ran and the real result. Keep each field concise, not padded.
1197
1496
  EVERY PR includes one that already exists when you push \u2014 opened by a person, by dependabot, or by an earlier run. After EVERY push to such a PR: read its current title and body (\`github_issue_get\` with the PR number works for pull requests; \`gh pr view\` where gh exists), judge them against the change as it now stands at the pushed head, and submit the object that describes the PR as it is NOW \u2014 carry forward what the existing body says that is still true (a dependency bump's release notes belong in whatWhy), add what you changed, and anchor the Tour at the new head. Switchboard replaces the PR's title and body with your rendering. A description that describes an earlier state of its branch is a bug; "it is someone else's PR" is never a reason to leave it.
1198
1497
  - **title**: the PR title \u2014 one line naming the change, specific enough to pick out of a PR list.
@@ -1203,6 +1502,8 @@ EVERY PR includes one that already exists when you push \u2014 opened by a perso
1203
1502
  - **Risks & implications** (\`risks\`): what could break, the blast radius, and any migration/rollout/compatibility concerns (or "none" \u2014 and why).
1204
1503
  - **Validation** (\`validation\`): what you tested and the actual results as { criterion, proof } rows (commands run, pass/fail), plus how the reviewer can verify it themselves; the optional summary line carries the overall result.`;
1205
1504
  NEVER_MERGE = `NEVER merge a pull request and NEVER approve one \u2014 no merge or approve command, no merge/approve API call, no pushing to the default branch. Your deliverable is the pushed branch plus the submitted description; Switchboard's own GitHub writes are the PR open/edit, never an approval or a merge \u2014 a human decides what merges.`;
1505
+ CONTRACT_HEADINGS_LIST = Object.values(CONTRACT_SECTION_HEADINGS).map((h) => `\`${h}\``).join(", ");
1506
+ UNIT_CONTRACT = `UNIT CONTRACT: when your first user turn carries a \`${CONTRACT_HEADING}\` block \u2014 its sub-headings, in this order: ${CONTRACT_HEADINGS_LIST} \u2014 it is the contract for one plan unit, rendered by Switchboard from the plan itself, and it outranks any free-text task beside it. Do its first instruction first: the rebase of the unit's branch onto the merged parent (a conflict ends the unit \u2014 report it and stop; never resolve it by force). Then implement the unit's section as written: every test scenario it lists is added as a test, every spec row it names is updated so its proof binding resolves, the agent rules are followed, and no guard it names is weakened. The review is handed the same block and checks the diff against it: a test scenario the unit listed and the diff did not add is a finding at minor severity \u2014 the same severity as a spec contradiction. Never edit the plan record itself; where the unit is wrong or a criterion could not be proven, say so in your final message.`;
1206
1507
  CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
1207
1508
 
1208
1509
  You work inside a dedicated workspace directory with bash, read_file, and write_file tools.
@@ -1224,6 +1525,8 @@ Workflow for shipping a PR:
1224
1525
 
1225
1526
  ${NEVER_MERGE}
1226
1527
 
1528
+ ${UNIT_CONTRACT}
1529
+
1227
1530
  ${PR_DESCRIPTION_TEMPLATE}
1228
1531
 
1229
1532
  Maintain the user-facing status card with the update_status tool: right after you decide your plan, post it as a checklist (\u25CB pending items), then update it whenever an item starts (\u2731) or finishes (\u2713). Items are short outcomes ("Clone repo and read the diff", "Run the test suite"), never commands. Mark an item \u2713 only after it has actually happened \u2014 never pre-mark reporting/posting steps. This is the only progress the user sees while you work.
@@ -1252,6 +1555,8 @@ Workflow for shipping a change:
1252
1555
 
1253
1556
  ${NEVER_MERGE}
1254
1557
 
1558
+ ${UNIT_CONTRACT}
1559
+
1255
1560
  ${PR_DESCRIPTION_TEMPLATE}
1256
1561
 
1257
1562
  Maintain the user-facing status card with the update_status tool: right after you decide your plan, post it as a checklist (\u25CB pending items), then update it whenever an item starts (\u2731) or finishes (\u2713). Items are short outcomes ("Implement the fix", "Run the test suite"), never commands. Mark an item \u2713 only after it has actually happened \u2014 never pre-mark reporting/posting steps. This is the only progress the user sees while you work.
@@ -1261,6 +1566,7 @@ Your final message is posted to Slack \u2014 keep it readable, lead with the out
1261
1566
  REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no blocking issues \u2014 nits alone are not blocking) or \`request_changes\`, a one-line summary, \`head\` = the output of \`git rev-parse HEAD\` in the checkout you reviewed, and \`findings\` \u2014 every issue you report as a structured entry with a stable id you assign in order (F1, F2, \u2026), a severity of exactly blocking|major|minor|nit, the file (plus line when it points at one), and a one-line title. The findings array is the index of your review: the full explanation of each finding stays in your prose, keyed by the same ids. Switchboard writes the verdict as the first line of the GitHub comment itself and lists the findings under it; a review with no submitted verdict is posted as not approving, so never skip it. An \`approve\` carrying a blocking finding is downgraded to \`request_changes\` \u2014 approve only when nothing blocking remains. Do not write "LGTM" in your own text \u2014 the verdict line carries it.`;
1262
1567
  REVIEW_SPEC_CHECK = `3. SPEC CONTRADICTION CHECK, when the repository has \`docs/reference/specs/\`: list the specs the change touches \u2014 \`npm run --silent specs:coverage -- --changed origin/<base>...HEAD\` (\`<base>\` is the PR's base branch) when the repository's package.json has that script, otherwise match the changed paths against each spec's \`- **Code**:\` / \`- **Tests**:\` header lines (a header path covers itself and everything beneath it). If the command fails for any reason \u2014 dependencies not installed, tsx missing, a cold checkout \u2014 fall back to matching the header lines by hand; never install dependencies or build to make it run. Then read ONLY those specs, never the whole specs tree \u2014 fold the reads into your gather batch where you can. For each touched spec, judge whether the diff contradicts a numbered behavior statement or a validation criterion: code that now does what the spec says it does not, a criterion whose named test the diff removed or retitled, a behavior the diff deleted that the spec still promises. A contradiction is a finding of severity \`minor\` or higher titled \`Spec contradiction \u2014 <spec file> item <n>: <what the code now does vs what the spec says>\`; a spec updated in the same diff to match the code is not a finding. A repository with no \`docs/reference/specs/\` has nothing to check \u2014 skip this step silently.
1263
1568
  3a. TEST GUARD, in the same repositories: run \`npm run --silent specs:coverage -- --changed origin/<base>...HEAD --test-guard\` (fold it into the same batch). It compares every test file the diff touches at the base and at the head and prints one line per thing lost, in two classes. A \`test-guard: <file> \u2014 removed: \u2026\` line is deterministic \u2014 the test file deleted, an it/test/describe title gone with no new title to pair with, a skip/only/todo marker (\`.skip(\`, \`.only(\`, \`xit(\`, \`xdescribe(\`, \`it.todo(\`, \`test.todo(\`) on a test the base ran \u2014 and each one is a finding of severity \`minor\` or higher titled \`Test removed \u2014 <file>: <what>\` whose explanation quotes the guard's line exactly as printed. A \`test-guard: <file> \u2014 check: \u2026\` line is a heuristic \u2014 fewer \`expect(\` calls in the file, a title gone while another arrived (a rename or a split) \u2014 and you dispose of every one of them explicitly in your review, never silently: either "weakened", which makes it a finding at \`minor\`, or "refactor, verification intact" with one clause saying why. A line ending \`\u2014 allowed by <spec>\` is licensed by a spec change in the same diff and is neither; \`test-guard ok\` is nothing to report. If the command fails for any reason, judge the same facts from the diff by hand \u2014 a deleted test file, a removed title, a new skip marker, fewer assertions \u2014 and file each one the diff does not license the same way; never install or build to make it run.`;
1569
+ REVIEW_UNIT_CONTRACT = ` 3b. UNIT CONTRACT, when this prompt carries a \`${CONTRACT_HEADING}\` block after the REVIEW TARGET block (its sub-headings, in order: ${CONTRACT_HEADINGS_LIST}): it is what the coding child was handed for this plan unit, rendered by Switchboard from the plan, and the diff is judged against it. Read the unit's Test scenarios and find each one in the diff: a test scenario the unit listed and the diff did not add is a finding of severity \`minor\` titled \`Contract \u2014 test scenario missing: <the scenario>\` \u2014 the same severity as a spec contradiction. For each spec row the block names, check its proof binding resolves in the diff; a named row the diff leaves untouched is disposed of out loud, as in 3a: one clause on why it needed no change, or a finding at \`minor\` titled \`Contract \u2014 spec row not updated: <spec> item <n>\`. A guard the block names that the diff weakens is the guard's own finding (3a). No \`${CONTRACT_HEADING}\` block in this prompt \u2192 nothing to check; skip this step silently.`;
1264
1570
  REVIEW_WHOLE_CHANGE = ` - READ THE WHOLE CHANGE: the REVIEW TARGET block states the PR's size as GitHub reports it (files, +/\u2212) and \`diff_digest\` states the totals of what it covered \u2014 they must agree, and every file the digest lists must be in the diff you read. A tool output ending in \`...[truncated N chars]\` was cut short; when the digest or your diff shows fewer files or lines than the PR, read the rest file by file (\`git diff <base>...HEAD -- <path>\`) until every file is covered. Never judge from a partial diff: Switchboard does not post a verdict whose digest covered less than the PR.`;
1265
1571
  REVIEW_SYSTEM = `You are Switchboard's code review agent, operating from a Slack request.
1266
1572
 
@@ -1276,6 +1582,7 @@ ${REVIEW_WHOLE_CHANGE}
1276
1582
  - if the PR is enormous (>~6k changed lines), print the riskiest files in full (state mutation, auth, concurrency, data deletion, public APIs) and only the diff hunks for the rest \u2014 and say which files you skimmed
1277
1583
  2. ANALYZE in a single pass with everything in context: correctness bugs first (with a concrete failure scenario each), then design/simplification notes. At most 2-3 targeted follow-up reads if a specific caller or callee is load-bearing \u2014 never a general exploration loop.
1278
1584
  ${REVIEW_SPEC_CHECK}
1585
+ ${REVIEW_UNIT_CONTRACT}
1279
1586
  4. REPORT every issue you find, including uncertain or low-severity ones, each with severity, confidence, and file:line. Order findings most-severe first. If the change looks correct, say so plainly \u2014 do not manufacture findings.
1280
1587
 
1281
1588
  Do NOT post your review to GitHub yourself \u2014 no \`gh pr comment\`, no API call to create a comment. When the review is of a PR, Switchboard posts your final message to that PR automatically by default (as a comment \u2014 never an approval or a merge); just produce the review as your final message. If the request asks not to post (e.g. "don't post" / "slack only"), Switchboard handles that too \u2014 you still only write the review.
@@ -1301,6 +1608,7 @@ ${REVIEW_WHOLE_CHANGE}
1301
1608
  - if the change is enormous (>~6k changed lines), print the riskiest files in full (state mutation, auth, concurrency, data deletion, public APIs) and only the diff hunks for the rest \u2014 and say which files you skimmed
1302
1609
  2. ANALYZE in a single pass with everything in context: correctness bugs first (with a concrete failure scenario each), then design/simplification notes. At most 2-3 targeted follow-up reads if a specific caller or callee is load-bearing \u2014 never a general exploration loop.
1303
1610
  ${REVIEW_SPEC_CHECK}
1611
+ ${REVIEW_UNIT_CONTRACT}
1304
1612
  4. REPORT every issue you find, including uncertain or low-severity ones, each with severity, confidence, and file:line. Order findings most-severe first. If the change looks correct, say so plainly \u2014 do not manufacture findings.
1305
1613
 
1306
1614
  Do NOT post your review to GitHub yourself \u2014 no API call to create a comment. When the review is of a PR, Switchboard posts your final message to that PR automatically by default (as a comment \u2014 never an approval or a merge); just produce the review as your final message. If the request asks not to post (e.g. "don't post" / "slack only"), Switchboard handles that too \u2014 you still only write the review.
@@ -10151,8 +10459,107 @@ ${quote(current ?? "")}`;
10151
10459
  }
10152
10460
  });
10153
10461
 
10154
- // ../../src/core/commands/deploy.ts
10462
+ // ../../src/core/commands/contract.ts
10463
+ import { basename, dirname as dirname8, join as join10 } from "node:path";
10155
10464
  import { z as z6 } from "zod";
10465
+ function repoRootOfPlan(planPath) {
10466
+ const plans = dirname8(planPath);
10467
+ const docs = dirname8(plans);
10468
+ if (basename(plans) === "plans" && basename(docs) === "docs") return dirname8(docs);
10469
+ return plans;
10470
+ }
10471
+ function renderOutput(output) {
10472
+ const o = output;
10473
+ const dropped = Array.isArray(o.dropped) && o.dropped.length > 0 ? o.dropped.map(String).join(", ") : "none";
10474
+ const over = o.overBudget === true ? " \u2014 still over the budget" : "";
10475
+ return `${String(o.text)}
10476
+
10477
+ (${String(o.chars)} characters; budget ${String(o.maxChars)}; dropped: ${dropped}${over})`;
10478
+ }
10479
+ function registerContractCommands(registry) {
10480
+ for (const cmd of contractCommands) registry.register(cmd);
10481
+ }
10482
+ var defineCommand3, SPECS_DIR, RULES_FILES, contractRender, contractCommands;
10483
+ var init_contract2 = __esm({
10484
+ "../../src/core/commands/contract.ts"() {
10485
+ "use strict";
10486
+ init_commandRegistry();
10487
+ init_contract();
10488
+ defineCommand3 = commandDefiner();
10489
+ SPECS_DIR = join10("docs", "reference", "specs");
10490
+ RULES_FILES = ["AGENTS.md", "CLAUDE.md"];
10491
+ contractRender = defineCommand3({
10492
+ id: "contract.render",
10493
+ options: z6.object({
10494
+ plan: z6.string().min(1).describe("path of the plan record (docs/plans/<file>.md)"),
10495
+ unit: z6.string().regex(/^U\d+$/, "expected a unit id like U<n>").describe("the unit to render, as its heading spells it (U<n>)"),
10496
+ root: z6.string().min(1).optional().describe("the repository root the specs and rules are read under (default: the root the plan path implies)"),
10497
+ branch: z6.string().min(1).optional().describe("the unit's branch, named in the first instruction"),
10498
+ onto: z6.string().min(1).optional().describe("the merged parent the branch is rebased onto"),
10499
+ maxChars: z6.coerce.number().int().positive().optional().describe(`the rendered block's budget in characters (default ${DEFAULT_CONTRACT_MAX_CHARS})`)
10500
+ }),
10501
+ action: "contract:read",
10502
+ effect: "read",
10503
+ surfaces: { chat: false, mcp: false, http: false },
10504
+ describe: "Render one plan unit's child contract \u2014 its section, the spec rows it names with their proof bindings, the repository's agent rules, the guards \u2014 as the `## Contract` block a coding prompt carries, and measure it.",
10505
+ render: renderOutput,
10506
+ handler: async ({ options, deps }) => {
10507
+ const planMarkdown = await deps.contract.readFile(options.plan);
10508
+ if (planMarkdown === void 0) throw new CommandError("not_found", `plan not found: ${options.plan}`);
10509
+ const root = options.root ?? repoRootOfPlan(options.plan);
10510
+ const unit = parsePlanUnit(planMarkdown, options.unit);
10511
+ const specs = /* @__PURE__ */ new Map();
10512
+ for (const spec of new Set(unit ? specItemRefs(unit.section).map((r) => r.spec) : []))
10513
+ specs.set(spec, await deps.contract.readFile(join10(root, SPECS_DIR, spec)));
10514
+ let agentRules;
10515
+ for (const file of RULES_FILES) {
10516
+ const text = await deps.contract.readFile(join10(root, file));
10517
+ if (text !== void 0) {
10518
+ agentRules = { file, text };
10519
+ break;
10520
+ }
10521
+ }
10522
+ let contract;
10523
+ try {
10524
+ contract = contractFromPlan({
10525
+ planMarkdown,
10526
+ unitId: options.unit,
10527
+ readSpec: (spec) => specs.get(spec),
10528
+ ...agentRules ? { agentRules } : {},
10529
+ rebase: { branch: options.branch, onto: options.onto }
10530
+ });
10531
+ } catch (err2) {
10532
+ throw new CommandError("invalid_input", err2 instanceof Error ? err2.message : String(err2));
10533
+ }
10534
+ const maxChars = options.maxChars ?? DEFAULT_CONTRACT_MAX_CHARS;
10535
+ const rendered = renderContract(contract, { maxChars });
10536
+ return {
10537
+ plan: options.plan,
10538
+ root,
10539
+ unit: { id: contract.unit.id, title: contract.unit.title },
10540
+ specRows: contract.specRows.map((r) => ({
10541
+ spec: r.spec,
10542
+ item: r.item,
10543
+ found: r.text !== void 0,
10544
+ validationRows: r.validation.length
10545
+ })),
10546
+ agentRules: agentRules?.file ?? null,
10547
+ chars: rendered.chars,
10548
+ maxChars,
10549
+ dropped: rendered.dropped,
10550
+ overBudget: rendered.overBudget,
10551
+ text: rendered.text
10552
+ };
10553
+ }
10554
+ });
10555
+ contractCommands = [
10556
+ contractRender
10557
+ ];
10558
+ }
10559
+ });
10560
+
10561
+ // ../../src/core/commands/deploy.ts
10562
+ import { z as z7 } from "zod";
10156
10563
  function toOptions(o, dryRun, affected) {
10157
10564
  return {
10158
10565
  only: o.only,
@@ -10262,7 +10669,7 @@ async function copyImages(copies, account2, deps) {
10262
10669
  function registerDeployCommands(registry) {
10263
10670
  for (const cmd of deployCommands) registry.register(cmd);
10264
10671
  }
10265
- var defineCommand3, workerList, positiveInt, gitRef, deployOptions, planJson, deployPlan, allOptions, copiedImage, listCopies, deployAll, restartOptions, deployRestart, initOptions, deployInit, configOptions, deployConfig, secretNames, secretsOptions, deploySecrets, imagesOptions, deployImages, deployCommands;
10672
+ var defineCommand4, workerList, positiveInt, gitRef, deployOptions, planJson, deployPlan, allOptions, copiedImage, listCopies, deployAll, restartOptions, deployRestart, initOptions, deployInit, configOptions, deployConfig, secretNames, secretsOptions, deploySecrets, imagesOptions, deployImages, deployCommands;
10266
10673
  var init_deploy = __esm({
10267
10674
  "../../src/core/commands/deploy.ts"() {
10268
10675
  "use strict";
@@ -10275,16 +10682,16 @@ var init_deploy = __esm({
10275
10682
  init_wranglerTemplate();
10276
10683
  init_secrets();
10277
10684
  init_commandRegistry();
10278
- defineCommand3 = commandDefiner();
10279
- workerList = z6.string().transform(
10685
+ defineCommand4 = commandDefiner();
10686
+ workerList = z7.string().transform(
10280
10687
  (s) => s.split(",").map((x) => x.trim()).filter(Boolean)
10281
10688
  ).refine(
10282
10689
  (names) => names.every((n2) => DEPLOY_ORDER.includes(n2)),
10283
10690
  `expected a comma list of Workers (${DEPLOY_ORDER.join(", ")})`
10284
10691
  ).transform((names) => [...new Set(names)]);
10285
- positiveInt = z6.coerce.number().int().positive();
10286
- gitRef = z6.string().regex(/^[A-Za-z0-9_./^~@{}][A-Za-z0-9_./^~@{}-]{0,199}$/, "expected a git revision (a sha, tag, branch, or HEAD^)");
10287
- deployOptions = z6.object({
10692
+ positiveInt = z7.coerce.number().int().positive();
10693
+ gitRef = z7.string().regex(/^[A-Za-z0-9_./^~@{}][A-Za-z0-9_./^~@{}-]{0,199}$/, "expected a git revision (a sha, tag, branch, or HEAD^)");
10694
+ deployOptions = z7.object({
10288
10695
  only: workerList.optional().describe(
10289
10696
  `deploy only these Workers (comma list of ${DEPLOY_ORDER.join(", ")}); with --affected, only those of them that are affected`
10290
10697
  ),
@@ -10303,7 +10710,7 @@ var init_deploy = __esm({
10303
10710
  poll: positiveInt.optional().describe("seconds between preflight retries (default 60)")
10304
10711
  });
10305
10712
  planJson = (plan) => plan;
10306
- deployPlan = defineCommand3({
10713
+ deployPlan = defineCommand4({
10307
10714
  id: "deploy.plan",
10308
10715
  options: deployOptions,
10309
10716
  action: "deploy:read",
@@ -10319,7 +10726,7 @@ var init_deploy = __esm({
10319
10726
  });
10320
10727
  copiedImage = (c) => ({ kind: c.kind, source: c.source, target: c.target });
10321
10728
  listCopies = (copies) => copies.map((c) => `${c.kind} \u2190 ${c.source}`).join(", ");
10322
- deployAll = defineCommand3({
10729
+ deployAll = defineCommand4({
10323
10730
  id: "deploy.all",
10324
10731
  options: allOptions,
10325
10732
  action: "deploy:write",
@@ -10370,15 +10777,15 @@ ${formatDeployResults(result.results, result.notAttempted)}`
10370
10777
  return output(result.results, copies, []);
10371
10778
  }
10372
10779
  });
10373
- restartOptions = z6.object({
10374
- only: z6.enum(["bot"]).optional().describe("the Worker to restart \u2014 only `bot` has a long-lived container (default bot)"),
10780
+ restartOptions = z7.object({
10781
+ only: z7.enum(["bot"]).optional().describe("the Worker to restart \u2014 only `bot` has a long-lived container (default bot)"),
10375
10782
  force: flag.optional().describe(
10376
10783
  "restart even when the bot cannot be consulted (no JSON on /healthz) \u2014 the fail-closed cases; runs in flight never refuse, they hand off"
10377
10784
  ),
10378
10785
  waitMax: positiveInt.optional().describe("minutes to wait out a refusal (the bot not answering with JSON) before giving up (default 10)"),
10379
10786
  poll: positiveInt.optional().describe("seconds between retries while refused (default 60)")
10380
10787
  });
10381
- deployRestart = defineCommand3({
10788
+ deployRestart = defineCommand4({
10382
10789
  id: "deploy.restart",
10383
10790
  options: restartOptions,
10384
10791
  action: "deploy:write",
@@ -10419,12 +10826,12 @@ ${formatDeployResults(result.results, result.notAttempted)}`
10419
10826
  };
10420
10827
  }
10421
10828
  });
10422
- initOptions = z6.object({
10829
+ initOptions = z7.object({
10423
10830
  check: flag.optional().describe(
10424
10831
  "compare only: report each rendered file that differs from the one on disk and fail when any does (the `deploy:check` gate); nothing is written"
10425
10832
  )
10426
10833
  });
10427
- deployInit = defineCommand3({
10834
+ deployInit = defineCommand4({
10428
10835
  id: "deploy.init",
10429
10836
  options: initOptions,
10430
10837
  action: "deploy:write",
@@ -10471,12 +10878,12 @@ ${formatDeployResults(result.results, result.notAttempted)}`
10471
10878
  return output;
10472
10879
  }
10473
10880
  });
10474
- configOptions = z6.object({
10475
- source: z6.string().min(1).optional().describe(
10881
+ configOptions = z7.object({
10882
+ source: z7.string().min(1).optional().describe(
10476
10883
  "read the config from this source instead of the profile's configSource (a path, github://owner/repo/path@ref, or op://Vault/Item/field)"
10477
10884
  )
10478
10885
  });
10479
- deployConfig = defineCommand3({
10886
+ deployConfig = defineCommand4({
10480
10887
  id: "deploy.config",
10481
10888
  options: configOptions,
10482
10889
  action: "deploy:write",
@@ -10511,21 +10918,21 @@ the bot reads it on its next start: \`deploy restart\``;
10511
10918
  return output;
10512
10919
  }
10513
10920
  });
10514
- secretNames = z6.string().transform(
10921
+ secretNames = z7.string().transform(
10515
10922
  (s) => s.split(",").map((x) => x.trim()).filter(Boolean)
10516
10923
  ).refine(
10517
10924
  (names) => names.length > 0 && names.every((n2) => /^[A-Z][A-Z0-9_]*$/.test(n2)),
10518
10925
  "expected a comma list of secret names (SCREAMING_SNAKE)"
10519
10926
  ).transform((names) => [...new Set(names)]);
10520
- secretsOptions = z6.object({
10927
+ secretsOptions = z7.object({
10521
10928
  only: secretNames.optional().describe("put only these secrets (comma list of names from deploy/secrets.manifest.json)")
10522
10929
  });
10523
- deploySecrets = defineCommand3({
10930
+ deploySecrets = defineCommand4({
10524
10931
  id: "deploy.secrets",
10525
10932
  args: [
10526
10933
  {
10527
10934
  name: "worker",
10528
- schema: z6.enum(DEPLOY_ORDER),
10935
+ schema: z7.enum(DEPLOY_ORDER),
10529
10936
  describe: `the Worker to provision (${DEPLOY_ORDER.join(", ")})`
10530
10937
  }
10531
10938
  ],
@@ -10587,12 +10994,12 @@ the bot reads it on its next start: \`deploy restart\``;
10587
10994
  return output;
10588
10995
  }
10589
10996
  });
10590
- imagesOptions = z6.object({
10997
+ imagesOptions = z7.object({
10591
10998
  dryRun: flag.optional().describe(
10592
10999
  "say which images the account registry already holds at the version and which would be copied; nothing is copied"
10593
11000
  )
10594
11001
  });
10595
- deployImages = defineCommand3({
11002
+ deployImages = defineCommand4({
10596
11003
  id: "deploy.images",
10597
11004
  options: imagesOptions,
10598
11005
  action: "deploy:write",
@@ -10643,25 +11050,25 @@ the bot reads it on its next start: \`deploy restart\``;
10643
11050
  });
10644
11051
 
10645
11052
  // ../../src/core/commands/env.ts
10646
- import { z as z7 } from "zod";
11053
+ import { z as z8 } from "zod";
10647
11054
  function registerEnvCommands(registry) {
10648
11055
  for (const cmd of envCommands) registry.register(cmd);
10649
11056
  }
10650
- var defineCommand4, envBootstrap, envCommands;
11057
+ var defineCommand5, envBootstrap, envCommands;
10651
11058
  var init_env = __esm({
10652
11059
  "../../src/core/commands/env.ts"() {
10653
11060
  "use strict";
10654
11061
  init_bootstrap();
10655
11062
  init_commandRegistry();
10656
- defineCommand4 = commandDefiner();
10657
- envBootstrap = defineCommand4({
11063
+ defineCommand5 = commandDefiner();
11064
+ envBootstrap = defineCommand5({
10658
11065
  id: "env.bootstrap",
10659
- options: z7.object({
10660
- env: z7.string().min(1).describe("environment to materialize (UAT only; anything else is refused)"),
10661
- service: z7.string().min(1).describe("which downstream service's toolchain env to fill"),
11066
+ options: z8.object({
11067
+ env: z8.string().min(1).describe("environment to materialize (UAT only; anything else is refused)"),
11068
+ service: z8.string().min(1).describe("which downstream service's toolchain env to fill"),
10662
11069
  apply: flag.optional().describe("resolve the refs and WRITE the chmod-600 env file (default: dry-run)"),
10663
- out: z7.string().min(1).optional().describe("env-file path (default .agent-env/<service>.<env>.env)"),
10664
- manifest: z7.string().min(1).optional().describe(`manifest path (default ${DEFAULT_MANIFEST_PATH})`)
11070
+ out: z8.string().min(1).optional().describe("env-file path (default .agent-env/<service>.<env>.env)"),
11071
+ manifest: z8.string().min(1).optional().describe(`manifest path (default ${DEFAULT_MANIFEST_PATH})`)
10665
11072
  }),
10666
11073
  action: "env:write",
10667
11074
  effect: "write",
@@ -12260,7 +12667,7 @@ var init_selfImprovement = __esm({
12260
12667
  });
12261
12668
 
12262
12669
  // ../../src/core/commands/friction.ts
12263
- import { z as z8 } from "zod";
12670
+ import { z as z9 } from "zod";
12264
12671
  async function ledgerOf(deps) {
12265
12672
  let ledger;
12266
12673
  try {
@@ -12288,7 +12695,7 @@ function inProgressHint(diagnosis, finished) {
12288
12695
  function registerFrictionCommands(registry) {
12289
12696
  for (const cmd of frictionCommands) registry.register(cmd);
12290
12697
  }
12291
- var defineCommand5, positiveInt2, repoSlug, NO_LEDGER_MESSAGE, NO_REPO_MESSAGE, asJson, render, visibleRuns, frictionReport, frictionPropose, frictionAnalyze, frictionCommands;
12698
+ var defineCommand6, positiveInt2, repoSlug, NO_LEDGER_MESSAGE, NO_REPO_MESSAGE, asJson, render, visibleRuns, frictionReport, frictionPropose, frictionAnalyze, frictionCommands;
12292
12699
  var init_friction = __esm({
12293
12700
  "../../src/core/commands/friction.ts"() {
12294
12701
  "use strict";
@@ -12299,19 +12706,19 @@ var init_friction = __esm({
12299
12706
  init_runEventLines();
12300
12707
  init_runFriction();
12301
12708
  init_selfImprovement();
12302
- defineCommand5 = commandDefiner();
12303
- positiveInt2 = z8.coerce.number().int().positive();
12304
- repoSlug = z8.string().refine((s) => /^[\w.-]+\/[\w.-]+$/.test(s), "expected an owner/name slug");
12709
+ defineCommand6 = commandDefiner();
12710
+ positiveInt2 = z9.coerce.number().int().positive();
12711
+ repoSlug = z9.string().refine((s) => /^[\w.-]+\/[\w.-]+$/.test(s), "expected an owner/name slug");
12305
12712
  NO_LEDGER_MESSAGE = "Run history is not configured in this deployment (`runHistory`), so there are no recent runs to analyze.";
12306
12713
  NO_REPO_MESSAGE = "Set `selfImprovement.repo` (an `owner/name`) in config.yaml to tell `friction propose` where to file issues.";
12307
12714
  asJson = (r) => r;
12308
12715
  render = (output) => formatSelfImprovementReport(output);
12309
12716
  visibleRuns = (caller) => predicateFor(caller.actor, "runs:read", "run");
12310
- frictionReport = defineCommand5({
12717
+ frictionReport = defineCommand6({
12311
12718
  id: "friction.report",
12312
12719
  enabledWhen: (caps) => caps.runHistory,
12313
- options: z8.object({
12314
- sinceMs: z8.coerce.number().int().nonnegative().optional().describe("only runs finished at or after this epoch ms"),
12720
+ options: z9.object({
12721
+ sinceMs: z9.coerce.number().int().nonnegative().optional().describe("only runs finished at or after this epoch ms"),
12315
12722
  limit: positiveInt2.optional().describe("newest n runs (default: the ledger's retained window)"),
12316
12723
  minRuns: positiveInt2.optional().describe("distinct runs a pattern must recur in (default selfImprovement.minRuns, else 2)")
12317
12724
  }),
@@ -12337,10 +12744,10 @@ var init_friction = __esm({
12337
12744
  });
12338
12745
  }
12339
12746
  });
12340
- frictionPropose = defineCommand5({
12747
+ frictionPropose = defineCommand6({
12341
12748
  id: "friction.propose",
12342
12749
  enabledWhen: (caps) => caps.runHistory,
12343
- options: z8.object({
12750
+ options: z9.object({
12344
12751
  dryRun: flag.optional().describe("compute and report everything, file nothing"),
12345
12752
  top: positiveInt2.optional().describe("proposals filed per pass (default selfImprovement.top, else 3)"),
12346
12753
  minRuns: positiveInt2.optional().describe("distinct runs a pattern must recur in (default selfImprovement.minRuns, else 2)"),
@@ -12372,17 +12779,17 @@ var init_friction = __esm({
12372
12779
  }
12373
12780
  }
12374
12781
  });
12375
- frictionAnalyze = defineCommand5({
12782
+ frictionAnalyze = defineCommand6({
12376
12783
  id: "friction.analyze",
12377
12784
  args: [
12378
12785
  {
12379
12786
  name: "source",
12380
- schema: z8.string().optional(),
12787
+ schema: z9.string().optional(),
12381
12788
  describe: "a saved run stream: JSON lines of run events or a curl'ed /runs/:id/events SSE capture (default: stdin)"
12382
12789
  }
12383
12790
  ],
12384
- options: z8.object({
12385
- slowMs: z8.coerce.number().nonnegative().optional().describe("a tool call slower than this many ms is a slow-tool finding"),
12791
+ options: z9.object({
12792
+ slowMs: z9.coerce.number().nonnegative().optional().describe("a tool call slower than this many ms is a slow-tool finding"),
12386
12793
  inProgress: flag.optional().describe("the capture was taken mid-run: a trailing call without a result is still executing, not a dead run")
12387
12794
  }),
12388
12795
  action: "friction:read",
@@ -12435,7 +12842,7 @@ var init_friction = __esm({
12435
12842
  });
12436
12843
 
12437
12844
  // ../../src/core/commandSurface.ts
12438
- import { z as z9 } from "zod";
12845
+ import { z as z10 } from "zod";
12439
12846
  function camelToKebab(s) {
12440
12847
  return s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
12441
12848
  }
@@ -12507,7 +12914,7 @@ function isBooleanSchema(schema) {
12507
12914
  return false;
12508
12915
  }
12509
12916
  function jsonSchemaOf(schema) {
12510
- const out = z9.toJSONSchema(unwrap(schema), { io: "input" });
12917
+ const out = z10.toJSONSchema(unwrap(schema), { io: "input" });
12511
12918
  delete out.$schema;
12512
12919
  return out;
12513
12920
  }
@@ -12519,7 +12926,7 @@ function jsonSchemaFor(cmd) {
12519
12926
  if (!acceptsUndefined(arg.schema)) required.push(arg.name);
12520
12927
  }
12521
12928
  if (cmd.options) {
12522
- const opts = z9.toJSONSchema(cmd.options, { io: "input" });
12929
+ const opts = z10.toJSONSchema(cmd.options, { io: "input" });
12523
12930
  Object.assign(properties, opts.properties ?? {});
12524
12931
  required.push(...opts.required ?? []);
12525
12932
  }
@@ -12731,15 +13138,15 @@ function chatCommandList(commands) {
12731
13138
  function registerHelpCommands(registry) {
12732
13139
  for (const cmd of helpCommands) registry.register(cmd);
12733
13140
  }
12734
- var defineCommand6, DIRECTIVES_HELP, helpShow, helpCommands;
13141
+ var defineCommand7, DIRECTIVES_HELP, helpShow, helpCommands;
12735
13142
  var init_help = __esm({
12736
13143
  "../../src/core/commands/help.ts"() {
12737
13144
  "use strict";
12738
13145
  init_commandRegistry();
12739
13146
  init_commandSurface();
12740
- defineCommand6 = commandDefiner();
13147
+ defineCommand7 = commandDefiner();
12741
13148
  DIRECTIVES_HELP = "`agent:review model:anthropic/claude-opus-5 effort:low look at PR 42` (effort: low | medium | high \u2014 lower = faster turns)";
12742
- helpShow = defineCommand6({
13149
+ helpShow = defineCommand7({
12743
13150
  id: "help.show",
12744
13151
  action: "help:read",
12745
13152
  effect: "read",
@@ -14503,7 +14910,7 @@ var init_service = __esm({
14503
14910
  });
14504
14911
 
14505
14912
  // ../../src/core/commands/mcp.ts
14506
- import { z as z10 } from "zod";
14913
+ import { z as z11 } from "zod";
14507
14914
  async function serviceOf(deps) {
14508
14915
  const svc = await deps.mcp.service();
14509
14916
  if ("unavailable" in svc) throw new CommandError("unavailable", svc.unavailable);
@@ -14597,7 +15004,7 @@ function renderShow(output) {
14597
15004
  function registerMcpCommands(registry) {
14598
15005
  for (const cmd of MCP_COMMANDS) registry.register(cmd);
14599
15006
  }
14600
- var defineCommand7, serverName, serverUrl, scopeOption, channelOption2, nameArg, mcpList, mcpAdd, mcpConnect, mcpShow, mcpRemove, MCP_COMMANDS;
15007
+ var defineCommand8, serverName, serverUrl, scopeOption, channelOption2, nameArg, mcpList, mcpAdd, mcpConnect, mcpShow, mcpRemove, MCP_COMMANDS;
14601
15008
  var init_mcp = __esm({
14602
15009
  "../../src/core/commands/mcp.ts"() {
14603
15010
  "use strict";
@@ -14605,18 +15012,18 @@ var init_mcp = __esm({
14605
15012
  init_service();
14606
15013
  init_authorize();
14607
15014
  init_commandRegistry();
14608
- defineCommand7 = commandDefiner();
14609
- serverName = z10.string().refine((s) => MCP_SERVER_NAME_PATTERN.test(s), "expected a slug: lowercase letters, digits, dashes (\u2264 32 chars)");
14610
- serverUrl = z10.string().refine((s) => /^https?:\/\/\S+$/.test(s), "expected an http(s) URL");
14611
- scopeOption = z10.enum(["me", "channel", "org"]).optional().describe(
15015
+ defineCommand8 = commandDefiner();
15016
+ serverName = z11.string().refine((s) => MCP_SERVER_NAME_PATTERN.test(s), "expected a slug: lowercase letters, digits, dashes (\u2264 32 chars)");
15017
+ serverUrl = z11.string().refine((s) => /^https?:\/\/\S+$/.test(s), "expected an http(s) URL");
15018
+ scopeOption = z11.enum(["me", "channel", "org"]).optional().describe(
14612
15019
  "whose server: yours (`me`, default), this channel's (`channel`, channel-config rights), or org-wide (`org`, admins)"
14613
15020
  );
14614
- channelOption2 = z10.string().optional().describe("target channel for `--scope channel` and for `list` (default: the channel you are speaking in)");
15021
+ channelOption2 = z11.string().optional().describe("target channel for `--scope channel` and for `list` (default: the channel you are speaking in)");
14615
15022
  nameArg = { name: "name", schema: serverName, describe: "the server's name (see `mcp list`)" };
14616
- mcpList = defineCommand7({
15023
+ mcpList = defineCommand8({
14617
15024
  id: "mcp.list",
14618
15025
  enabledWhen: (caps) => caps.mcp,
14619
- options: z10.object({ channel: channelOption2 }),
15026
+ options: z11.object({ channel: channelOption2 }),
14620
15027
  action: "mcp:read",
14621
15028
  effect: "read",
14622
15029
  describe: "External MCP servers your runs in this channel can use \u2014 org-wide, this channel's, and your own \u2014 with state and agents; never a credential.",
@@ -14628,7 +15035,7 @@ var init_mcp = __esm({
14628
15035
  return { servers };
14629
15036
  }
14630
15037
  });
14631
- mcpAdd = defineCommand7({
15038
+ mcpAdd = defineCommand8({
14632
15039
  id: "mcp.add",
14633
15040
  enabledWhen: (caps) => caps.mcp,
14634
15041
  args: [
@@ -14638,15 +15045,15 @@ var init_mcp = __esm({
14638
15045
  describe: "a short slug for the server (becomes the tool prefix mcp__<name>__)"
14639
15046
  }
14640
15047
  ],
14641
- options: z10.object({
15048
+ options: z11.object({
14642
15049
  url: serverUrl.describe("the server's Streamable-HTTP endpoint (https://\u2026/mcp)"),
14643
15050
  scope: scopeOption,
14644
- agents: z10.string().transform(
15051
+ agents: z11.string().transform(
14645
15052
  (s) => s.split(",").map((a) => a.trim()).filter(Boolean)
14646
15053
  ).optional().describe(
14647
15054
  "comma-separated agents that may use it (default general,research; only org servers may name coding/review/ship)"
14648
15055
  ),
14649
- auth: z10.enum(["oauth", "bearer", "none"]).optional().describe(
15056
+ auth: z11.enum(["oauth", "bearer", "none"]).optional().describe(
14650
15057
  "how the server authenticates: `oauth` (you sign in on a one-time link), `bearer` (you paste a token on a one-time link), or `none`; omit to detect it from the server"
14651
15058
  ),
14652
15059
  channel: channelOption2
@@ -14671,11 +15078,11 @@ var init_mcp = __esm({
14671
15078
  );
14672
15079
  }
14673
15080
  });
14674
- mcpConnect = defineCommand7({
15081
+ mcpConnect = defineCommand8({
14675
15082
  id: "mcp.connect",
14676
15083
  enabledWhen: (caps) => caps.mcp,
14677
15084
  args: [nameArg],
14678
- options: z10.object({ scope: scopeOption, channel: channelOption2 }),
15085
+ options: z11.object({ scope: scopeOption, channel: channelOption2 }),
14679
15086
  action: "mcp:write",
14680
15087
  effect: "write",
14681
15088
  describe: "A fresh one-time link to sign in to an OAuth server or enter (or replace) a bearer server's token \u2014 only you can complete it; it expires in 10 minutes.",
@@ -14689,11 +15096,11 @@ var init_mcp = __esm({
14689
15096
  return await via(() => svc.connect(actor, target, args.name));
14690
15097
  }
14691
15098
  });
14692
- mcpShow = defineCommand7({
15099
+ mcpShow = defineCommand8({
14693
15100
  id: "mcp.show",
14694
15101
  enabledWhen: (caps) => caps.mcp,
14695
15102
  args: [nameArg],
14696
- options: z10.object({ scope: scopeOption, channel: channelOption2 }),
15103
+ options: z11.object({ scope: scopeOption, channel: channelOption2 }),
14697
15104
  action: "mcp:read",
14698
15105
  effect: "read",
14699
15106
  describe: "One MCP server's entry plus a live probe of the tools it offers (names, read-only flags); never a credential.",
@@ -14709,11 +15116,11 @@ var init_mcp = __esm({
14709
15116
  return await via(() => svc.show(actor, target, args.name));
14710
15117
  }
14711
15118
  });
14712
- mcpRemove = defineCommand7({
15119
+ mcpRemove = defineCommand8({
14713
15120
  id: "mcp.remove",
14714
15121
  enabledWhen: (caps) => caps.mcp,
14715
15122
  args: [nameArg],
14716
- options: z10.object({ scope: scopeOption, channel: channelOption2 }),
15123
+ options: z11.object({ scope: scopeOption, channel: channelOption2 }),
14717
15124
  action: "mcp:write",
14718
15125
  effect: "write",
14719
15126
  describe: "Remove an MCP server you added and its stored credential (yours freely; channel ones need channel-config rights, org-wide ones admin rights).",
@@ -15151,7 +15558,7 @@ var init_llmOutput = __esm({
15151
15558
  });
15152
15559
 
15153
15560
  // ../../src/core/memory/reflection.ts
15154
- import { z as z11 } from "zod";
15561
+ import { z as z12 } from "zod";
15155
15562
  function shouldReflect(input) {
15156
15563
  if (input.agentName !== void 0 && NO_REFLECT_AGENTS.has(input.agentName)) return false;
15157
15564
  return input.toolCalls > 0 || input.historyTurns >= REFLECT_MIN_TURNS;
@@ -15370,7 +15777,7 @@ var init_reflection = __esm({
15370
15777
  "Output raw JSON with no code fence and no prose."
15371
15778
  ].join("\n");
15372
15779
  SUMMARY_INHERITANCE = ["user", "repo", "channel"];
15373
- REFLECTION_ENVELOPE = jsonOutput(z11.object({ facts: z11.array(z11.unknown()), summary: z11.unknown().optional() }), {
15780
+ REFLECTION_ENVELOPE = jsonOutput(z12.object({ facts: z12.array(z12.unknown()), summary: z12.unknown().optional() }), {
15374
15781
  name: "reflection-envelope"
15375
15782
  });
15376
15783
  SCOPE_KINDS = ["org", "user", "repo", "channel"];
@@ -15602,7 +16009,7 @@ var init_memory = __esm({
15602
16009
  });
15603
16010
 
15604
16011
  // ../../src/core/commands/memory.ts
15605
- import { z as z12 } from "zod";
16012
+ import { z as z13 } from "zod";
15606
16013
  function scopeKeyOfMemoryId(id) {
15607
16014
  const m = /^mem:(.+):\d+$/.exec(id);
15608
16015
  return m ? m[1] : void 0;
@@ -15656,18 +16063,18 @@ _(limit ${limit} reached \u2014 there may be more; narrow with words or raise \`
15656
16063
  function registerMemoryCommands(registry) {
15657
16064
  for (const cmd of memoryCommands) registry.register(cmd);
15658
16065
  }
15659
- var defineCommand8, MEMORY_LIST_LIMIT, MEMORY_LIST_MAX_LIMIT, MEMORY_OFF_MESSAGE, memoryId, repoScopeKey, channelScopeKey, isSharedScope, LIST_SCOPES, memoryList, memoryForget, memoryCommands;
16066
+ var defineCommand9, MEMORY_LIST_LIMIT, MEMORY_LIST_MAX_LIMIT, MEMORY_OFF_MESSAGE, memoryId, repoScopeKey, channelScopeKey, isSharedScope, LIST_SCOPES, memoryList, memoryForget, memoryCommands;
15660
16067
  var init_memory2 = __esm({
15661
16068
  "../../src/core/commands/memory.ts"() {
15662
16069
  "use strict";
15663
16070
  init_authorize();
15664
16071
  init_commandRegistry();
15665
16072
  init_memory();
15666
- defineCommand8 = commandDefiner();
16073
+ defineCommand9 = commandDefiner();
15667
16074
  MEMORY_LIST_LIMIT = 20;
15668
16075
  MEMORY_LIST_MAX_LIMIT = 50;
15669
16076
  MEMORY_OFF_MESSAGE = "Memory is off in this deployment (`memory.enabled` is not set), so there is nothing to list or forget.";
15670
- memoryId = z12.string().refine(
16077
+ memoryId = z13.string().refine(
15671
16078
  (id) => scopeKeyOfMemoryId(id) !== void 0,
15672
16079
  "expected a memory id like mem:<scope>:<n> (see `memory list`)"
15673
16080
  );
@@ -15675,21 +16082,21 @@ var init_memory2 = __esm({
15675
16082
  channelScopeKey = (channelId) => deriveScopeKey("channel", { channelId });
15676
16083
  isSharedScope = (key, keys) => key === keys.org || key.startsWith("repo:") || key.startsWith("channel:");
15677
16084
  LIST_SCOPES = ["me", "org", "repo", "channel", "all"];
15678
- memoryList = defineCommand8({
16085
+ memoryList = defineCommand9({
15679
16086
  id: "memory.list",
15680
16087
  enabledWhen: (caps) => caps.memory,
15681
16088
  args: [
15682
16089
  {
15683
16090
  name: "query",
15684
- schema: z12.string().optional(),
16091
+ schema: z13.string().optional(),
15685
16092
  describe: "words a record must mention (whole-token match)",
15686
16093
  rest: true
15687
16094
  }
15688
16095
  ],
15689
- options: z12.object({
15690
- scope: z12.enum(LIST_SCOPES).optional().describe("which scopes to list (default all: yours, this repo's, this channel's, and the shared org scope)"),
15691
- limit: z12.coerce.number().int().min(1).max(MEMORY_LIST_MAX_LIMIT).optional().describe(`records per scope (default ${MEMORY_LIST_LIMIT}, max ${MEMORY_LIST_MAX_LIMIT})`),
15692
- repo: z12.string().refine((s) => /^[\w.-]+\/[\w.-]+$/.test(s), "expected an owner/name slug").optional().describe("the repo whose scope to list (default: the repo this thread is bound to)")
16096
+ options: z13.object({
16097
+ scope: z13.enum(LIST_SCOPES).optional().describe("which scopes to list (default all: yours, this repo's, this channel's, and the shared org scope)"),
16098
+ limit: z13.coerce.number().int().min(1).max(MEMORY_LIST_MAX_LIMIT).optional().describe(`records per scope (default ${MEMORY_LIST_LIMIT}, max ${MEMORY_LIST_MAX_LIMIT})`),
16099
+ repo: z13.string().refine((s) => /^[\w.-]+\/[\w.-]+$/.test(s), "expected an owner/name slug").optional().describe("the repo whose scope to list (default: the repo this thread is bound to)")
15693
16100
  }),
15694
16101
  action: "memory:read",
15695
16102
  effect: "read",
@@ -15743,7 +16150,7 @@ var init_memory2 = __esm({
15743
16150
  };
15744
16151
  }
15745
16152
  });
15746
- memoryForget = defineCommand8({
16153
+ memoryForget = defineCommand9({
15747
16154
  id: "memory.forget",
15748
16155
  enabledWhen: (caps) => caps.memory,
15749
16156
  args: [{ name: "id", schema: memoryId, describe: "the record id (`mem:<scope>:<n>`, from `memory list`)" }],
@@ -15878,7 +16285,7 @@ var init_residentDiskBudget = __esm({
15878
16285
  });
15879
16286
 
15880
16287
  // ../../src/core/commands/repo.ts
15881
- import { z as z13 } from "zod";
16288
+ import { z as z14 } from "zod";
15882
16289
  async function adminOf(deps, span) {
15883
16290
  const api = await deps.repo.admin();
15884
16291
  if ("unavailable" in api) throw new CommandError("unavailable", api.unavailable);
@@ -15986,7 +16393,7 @@ ${clipOpOutput(text)}
15986
16393
  \`\`\`` : `${icon} ${str3(o.summary)}`;
15987
16394
  }
15988
16395
  function defineOp(op) {
15989
- return defineCommand9({
16396
+ return defineCommand10({
15990
16397
  id: `repo.${op}`,
15991
16398
  // The op needs a backend: the resident service, or the local workspace (`defaultOperations`).
15992
16399
  enabledWhen: (caps) => caps.residents || caps.execution === "local",
@@ -16044,7 +16451,7 @@ function defineOp(op) {
16044
16451
  function registerRepoCommands(registry) {
16045
16452
  for (const cmd of repoCommands) registry.register(cmd);
16046
16453
  }
16047
- var defineCommand9, DEFAULT_COMMANDS, DEFAULT_REF, SETTLE_POLL_MS, SETTLE_MAX_MS, repoSlug2, gitRef2, command, slugArg, n, obj, str3, repoList, repoOnboard, COMMAND_KEYS, dryRunOptions, repoOffboard, repoRebuild, repoReconfigure, NO_OPS_BACKEND_MESSAGE, repoTest, repoBuild, repoCommands;
16454
+ var defineCommand10, DEFAULT_COMMANDS, DEFAULT_REF, SETTLE_POLL_MS, SETTLE_MAX_MS, repoSlug2, gitRef2, command, slugArg, n, obj, str3, repoList, repoOnboard, COMMAND_KEYS, dryRunOptions, repoOffboard, repoRebuild, repoReconfigure, NO_OPS_BACKEND_MESSAGE, repoTest, repoBuild, repoCommands;
16048
16455
  var init_repo = __esm({
16049
16456
  "../../src/core/commands/repo.ts"() {
16050
16457
  "use strict";
@@ -16052,19 +16459,19 @@ var init_repo = __esm({
16052
16459
  init_residentAdmin();
16053
16460
  init_repoToolchain();
16054
16461
  init_residentDiskBudget();
16055
- defineCommand9 = commandDefiner();
16462
+ defineCommand10 = commandDefiner();
16056
16463
  DEFAULT_COMMANDS = NPM_FALLBACK_COMMANDS;
16057
16464
  DEFAULT_REF = "main";
16058
16465
  SETTLE_POLL_MS = 1e4;
16059
16466
  SETTLE_MAX_MS = 12 * 6e4;
16060
- repoSlug2 = z13.string().refine((s) => parseSlug(s) !== void 0, "expected a GitHub owner/name slug").transform((s) => parseSlug(s));
16061
- gitRef2 = z13.string().refine((s) => validRef(s) !== void 0, "expected a plausible git branch ref (e.g. main)");
16062
- command = z13.string().min(1);
16467
+ repoSlug2 = z14.string().refine((s) => parseSlug(s) !== void 0, "expected a GitHub owner/name slug").transform((s) => parseSlug(s));
16468
+ gitRef2 = z14.string().refine((s) => validRef(s) !== void 0, "expected a plausible git branch ref (e.g. main)");
16469
+ command = z14.string().min(1);
16063
16470
  slugArg = { name: "slug", schema: repoSlug2, describe: "GitHub owner/name of the repo" };
16064
16471
  n = (v) => String(typeof v === "number" ? v : v ?? "?");
16065
16472
  obj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
16066
16473
  str3 = (v) => typeof v === "string" ? v : String(v ?? "?");
16067
- repoList = defineCommand9({
16474
+ repoList = defineCommand10({
16068
16475
  id: "repo.list",
16069
16476
  enabledWhen: (caps) => caps.residents,
16070
16477
  action: "repo:read",
@@ -16081,11 +16488,11 @@ var init_repo = __esm({
16081
16488
  return res.data;
16082
16489
  }
16083
16490
  });
16084
- repoOnboard = defineCommand9({
16491
+ repoOnboard = defineCommand10({
16085
16492
  id: "repo.onboard",
16086
16493
  enabledWhen: (caps) => caps.residents,
16087
16494
  args: [slugArg],
16088
- options: z13.object({
16495
+ options: z14.object({
16089
16496
  ref: gitRef2.optional().describe(`default branch to keep warm (default ${DEFAULT_REF})`),
16090
16497
  test: command.optional().describe(
16091
16498
  "the repo's test command (default: detected from the repo root \u2014 `<pm> test`, or a no-op without a test script)"
@@ -16172,10 +16579,10 @@ var init_repo = __esm({
16172
16579
  settle: (output, { deps }) => settleProvisioning(deps, str3(output.slug))
16173
16580
  });
16174
16581
  COMMAND_KEYS = ["install", "build", "test"];
16175
- dryRunOptions = z13.object({
16582
+ dryRunOptions = z14.object({
16176
16583
  dryRun: flag.optional().describe("compute and return the itemized plan without executing")
16177
16584
  });
16178
- repoOffboard = defineCommand9({
16585
+ repoOffboard = defineCommand10({
16179
16586
  id: "repo.offboard",
16180
16587
  enabledWhen: (caps) => caps.residents,
16181
16588
  args: [slugArg],
@@ -16209,7 +16616,7 @@ var init_repo = __esm({
16209
16616
  return { ...r.data, slug: args.slug, dryRun };
16210
16617
  }
16211
16618
  });
16212
- repoRebuild = defineCommand9({
16619
+ repoRebuild = defineCommand10({
16213
16620
  id: "repo.rebuild",
16214
16621
  enabledWhen: (caps) => caps.residents,
16215
16622
  args: [slugArg],
@@ -16248,11 +16655,11 @@ var init_repo = __esm({
16248
16655
  return { ...r.data, slug: args.slug, dryRun };
16249
16656
  }
16250
16657
  });
16251
- repoReconfigure = defineCommand9({
16658
+ repoReconfigure = defineCommand10({
16252
16659
  id: "repo.reconfigure",
16253
16660
  enabledWhen: (caps) => caps.residents,
16254
16661
  args: [slugArg],
16255
- options: z13.object({
16662
+ options: z14.object({
16256
16663
  ref: gitRef2.optional().describe("new default branch"),
16257
16664
  test: command.optional().describe("new test command"),
16258
16665
  build: command.optional().describe("new build command"),
@@ -17479,7 +17886,7 @@ var init_runsService = __esm({
17479
17886
  });
17480
17887
 
17481
17888
  // ../../src/core/commands/review.ts
17482
- import { z as z14 } from "zod";
17889
+ import { z as z15 } from "zod";
17483
17890
  function artifactJson(a) {
17484
17891
  return {
17485
17892
  ...a.model !== void 0 ? { model: a.model } : {},
@@ -17548,7 +17955,7 @@ function refused2(err2) {
17548
17955
  function registerReviewCommands(registry) {
17549
17956
  for (const cmd of reviewCommands) registry.register(cmd);
17550
17957
  }
17551
- var defineCommand10, runId, ABRIDGE_OFF_MESSAGE, reviewAbridge, reviewCommands;
17958
+ var defineCommand11, runId, ABRIDGE_OFF_MESSAGE, reviewAbridge, reviewCommands;
17552
17959
  var init_review = __esm({
17553
17960
  "../../src/core/commands/review.ts"() {
17554
17961
  "use strict";
@@ -17557,17 +17964,17 @@ var init_review = __esm({
17557
17964
  init_reviewAbridge();
17558
17965
  init_runRecord();
17559
17966
  init_runsService();
17560
- defineCommand10 = commandDefiner();
17561
- runId = z14.string().regex(RUN_ID_PATTERN);
17967
+ defineCommand11 = commandDefiner();
17968
+ runId = z15.string().regex(RUN_ID_PATTERN);
17562
17969
  ABRIDGE_OFF_MESSAGE = "The abridged reading diff is off in this deployment: it needs the `meat` binary on the bot host, the Anthropic provider's credential, `review.readingDiff.provider` not `off`, and run history to store it.";
17563
- reviewAbridge = defineCommand10({
17970
+ reviewAbridge = defineCommand11({
17564
17971
  id: "review.abridge",
17565
17972
  // Hidden unless the abridging can happen here (the binary, the credential,
17566
17973
  // the switch) AND there is a record to append to.
17567
17974
  enabledWhen: (caps) => caps.runHistory && caps.readingDiffAbridge,
17568
17975
  args: [{ name: "id", schema: runId, describe: "run id of a finished PR review" }],
17569
- options: z14.object({
17570
- model: z14.string().min(1).optional().describe("meat's model (default review.readingDiff.meatModel, else claude-opus-5)"),
17976
+ options: z15.object({
17977
+ model: z15.string().min(1).optional().describe("meat's model (default review.readingDiff.meatModel, else claude-opus-5)"),
17571
17978
  force: flag.optional().describe("recompute even when an abridged diff is already stored, or retry a failed one"),
17572
17979
  wait: flag.optional().describe("block until the abridging finishes and answer the outcome instead of `running`")
17573
17980
  }),
@@ -17599,7 +18006,7 @@ var init_review = __esm({
17599
18006
  });
17600
18007
 
17601
18008
  // ../../src/core/commands/runs.ts
17602
- import { z as z15 } from "zod";
18009
+ import { z as z16 } from "zod";
17603
18010
  function unwrap2(res) {
17604
18011
  if (res.ok) return res.value;
17605
18012
  throw new CommandError(res.error, res.error === "not_found" ? "run not found" : "run already finished");
@@ -17640,7 +18047,7 @@ function wrapEvent(e) {
17640
18047
  function registerRunsCommands(registry) {
17641
18048
  for (const cmd of runsCommands) registry.register(cmd);
17642
18049
  }
17643
- var defineCommand11, runId2, positiveInt3, idArg, logDenied, asJson2, runsList, runsGet, runsEvents, runsFriction, runsStop, runsCommands;
18050
+ var defineCommand12, runId2, positiveInt3, idArg, logDenied, asJson2, runsList, runsGet, runsEvents, runsFriction, runsStop, runsCommands;
17644
18051
  var init_runs = __esm({
17645
18052
  "../../src/core/commands/runs.ts"() {
17646
18053
  "use strict";
@@ -17649,21 +18056,21 @@ var init_runs = __esm({
17649
18056
  init_commandRegistry();
17650
18057
  init_runRecord();
17651
18058
  init_runsService();
17652
- defineCommand11 = commandDefiner();
17653
- runId2 = z15.string().regex(RUN_ID_PATTERN);
17654
- positiveInt3 = z15.coerce.number().int().positive();
18059
+ defineCommand12 = commandDefiner();
18060
+ runId2 = z16.string().regex(RUN_ID_PATTERN);
18061
+ positiveInt3 = z16.coerce.number().int().positive();
17655
18062
  idArg = { name: "id", schema: runId2, describe: "run id" };
17656
18063
  logDenied = (entry) => console.log(JSON.stringify({ audit: "authz", ...entry }));
17657
18064
  asJson2 = (v) => v;
17658
- runsList = defineCommand11({
18065
+ runsList = defineCommand12({
17659
18066
  id: "runs.list",
17660
- options: z15.object({
17661
- status: z15.enum(["active", "finished", "all"]).default("active").describe("which runs: live (default), persisted, or both"),
17662
- agent: z15.string().min(1).optional().describe("only runs of this agent"),
17663
- channel: z15.string().min(1).optional().describe("only runs in this platform-namespaced channel (`slack:C0123`, `http:ops`)"),
17664
- sinceMs: z15.coerce.number().int().nonnegative().optional().describe("only runs started at or after this epoch ms"),
18067
+ options: z16.object({
18068
+ status: z16.enum(["active", "finished", "all"]).default("active").describe("which runs: live (default), persisted, or both"),
18069
+ agent: z16.string().min(1).optional().describe("only runs of this agent"),
18070
+ channel: z16.string().min(1).optional().describe("only runs in this platform-namespaced channel (`slack:C0123`, `http:ops`)"),
18071
+ sinceMs: z16.coerce.number().int().nonnegative().optional().describe("only runs started at or after this epoch ms"),
17665
18072
  limit: positiveInt3.max(RUN_LIST_MAX_LIMIT).optional().describe(`page size (max ${RUN_LIST_MAX_LIMIT})`),
17666
- before: z15.coerce.number().int().nonnegative().optional().describe("page cursor: runs finished before this epoch ms"),
18073
+ before: z16.coerce.number().int().nonnegative().optional().describe("page cursor: runs finished before this epoch ms"),
17667
18074
  beforeId: runId2.optional().describe("page cursor tie-breaker: the last id of the previous page")
17668
18075
  }),
17669
18076
  action: "runs:read",
@@ -17674,11 +18081,11 @@ var init_runs = __esm({
17674
18081
  return asJson2(await (await deps.runs()).listRuns({ ...options, visibleTo }));
17675
18082
  }
17676
18083
  });
17677
- runsGet = defineCommand11({
18084
+ runsGet = defineCommand12({
17678
18085
  id: "runs.get",
17679
18086
  args: [idArg],
17680
- options: z15.object({
17681
- include: z15.enum(["messages"]).optional().describe("add the run's events, free text wrapped as untrusted content")
18087
+ options: z16.object({
18088
+ include: z16.enum(["messages"]).optional().describe("add the run's events, free text wrapped as untrusted content")
17682
18089
  }),
17683
18090
  action: "runs:read",
17684
18091
  effect: "read",
@@ -17698,11 +18105,11 @@ var init_runs = __esm({
17698
18105
  return asJson2(view);
17699
18106
  }
17700
18107
  });
17701
- runsEvents = defineCommand11({
18108
+ runsEvents = defineCommand12({
17702
18109
  id: "runs.events",
17703
18110
  args: [idArg],
17704
- options: z15.object({
17705
- afterSeq: z15.coerce.number().int().nonnegative().optional().describe("events with seq greater than this"),
18111
+ options: z16.object({
18112
+ afterSeq: z16.coerce.number().int().nonnegative().optional().describe("events with seq greater than this"),
17706
18113
  limit: positiveInt3.max(MAX_EVENTS_PAGE).optional().describe(`page size (max ${MAX_EVENTS_PAGE})`)
17707
18114
  }),
17708
18115
  action: "runs:read",
@@ -17716,7 +18123,7 @@ var init_runs = __esm({
17716
18123
  return asJson2({ ...page2, events: page2.events.map(wrapEvent) });
17717
18124
  }
17718
18125
  });
17719
- runsFriction = defineCommand11({
18126
+ runsFriction = defineCommand12({
17720
18127
  id: "runs.friction",
17721
18128
  args: [idArg],
17722
18129
  action: "runs:read",
@@ -17729,10 +18136,10 @@ var init_runs = __esm({
17729
18136
  return asJson2(unwrap2(await runs.getRunFriction(args.id)));
17730
18137
  }
17731
18138
  });
17732
- runsStop = defineCommand11({
18139
+ runsStop = defineCommand12({
17733
18140
  id: "runs.stop",
17734
18141
  args: [idArg],
17735
- options: z15.object({ mode: z15.enum(["soft", "hard"]).describe("soft = finish the current step; hard = abort now") }),
18142
+ options: z16.object({ mode: z16.enum(["soft", "hard"]).describe("soft = finish the current step; hard = abort now") }),
17736
18143
  action: "runs:write",
17737
18144
  effect: "write",
17738
18145
  describe: "Request a live run to stop (`--mode soft` = finish the current step; `hard` = abort now). Records the caller as the actor.",
@@ -17756,16 +18163,16 @@ var init_runs = __esm({
17756
18163
  function registerScheduleCommands(registry) {
17757
18164
  for (const cmd of scheduleCommands) registry.register(cmd);
17758
18165
  }
17759
- var defineCommand12, ACTION_LABEL, utc, scheduleList, scheduleCommands;
18166
+ var defineCommand13, ACTION_LABEL, utc, scheduleList, scheduleCommands;
17760
18167
  var init_schedule = __esm({
17761
18168
  "../../src/core/commands/schedule.ts"() {
17762
18169
  "use strict";
17763
18170
  init_commandRegistry();
17764
18171
  init_schedules();
17765
- defineCommand12 = commandDefiner();
18172
+ defineCommand13 = commandDefiner();
17766
18173
  ACTION_LABEL = { healthz: "keep-alive", watchdog: "resident watchdog" };
17767
18174
  utc = (ms) => `${new Date(ms).toISOString().slice(0, 16).replace("T", " ")} UTC`;
17768
- scheduleList = defineCommand12({
18175
+ scheduleList = defineCommand13({
17769
18176
  id: "schedule.list",
17770
18177
  enabledWhen: (caps) => caps.schedules,
17771
18178
  action: "schedule:read",
@@ -18029,7 +18436,7 @@ var init_plan2 = __esm({
18029
18436
  });
18030
18437
 
18031
18438
  // ../../src/core/commands/setup.ts
18032
- import { z as z16 } from "zod";
18439
+ import { z as z17 } from "zod";
18033
18440
  async function gatherAnswers(o, setup) {
18034
18441
  const ask = setup.prompt;
18035
18442
  const given = (v) => v !== void 0;
@@ -18099,33 +18506,33 @@ function capabilityLine(c) {
18099
18506
  function registerSetupCommands(registry) {
18100
18507
  for (const cmd of setupCommands) registry.register(cmd);
18101
18508
  }
18102
- var defineCommand13, secret, digits, workerName, hostname2, httpUrl, account, initOptions2, DEFAULT_NAME, setupInit, ASK, setupCommands;
18509
+ var defineCommand14, secret, digits, workerName, hostname2, httpUrl, account, initOptions2, DEFAULT_NAME, setupInit, ASK, setupCommands;
18103
18510
  var init_setup = __esm({
18104
18511
  "../../src/core/commands/setup.ts"() {
18105
18512
  "use strict";
18106
18513
  init_plan2();
18107
18514
  init_commandRegistry();
18108
18515
  init_deploy();
18109
- defineCommand13 = commandDefiner();
18110
- secret = z16.string().min(1);
18111
- digits = z16.string().regex(/^\d+$/, "expected a number");
18112
- workerName = z16.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "expected a Worker name: lowercase letters, digits, hyphens");
18113
- hostname2 = z16.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/, "expected a bare DNS name (example.com)");
18114
- httpUrl = z16.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL");
18115
- account = z16.string().regex(/^[0-9a-f]{32}$/, "expected a Cloudflare account id: 32 hex characters");
18116
- initOptions2 = z16.object({
18117
- organization: z16.string().min(1).optional().describe("the GitHub organization (or user) this installation serves \u2014 config.yaml's `organization`"),
18516
+ defineCommand14 = commandDefiner();
18517
+ secret = z17.string().min(1);
18518
+ digits = z17.string().regex(/^\d+$/, "expected a number");
18519
+ workerName = z17.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "expected a Worker name: lowercase letters, digits, hyphens");
18520
+ hostname2 = z17.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/, "expected a bare DNS name (example.com)");
18521
+ httpUrl = z17.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL");
18522
+ account = z17.string().regex(/^[0-9a-f]{32}$/, "expected a Cloudflare account id: 32 hex characters");
18523
+ initOptions2 = z17.object({
18524
+ organization: z17.string().min(1).optional().describe("the GitHub organization (or user) this installation serves \u2014 config.yaml's `organization`"),
18118
18525
  anthropicKey: secret.optional().describe("an Anthropic API key \u2192 ANTHROPIC_API_KEY and the `anthropic` provider"),
18119
18526
  openaiCompatible: httpUrl.optional().describe(
18120
18527
  "an OpenAI-compatible endpoint's base URL (https://api.openai.com/v1, http://localhost:11434/v1) \u2192 the `openai` provider"
18121
18528
  ),
18122
- model: z16.string().min(1).optional().describe("with --openai-compatible: the model every agent runs on at that endpoint (openai/<model>)"),
18529
+ model: z17.string().min(1).optional().describe("with --openai-compatible: the model every agent runs on at that endpoint (openai/<model>)"),
18123
18530
  modelKey: secret.optional().describe("with --openai-compatible: the endpoint's key \u2192 OPENAI_API_KEY (a local endpoint needs none)"),
18124
18531
  slackAppToken: secret.optional().describe("the Slack app-level token (xapp-\u2026) \u2192 SLACK_APP_TOKEN"),
18125
18532
  slackBotToken: secret.optional().describe("the Slack bot token (xoxb-\u2026) \u2192 SLACK_BOT_TOKEN"),
18126
18533
  githubAppId: digits.optional().describe("the GitHub App's id \u2192 GITHUB_APP_ID"),
18127
18534
  githubInstallationId: digits.optional().describe("the GitHub App's installation id \u2192 GITHUB_APP_INSTALLATION_ID"),
18128
- githubPrivateKeyFile: z16.string().min(1).optional().describe("path to the GitHub App's private key (PEM) \u2192 GITHUB_APP_PRIVATE_KEY, one quoted line"),
18535
+ githubPrivateKeyFile: z17.string().min(1).optional().describe("path to the GitHub App's private key (PEM) \u2192 GITHUB_APP_PRIVATE_KEY, one quoted line"),
18129
18536
  cloudflare: account.optional().describe(
18130
18537
  "a Cloudflare account id: also write deploy/profile.json and render the Worker configs (needs --zone; from a checkout's root, or anywhere when this CLI is the published package)"
18131
18538
  ),
@@ -18135,7 +18542,7 @@ var init_setup = __esm({
18135
18542
  dryRun: flag.optional().describe("write nothing; print each file as it would be written, secrets masked")
18136
18543
  });
18137
18544
  DEFAULT_NAME = "switchboard";
18138
- setupInit = defineCommand13({
18545
+ setupInit = defineCommand14({
18139
18546
  id: "setup.init",
18140
18547
  options: initOptions2,
18141
18548
  action: "setup:write",
@@ -18235,13 +18642,13 @@ function renderStatus(output) {
18235
18642
  function registerStatusCommands(registry) {
18236
18643
  for (const cmd of statusCommands) registry.register(cmd);
18237
18644
  }
18238
- var defineCommand14, statusShow, statusCommands;
18645
+ var defineCommand15, statusShow, statusCommands;
18239
18646
  var init_status = __esm({
18240
18647
  "../../src/core/commands/status.ts"() {
18241
18648
  "use strict";
18242
18649
  init_commandRegistry();
18243
- defineCommand14 = commandDefiner();
18244
- statusShow = defineCommand14({
18650
+ defineCommand15 = commandDefiner();
18651
+ statusShow = defineCommand15({
18245
18652
  id: "status.show",
18246
18653
  action: "status:read",
18247
18654
  effect: "read",
@@ -18278,6 +18685,7 @@ function registerCoreCommands(registry) {
18278
18685
  registerDeployCommands(registry);
18279
18686
  registerEnvCommands(registry);
18280
18687
  registerSetupCommands(registry);
18688
+ registerContractCommands(registry);
18281
18689
  }
18282
18690
  function coreCommandGroups() {
18283
18691
  const registry = new CommandRegistry({ audit: () => {
@@ -18290,6 +18698,7 @@ var init_all = __esm({
18290
18698
  "use strict";
18291
18699
  init_commandRegistry();
18292
18700
  init_config3();
18701
+ init_contract2();
18293
18702
  init_deploy();
18294
18703
  init_env();
18295
18704
  init_friction();
@@ -18445,7 +18854,8 @@ var init_githubRepoInspect = __esm({
18445
18854
  });
18446
18855
 
18447
18856
  // ../../src/core/commandCatalogue.ts
18448
- import { readFileSync as readFileSync12 } from "node:fs";
18857
+ import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
18858
+ import { readFile as readFile2 } from "node:fs/promises";
18449
18859
  function unstampedStatus() {
18450
18860
  let version2 = "unknown";
18451
18861
  try {
@@ -18471,6 +18881,9 @@ function defaultOperations(config, secrets, caller) {
18471
18881
  function readSource(source2) {
18472
18882
  return Promise.resolve(readFileSync12(source2 === "-" ? 0 : source2, "utf8"));
18473
18883
  }
18884
+ async function readOptionalFile(path) {
18885
+ return existsSync10(path) ? readFile2(path, "utf8") : void 0;
18886
+ }
18474
18887
  function once(provided) {
18475
18888
  if (typeof provided !== "function") {
18476
18889
  const ready = Promise.resolve(provided);
@@ -18559,7 +18972,9 @@ function buildCoreCommands(config, store, wiring) {
18559
18972
  env: { bootstrap: bootstrapOnHost },
18560
18973
  status: { snapshot: () => (wiring.status ?? unstampedStatus)() },
18561
18974
  // `setup init` writes the operator's working directory — the one the CLI runs in.
18562
- setup: hostSetupIO()
18975
+ setup: hostSetupIO(),
18976
+ // `contract render` reads the paths the CLI caller names (CLI-only).
18977
+ contract: { readFile: readOptionalFile }
18563
18978
  };
18564
18979
  return bindCommands(registry, deps);
18565
18980
  }
@@ -19535,7 +19950,7 @@ var init_tracingExecutor = __esm({
19535
19950
  });
19536
19951
 
19537
19952
  // ../../src/core/prDescription.ts
19538
- import { z as z17 } from "zod";
19953
+ import { z as z18 } from "zod";
19539
19954
  function parsePrDescription(input) {
19540
19955
  return PrDescriptionSchema.parse(input);
19541
19956
  }
@@ -19823,34 +20238,34 @@ var init_prDescription = __esm({
19823
20238
  "../../src/core/prDescription.ts"() {
19824
20239
  "use strict";
19825
20240
  init_redact();
19826
- line = z17.string().trim().min(1).refine((s) => !/[\r\n]/.test(s), "must be a single line (no embedded newlines)");
19827
- prose = z17.string().trim().min(1);
19828
- TourAnchorSchema = z17.object({
20241
+ line = z18.string().trim().min(1).refine((s) => !/[\r\n]/.test(s), "must be a single line (no embedded newlines)");
20242
+ prose = z18.string().trim().min(1);
20243
+ TourAnchorSchema = z18.object({
19829
20244
  // Repo-relative: no leading `/`, no `..` SEGMENT (a filename like `a..b.ts` is fine).
19830
- path: z17.string().trim().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), "path must be repo-relative"),
19831
- from: z17.number().int().positive(),
19832
- to: z17.number().int().positive()
20245
+ path: z18.string().trim().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), "path must be repo-relative"),
20246
+ from: z18.number().int().positive(),
20247
+ to: z18.number().int().positive()
19833
20248
  }).refine((a) => a.to >= a.from, "to must be >= from");
19834
- TourStepSchema = z17.object({
20249
+ TourStepSchema = z18.object({
19835
20250
  title: line,
19836
20251
  description: prose,
19837
20252
  lookFor: prose.optional(),
19838
20253
  anchor: TourAnchorSchema
19839
20254
  });
19840
- PrDescriptionSchema = z17.object({
20255
+ PrDescriptionSchema = z18.object({
19841
20256
  /** The PR title's single source. Metadata for the PR's own title field —
19842
20257
  * never rendered into the body (GitHub shows the title itself). */
19843
20258
  title: line,
19844
20259
  tldr: prose,
19845
20260
  whatWhy: prose,
19846
- tour: z17.array(TourStepSchema).min(1),
20261
+ tour: z18.array(TourStepSchema).min(1),
19847
20262
  /** Every touched file the Tour steps did not cover, one line each. */
19848
- remaining: z17.array(z17.object({ path: line, note: prose })),
19849
- decisions: z17.array(z17.object({ title: line, rationale: prose })).min(1),
20263
+ remaining: z18.array(z18.object({ path: line, note: prose })),
20264
+ decisions: z18.array(z18.object({ title: line, rationale: prose })).min(1),
19850
20265
  risks: prose,
19851
- validation: z17.object({
20266
+ validation: z18.object({
19852
20267
  summary: prose.optional(),
19853
- criteria: z17.array(z17.object({ criterion: prose, proof: prose })).min(1)
20268
+ criteria: z18.array(z18.object({ criterion: prose, proof: prose })).min(1)
19854
20269
  })
19855
20270
  });
19856
20271
  REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
@@ -19994,7 +20409,7 @@ var init_reviewVerdict = __esm({
19994
20409
  });
19995
20410
 
19996
20411
  // ../../src/core/diffDigest.ts
19997
- function basename(p) {
20412
+ function basename2(p) {
19998
20413
  const i = p.lastIndexOf("/");
19999
20414
  return i === -1 ? p : p.slice(i + 1);
20000
20415
  }
@@ -20031,7 +20446,7 @@ function riskReasons(entry, paths) {
20031
20446
  if (any(AUTH_RE)) reasons.push("auth/permission-sensitive");
20032
20447
  if (any(INFRA_RE)) reasons.push("infra/deploy config");
20033
20448
  if (entry.status === "deleted") reasons.push("whole-file deletion");
20034
- if (candidates.some((p) => LOCKFILES.has(basename(p).toLowerCase()))) reasons.push("lockfile");
20449
+ if (candidates.some((p) => LOCKFILES.has(basename2(p).toLowerCase()))) reasons.push("lockfile");
20035
20450
  const churn = entry.adds + entry.dels;
20036
20451
  if (churn >= LARGE_CHURN) reasons.push(`large change (${churn} lines)`);
20037
20452
  return reasons;
@@ -20626,7 +21041,7 @@ ${skill.body}${footer}`;
20626
21041
  });
20627
21042
 
20628
21043
  // ../../src/tools/workspace.ts
20629
- import { z as z18 } from "zod";
21044
+ import { z as z19 } from "zod";
20630
21045
  var bashTool, readFileTool, writeFileTool, NAME_STATUS_MARK, NAME_STATUS_MARK_LINE, OUTPUT_CUT_RE, diffDigestTool, submitVerdictTool, submitDispositionsTool, submitPrDescriptionTool, updateStatusTool, TOOLSETS;
20631
21046
  var init_workspace = __esm({
20632
21047
  "../../src/tools/workspace.ts"() {
@@ -20930,7 +21345,7 @@ ${raw.trim()}`;
20930
21345
  try {
20931
21346
  desc = parsePrDescription(input);
20932
21347
  } catch (err2) {
20933
- const detail = err2 instanceof z18.ZodError ? err2.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ") : String(err2);
21348
+ const detail = err2 instanceof z19.ZodError ? err2.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ") : String(err2);
20934
21349
  return `error: invalid PR description \u2014 ${detail}`;
20935
21350
  }
20936
21351
  ctx.onPrDescription?.(desc);
@@ -21791,14 +22206,9 @@ Target repository: ${input.repo}. ` + (workspace ? `Your shell starts in the wor
21791
22206
  ...head.verified ? { verifiedAtAttach: true } : {}
21792
22207
  }) : void 0;
21793
22208
  const agentSystem = (head) => {
21794
- const target = targetBlock(head);
21795
- const baseSystem = target ? `${residentSystem ?? agent.system}
21796
-
21797
- ${target}` : residentSystem;
21798
- const trailing = [blocks.skills, blocks.mcp].filter((b) => Boolean(b));
21799
- return trailing.length > 0 ? [baseSystem ?? agent.system, ...trailing].join("\n\n") : baseSystem;
22209
+ return [residentSystem ?? agent.system, targetBlock(head), input.contract, blocks.skills, blocks.mcp].filter((b) => Boolean(b)).join("\n\n");
21800
22210
  };
21801
- return (head) => [blocks.memory, blocks.config, blocks.about, blocks.instructions, agentSystem(head) ?? agent.system].filter((part) => Boolean(part)).join("\n\n");
22211
+ return (head) => [blocks.memory, blocks.config, blocks.about, blocks.instructions, agentSystem(head)].filter((part) => Boolean(part)).join("\n\n");
21802
22212
  }
21803
22213
  async function settleReviewedHead(input) {
21804
22214
  return input.span ? input.span.span("run.settle_reviewed_head", (span) => settle(input, span)) : settle(input, void 0);
@@ -24129,15 +24539,15 @@ var init_frontmatter = __esm({
24129
24539
  });
24130
24540
 
24131
24541
  // ../../src/skills/stores.ts
24132
- import { existsSync as existsSync10, readFileSync as readFileSync13, readdirSync as readdirSync2 } from "node:fs";
24133
- import { join as join10 } from "node:path";
24542
+ import { existsSync as existsSync11, readFileSync as readFileSync13, readdirSync as readdirSync2 } from "node:fs";
24543
+ import { join as join11 } from "node:path";
24134
24544
  function loadBundledSkills(dir) {
24135
- if (!existsSync10(dir)) return [];
24545
+ if (!existsSync11(dir)) return [];
24136
24546
  const skills = [];
24137
24547
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
24138
24548
  if (!entry.isDirectory()) continue;
24139
- const file = join10(dir, entry.name, "SKILL.md");
24140
- if (!existsSync10(file)) continue;
24549
+ const file = join11(dir, entry.name, "SKILL.md");
24550
+ if (!existsSync11(file)) continue;
24141
24551
  try {
24142
24552
  skills.push(parseSkillMarkdown(readFileSync13(file, "utf8")));
24143
24553
  } catch (err2) {
@@ -25746,9 +26156,18 @@ ${findings}
25746
26156
  Review:
25747
26157
  ${input.review}`;
25748
26158
  }
26159
+ function withContractInFirstUserTurn(messages, block) {
26160
+ const at = messages.findIndex((m) => m.role === "user");
26161
+ if (at < 0) return [...messages, { role: "user", content: [{ type: "text", text: block }] }];
26162
+ return messages.map((m, i) => i === at ? { ...m, content: [...m.content, { type: "text", text: block }] } : m);
26163
+ }
25749
26164
  async function runShipCodingChild(input, ctx, opts, roundSpan) {
25750
26165
  const { entry, clip: clip3, now } = ctx;
25751
26166
  const { control, github, logKey } = input;
26167
+ const messages = opts.contract ? withContractInFirstUserTurn(
26168
+ opts.messages,
26169
+ renderContract(opts.contract, { maxChars: DEFAULT_CONTRACT_MAX_CHARS }).text
26170
+ ) : opts.messages;
25752
26171
  const spec = input.child("coding");
25753
26172
  const ws = await attachRoundWorkspace({
25754
26173
  factory: input.factory,
@@ -25828,7 +26247,7 @@ ${shipBranchContract(entry.branch)}`;
25828
26247
  provider: spec.provider,
25829
26248
  model: spec.model,
25830
26249
  agent: clip3(spec.agent),
25831
- messages: opts.messages,
26250
+ messages,
25832
26251
  ...roundSpan ? { span: roundSpan } : {},
25833
26252
  backend: ws.selection.backend,
25834
26253
  system,
@@ -25850,7 +26269,7 @@ ${shipBranchContract(entry.branch)}`;
25850
26269
  });
25851
26270
  if (turnTarget) {
25852
26271
  descriptionTurnRan = true;
25853
- const turnMessages = [...opts.messages];
26272
+ const turnMessages = [...messages];
25854
26273
  const run2 = (span) => runDescriptionTurn({
25855
26274
  ...span ? { span } : {},
25856
26275
  target: turnTarget,
@@ -25920,6 +26339,7 @@ var init_codingChild = __esm({
25920
26339
  init_reviewVerdict();
25921
26340
  init_codingPrPostStep();
25922
26341
  init_reviewRound();
26342
+ init_contract();
25923
26343
  }
25924
26344
  });
25925
26345
 
@@ -26003,6 +26423,7 @@ async function runShipReviewChild(input, ctx, round, roundSpan) {
26003
26423
  repo: entry.repo,
26004
26424
  workspace: binding?.workspace,
26005
26425
  prTarget: { repo: entry.repo, pr: pr.number, ref: entry.branch, baseRef: entry.base },
26426
+ ...round.contract ? { contract: renderContract(round.contract, { maxChars: DEFAULT_CONTRACT_MAX_CHARS }).text } : {},
26006
26427
  blocks: input.blocks(spec)
26007
26428
  });
26008
26429
  const clipped = clip3(spec.agent);
@@ -26091,6 +26512,7 @@ var init_reviewChild = __esm({
26091
26512
  init_reviewVerdict();
26092
26513
  init_reviewedHead();
26093
26514
  init_reviewRound();
26515
+ init_contract();
26094
26516
  }
26095
26517
  });
26096
26518
 
@@ -27775,8 +28197,8 @@ var init_runHistoryWriter = __esm({
27775
28197
  });
27776
28198
 
27777
28199
  // ../../src/core/runStore.ts
27778
- import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync14, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
27779
- import { join as join11, resolve as resolve9 } from "node:path";
28200
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync14, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
28201
+ import { join as join12, resolve as resolve9 } from "node:path";
27780
28202
  function isValidRunId(id) {
27781
28203
  return typeof id === "string" && RUN_ID_PATTERN.test(id);
27782
28204
  }
@@ -27818,7 +28240,7 @@ function buildRunStore(cfg, secrets, deps) {
27818
28240
  const now = deps.now ?? Date.now;
27819
28241
  const policy = retentionPolicyOf(cfg);
27820
28242
  if (cfg.store === "file") {
27821
- const store = new FileRunStore(join11(deps.dataDir, RUNS_DIR), { policy, now });
28243
+ const store = new FileRunStore(join12(deps.dataDir, RUNS_DIR), { policy, now });
27822
28244
  store.sweep();
27823
28245
  const timer = (deps.setInterval ?? setInterval)(() => store.sweep(), SWEEP_INTERVAL_MS);
27824
28246
  timer.unref?.();
@@ -27885,7 +28307,7 @@ var init_runStore = __esm({
27885
28307
  this.now = opts.now ?? Date.now;
27886
28308
  }
27887
28309
  recordPath(id) {
27888
- return join11(this.dir, `${id}.json`);
28310
+ return join12(this.dir, `${id}.json`);
27889
28311
  }
27890
28312
  ensureDir() {
27891
28313
  mkdirSync8(this.dir, { recursive: true, mode: DIR_MODE });
@@ -27907,8 +28329,8 @@ var init_runStore = __esm({
27907
28329
  }
27908
28330
  /** Every valid index line, a repeated id resolved to its last line. */
27909
28331
  readIndex() {
27910
- const path = join11(this.dir, INDEX_FILE);
27911
- if (!existsSync11(path)) return [];
28332
+ const path = join12(this.dir, INDEX_FILE);
28333
+ if (!existsSync12(path)) return [];
27912
28334
  const byId = /* @__PURE__ */ new Map();
27913
28335
  for (const line2 of readFileSync14(path, "utf8").split("\n")) {
27914
28336
  if (!line2.trim()) continue;
@@ -27939,7 +28361,7 @@ var init_runStore = __esm({
27939
28361
  const keptIds = new Set(kept.map((r) => r.id));
27940
28362
  for (const item of index) if (!keptIds.has(item.id)) rmSync3(this.recordPath(item.id), { force: true });
27941
28363
  this.writeAtomic(
27942
- join11(this.dir, INDEX_FILE),
28364
+ join12(this.dir, INDEX_FILE),
27943
28365
  kept.map((r) => JSON.stringify(r)).join("\n") + (kept.length ? "\n" : "")
27944
28366
  );
27945
28367
  return kept;
@@ -27989,7 +28411,7 @@ var init_runStore = __esm({
27989
28411
  async delete(id) {
27990
28412
  if (!isValidRunId(id)) return;
27991
28413
  const index = this.readIndex();
27992
- if (!index.some((r) => r.id === id) && !existsSync11(this.recordPath(id))) return;
28414
+ if (!index.some((r) => r.id === id) && !existsSync12(this.recordPath(id))) return;
27993
28415
  this.ensureDir();
27994
28416
  rmSync3(this.recordPath(id), { force: true });
27995
28417
  this.compact(index.filter((r) => r.id !== id));
@@ -27997,7 +28419,7 @@ var init_runStore = __esm({
27997
28419
  /** Delete everything outside policy now (start-up and the 6 h timer). A
27998
28420
  * missing directory means nothing to sweep. */
27999
28421
  sweep() {
28000
- if (!existsSync11(this.dir)) return;
28422
+ if (!existsSync12(this.dir)) return;
28001
28423
  this.compact(this.readIndex());
28002
28424
  }
28003
28425
  };
@@ -28807,8 +29229,8 @@ var init_registry3 = __esm({
28807
29229
  });
28808
29230
 
28809
29231
  // ../../src/mcp/secretStore.ts
28810
- import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
28811
- import { dirname as dirname8, resolve as resolve10 } from "node:path";
29232
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
29233
+ import { dirname as dirname9, resolve as resolve10 } from "node:path";
28812
29234
  var FileMcpSecretStore, MCP_SECRET_WORKER_TIMEOUT_MS, WorkerMcpSecretStore;
28813
29235
  var init_secretStore = __esm({
28814
29236
  "../../src/mcp/secretStore.ts"() {
@@ -28821,7 +29243,7 @@ var init_secretStore = __esm({
28821
29243
  this.path = resolve10(path);
28822
29244
  }
28823
29245
  read() {
28824
- if (!existsSync12(this.path)) return { credentials: {}, tickets: {} };
29246
+ if (!existsSync13(this.path)) return { credentials: {}, tickets: {} };
28825
29247
  const raw = JSON.parse(readFileSync15(this.path, "utf8"));
28826
29248
  const credentials = {};
28827
29249
  for (const [k, v] of Object.entries(raw.credentials ?? {})) if (isSealedCredential(v)) credentials[k] = v;
@@ -28830,7 +29252,7 @@ var init_secretStore = __esm({
28830
29252
  return { credentials, tickets };
28831
29253
  }
28832
29254
  write(doc) {
28833
- mkdirSync9(dirname8(this.path), { recursive: true });
29255
+ mkdirSync9(dirname9(this.path), { recursive: true });
28834
29256
  writeFileSync8(this.path, JSON.stringify(doc, null, 2));
28835
29257
  }
28836
29258
  async putCredential(sealed) {
@@ -31582,18 +32004,18 @@ var init_liveView = __esm({
31582
32004
 
31583
32005
  // ../../src/channels/webAssets.ts
31584
32006
  import { readdirSync as readdirSync3, readFileSync as readFileSync16, statSync as statSync3 } from "node:fs";
31585
- import { join as join12 } from "node:path";
32007
+ import { join as join13 } from "node:path";
31586
32008
  function contentTypeFor(path) {
31587
32009
  const dot = path.lastIndexOf(".");
31588
32010
  return (dot === -1 ? void 0 : CONTENT_TYPES[path.slice(dot)]) ?? "application/octet-stream";
31589
32011
  }
31590
32012
  function webDistDir(env, packageRoot) {
31591
- return env.SWITCHBOARD_WEB_DIST ?? join12(packageRoot, WEB_DIST_DIR);
32013
+ return env.SWITCHBOARD_WEB_DIST ?? join13(packageRoot, WEB_DIST_DIR);
31592
32014
  }
31593
32015
  function loadWebAssets(distDir) {
31594
32016
  let manifest;
31595
32017
  try {
31596
- manifest = JSON.parse(readFileSync16(join12(distDir, ".vite", "manifest.json"), "utf8"));
32018
+ manifest = JSON.parse(readFileSync16(join13(distDir, ".vite", "manifest.json"), "utf8"));
31597
32019
  } catch (err2) {
31598
32020
  throw new Error(
31599
32021
  `web app manifest not found under ${distDir} \u2014 build it first (npm run build in web/): ${err2 instanceof Error ? err2.message : String(err2)}`,
@@ -31607,9 +32029,9 @@ function loadWebAssets(distDir) {
31607
32029
  css: (entryChunk.css ?? []).map((f) => `/${f}`)
31608
32030
  };
31609
32031
  const files = /* @__PURE__ */ new Map();
31610
- const assetsDir = join12(distDir, "assets");
32032
+ const assetsDir = join13(distDir, "assets");
31611
32033
  for (const name of readdirSync3(assetsDir)) {
31612
- const full = join12(assetsDir, name);
32034
+ const full = join13(assetsDir, name);
31613
32035
  if (statSync3(full).isFile()) files.set(`/assets/${name}`, readFileSync16(full));
31614
32036
  }
31615
32037
  const serve = (req, res) => {
@@ -33587,7 +34009,7 @@ __export(index_exports, {
33587
34009
  runBot: () => runBot
33588
34010
  });
33589
34011
  import { createServer } from "node:http";
33590
- import { join as join13 } from "node:path";
34012
+ import { join as join14 } from "node:path";
33591
34013
  async function runBot() {
33592
34014
  for (const v of ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"]) {
33593
34015
  if (!processSecrets.get(v)) {
@@ -34155,7 +34577,7 @@ var init_index = __esm({
34155
34577
  init_secrets2();
34156
34578
  DATA_DIR = installationPath(OPERATOR_ROOT, "data");
34157
34579
  CONFIG_PATH2 = process.env.SWITCHBOARD_CONFIG ?? installationPath(OPERATOR_ROOT, "config/config.yaml");
34158
- OVERRIDES_PATH = process.env.SWITCHBOARD_OVERRIDES ?? join13(DATA_DIR, "overrides.json");
34580
+ OVERRIDES_PATH = process.env.SWITCHBOARD_OVERRIDES ?? join14(DATA_DIR, "overrides.json");
34159
34581
  PROCESS_STARTED_AT = systemClock();
34160
34582
  sampleProcessMetrics = startProcessMetrics();
34161
34583
  INTERRUPTED_WRITE_BUDGET_MS = 1e4;
@@ -34199,8 +34621,8 @@ init_source();
34199
34621
  init_invokedAsScript();
34200
34622
  init_secrets2();
34201
34623
  import { Console } from "node:console";
34202
- import { existsSync as existsSync13 } from "node:fs";
34203
- import { join as join14 } from "node:path";
34624
+ import { existsSync as existsSync14 } from "node:fs";
34625
+ import { join as join15 } from "node:path";
34204
34626
  var CONFIG_PATH3 = process.env.SWITCHBOARD_CONFIG ?? installationPath(OPERATOR_ROOT, "config/config.yaml");
34205
34627
  var DATA_DIR2 = installationPath(OPERATOR_ROOT, "data");
34206
34628
  var CLI_CALLER = { kind: "cli", id: CLI_ACTOR.id, actor: CLI_ACTOR };
@@ -34387,7 +34809,7 @@ function missingBotConfig(configPath) {
34387
34809
  );
34388
34810
  }
34389
34811
  async function loadBotConfig(configPath, overridesPath, opts = {}) {
34390
- if (parseConfigLocation(configPath).kind === "file" && !(opts.exists ?? existsSync13)(configPath))
34812
+ if (parseConfigLocation(configPath).kind === "file" && !(opts.exists ?? existsSync14)(configPath))
34391
34813
  throw missingBotConfig(configPath);
34392
34814
  return openConfigStore(configPath, {
34393
34815
  overridesPath,
@@ -34416,7 +34838,7 @@ function bindBotConfig(configPath, overridesPath, opts = {}) {
34416
34838
  }
34417
34839
  function cliCapabilities(configPath, env, secrets, opts = {}) {
34418
34840
  if (parseConfigLocation(configPath).kind !== "file") return ALL_CAPABILITIES;
34419
- if (!(opts.exists ?? existsSync13)(configPath)) return ALL_CAPABILITIES;
34841
+ if (!(opts.exists ?? existsSync14)(configPath)) return ALL_CAPABILITIES;
34420
34842
  try {
34421
34843
  return capabilitiesFrom((opts.load ?? loadAppConfig)(configPath), env, secrets, { meatBinary: meatOnPath(env) });
34422
34844
  } catch {
@@ -34425,7 +34847,7 @@ function cliCapabilities(configPath, env, secrets, opts = {}) {
34425
34847
  }
34426
34848
  function wireCli() {
34427
34849
  const warn = (m) => console.error(m);
34428
- const botConfig = bindBotConfig(CONFIG_PATH3, join14(DATA_DIR2, "cli-overrides.json"));
34850
+ const botConfig = bindBotConfig(CONFIG_PATH3, join15(DATA_DIR2, "cli-overrides.json"));
34429
34851
  let loaded;
34430
34852
  const bot = () => loaded ??= botConfig().then((config) => ({
34431
34853
  config,