@tablation/crew 0.75.1 → 0.75.2

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 +63 -6
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5168,6 +5168,7 @@ var DEFAULT_CONTRACT = {
5168
5168
  resolutionNote: "resolution_note",
5169
5169
  verifiedSha: "verified_sha",
5170
5170
  prRef: "pr_ref",
5171
+ externalId: "external_id",
5171
5172
  position: "position"
5172
5173
  },
5173
5174
  comments: {
@@ -5652,6 +5653,29 @@ var State = class _State {
5652
5653
  }
5653
5654
  };
5654
5655
  }
5656
+ /**
5657
+ * The previous cycle's unroutable ticket ids for one route (CREW-2548): open
5658
+ * tickets with no repository that are not exempt. `poll.ts` diffs against it
5659
+ * so each ticket warns once when it becomes unroutable, not every cycle; the
5660
+ * caller persists the new set beside `attention(route).persist(...)`. A
5661
+ * missing or corrupt file reads as empty, so a fresh install warns once.
5662
+ */
5663
+ unroutable(route) {
5664
+ const file = `.unroutable-${_State.safe(route)}.json`;
5665
+ return {
5666
+ previous: () => {
5667
+ try {
5668
+ const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5669
+ return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
5670
+ } catch {
5671
+ return [];
5672
+ }
5673
+ },
5674
+ persist: (next) => {
5675
+ writeFileSync8(this.path(file), JSON.stringify(next));
5676
+ }
5677
+ };
5678
+ }
5655
5679
  /**
5656
5680
  * Open ship-level attention items (CREW-1373), ship-wide rather than
5657
5681
  * per-route: a parked role or a missing hook command is about this
@@ -9349,6 +9373,14 @@ function filingErrors(tickets, opts = {}) {
9349
9373
  return !exempt.has(t[reportTypeColumn]);
9350
9374
  });
9351
9375
  }
9376
+ function unroutableChanges(unroutable, previous, tickets, externalIdColumn) {
9377
+ const prev = new Set(previous);
9378
+ const now = new Set(unroutable.map((t) => t.id));
9379
+ return {
9380
+ added: unroutable.filter((t) => !prev.has(t.id)).map((t) => ({ ticket: t, synced: !!t[externalIdColumn] })),
9381
+ gained: tickets.filter((t) => prev.has(t.id) && !now.has(t.id) && !!t.repo_id)
9382
+ };
9383
+ }
9352
9384
  function sweepComment(step) {
9353
9385
  return step.action === "park" ? `Parked as **Blocked** by the crew \u2014 waiting on: ${step.blockers}.
9354
9386
 
@@ -10131,15 +10163,30 @@ async function decideCycle(o) {
10131
10163
  emit.emit(`every child is done \u2014 this coordinating ticket is too`, { ticket: r.parent.issue_id });
10132
10164
  }
10133
10165
  }
10134
- for (const t of filingErrors(tickets, {
10166
+ const unroutableTickets = filingErrors(tickets, {
10135
10167
  ...coordinating,
10136
10168
  repoExemptValues: tracker.contract.repoExemptReportTypes
10137
- })) {
10169
+ });
10170
+ const unroutable = unroutableTickets.map((t) => t.id);
10171
+ const { added, gained } = unroutableChanges(
10172
+ unroutableTickets,
10173
+ state.unroutable(route.route).previous(),
10174
+ tickets,
10175
+ tracker.contract.columns.externalId
10176
+ );
10177
+ for (const { ticket: t, synced } of added) {
10138
10178
  emit.warn(
10139
- "no repo and not marked coordinating/question/investigation \u2014 a filing error, not an epic, and unroutable as written",
10179
+ synced ? "no repository \u2014 not crew work until one is set" : "no repo and not marked coordinating/question/investigation \u2014 a filing error, not an epic, and unroutable as written",
10140
10180
  { ticket: t.issue_id }
10141
10181
  );
10142
10182
  }
10183
+ for (const t of gained) emit.emit("now has a repository \u2014 no longer unroutable", { ticket: t.issue_id });
10184
+ if (unroutableTickets.length) {
10185
+ emit.emit(
10186
+ `${unroutableTickets.length} open ticket(s) have no repository and are not routable (${added.length} new since last cycle)`,
10187
+ { step: "sweep", data: { unroutable: unroutableTickets.map((t) => t.issue_id), newUnroutable: added.map((a) => a.ticket.issue_id) } }
10188
+ );
10189
+ }
10143
10190
  const attentionCurrent = {};
10144
10191
  for (const t of tickets) {
10145
10192
  const reasons = attentionReasons(t, tracker.contract);
@@ -10212,7 +10259,8 @@ async function decideCycle(o) {
10212
10259
  watermark,
10213
10260
  directionEvents,
10214
10261
  selectionInput,
10215
- attention: attentionCurrent
10262
+ attention: attentionCurrent,
10263
+ unroutable
10216
10264
  };
10217
10265
  const advanced = state.advanceWatermark([...comments, ...tickets]);
10218
10266
  if (advanced !== watermark) {
@@ -13128,7 +13176,10 @@ async function decideFleet(o) {
13128
13176
  if (e.error || !e.decision) continue;
13129
13177
  const won = best?.route.route === e.route.route;
13130
13178
  o.state.fairness(e.route.route).record(e.decision.actionable.count, e.decision.actionable.top?.issue_id, won);
13131
- if (!o.dryRun) o.state.attention(e.route.route).persist(e.decision.attention);
13179
+ if (!o.dryRun) {
13180
+ o.state.attention(e.route.route).persist(e.decision.attention);
13181
+ o.state.unroutable(e.route.route).persist(e.decision.unroutable);
13182
+ }
13132
13183
  }
13133
13184
  return {
13134
13185
  entries,
@@ -16276,6 +16327,7 @@ async function runOnePassUnguarded(o) {
16276
16327
  }
16277
16328
  const decision = await decide(ctx);
16278
16329
  o.state.attention(o.route.route).persist(decision.attention);
16330
+ o.state.unroutable(o.route.route).persist(decision.unroutable);
16279
16331
  if (decision.sweep.length) {
16280
16332
  const tracker = new Tracker(o.route, o.ship);
16281
16333
  const seat = o.route.resolved?.seats.qa ?? o.route.resolved?.seats.dev ?? "";
@@ -18034,7 +18086,10 @@ ${changed2.map((c) => ` - ${c}
18034
18086
  break;
18035
18087
  }
18036
18088
  const decision = await decideCycle({ route, ship: cfg.ship, state, emit });
18037
- if (!dryRun) state.attention(route.route).persist(decision.attention);
18089
+ if (!dryRun) {
18090
+ state.attention(route.route).persist(decision.attention);
18091
+ state.unroutable(route.route).persist(decision.unroutable);
18092
+ }
18038
18093
  if (decision.sweep.length) {
18039
18094
  if (dryRun) {
18040
18095
  for (const s of decision.sweep) {
@@ -20567,6 +20622,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20567
20622
  // Additive (CREW-1496): each project and its work mode. `version` stays 1.
20568
20623
  projects: projects.map((p) => ({ id: p.id, name: p.name, workMode: p.mode })),
20569
20624
  dir: route.dir,
20625
+ // Additive (CREW-2548): open tickets with no repository, as of the last cycle.
20626
+ unroutable: state.unroutable(route.route).previous().length,
20570
20627
  enabled: route.enabled,
20571
20628
  area: route.area ?? null,
20572
20629
  // Additive (CREW-1511): the toolchain this route's sessions run on.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.75.1",
3
+ "version": "0.75.2",
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",