@tablation/crew 0.23.0 → 0.24.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.
package/dist/cli.js CHANGED
@@ -7684,6 +7684,45 @@ Still failing: ${n.detail ?? n.headline}`, memberId);
7684
7684
  }
7685
7685
  }
7686
7686
 
7687
+ // src/projects-cmd.ts
7688
+ async function buildProjectsReport(routes, open) {
7689
+ return Promise.all(routes.map(async (r) => {
7690
+ const areaId = r.areaId ?? null;
7691
+ try {
7692
+ const rows = await open(r.route).projectRowsWithModes();
7693
+ return {
7694
+ route: r.route,
7695
+ areaId,
7696
+ projects: rows.map((p) => ({
7697
+ id: p.id,
7698
+ name: p.name ?? p.id,
7699
+ issuePrefix: p.issue_prefix ?? null,
7700
+ workMode: p.mode,
7701
+ current: p.id === areaId
7702
+ }))
7703
+ };
7704
+ } catch (e) {
7705
+ return { route: r.route, areaId, error: e.message };
7706
+ }
7707
+ }));
7708
+ }
7709
+ function formatProjectsReport(report) {
7710
+ const lines = [];
7711
+ for (const r of report) {
7712
+ lines.push(`route ${r.route}`);
7713
+ if ("error" in r) {
7714
+ lines.push(` error: ${r.error}`);
7715
+ continue;
7716
+ }
7717
+ if (r.projects.length === 0) lines.push(" (no projects readable)");
7718
+ for (const p of r.projects) {
7719
+ lines.push(` ${p.current ? "*" : " "} ${p.name} ${p.issuePrefix ?? "-"} ${p.workMode} ${p.id}`);
7720
+ }
7721
+ }
7722
+ return `${lines.join("\n")}
7723
+ `;
7724
+ }
7725
+
7687
7726
  // src/cli.ts
7688
7727
  import { StaleWriteError as StaleWriteError8 } from "@tablation/client";
7689
7728
 
@@ -10522,6 +10561,7 @@ function usage() {
10522
10561
  crew deploy [route] release now, even with nothing new to merge
10523
10562
  crew watch [route] live view of what the crew is doing
10524
10563
  crew status [route] [--json] paused/running state; --json for a machine reader
10564
+ crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
10525
10565
  crew doctor [route] [--fix] preflight; --fix enables clean routes and offers crew install
10526
10566
  crew ports [route] which checkout owns which ports, and what is up
10527
10567
  crew reap [route] kill orphaned servers, drop worktrees for closed tickets
@@ -10685,7 +10725,7 @@ if (command === "passenger-sync-daemon") {
10685
10725
  const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
10686
10726
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
10687
10727
  const named = command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
10688
- const fleetWide = command === "inbox" || command === "connect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
10728
+ const fleetWide = command === "inbox" || command === "connect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
10689
10729
  let route;
10690
10730
  try {
10691
10731
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -13000,6 +13040,23 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
13000
13040
  }
13001
13041
  break;
13002
13042
  }
13043
+ case "projects": {
13044
+ const only = value("route") ?? positional[1];
13045
+ const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
13046
+ if (only && wanted.length === 0) {
13047
+ process.stderr.write(`crew projects: no route "${only}" (have: ${cfg.routes.map((r) => r.route).join(", ")})
13048
+ `);
13049
+ process.exit(2);
13050
+ }
13051
+ const report = await buildProjectsReport(
13052
+ wanted.map((r) => ({ route: r.route, areaId: r.resolved?.areaId })),
13053
+ (name) => new Tracker(wanted.find((r) => r.route === name), cfg.ship)
13054
+ );
13055
+ process.stdout.write(flag("json") ? `${JSON.stringify({ routes: report }, null, 2)}
13056
+ ` : formatProjectsReport(report));
13057
+ if (report.length > 0 && report.every((r) => "error" in r)) process.exitCode = 1;
13058
+ break;
13059
+ }
13003
13060
  case "status": {
13004
13061
  const fairness = state.fairness(route.route);
13005
13062
  const waiting = fairness.waiting();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.23.0",
3
+ "version": "0.24.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",
@@ -148,7 +148,7 @@ human review, so "probably fine" is not a pass.
148
148
  branch you expected. A person built it; the operator has to push it.
149
149
  - **It doesn't** → `status` = `in_progress`, `assignee_id` = the building
150
150
  lane's own Crew row id from the roster above — the dev seat if
151
- `needs_design` is false or null, the design seat if true. **Exception:** when the Issues table has a `held_by_ship_id` column, **clear** `assignee_id` *and* `held_by_ship_id` instead of naming a seat — the
151
+ `needs_design` is false or null, the design seat if true. **Exceptions:** in a `hybrid` project (the digest's `mode` column) an unassigned ticket is off limits to every lane, so clearing the assignee would strand the bounce: assign the building seat on this ship (ids under "Seats on this ship" in the Environment section, else the roster) and, if the Issues table has a `held_by_ship_id` column, still clear that. In an `automatic` project, when the Issues table has a `held_by_ship_id` column, **clear** `assignee_id` *and* `held_by_ship_id` instead of naming a seat — the
152
152
  hold on a `qa` ticket is yours, as verifier, and seats are per ship, so
153
153
  naming this ship's seat would hand the bounce to a ship that never built
154
154
  it. An unassigned, unheld `in_progress` ticket is picked up by whichever
@@ -134,7 +134,7 @@ human review, so "probably fine" is not a pass.
134
134
  builder: set `needs_info` and assign the operator, with a comment naming the
135
135
  branch you expected. A person built it; the operator has to push it.
136
136
  - **It doesn't** → `status` = `in_progress`, `assignee_id` = the dev
137
- seat's own Crew row id from the roster above. **Exception:** when the Issues table has a `held_by_ship_id` column, **clear** `assignee_id` *and* `held_by_ship_id` instead of naming a seat — the
137
+ seat's own Crew row id from the roster above. **Exceptions:** in a `hybrid` project (the digest's `mode` column) an unassigned ticket is off limits to every lane, so clearing the assignee would strand the bounce: assign the building seat on this ship (ids under "Seats on this ship" in the Environment section, else the roster) and, if the Issues table has a `held_by_ship_id` column, still clear that. In an `automatic` project, when the Issues table has a `held_by_ship_id` column, **clear** `assignee_id` *and* `held_by_ship_id` instead of naming a seat — the
138
138
  hold on a `qa` ticket is yours, as verifier, and seats are per ship, so
139
139
  naming this ship's seat would hand the bounce to a ship that never built
140
140
  it. An unassigned, unheld `in_progress` ticket is picked up by whichever