@tablation/crew 0.65.0 → 0.67.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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +34 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -263,7 +263,7 @@ crew inbox [--member NAME] your tickets across every workspace
263
263
  crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
264
264
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
265
265
  crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
266
- crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo writes them, --recipes, --agent, --json)
266
+ crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo writes them, --recipes, --hook KEY=CMD, --agent, --json)
267
267
  crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
268
268
  crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
269
269
  ```
package/dist/cli.js CHANGED
@@ -1449,6 +1449,7 @@ var HOOK_NAMES = [
1449
1449
  "ports",
1450
1450
  "isolate",
1451
1451
  "handoff",
1452
+ "ci_status",
1452
1453
  "version",
1453
1454
  "bump",
1454
1455
  "pr",
@@ -2792,7 +2793,8 @@ var HOOK_PURPOSE = [
2792
2793
  ["ports", "this worktree's port assignments; prints KEY=value overrides to export"],
2793
2794
  ["handoff", "leave the operator able to open what you built; put its output in your progress comment"],
2794
2795
  ["test", "run the suite"],
2795
- ["build", "build it"]
2796
+ ["build", "build it"],
2797
+ ["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"]
2796
2798
  ];
2797
2799
  function hookTable(repo) {
2798
2800
  const declared = HOOK_PURPOSE.filter(([name]) => repo.hooks[name]);
@@ -5198,6 +5200,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
5198
5200
  "dir",
5199
5201
  "extra-path",
5200
5202
  "harness",
5203
+ "hook",
5201
5204
  "key",
5202
5205
  "level",
5203
5206
  "limit",
@@ -13281,11 +13284,11 @@ function detectRecipes(f) {
13281
13284
  {
13282
13285
  id: "github-actions-ci-status",
13283
13286
  title: "Check CI status before hand-off",
13284
- description: "Ask GitHub whether the latest run on the branch passed before a ticket is handed to QA. The repo spec has no hook for this yet, so it is reported but cannot be written.",
13287
+ description: "Ask GitHub whether the latest run on the branch passed before a ticket is handed to QA (`hooks.ci_status`).",
13285
13288
  evidence,
13286
- hooks: {},
13287
- check: `gh run list --branch "$CREW_BRANCH" --limit 1 --json conclusion -q '.[0].conclusion == "success"'`,
13288
- writable: false,
13289
+ // `gh` prints the conclusion and exits 0 either way, so the test is on the text.
13290
+ hooks: { ci_status: `test "$(gh run list --branch "$CREW_BRANCH" --limit 1 --json conclusion -q '.[0].conclusion')" = success` },
13291
+ writable: true,
13289
13292
  defaultOn: false
13290
13293
  }
13291
13294
  ];
@@ -13318,6 +13321,21 @@ function selectRecipes(s, ids) {
13318
13321
  }
13319
13322
  return { hooks, ...release ? { release } : {} };
13320
13323
  }
13324
+ var HOOK_KEYS = ["test", "build", "version", "bump", "released", "ci_status"];
13325
+ function parseHookOverrides(specs) {
13326
+ const out = {};
13327
+ for (const spec of specs) {
13328
+ const eq2 = spec.indexOf("=");
13329
+ const key = eq2 < 0 ? spec : spec.slice(0, eq2);
13330
+ const val = eq2 < 0 ? "" : spec.slice(eq2 + 1);
13331
+ if (!HOOK_KEYS.includes(key)) {
13332
+ throw new ReposError(`--hook: unknown key "${key}" (valid keys: ${HOOK_KEYS.join(", ")})`);
13333
+ }
13334
+ if (val.trim() === "") throw new ReposError(`--hook ${key}= needs a command (expected key=value)`);
13335
+ out[key] = val;
13336
+ }
13337
+ return out;
13338
+ }
13321
13339
  function parseHooksArgs(args, routes) {
13322
13340
  const rest = [...args];
13323
13341
  let route;
@@ -13326,7 +13344,7 @@ function parseHooksArgs(args, routes) {
13326
13344
  if (routes.length === 1) route = routes[0];
13327
13345
  else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
13328
13346
  }
13329
- if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--json]");
13347
+ if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--hook KEY=CMD] [--json]");
13330
13348
  return { route, name: rest[0] };
13331
13349
  }
13332
13350
  function repoDirOf(text, base, route, name) {
@@ -13410,7 +13428,7 @@ var AgentSuggestError = class extends Error {
13410
13428
  this.reason = reason;
13411
13429
  }
13412
13430
  };
13413
- var HOOK_KEYS = ["test", "build", "version", "bump", "released"];
13431
+ var HOOK_KEYS2 = ["test", "build", "version", "bump", "released"];
13414
13432
  var READ_ONLY_TOOLS = ["Read", "Grep", "Glob"];
13415
13433
  var WRITING_TOOLS = ["Bash", "Edit", "Write", "NotebookEdit", "WebFetch", "WebSearch", "Task", "Agent"];
13416
13434
  var DEFAULT_TIMEOUT_MS = 18e4;
@@ -13492,7 +13510,7 @@ function parseAgentHooks(text) {
13492
13510
  }
13493
13511
  const raw = obj && typeof obj === "object" && obj.hooks && typeof obj.hooks === "object" ? obj.hooks : {};
13494
13512
  const hooks = {};
13495
- for (const k of HOOK_KEYS) if (typeof raw[k] === "string" && raw[k].trim()) hooks[k] = raw[k].trim();
13513
+ for (const k of HOOK_KEYS2) if (typeof raw[k] === "string" && raw[k].trim()) hooks[k] = raw[k].trim();
13496
13514
  const notes = Array.isArray(obj.notes) ? obj.notes.filter((n) => typeof n === "string") : [];
13497
13515
  return { hooks, notes };
13498
13516
  }
@@ -13567,7 +13585,7 @@ ${run2.err}` });
13567
13585
  throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text || run2.err).trim().slice(0, 300) || `exited ${run2.code}`);
