@tablation/crew 0.115.0 → 0.116.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +73 -6
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2382,6 +2382,7 @@ var HOOK_NAMES = [
2382
2382
  "isolate",
2383
2383
  "handoff",
2384
2384
  "preview",
2385
+ "map",
2385
2386
  "ci_status",
2386
2387
  "version",
2387
2388
  "bump",
@@ -3014,6 +3015,7 @@ var ConfigError = class extends Error {
3014
3015
  };
3015
3016
  var DEFAULT_MAX_PERISCOPES = 3;
3016
3017
  var DEFAULT_PERISCOPE_IDLE_MINUTES = 120;
3018
+ var DEFAULT_MAP_TOKEN_BUDGET = 1e3;
3017
3019
  function parseCloneScheme(v) {
3018
3020
  if (v === void 0 || v === null || v === "") return void 0;
3019
3021
  if (v === "ssh" || v === "https") return v;
@@ -3106,6 +3108,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
3106
3108
  "previewHost",
3107
3109
  "maxPeriscopes",
3108
3110
  "periscopeIdleMinutes",
3111
+ "mapTokenBudget",
3109
3112
  "secretStore",
3110
3113
  "env",
3111
3114
  "runner",
@@ -3454,6 +3457,10 @@ Create the first of those:
3454
3457
  if (!Number.isInteger(periscopeIdleMinutes) || periscopeIdleMinutes < 1) {
3455
3458
  missing.add(`ship.periscopeIdleMinutes must be a positive integer (got "${shipRaw.periscopeIdleMinutes}")`);
3456
3459
  }
3460
+ const mapTokenBudget = shipRaw.mapTokenBudget === void 0 ? DEFAULT_MAP_TOKEN_BUDGET : Number(shipRaw.mapTokenBudget);
3461
+ if (!Number.isInteger(mapTokenBudget) || mapTokenBudget < 1) {
3462
+ missing.add(`ship.mapTokenBudget must be a positive integer (got "${shipRaw.mapTokenBudget}")`);
3463
+ }
3457
3464
  const secretStoreRaw = shipRaw.secretStore === void 0 ? "auto" : shipRaw.secretStore;
3458
3465
  if (!isSecretStoreMode(secretStoreRaw)) {
3459
3466
  missing.add(`ship.secretStore must be one of ${SECRET_STORE_MODES.join(" | ")} (got "${String(shipRaw.secretStore)}")`);
@@ -3537,6 +3544,7 @@ Create the first of those:
3537
3544
  previewHost: shipRaw.previewHost ? String(shipRaw.previewHost).trim() || void 0 : void 0,
3538
3545
  maxPeriscopes,
3539
3546
  periscopeIdleMinutes,
3547
+ mapTokenBudget,
3540
3548
  secretStore,
3541
3549
  env: parseShipEnv(shipRaw.env)
3542
3550
  },
@@ -4039,6 +4047,7 @@ function runScript(script, opts) {
4039
4047
  stdio: ["ignore", "pipe", "pipe"]
4040
4048
  });
4041
4049
  let output = "";
4050
+ let stdout = "";
4042
4051
  let pending = "";
4043
4052
  const absorb = (buf) => {
4044
4053
  const text2 = buf.toString();
@@ -4049,18 +4058,21 @@ function runScript(script, opts) {
4049
4058
  pending = lines2.pop() ?? "";
4050
4059
  for (const l of lines2) opts.onLine(l);
4051
4060
  };
4052
- child.stdout.on("data", absorb);
4061
+ child.stdout.on("data", (buf) => {
4062
+ stdout += buf.toString();
4063
+ absorb(buf);
4064
+ });
4053
4065
  child.stderr.on("data", absorb);
4054
4066
  const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : void 0;
4055
4067
  child.on("error", (err3) => {
4056
4068
  if (timer) clearTimeout(timer);
4057
4069
  resolvePromise({ code: 127, output: `${output}
4058
- crew: cannot run ${shell.bin}: ${err3.message}` });
4070
+ crew: cannot run ${shell.bin}: ${err3.message}`, stdout });
4059
4071
  });
4060
4072
  child.on("close", (code) => {
4061
4073
  if (timer) clearTimeout(timer);
4062
4074
  if (pending && opts.onLine) opts.onLine(pending);
4063
- resolvePromise({ code: code ?? 1, output });
4075
+ resolvePromise({ code: code ?? 1, output, stdout });
4064
4076
  });
4065
4077
  });
4066
4078
  }
@@ -6280,6 +6292,7 @@ var HOOK_PURPOSE = [
6280
6292
  ["ports", "this worktree's port assignments; prints KEY=value overrides to export"],
6281
6293
  ["handoff", "leave the operator able to open what you built; put its output in your progress comment"],
6282
6294
  ["preview", "start this worktree's dev server for a viewer, bound to CREW_PREVIEW_HOST, detached; its last stdout line is the URL"],
6295
+ ["map", 'prints an outline of the repository; the crew places it under "Repository map" below'],
6283
6296
  ["test", "run the suite"],
6284
6297
  ["build", "build it"],
6285
6298
  ["ci_status", "after you push and before you hand off: exit 0 means CI is green for your branch; on failure fix and push again, and say in your progress comment if it is still pending"]
@@ -6376,6 +6389,13 @@ function repoSection(r, key, heading) {
6376
6389
  }
6377
6390
  return lines2;
6378
6391
  }
6392
+ function mapSection(r, heading) {
6393
+ if (r.map) return ["", heading, "", r.map, ""];
6394
+ if (r.mapSkipped) {
6395
+ return ["", `Note: ${r.mapSkipped}. Say so in one line of your progress comment; it does not block the work.`, ""];
6396
+ }
6397
+ return [];
6398
+ }
6379
6399
  function renderEnvironment(i) {
6380
6400
  const { route: route2, contract } = i;
6381
6401
  const repos = i.repos;
@@ -6567,12 +6587,16 @@ function renderEnvironment(i) {
6567
6587
  for (const r of repos) {
6568
6588
  lines2.push(...i.sandbox ? ["", `### ${r.name}`] : repoSection(r, key, `### ${r.name} \u2014 \`${r.dir}\``));
6569
6589
  lines2.push("", `Hooks for ${r.name} \u2014 the only commands you should run here:`, "", hookTable(r.config));
6590
+ lines2.push(...mapSection(r, `#### Repository map \u2014 ${r.name}`));
6570
6591
  }
6571
6592
  }
6572
6593
  if (i.repoDir) {
6573
6594
  lines2.push("", `This ticket's work happens in \`${i.repoDir}\`.`);
6574
6595
  }
6575
- if (only) lines2.push("", "## Hooks \u2014 the only commands you should run", "", hookTable(only.config));
6596
+ if (only) {
6597
+ lines2.push("", "## Hooks \u2014 the only commands you should run", "", hookTable(only.config));
6598
+ lines2.push(...mapSection(only, "## Repository map"));
6599
+ }
6576
6600
  return lines2.join("\n");
6577
6601
  }
6578
6602
 
@@ -17764,6 +17788,49 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
17764
17788
  return { merged, conflicts, unbuildable, version, tag, deployed, integrated, confirmed, decision };
17765
17789
  }
