@tablation/crew 0.6.1 → 0.7.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
@@ -824,6 +824,7 @@ function parseResolved(raw, m, where) {
824
824
  operator: m.req(raw.operator, `${where}.resolved.operator`),
825
825
  holds: holds2,
826
826
  contract: raw.contract ?? void 0,
827
+ ...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
827
828
  ...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
828
829
  ...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
829
830
  };
@@ -1083,6 +1084,10 @@ async function hydrateApiKeys(routes, getStore = getSessionStore) {
1083
1084
  }
1084
1085
  }
1085
1086
  }
1087
+ function releaseSeat(rt) {
1088
+ const r = rt.resolved;
1089
+ return r?.releaseSeat ?? r?.seats.dev ?? r?.seats.qa ?? "";
1090
+ }
1086
1091
  function configuredMembers(rt) {
1087
1092
  const r = rt.resolved;
1088
1093
  if (!r) throw new ConfigError(`route "${rt.route}" has no resolved ids \u2014 run \`crew connect\``);
@@ -1675,7 +1680,9 @@ var DEFAULT_CONTRACT = {
1675
1680
  updatedAt: "updated_at",
1676
1681
  needsDesign: "needs_design",
1677
1682
  reportType: "report_type",
1678
- epic: "epic_id"
1683
+ epic: "epic_id",
1684
+ author: "filed_by_id",
1685
+ ship: "ship_id"
1679
1686
  },
1680
1687
  comments: {
1681
1688
  parent: "ticket_id",
@@ -1683,7 +1690,8 @@ var DEFAULT_CONTRACT = {
1683
1690
  author: "team_member_id",
1684
1691
  kind: "kind",
1685
1692
  eventKind: "event",
1686
- createdAt: "created_at"
1693
+ createdAt: "created_at",
1694
+ ship: "ship_id"
1687
1695
  },
1688
1696
  statuses: {
1689
1697
  open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
@@ -2471,6 +2479,12 @@ var Tracker = class {
2471
2479
  client;
2472
2480
  models;
2473
2481
  userAgent;
2482
+ shipName;
2483
+ /**
2484
+ * The once-per-process authorship probe (CREW-1371), memoised as a promise
2485
+ * so concurrent first writes share one round of field-metadata reads.
2486
+ */
2487
+ authorship;
2474
2488
  stateDir;
2475
2489
  constructor(route, ship) {
2476
2490
  this.route = route;
@@ -2482,6 +2496,7 @@ var Tracker = class {
2482
2496
  this.models = route.resolved.models;
2483
2497
  this.contract = resolveContract(route.contract);
2484
2498
  this.userAgent = ship.userAgent;
2499
+ this.shipName = ship.name;
2485
2500
  this.stateDir = ship.stateDir;
2486
2501
  this.client = new TablationClient({
2487
2502
  baseUrl: `${route.baseUrl}/api`,
@@ -2834,13 +2849,60 @@ var Tracker = class {
2834
2849
  * someone else" wake signal.
2835
2850
  */
2836
2851
  async postEvent(ticketId, body, memberId) {
2852
+ const auth = await this.authorshipProbe();
2837
2853
  await this.client.records.create(this.models.comments, {
2838
2854
  ticket_id: ticketId,
2839
2855
  body,
2840
2856
  team_member_id: memberId,
2841
- kind: "event"
2857
+ kind: "event",
2858
+ ...auth.commentShip && auth.shipId ? { [this.contract.comments.ship]: auth.shipId } : {}
2842
2859
  });
2843
2860
  }
2861
+ /**
2862
+ * The authorship probe (CREW-1371), read once per process: which optional
2863
+ * `filed_by_id`/`ship_id` columns exist on Issues and Comments, and this
2864
+ * ship's own Ships row id. Memoised as a promise so concurrent first writes
2865
+ * share one round of metadata reads. A table whose metadata cannot be read
2866
+ * reads as "column absent", the same way `connect.ts`'s `owner_id` probe does.
2867
+ */
2868
+ authorshipProbe() {
2869
+ this.authorship ??= this.probeAuthorship();
2870
+ return this.authorship;
2871
+ }
2872
+ async probeAuthorship() {
2873
+ const [issueFields, commentFields, shipRow] = await Promise.all([
2874
+ this.columnNames(this.models.issues),
2875
+ this.columnNames(this.models.comments),
2876
+ // No name (a caller that only beats or patches, never stamps) means no row to find.
2877
+ this.shipName ? this.myShipRow(this.shipName) : Promise.resolve(void 0)
2878
+ ]);
2879
+ return {
2880
+ issueAuthor: issueFields.has(this.contract.columns.author),
2881
+ issueShip: issueFields.has(this.contract.columns.ship),
2882
+ commentShip: commentFields.has(this.contract.comments.ship),
2883
+ shipId: shipRow?.id ?? null
2884
+ };
2885
+ }
2886
+ async columnNames(modelId) {
2887
+ try {
2888
+ const model = await this.client.dataModels.get(modelId);
2889
+ return new Set(model.fields.map((f) => f.columnName));
2890
+ } catch {
2891
+ return /* @__PURE__ */ new Set();
2892
+ }
2893
+ }
2894
+ /**
2895
+ * The authorship columns this workspace is missing, as `table.column` names
2896
+ * for `crew doctor` to report (CREW-1371). Empty on a fully updated install.
2897
+ */
2898
+ async missingAuthorshipColumns() {
2899
+ const auth = await this.authorshipProbe();
2900
+ const missing = [];
2901
+ if (!auth.issueAuthor) missing.push(`Issues.${this.contract.columns.author}`);
2902
+ if (!auth.issueShip) missing.push(`Issues.${this.contract.columns.ship}`);
2903
+ if (!auth.commentShip) missing.push(`Comments.${this.contract.comments.ship}`);
2904
+ return missing;
2905
+ }
2844
2906
  /**
2845
2907
  * The public `/v/<slug>/r/<token>` link for one ticket (ISSUE-928) —
2846
2908
  * fetch-or-create via `POST /views/:viewId/record-links`, so a repeated
@@ -2882,8 +2944,13 @@ var Tracker = class {
2882
2944
  * not among them (`postEvent` above hardcodes its own fields the same
2883
2945
  * way).
2884
2946
  */
2885
- async fileTicket(fields) {
2886
- return this.client.records.create(this.models.issues, fields);
2947
+ async fileTicket(fields, memberId) {
2948
+ const auth = await this.authorshipProbe();
2949
+ return this.client.records.create(this.models.issues, {
2950
+ ...fields,
2951
+ ...auth.issueAuthor && memberId ? { [this.contract.columns.author]: memberId } : {},
2952
+ ...auth.issueShip && auth.shipId ? { [this.contract.columns.ship]: auth.shipId } : {}
2953
+ });
2887
2954
  }
2888
2955
  };
2889
2956
 
@@ -2933,7 +3000,8 @@ function rosterMarkdown(roster, meId) {
2933
3000
  `You are **${selfName}**. Address your shipmates by name in ticket`,
2934
3001
  "comments the way you would a colleague; add the role in parentheses only",
2935
3002
  "where a reader would otherwise not know which seat you mean. Your own",
2936
- "row id is what you write to `assignee_id` and `team_member_id`.",
3003
+ "row id is what you write to `assignee_id` and `team_member_id`, and to",
3004
+ "`filed_by_id` on a ticket you file (where the Environment section names it).",
2937
3005
  "",
2938
3006
  "| seat | name | Crew row id |",
2939
3007
  "|---|---|---|"
@@ -3390,20 +3458,36 @@ function attentionTransitions(previous, current) {
3390
3458
  function displayKey2(t) {
3391
3459
  return t.issue_tag ?? t.issue_id ?? "?";
3392
3460
  }
3393
- function author(c, me, roster) {
3461
+ function author(c, i) {
3462
+ const { me, roster } = i;
3394
3463
  if (c.team_member_id === me) return "you";
3395
3464
  if (c.team_member_id === null || c.team_member_id === void 0) {
3396
3465
  const byIdentity = memberByIdentity(roster, c.created_by_id);
3397
3466
  if (byIdentity) return byIdentity.id === me ? "you" : crewLabel(byIdentity) ?? byIdentity.name;
3398
3467
  return `${c.reporter_name ?? "anon"} (no identity)`;
3399
3468
  }
3400
- return crewLabel(roster.get(c.team_member_id)) ?? "someone off this ship";
3469
+ return (crewLabel(roster.get(c.team_member_id)) ?? "someone off this ship") + shipSuffix(c, i);
3470
+ }
3471
+ function shipSuffix(c, i) {
3472
+ if (!c.ship_id || c.ship_id === i.myShipId) return "";
3473
+ const name = i.ships?.find((s) => s.id === c.ship_id)?.name;
3474
+ return name ? ` (${name})` : "";
3475
+ }
3476
+ function filedBy(t, i) {
3477
+ const member = (t.filed_by_id ? i.roster.get(t.filed_by_id) : void 0) ?? memberByIdentity(i.roster, t.created_by_id);
3478
+ if (member) {
3479
+ if (member.id === i.me) return "you";
3480
+ const label = crewLabel(member) ?? member.name;
3481
+ const ship = t.ship_id && t.ship_id !== i.myShipId ? i.ships?.find((s) => s.id === t.ship_id)?.name : void 0;
3482
+ return ship ? `${label} (${ship})` : label;
3483
+ }
3484
+ return t.reporter_name ? `${t.reporter_name} (unverified)` : "\u2014";
3401
3485
  }
3402
3486
  function lastComment(i, ticketId) {
3403
3487
  const forTicket = i.comments.filter((c) => c.ticket_id === ticketId).sort((a, b) => a.created_at < b.created_at ? -1 : a.created_at > b.created_at ? 1 : 0);
3404
3488
  const last = forTicket[forTicket.length - 1];
3405
3489
  if (!last) return "\u2014";
3406
- return `${last.created_at.slice(0, 16)}Z ${author(last, i.me, i.roster)}`;
3490
+ return `${last.created_at.slice(0, 16)}Z ${author(last, i)}`;
3407
3491
  }
3408
3492
  function newFromOthers(i, ticketId) {
3409
3493
  return i.comments.filter(
@@ -3435,14 +3519,14 @@ ${sorted(ts).map(row).join("\n")}
3435
3519
  `;
3436
3520
  }
3437
3521
  function buildingDigest(i) {
3438
- const header = "| ticket | repo | branch | worktree | status | assignee | sev | pri | eff | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|";
3522
+ const header = "| ticket | repo | branch | worktree | status | assignee | filed by | sev | pri | eff | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|";
3439
3523
  const row = (t) => {
3440
3524
  const n = newFromOthers(i, t.id);
3441
3525
  const branch = i.branchFor ? i.branchFor(t) : "";
3442
3526
  const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
3443
3527
  const dir = i.dirFor ? i.dirFor(t) : null;
3444
3528
  const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
3445
- return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${t.status} | ${who(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
3529
+ return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
3446
3530
  };
3447
3531
  const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
3448
3532
  const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
@@ -3474,7 +3558,7 @@ ${sorted(ts).map(blockedRow).join("\n")}
3474
3558
  ]);
3475
3559
  }
3476
3560
  function qaDigest(i) {
3477
- const header = "| ticket | repo | status | built by | assignee | sev | pri | eff | branch | worktree | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|";
3561
+ const header = "| ticket | repo | status | built by | assignee | filed by | sev | pri | eff | branch | worktree | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
3478
3562
  const branchCell = (t) => {
3479
3563
  if (i.dirFor && i.dirFor(t) === null) return "\u2014";
3480
3564
  if (!i.existingBranchFor) return "";
@@ -3489,7 +3573,7 @@ function qaDigest(i) {
3489
3573
  const n = newFromOthers(i, t.id);
3490
3574
  const dir = i.dirFor ? i.dirFor(t) : null;
3491
3575
  const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
3492
- return `| ${displayKey2(t)} | ${repo} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
3576
+ return `| ${displayKey2(t)} | ${repo} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
3493
3577
  };
3494
3578
  return stream([
3495
3579
  "## Current queue \u2014 built for you by the poll\n",
@@ -3952,11 +4036,12 @@ async function decideCycle(o) {
3952
4036
  const { route, ship, state, emit } = o;
3953
4037
  const tracker = new Tracker(route, ship);
3954
4038
  emit.enter("poll");
3955
- const [tickets, comments, crewRows, epicRows] = await Promise.all([
4039
+ const [tickets, comments, crewRows, epicRows, ships] = await Promise.all([
3956
4040
  tracker.openTickets(),
3957
4041
  tracker.comments(200),
3958
4042
  tracker.crewRows(),
3959
- tracker.epicRows()
4043
+ tracker.epicRows(),
4044
+ tracker.shipRows()
3960
4045
  ]);
3961
4046
  emit.emit(`${tickets.length} open ticket(s), ${comments.length} comment(s)`, {
3962
4047
  data: { tickets: tickets.length, comments: comments.length }
@@ -4069,6 +4154,7 @@ async function decideCycle(o) {
4069
4154
  tickets,
4070
4155
  comments,
4071
4156
  roster,
4157
+ ships,
4072
4158
  blocked,
4073
4159
  info,
4074
4160
  sweep,
@@ -4147,6 +4233,10 @@ function writeDigest(o, d, role, stateDir) {
4147
4233
  comments: d.comments,
4148
4234
  me,
4149
4235
  roster: d.roster,
4236
+ // CREW-1371: a comment from another ship is labelled with that ship's
4237
+ // name, so two ships under one API key are distinguishable on the queue.
4238
+ ships: d.ships,
4239
+ myShipId: d.ships.find((s) => (s.name ?? "").trim() === o.ship.name.trim())?.id ?? null,
4150
4240
  watermark: d.watermark,
4151
4241
  blocked: d.blocked,
4152
4242
  blockerInfo: d.info
@@ -4422,6 +4512,14 @@ import { createHash as createHash2 } from "crypto";
4422
4512
  // src/environment.ts
4423
4513
  import { basename as basename4 } from "path";
4424
4514
  var API_KEY_VAR = "CREW_API_KEY";
4515
+ function authorshipBullets(a, contract) {
4516
+ if (!a) return [];
4517
+ return [
4518
+ ...a.issueAuthor ? [`- \`${contract.columns.author}\`: your own Crew row id, from "Your crew" above \u2014 who filed it`] : [],
4519
+ ...a.issueShip && a.shipId ? [`- \`${contract.columns.ship}\`: \`${a.shipId}\` \u2014 this ship, where it was filed from`] : [],
4520
+ ...a.commentShip && a.shipId ? [`- \`${contract.comments.ship}\` on every comment you post: \`${a.shipId}\` \u2014 this ship`] : []
4521
+ ];
4522
+ }
4425
4523
  var HOOK_PURPOSE = [
4426
4524
  ["setup", "make a fresh worktree usable \u2014 dependencies, generated clients"],
4427
4525
  ["isolate", "point this worktree at state of its own; prints KEY=value overrides to export"],
@@ -4628,6 +4726,7 @@ function renderEnvironment(i) {
4628
4726
  ...only?.id ? [`- \`${contract.columns.repo}\`: \`${only.id}\` (this repository)`] : repos.filter((r) => r.id).map(
4629
4727
  (r) => `- \`${contract.columns.repo}\`: \`${r.id}\` \u2014 when the new ticket belongs to ${r.name}`
4630
4728
  ),
4729
+ ...authorshipBullets(i.authorship, contract),
4631
4730
  "",
4632
4731
  ...i.sourceTicket ? [
4633
4732
  `Name ${i.sourceTicket} \u2014 the ticket you were working when you noticed this \u2014 in the new`,
@@ -6128,7 +6227,7 @@ Still failing: ${n.detail ?? n.headline}`, memberId);
6128
6227
  report_type: "bug",
6129
6228
  severity: "s2",
6130
6229
  reporter_name: "Crew release"
6131
- });
6230
+ }, memberId);
6132
6231
  const label = created.issue_id ? displayKey(created) : created.id;
6133
6232
  log.warn(`release failure \u2014 filed ${label}`, { step: "release" });
6134
6233
  return { kind: "filed", issueId: label };
@@ -7158,7 +7257,19 @@ async function discover(o) {
7158
7257
  }
7159
7258
  const pairId = seatFor(scoped, "pair") || (shipRowId ? seatFor(unclaimed, "pair") : void 0);
7160
7259
  if (pairId) out.seats.pair = pairId;
7161
- const seatIds = new Set(Object.values(out.seats));
7260
+ if (shipRowId) {
7261
+ const releaseId = seatFor(scoped, "release");
7262
+ if (releaseId) out.releaseSeat = releaseId;
7263
+ else {
7264
+ const created = await post2(o, `/data-models/${out.models.crew}/records`, {
7265
+ name: "Release agent",
7266
+ ship_id: shipRowId
7267
+ });
7268
+ out.releaseSeat = created.id;
7269
+ provisioning.push("created Crew row for the release seat");
7270
+ }
7271
+ }
7272
+ const seatIds = /* @__PURE__ */ new Set([...Object.values(out.seats), ...out.releaseSeat ? [out.releaseSeat] : []]);
7162
7273
  out.holds = rows.filter((r) => !seatIds.has(r.id)).map((r) => ({ id: r.id, name: r.name ?? "" }));
7163
7274
  if (out.holds.length === 0) {
7164
7275
  problems.push("no non-seat Crew rows \u2014 which row is the operator?");
@@ -8732,7 +8843,8 @@ async function runRoleAgent(role, decision, ctx) {
8732
8843
  }
8733
8844
  }
8734
8845
  async function buildEnvironment(route, ship, ticket) {
8735
- const contract = new Tracker(route, ship).contract;
8846
+ const tracker = new Tracker(route, ship);
8847
+ const contract = tracker.contract;
8736
8848
  const repos = reposOf(route).map((t) => {
8737
8849
  const id = repoIdForName(route, t.name);
8738
8850
  const o = route.repoOverrides[t.name];
@@ -8747,7 +8859,8 @@ async function buildEnvironment(route, ship, ticket) {
8747
8859
  }, t.dir)
8748
8860
  };
8749
8861
  });
8750
- return renderEnvironment({ route, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket });
8862
+ const authorship = await tracker.authorshipProbe().catch(() => null);
8863
+ return renderEnvironment({ route, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship });
8751
8864
  }
8752
8865
 
8753
8866
  // src/cli.ts
@@ -9336,13 +9449,15 @@ ${changed2.map((c) => ` - ${c}
9336
9449
  return (await resolvedRepos(c)).map((r) => `${r.name} requires ${r.config.platform}`).join(", ");
9337
9450
  }
9338
9451
  async function environmentFor(c, ticket) {
9339
- const contract = new Tracker(c, cfg.ship).contract;
9452
+ const tracker = new Tracker(c, cfg.ship);
9453
+ const contract = tracker.contract;
9340
9454
  return renderEnvironment({
9341
9455
  route: c,
9342
9456
  userAgent: cfg.ship.userAgent,
9343
9457
  repos: await resolvedRepos(c),
9344
9458
  contract,
9345
- sourceTicket: ticket
9459
+ sourceTicket: ticket,
9460
+ authorship: await tracker.authorshipProbe().catch(() => null)
9346
9461
  });
9347
9462
  }
9348
9463
  const unplaceableReported = /* @__PURE__ */ new Set();
@@ -9444,7 +9559,7 @@ ${changed2.map((c) => ` - ${c}
9444
9559
  try {
9445
9560
  const g = outcome.gateRed;
9446
9561
  const comments = await tracker.comments();
9447
- const seat = c.resolved?.seats.dev ?? c.resolved?.seats.qa ?? "";
9562
+ const seat = releaseSeat(c);
9448
9563
  for (const t of gateRedTickets(outcome.merged, outcome.decision.merges)) {
9449
9564
  if (alreadyReported(t, g, comments)) continue;
9450
9565
  await tracker.postEvent(t.id, gateRedComment(g), seat);
@@ -9462,7 +9577,7 @@ ${changed2.map((c) => ` - ${c}
9462
9577
  if (news) {
9463
9578
  const notified = await notify(c, cfg.ship, news, remit, dryRun);
9464
9579
  if (!notified && news.level === "fail") {
9465
- const memberId = c.resolved?.seats.dev ?? c.resolved?.seats.qa ?? "";
9580
+ const memberId = releaseSeat(c);
9466
9581
  await applyFailureAlert(tracker, news, scope, all, memberId, remit, dryRun).catch((e) => {
9467
9582
  remit.warn(`could not file/update a failure ticket: ${e.message}`, { step: "release" });
9468
9583
  });
@@ -9470,7 +9585,7 @@ ${changed2.map((c) => ` - ${c}
9470
9585
  }
9471
9586
  if (outcome.conflicts?.length || outcome.unbuildable?.length) {
9472
9587
  const comments = await tracker.comments();
9473
- const seat = c.resolved?.seats.dev ?? c.resolved?.seats.qa ?? "";
9588
+ const seat = releaseSeat(c);
9474
9589
  for (const f of outcome.conflicts ?? []) {
9475
9590
  try {
9476
9591
  const bounce = planConflictBounce(
@@ -10616,6 +10731,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10616
10731
  models: found.models,
10617
10732
  seats: found.seats,
10618
10733
  holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
10734
+ ...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
10619
10735
  ...operator ? { operator } : {},
10620
10736
  ...contract ? { contract } : {},
10621
10737
  ...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
@@ -11264,6 +11380,14 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
11264
11380
  `);
11265
11381
  }
11266
11382
  }
11383
+ const missingAuthorship = await tracker.missingAuthorshipColumns();
11384
+ process.stdout.write(
11385
+ missingAuthorship.length === 0 ? "authorship: filed_by_id, ship_id present\n" : `authorship: missing ${missingAuthorship.join(", ")} \u2014 update the Issues template
11386
+ `
11387
+ );
11388
+ process.stdout.write(
11389
+ route.resolved?.releaseSeat ? "release seat: this ship has its own Release Crew row\n" : "release seat: none \u2014 release comments are credited to the dev seat; run `crew connect` once the Crew table has this ship's row\n"
11390
+ );
11267
11391
  const [ships, crewRows] = await Promise.all([tracker.shipRows(), tracker.crewRows()]);
11268
11392
  if (ships.length === 0) {
11269
11393
  process.stdout.write("ship record: this workspace has no Ships table (fine)\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.6.1",
3
+ "version": "0.7.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",
@@ -619,6 +619,11 @@ filed without them has nowhere to route to — it sits unclaimed by any
619
619
  per-repo queue until a person notices and fixes it by hand, however
620
620
  urgent its priority.
621
621
 
622
+ If that block also lists `filed_by_id` (your own Crew row) or `ship_id` (the
623
+ ship you are running on), set those too, and `ship_id` on every comment you
624
+ post. They record who filed the ticket and from where. The block lists a column
625
+ only when this workspace has it, so a column it does not name is not yours to set.
626
+
622
627
  Name the ticket you were working when you noticed it in the new ticket's
623
628
  description. Only put it in the new ticket's `blocked by` column if your
624
629
  own ticket genuinely cannot proceed without the new one being fixed
@@ -600,6 +600,11 @@ filed without them has nowhere to route to — it sits unclaimed by any
600
600
  per-repo queue until a person notices and fixes it by hand, however
601
601
  urgent its priority.
602
602
 
603
+ If that block also lists `filed_by_id` (your own Crew row) or `ship_id` (the
604
+ ship you are running on), set those too, and `ship_id` on every comment you
605
+ post. They record who filed the ticket and from where. The block lists a column
606
+ only when this workspace has it, so a column it does not name is not yours to set.
607
+
603
608
  Name the ticket you were working when you noticed it in the new ticket's
604
609
  description. Only put it in the new ticket's `blocked by` column if your
605
610
  own ticket genuinely cannot proceed without the new one being fixed