13568
13586
  }
13569
13587
  const { hooks, notes } = parseAgentHooks(parsed.text);
13570
- const differsFromSniff = HOOK_KEYS.filter((k) => hooks[k] !== void 0 && hooks[k] !== o.sniff.hooks[k]);
13588
+ const differsFromSniff = HOOK_KEYS2.filter((k) => hooks[k] !== void 0 && hooks[k] !== o.sniff.hooks[k]);
13571
13589
  return { source: "agent", hooks, filesRead: parsed.filesRead, differsFromSniff, notes, toolchain: o.spec.name, model };
13572
13590
  }
13573
13591
 
@@ -17160,7 +17178,7 @@ ${changed2.map((c) => ` - ${c}
17160
17178
  case "repos": {
17161
17179
  const sub = positional[1];
17162
17180
  const usage2 = () => {
17163
- process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--json]\n");
17181
+ process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
17164
17182
  process.exit(2);
17165
17183
  };
17166
17184
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
@@ -17207,9 +17225,13 @@ ${changed2.map((c) => ` - ${c}
17207
17225
  sigintOwnedByCommand = false;
17208
17226
  }
17209
17227
  }
17210
- const picked = selectRecipes(suggestion, recipeIds);
17228
+ const hookSpecs = argv.flatMap((x, i) => x === "--hook" && argv[i + 1] !== void 0 ? [argv[i + 1]] : x.startsWith("--hook=") ? [x.slice(7)] : []);
17229
+ const overrides = parseHookOverrides(hookSpecs);
17230
+ if (hookSpecs.length > 0 && !write) throw new ReposError("--hook only applies with --write route|repo");
17231
+ const base2 = selectRecipes(suggestion, recipeIds);
17232
+ const picked = { ...base2, hooks: { ...base2.hooks, ...overrides } };
17211
17233
  let written;
17212
- if (write && suggestion.source === null && recipeIds.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
17234
+ if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
17213
17235
  if (write === "route") {
17214
17236
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
17215
17237
  writeFileSync19(cfg.configFile, plan.text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.65.0",
3
+ "version": "0.67.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",