17766
17790
 
17791
+ // src/repo-map.ts
17792
+ var CHARS_PER_TOKEN = 4;
17793
+ var MAP_TIMEOUT_MS = 6e4;
17794
+ function budgetMap(text2, tokenBudget) {
17795
+ const body = text2.replace(/\s+$/, "");
17796
+ const limit = tokenBudget * CHARS_PER_TOKEN;
17797
+ if (body.length <= limit) return body;
17798
+ const lines2 = body.split("\n");
17799
+ const noteFor = (kept2) => `\u2026 ${lines2.length - kept2} of ${lines2.length} lines cut to fit ship.mapTokenBudget (${tokenBudget} tokens)`;
17800
+ let kept = lines2.length;
17801
+ let size = body.length;
17802
+ while (kept > 0 && size + 1 + noteFor(kept).length > limit) {
17803
+ kept--;
17804
+ size -= lines2[kept].length + 1;
17805
+ }
17806
+ return [...lines2.slice(0, kept), noteFor(kept)].join("\n");
17807
+ }
17808
+ function loadRepoMap(repo, tokenBudget, shellFallback) {
17809
+ const script = repo.config.hooks.map;
17810
+ if (!script) return Promise.resolve({ skipped: "" });
17811
+ return runScript(script, {
17812
+ cwd: repo.dir,
17813
+ shell: resolveShell(repo.config.shell ?? shellFallback),
17814
+ env: { CREW_REPO: repo.name },
17815
+ timeoutMs: MAP_TIMEOUT_MS
17816
+ }).then(
17817
+ (r) => {
17818
+ if (r.code !== 0) return { skipped: `the \`map\` hook for ${repo.name} exited ${r.code}, so the brief has no repository map` };
17819
+ if (!r.stdout.trim()) return { skipped: `the \`map\` hook for ${repo.name} printed nothing, so the brief has no repository map` };
17820
+ return { text: budgetMap(r.stdout, tokenBudget) };
17821
+ },
17822
+ (e) => ({ skipped: `the \`map\` hook for ${repo.name} could not run (${e instanceof Error ? e.message : String(e)}), so the brief has no repository map` })
17823
+ );
17824
+ }
17825
+ async function withRepoMaps(repos, tokenBudget, shellFallback) {
17826
+ return Promise.all(repos.map(async (r) => {
17827
+ if (!r.config.hooks.map) return r;
17828
+ const out = await loadRepoMap(r, tokenBudget, shellFallback);
17829
+ if ("skipped" in out) return out.skipped ? { ...r, mapSkipped: out.skipped } : r;
17830
+ return { ...r, map: out.text };
17831
+ }));
17832
+ }
17833
+
17767
17834
  // src/failure-alert.ts
17768
17835
  var FAILURE_ALERT_MARKER = calloutHeader("CAUTION", "Failure Alert");
17769
17836
  function findOpenFailureAlert(tickets, headline) {
@@ -21971,7 +22038,7 @@ async function buildEnvironment(route2, ship, ticket) {
21971
22038
  });
21972
22039
  const authorship = await tracker.authorshipProbe().catch(() => null);
21973
22040
  const attachments = await tracker.attachmentsProbe().catch(() => null);
21974
- return renderEnvironment({ route: route2, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name, ...sandboxEnv(ship) });
22041
+ return renderEnvironment({ route: route2, userAgent: ship.userAgent, repos: await withRepoMaps(repos, ship.mapTokenBudget, ship.shell), contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name, ...sandboxEnv(ship) });
21975
22042
  }
21976
22043
 
21977
22044
  // src/control-api.ts
@@ -24137,7 +24204,7 @@ ${changed2.map((c) => ` - ${c}
24137
24204
  return renderEnvironment({
24138
24205
  route: c,
24139
24206
  userAgent: cfg.ship.userAgent,
24140
- repos: await resolvedRepos(c),
24207
+ repos: await withRepoMaps(await resolvedRepos(c), cfg.ship.mapTokenBudget, cfg.ship.shell),
24141
24208
  contract,
24142
24209
  sourceTicket: ticket,
24143
24210
  authorship: await tracker.authorshipProbe().catch(() => null),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.115.0",
3
+ "version": "0.116.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",