@tablation/crew 0.61.0 → 0.63.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 +465 -267
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5183,7 +5183,7 @@ function doctorSelftestLines(stored, toolchains) {
5183
5183
  }
5184
5184
 
5185
5185
  // src/cli.ts
5186
- import { resolve as resolve5, dirname as dirname17, join as join30, basename as basename10 } from "path";
5186
+ import { resolve as resolve5, dirname as dirname18, join as join31, basename as basename10 } from "path";
5187
5187
  import { fileURLToPath } from "url";
5188
5188
  import { hostname as hostname2, homedir as homedir8 } from "os";
5189
5189
  import { spawnSync as spawnSync6 } from "child_process";
@@ -5859,154 +5859,8 @@ function resetIncomplete(steps) {
5859
5859
  return steps.some((s) => s.action === "failed" || s.action === "kept");
5860
5860
  }
5861
5861
 
5862
- // src/tracker.ts
5863
- import { hostname } from "os";
5864
- import { TablationClient as TablationClient3 } from "@tablation/client";
5865
-
5866
- // src/contract.ts
5867
- var DEFAULT_CONTRACT = {
5868
- columns: {
5869
- key: "issue_id",
5870
- tag: "issue_tag",
5871
- title: "title",
5872
- status: "status",
5873
- assignee: "assignee_id",
5874
- priority: "priority",
5875
- severity: "severity",
5876
- slice: "project_id",
5877
- repo: "repo_id",
5878
- parent: "parent_id",
5879
- blockedBy: "blocked_by",
5880
- updatedAt: "updated_at",
5881
- needsDesign: "needs_design",
5882
- attachments: "attachments",
5883
- workMode: "work_mode",
5884
- reportType: "report_type",
5885
- epic: "epic_id",
5886
- author: "filed_by_id",
5887
- ship: "ship_id",
5888
- heldBy: "held_by_ship_id",
5889
- resolutionNote: "resolution_note",
5890
- verifiedSha: "verified_sha",
5891
- prRef: "pr_ref",
5892
- position: "position"
5893
- },
5894
- comments: {
5895
- parent: "ticket_id",
5896
- body: "body",
5897
- author: "team_member_id",
5898
- createdAt: "created_at",
5899
- ship: "ship_id",
5900
- publicReply: "is_public_reply",
5901
- authorName: "author_name"
5902
- },
5903
- statuses: {
5904
- open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
5905
- resolved: [
5906
- "verified",
5907
- "closed_deployed",
5908
- "closed_wont_fix",
5909
- "closed_duplicate",
5910
- "closed_completed",
5911
- "closed_cant_reproduce"
5912
- ],
5913
- approved: "accepted",
5914
- parked: "blocked",
5915
- building: "in_progress",
5916
- handoff: "fixed",
5917
- verifying: "qa",
5918
- verified: "verified",
5919
- deployed: "closed_deployed",
5920
- needsHuman: "needs_info",
5921
- reviewing: null,
5922
- wontFix: "closed_wont_fix",
5923
- draft: "draft"
5924
- },
5925
- epics: {
5926
- statusColumn: "status",
5927
- keyColumn: "epic_id",
5928
- statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
5929
- },
5930
- priorityOrder: ["p0", "p1", "p2", "p3"],
5931
- severityOrder: ["s1", "s2", "s3", "s4"],
5932
- unknownPriorityRank: 2,
5933
- coordinatingValue: "coordinating",
5934
- manualWorkMode: "manual",
5935
- hybridWorkMode: "hybrid",
5936
- repoExemptReportTypes: ["coordinating", "question", "investigation"]
5937
- };
5938
- function closedStatuses(c) {
5939
- return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
5940
- }
5941
- function resolveContract(override) {
5942
- if (!override) return DEFAULT_CONTRACT;
5943
- return {
5944
- columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
5945
- comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
5946
- statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
5947
- epics: {
5948
- ...DEFAULT_CONTRACT.epics,
5949
- ...override.epics,
5950
- statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
5951
- },
5952
- priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
5953
- severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
5954
- unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
5955
- coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
5956
- manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
5957
- hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
5958
- // A workspace overriding `coordinatingValue` alone (renaming the rollup
5959
- // marker) still gets that renamed value exempted here by default,
5960
- // without also having to repeat it — only an explicit
5961
- // `repoExemptReportTypes` overrides this derived default.
5962
- repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
5963
- };
5964
- }
5965
- function validateContract(c) {
5966
- const problems = [];
5967
- const open = new Set(c.statuses.open);
5968
- const resolved = new Set(c.statuses.resolved);
5969
- for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
5970
- const v = c.statuses[role];
5971
- if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
5972
- }
5973
- if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
5974
- problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
5975
- }
5976
- if (!resolved.has(c.statuses.deployed)) {
5977
- problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
5978
- }
5979
- if (resolved.has(c.statuses.handoff)) {
5980
- problems.push(
5981
- `statuses.handoff ("${c.statuses.handoff}") is listed as resolved \u2014 it means built-but-unchecked, so a ticket blocked by it is still blocked; calling it resolved would build on unverified work`
5982
- );
5983
- }
5984
- if (!resolved.has(c.statuses.wontFix)) {
5985
- problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
5986
- }
5987
- if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
5988
- problems.push(
5989
- `statuses.reviewing ("${c.statuses.reviewing}") is listed as resolved \u2014 it is unmerged work under review, so a ticket blocked by it is still blocked`
5990
- );
5991
- }
5992
- if (resolved.has(c.statuses.approved)) {
5993
- problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
5994
- }
5995
- if (c.statuses.parked === c.statuses.approved) {
5996
- problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
5997
- }
5998
- const epicValues = Object.values(c.epics.statuses);
5999
- if (new Set(epicValues).size !== epicValues.length) {
6000
- problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
6001
- }
6002
- if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
6003
- if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
6004
- for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
6005
- if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
6006
- }
6007
- return problems;
6008
- }
6009
- var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
5862
+ // src/crew-floor.ts
5863
+ import { TablationApiError as TablationApiError3 } from "@tablation/client";
6010
5864
 
6011
5865
  // src/callout.ts
6012
5866
  var calloutHeader = (kind, label) => `> [!${kind}]
@@ -7055,6 +6909,223 @@ var SHELL_WORDS = /* @__PURE__ */ new Set([
7055
6909
  "ok"
7056
6910
  ]);
7057
6911
 
6912
+ // src/crew-floor.ts
6913
+ var CREW_OUTDATED_KEY = "crew_outdated";
6914
+ function parseVersion(v) {
6915
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
6916
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
6917
+ }
6918
+ function compareVersions(a, b) {
6919
+ const pa = parseVersion(a);
6920
+ const pb = parseVersion(b);
6921
+ if (!pa || !pb) return null;
6922
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
6923
+ return 0;
6924
+ }
6925
+ function belowFloor(have, floor) {
6926
+ if (!floor) return false;
6927
+ const c = compareVersions(have, floor);
6928
+ return c !== null && c < 0;
6929
+ }
6930
+ function outdatedItem(have, floor, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
6931
+ return {
6932
+ kind: "crew_outdated",
6933
+ key: CREW_OUTDATED_KEY,
6934
+ since: nowIso,
6935
+ message: `crew ${have} is below this workspace's minimum ${floor} \u2014 no ticket will be picked until it is updated (the Crew app updates it, or \`npm install -g @tablation/crew\`). Resumes automatically once the version satisfies the floor`
6936
+ };
6937
+ }
6938
+ function sentVersion(ship) {
6939
+ return /CrewAgent\/(\d+\.\d+\.\d+\S*)/.exec(ship.userAgent)?.[1] ?? null;
6940
+ }
6941
+ async function readCrewFloor(route, ship) {
6942
+ const r = route.resolved;
6943
+ if (!r?.projectId) return void 0;
6944
+ try {
6945
+ const res = await fetch(`${route.baseUrl}/api/projects/${encodeURIComponent(r.projectId)}?workspaceId=${r.workspaceId}`, {
6946
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": ship.userAgent }
6947
+ });
6948
+ if (!res.ok) return void 0;
6949
+ const body = await res.json();
6950
+ return body.crewMinVersion ?? null;
6951
+ } catch {
6952
+ return void 0;
6953
+ }
6954
+ }
6955
+ async function checkCrewFloor(o, deps = {}) {
6956
+ const have = deps.version ?? sentVersion(o.ship);
6957
+ if (!have) return null;
6958
+ const floor = await (deps.read ?? readCrewFloor)(o.route, o.ship);
6959
+ if (floor === void 0) return openOutdated(o.state);
6960
+ if (belowFloor(have, floor)) {
6961
+ const item = outdatedItem(have, floor);
6962
+ await raiseShipAttention(o, item);
6963
+ return item.message;
6964
+ }
6965
+ clearShipAttention(o.state, CREW_OUTDATED_KEY);
6966
+ return null;
6967
+ }
6968
+ function openOutdated(state) {
6969
+ return state.shipAttention().previous().find((i) => i.key === CREW_OUTDATED_KEY)?.message ?? null;
6970
+ }
6971
+ function isUpgradeRequired(err) {
6972
+ return err instanceof TablationApiError3 && err.status === 426;
6973
+ }
6974
+ async function raiseFromUpgradeRequired(o, err) {
6975
+ const have = sentVersion(o.ship) ?? "unknown";
6976
+ const floor = /(\d+\.\d+\.\d+)/.exec(err.message.replace(have, ""))?.[1] ?? "a newer version";
6977
+ await raiseShipAttention(o, outdatedItem(have, floor));
6978
+ }
6979
+
6980
+ // src/tracker.ts
6981
+ import { hostname } from "os";
6982
+ import { TablationClient as TablationClient3 } from "@tablation/client";
6983
+
6984
+ // src/contract.ts
6985
+ var DEFAULT_CONTRACT = {
6986
+ columns: {
6987
+ key: "issue_id",
6988
+ tag: "issue_tag",
6989
+ title: "title",
6990
+ status: "status",
6991
+ assignee: "assignee_id",
6992
+ priority: "priority",
6993
+ severity: "severity",
6994
+ slice: "project_id",
6995
+ repo: "repo_id",
6996
+ parent: "parent_id",
6997
+ blockedBy: "blocked_by",
6998
+ updatedAt: "updated_at",
6999
+ needsDesign: "needs_design",
7000
+ attachments: "attachments",
7001
+ workMode: "work_mode",
7002
+ reportType: "report_type",
7003
+ epic: "epic_id",
7004
+ author: "filed_by_id",
7005
+ ship: "ship_id",
7006
+ heldBy: "held_by_ship_id",
7007
+ resolutionNote: "resolution_note",
7008
+ verifiedSha: "verified_sha",
7009
+ prRef: "pr_ref",
7010
+ position: "position"
7011
+ },
7012
+ comments: {
7013
+ parent: "ticket_id",
7014
+ body: "body",
7015
+ author: "team_member_id",
7016
+ createdAt: "created_at",
7017
+ ship: "ship_id",
7018
+ publicReply: "is_public_reply",
7019
+ authorName: "author_name"
7020
+ },
7021
+ statuses: {
7022
+ open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
7023
+ resolved: [
7024
+ "verified",
7025
+ "closed_deployed",
7026
+ "closed_wont_fix",
7027
+ "closed_duplicate",
7028
+ "closed_completed",
7029
+ "closed_cant_reproduce"
7030
+ ],
7031
+ approved: "accepted",
7032
+ parked: "blocked",
7033
+ building: "in_progress",
7034
+ handoff: "fixed",
7035
+ verifying: "qa",
7036
+ verified: "verified",
7037
+ deployed: "closed_deployed",
7038
+ needsHuman: "needs_info",
7039
+ reviewing: null,
7040
+ wontFix: "closed_wont_fix",
7041
+ draft: "draft"
7042
+ },
7043
+ epics: {
7044
+ statusColumn: "status",
7045
+ keyColumn: "epic_id",
7046
+ statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
7047
+ },
7048
+ priorityOrder: ["p0", "p1", "p2", "p3"],
7049
+ severityOrder: ["s1", "s2", "s3", "s4"],
7050
+ unknownPriorityRank: 2,
7051
+ coordinatingValue: "coordinating",
7052
+ manualWorkMode: "manual",
7053
+ hybridWorkMode: "hybrid",
7054
+ repoExemptReportTypes: ["coordinating", "question", "investigation"]
7055
+ };
7056
+ function closedStatuses(c) {
7057
+ return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
7058
+ }
7059
+ function resolveContract(override) {
7060
+ if (!override) return DEFAULT_CONTRACT;
7061
+ return {
7062
+ columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
7063
+ comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
7064
+ statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
7065
+ epics: {
7066
+ ...DEFAULT_CONTRACT.epics,
7067
+ ...override.epics,
7068
+ statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
7069
+ },
7070
+ priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
7071
+ severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
7072
+ unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
7073
+ coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
7074
+ manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
7075
+ hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
7076
+ // A workspace overriding `coordinatingValue` alone (renaming the rollup
7077
+ // marker) still gets that renamed value exempted here by default,
7078
+ // without also having to repeat it — only an explicit
7079
+ // `repoExemptReportTypes` overrides this derived default.
7080
+ repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
7081
+ };
7082
+ }
7083
+ function validateContract(c) {
7084
+ const problems = [];
7085
+ const open = new Set(c.statuses.open);
7086
+ const resolved = new Set(c.statuses.resolved);
7087
+ for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
7088
+ const v = c.statuses[role];
7089
+ if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
7090
+ }
7091
+ if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
7092
+ problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
7093
+ }
7094
+ if (!resolved.has(c.statuses.deployed)) {
7095
+ problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
7096
+ }
7097
+ if (resolved.has(c.statuses.handoff)) {
7098
+ problems.push(
7099
+ `statuses.handoff ("${c.statuses.handoff}") is listed as resolved \u2014 it means built-but-unchecked, so a ticket blocked by it is still blocked; calling it resolved would build on unverified work`
7100
+ );
7101
+ }
7102
+ if (!resolved.has(c.statuses.wontFix)) {
7103
+ problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
7104
+ }
7105
+ if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
7106
+ problems.push(
7107
+ `statuses.reviewing ("${c.statuses.reviewing}") is listed as resolved \u2014 it is unmerged work under review, so a ticket blocked by it is still blocked`
7108
+ );
7109
+ }
7110
+ if (resolved.has(c.statuses.approved)) {
7111
+ problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
7112
+ }
7113
+ if (c.statuses.parked === c.statuses.approved) {
7114
+ problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
7115
+ }
7116
+ const epicValues = Object.values(c.epics.statuses);
7117
+ if (new Set(epicValues).size !== epicValues.length) {
7118
+ problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
7119
+ }
7120
+ if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
7121
+ if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
7122
+ for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
7123
+ if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
7124
+ }
7125
+ return problems;
7126
+ }
7127
+ var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
7128
+
7058
7129
  // src/work-mode.ts
7059
7130
  function projectWorkModes(rows, contract) {
7060
7131
  const out = /* @__PURE__ */ new Map();
@@ -8928,8 +8999,10 @@ async function decideCycle(o) {
8928
8999
  emit.emit(`${what}: no longer missing from the scheduler PATH \u2014 stop cleared`, { step: "select" });
8929
9000
  }
8930
9001
  await checkRepoDisks({ state, emit, route, ship });
9002
+ const floorStop = await checkCrewFloor({ state, emit, route, ship });
9003
+ const itemStop = repoStopFor(openShipAttention(state), route);
8931
9004
  const selectionInput = {
8932
- repoStop: repoStopFor(openShipAttention(state), route),
9005
+ repoStop: floorStop ? () => floorStop : itemStop,
8933
9006
  workModes,
8934
9007
  tickets,
8935
9008
  comments,
@@ -12162,24 +12235,49 @@ async function discover(o) {
12162
12235
  o,
12163
12236
  `/data-models/${out.shipsModelId}/records?limit=200`
12164
12237
  );
12165
- const mine = shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim());
12166
12238
  const hostPassengers = o.ship.hostPassengers ?? false;
12167
12239
  const sshPublicKey = o.ship.sshPublicKey;
12240
+ let shipColumns = /* @__PURE__ */ new Set();
12241
+ try {
12242
+ const shipsModel = await get(o, `/data-models/${out.shipsModelId}`);
12243
+ shipColumns = new Set((shipsModel.fields ?? []).map((f) => f.columnName));
12244
+ } catch {
12245
+ }
12168
12246
  let ownerId;
12169
12247
  if (out.meId) {
12170
- let hasOwnerColumn = false;
12171
- try {
12172
- const shipsModel = await get(o, `/data-models/${out.shipsModelId}`);
12173
- hasOwnerColumn = (shipsModel.fields ?? []).some((f) => f.columnName === "owner_id");
12174
- } catch {
12175
- }
12176
- if (hasOwnerColumn) ownerId = out.meId;
12248
+ if (shipColumns.has("owner_id")) ownerId = out.meId;
12177
12249
  else provisioning.push("could not record the ship owner \u2014 the Ships table has no owner_id column yet (update the Issues template)");
12178
12250
  }
12251
+ const machineId = shipColumns.has("machine_id") ? o.ship.machineId : void 0;
12252
+ let mine;
12253
+ let matchedBy = "name";
12254
+ let writeMachineId = false;
12255
+ const byName = shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim());
12256
+ if (machineId) {
12257
+ const byId = shipRows.filter((r) => (r.machine_id ?? "") === machineId);
12258
+ if (byId.length > 0) {
12259
+ mine = byId;
12260
+ matchedBy = "machine_id";
12261
+ } else {
12262
+ mine = byName.filter((r) => !r.machine_id);
12263
+ writeMachineId = true;
12264
+ }
12265
+ } else {
12266
+ mine = byName;
12267
+ }
12179
12268
  if (mine.length === 1) {
12180
12269
  shipRowId = mine[0].id;
12181
- out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched" };
12270
+ out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched", matchedBy };
12182
12271
  const patchBody = {};
12272
+ if (matchedBy === "machine_id") {
12273
+ const keptName = (mine[0].name ?? "").trim();
12274
+ if (keptName !== o.ship.name.trim()) {
12275
+ out.shipRow.name = keptName;
12276
+ provisioning.push(`matched Ships row "${keptName}" by machine id \u2014 this host is now called "${o.ship.name}"; the row's name is kept`);
12277
+ }
12278
+ patchBody.last_seen = (/* @__PURE__ */ new Date()).toISOString();
12279
+ }
12280
+ if (writeMachineId) patchBody.machine_id = machineId;
12183
12281
  if ((mine[0].host_passengers ?? false) !== hostPassengers) patchBody.host_passengers = hostPassengers;
12184
12282
  if (sshPublicKey !== void 0 && (mine[0].ssh_public_key ?? "") !== sshPublicKey) {
12185
12283
  patchBody.ssh_public_key = sshPublicKey;
@@ -12197,13 +12295,16 @@ async function discover(o) {
12197
12295
  ...o.ship.platform ? { platform: o.ship.platform } : {},
12198
12296
  host_passengers: hostPassengers,
12199
12297
  ...sshPublicKey !== void 0 ? { ssh_public_key: sshPublicKey } : {},
12200
- ...ownerId !== void 0 ? { owner_id: ownerId } : {}
12298
+ ...ownerId !== void 0 ? { owner_id: ownerId } : {},
12299
+ ...writeMachineId ? { machine_id: machineId } : {}
12201
12300
  });
12202
12301
  shipRowId = created.id;
12203
- out.shipRow = { id: shipRowId, name: o.ship.name, action: "created" };
12302
+ out.shipRow = { id: shipRowId, name: o.ship.name, action: "created", matchedBy };
12204
12303
  provisioning.push(`created Ships row "${o.ship.name}"`);
12205
12304
  } else {
12206
- problems.push(`${mine.length} Ships rows named "${o.ship.name}" \u2014 which one is this machine?`);
12305
+ problems.push(
12306
+ matchedBy === "machine_id" ? `${mine.length} Ships rows carry this machine's id \u2014 which one is this machine?` : `${mine.length} Ships rows named "${o.ship.name}" \u2014 which one is this machine?`
12307
+ );
12207
12308
  }
12208
12309
  }
12209
12310
  let agentIdFor = () => void 0;
@@ -13581,7 +13682,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13581
13682
  }
13582
13683
 
13583
13684
  // src/cli.ts
13584
- import { realpathSync as realpathSync2, readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
13685
+ import { realpathSync as realpathSync2, readFileSync as readFileSync24, existsSync as existsSync31, mkdirSync as mkdirSync17, writeFileSync as writeFileSync18, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
13585
13686
  import { createInterface } from "readline/promises";
13586
13687
  import { dirname as dirOf, resolve as resolvePath } from "path";
13587
13688
 
@@ -13633,12 +13734,62 @@ function sshKeygenAvailable() {
13633
13734
  }
13634
13735
  }
13635
13736
 
13636
- // src/passenger-containers.ts
13737
+ // src/machine-id.ts
13637
13738
  import { execFileSync as execFileSync5 } from "child_process";
13638
13739
  import { createHash as createHash4 } from "crypto";
13740
+ import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
13741
+ import { dirname as dirname17, join as join26 } from "path";
13742
+ function machineIdPathFor(stateDir) {
13743
+ return join26(stateDir, "machine-id");
13744
+ }
13745
+ var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
13746
+ function readRawMachineId(platform2 = process.platform) {
13747
+ try {
13748
+ if (platform2 === "darwin") {
13749
+ const out = execFileSync5("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
13750
+ encoding: "utf8",
13751
+ stdio: ["ignore", "pipe", "ignore"]
13752
+ });
13753
+ return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
13754
+ }
13755
+ for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
13756
+ if (existsSync26(p)) {
13757
+ const id = readFileSync19(p, "utf8").trim();
13758
+ if (id) return id;
13759
+ }
13760
+ }
13761
+ } catch {
13762
+ }
13763
+ return void 0;
13764
+ }
13765
+ function ensureMachineId(stateDir, opts = {}) {
13766
+ if (opts.override?.trim()) return sha256(opts.override.trim());
13767
+ const path = machineIdPathFor(stateDir);
13768
+ if (existsSync26(path)) {
13769
+ const cached = readFileSync19(path, "utf8").trim();
13770
+ if (/^[0-9a-f]{64}$/.test(cached)) return cached;
13771
+ }
13772
+ const raw = (opts.read ?? readRawMachineId)();
13773
+ if (!raw) return void 0;
13774
+ const hashed = sha256(raw);
13775
+ mkdirSync13(dirname17(path), { recursive: true });
13776
+ writeFileSync14(path, `${hashed}
13777
+ `, { mode: 384 });
13778
+ return hashed;
13779
+ }
13780
+ function matchShipRows(ships, shipName2, machineId) {
13781
+ const hasColumn = ships.some((r) => "machine_id" in r);
13782
+ const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
13783
+ if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
13784
+ return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
13785
+ }
13786
+
13787
+ // src/passenger-containers.ts
13788
+ import { execFileSync as execFileSync6 } from "child_process";
13789
+ import { createHash as createHash5 } from "crypto";
13639
13790
  var realExec = (cmd, args) => {
13640
13791
  try {
13641
- return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13792
+ return execFileSync6(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13642
13793
  } catch (e) {
13643
13794
  const err = e;
13644
13795
  const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
@@ -13672,12 +13823,12 @@ function containerNameFor(workspaceId) {
13672
13823
  return `crew-passenger-${workspaceId}`;
13673
13824
  }
13674
13825
  function portFor(workspaceId, base = 28765, span = 1e3) {
13675
- const digest = createHash4("sha256").update(workspaceId).digest();
13826
+ const digest = createHash5("sha256").update(workspaceId).digest();
13676
13827
  return base + digest.readUInt32BE(0) % span;
13677
13828
  }
13678
13829
  function mountsDigest(mounts, image) {
13679
13830
  const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
13680
- return createHash4("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
13831
+ return createHash5("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
13681
13832
  }
13682
13833
  function planContainers(cfg) {
13683
13834
  const byWorkspace = /* @__PURE__ */ new Map();
@@ -13837,9 +13988,9 @@ function syncAllPassengerCheckouts(cfg) {
13837
13988
 
13838
13989
  // src/tunnel.ts
13839
13990
  import { spawn as spawn4 } from "child_process";
13840
- import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
13991
+ import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
13841
13992
  import { randomInt } from "crypto";
13842
- import { join as join26 } from "path";
13993
+ import { join as join27 } from "path";
13843
13994
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
13844
13995
  var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
13845
13996
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -13906,21 +14057,21 @@ function startTunnel(opts) {
13906
14057
  };
13907
14058
  }
13908
14059
  function tunnelStatePath(stateDir, workspaceId) {
13909
- return join26(stateDir, "passengers", `${workspaceId}.json`);
14060
+ return join27(stateDir, "passengers", `${workspaceId}.json`);
13910
14061
  }
13911
14062
  function readPersistedTunnel(stateDir, workspaceId) {
13912
14063
  const path = tunnelStatePath(stateDir, workspaceId);
13913
- if (!existsSync26(path)) return void 0;
14064
+ if (!existsSync27(path)) return void 0;
13914
14065
  try {
13915
- return JSON.parse(readFileSync19(path, "utf8"));
14066
+ return JSON.parse(readFileSync20(path, "utf8"));
13916
14067
  } catch {
13917
14068
  return void 0;
13918
14069
  }
13919
14070
  }
13920
14071
  function writePersistedTunnel(stateDir, state) {
13921
14072
  const path = tunnelStatePath(stateDir, state.workspaceId);
13922
- mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
13923
- writeFileSync14(path, `${JSON.stringify(state, null, 2)}
14073
+ mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
14074
+ writeFileSync15(path, `${JSON.stringify(state, null, 2)}
13924
14075
  `);
13925
14076
  }
13926
14077
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -14017,13 +14168,13 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
14017
14168
  }
14018
14169
  }
14019
14170
  function readAllPersistedWorkspaceIds(stateDir) {
14020
- const dir = join26(stateDir, "passengers");
14021
- if (!existsSync26(dir)) return [];
14171
+ const dir = join27(stateDir, "passengers");
14172
+ if (!existsSync27(dir)) return [];
14022
14173
  return readdirSync8(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
14023
14174
  }
14024
14175
 
14025
14176
  // src/mcp-endpoint-registry.ts
14026
- import { createHash as createHash5 } from "crypto";
14177
+ import { createHash as createHash6 } from "crypto";
14027
14178
  function endpointsUrl(api, workspaceId, id) {
14028
14179
  const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
14029
14180
  return id ? `${base}/${id}` : base;
@@ -14091,7 +14242,7 @@ async function probeEndpointAccess(api, workspaceId) {
14091
14242
  }
14092
14243
  var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
14093
14244
  function registrationKey(url, mcpSecret) {
14094
- return createHash5("sha256").update(`${url}
14245
+ return createHash6("sha256").update(`${url}
14095
14246
  ${mcpSecret ?? ""}`).digest("hex").slice(0, 16);
14096
14247
  }
14097
14248
  async function reconcileEndpoint(workspaceId, d) {
@@ -14139,8 +14290,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
14139
14290
  import { createServer as createHttpServer } from "http";
14140
14291
  import { spawn as spawn5 } from "child_process";
14141
14292
  import { randomBytes } from "crypto";
14142
- import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
14143
- import { join as join27 } from "path";
14293
+ import { existsSync as existsSync28, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
14294
+ import { join as join28 } from "path";
14144
14295
 
14145
14296
  // src/secret-compare.ts
14146
14297
  import { timingSafeEqual } from "crypto";
@@ -14229,21 +14380,21 @@ var defaultSpawn2 = (cmd, args, env) => {
14229
14380
  return child;
14230
14381
  };
14231
14382
  function syncDaemonStatePath(stateDir, workspaceId) {
14232
- return join27(stateDir, "passengers", `${workspaceId}-sync.json`);
14383
+ return join28(stateDir, "passengers", `${workspaceId}-sync.json`);
14233
14384
  }
14234
14385
  function readPersistedSyncDaemon(stateDir, workspaceId) {
14235
14386
  const path = syncDaemonStatePath(stateDir, workspaceId);
14236
- if (!existsSync27(path)) return void 0;
14387
+ if (!existsSync28(path)) return void 0;
14237
14388
  try {
14238
- return JSON.parse(readFileSync20(path, "utf8"));
14389
+ return JSON.parse(readFileSync21(path, "utf8"));
14239
14390
  } catch {
14240
14391
  return void 0;
14241
14392
  }
14242
14393
  }
14243
14394
  function writePersistedSyncDaemon(stateDir, state) {
14244
14395
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
14245
- mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
14246
- writeFileSync15(path, `${JSON.stringify(state, null, 2)}
14396
+ mkdirSync15(join28(stateDir, "passengers"), { recursive: true });
14397
+ writeFileSync16(path, `${JSON.stringify(state, null, 2)}
14247
14398
  `);
14248
14399
  }
14249
14400
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -14263,8 +14414,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
14263
14414
  }
14264
14415
  }
14265
14416
  function readAllPersistedSyncWorkspaceIds(stateDir) {
14266
- const dir = join27(stateDir, "passengers");
14267
- if (!existsSync27(dir)) return [];
14417
+ const dir = join28(stateDir, "passengers");
14418
+ if (!existsSync28(dir)) return [];
14268
14419
  const suffix = "-sync.json";
14269
14420
  return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
14270
14421
  }
@@ -14321,12 +14472,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
14321
14472
  }
14322
14473
 
14323
14474
  // src/daemon.ts
14324
- import { existsSync as existsSync29, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync16 } from "fs";
14325
- import { join as join29 } from "path";
14475
+ import { existsSync as existsSync30, mkdirSync as mkdirSync16, readFileSync as readFileSync23, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync17 } from "fs";
14476
+ import { join as join30 } from "path";
14326
14477
 
14327
14478
  // src/interrupted.ts
14328
- import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync21 } from "fs";
14329
- import { join as join28 } from "path";
14479
+ import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
14480
+ import { join as join29 } from "path";
14330
14481
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
14331
14482
  function interruptedComment(b) {
14332
14483
  const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
@@ -14345,21 +14496,21 @@ var pidAlive = (pid) => {
14345
14496
  };
14346
14497
  function runFinished(streamPath) {
14347
14498
  const path = `${streamPath}.meta.json`;
14348
- if (!existsSync28(path)) return false;
14499
+ if (!existsSync29(path)) return false;
14349
14500
  try {
14350
- return JSON.parse(readFileSync21(path, "utf8")).running !== true;
14501
+ return JSON.parse(readFileSync22(path, "utf8")).running !== true;
14351
14502
  } catch {
14352
14503
  return true;
14353
14504
  }
14354
14505
  }
14355
14506
  function orphanedStreams(o) {
14356
- const dir = join28(o.stateDir, "streams");
14357
- if (!existsSync28(dir)) return [];
14507
+ const dir = join29(o.stateDir, "streams");
14508
+ if (!existsSync29(dir)) return [];
14358
14509
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
14359
14510
  const out = [];
14360
14511
  for (const name of readdirSync10(dir)) {
14361
14512
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
14362
- const streamPath = join28(dir, name);
14513
+ const streamPath = join29(dir, name);
14363
14514
  if (runFinished(streamPath)) continue;
14364
14515
  const sidecar = readRunSidecar(streamPath);
14365
14516
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -14459,7 +14610,7 @@ function latestMtime(path) {
14459
14610
  if (!stat.isDirectory()) return 0;
14460
14611
  let latest = stat.mtimeMs;
14461
14612
  for (const entry of readdirSync11(path, { withFileTypes: true })) {
14462
- latest = Math.max(latest, latestMtime(join29(path, entry.name)));
14613
+ latest = Math.max(latest, latestMtime(join30(path, entry.name)));
14463
14614
  }
14464
14615
  return latest;
14465
14616
  }
@@ -14468,23 +14619,23 @@ function computeTreeSignature(paths) {
14468
14619
  }
14469
14620
  function makeStaleChecker(paths, opts = {}) {
14470
14621
  const initial = computeTreeSignature(paths);
14471
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync29(opts.reloadSentinel);
14622
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync30(opts.reloadSentinel);
14472
14623
  }
14473
14624
  function reloadSentinelFor(stateDir) {
14474
- return join29(stateDir, "reload-requested");
14625
+ return join30(stateDir, "reload-requested");
14475
14626
  }
14476
14627
  function daemonStampFor(stateDir) {
14477
- return join29(stateDir, "daemon.started");
14628
+ return join30(stateDir, "daemon.started");
14478
14629
  }
14479
14630
  function noteDaemonStarted(stateDir, now = Date.now()) {
14480
- mkdirSync15(stateDir, { recursive: true });
14631
+ mkdirSync16(stateDir, { recursive: true });
14481
14632
  rmSync11(reloadSentinelFor(stateDir), { force: true });
14482
- writeFileSync16(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14633
+ writeFileSync17(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14483
14634
  `);
14484
14635
  }
14485
14636
  function readDaemonStarted(stateDir) {
14486
14637
  try {
14487
- const v = JSON.parse(readFileSync22(daemonStampFor(stateDir), "utf8")).startedAt;
14638
+ const v = JSON.parse(readFileSync23(daemonStampFor(stateDir), "utf8")).startedAt;
14488
14639
  return typeof v === "number" ? v : null;
14489
14640
  } catch {
14490
14641
  return null;
@@ -14530,6 +14681,16 @@ var SpawnBreaker = class {
14530
14681
  }
14531
14682
  };
14532
14683
  async function runOnePass(o) {
14684
+ try {
14685
+ return await runOnePassUnguarded(o);
14686
+ } catch (e) {
14687
+ if (!isUpgradeRequired(e)) throw e;
14688
+ const emit = o.newEmitter();
14689
+ await raiseFromUpgradeRequired({ state: o.state, emit, route: o.route, ship: o.ship }, e);
14690
+ return { pending: [], started: [], skipped: [] };
14691
+ }
14692
+ }
14693
+ async function runOnePassUnguarded(o) {
14533
14694
  if (o.state.isPaused()) return { pending: [], started: [], skipped: [] };
14534
14695
  await hydrateApiKeys([o.route], o.getSessionStore);
14535
14696
  const emit = o.newEmitter();
@@ -14840,7 +15001,7 @@ async function buildEnvironment(route, ship, ticket) {
14840
15001
  }
14841
15002
 
14842
15003
  // src/cli.ts
14843
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname17(process.execPath) : resolve5(dirname17(fileURLToPath(import.meta.url)), "..");
15004
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname18(process.execPath) : resolve5(dirname18(fileURLToPath(import.meta.url)), "..");
14844
15005
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
14845
15006
  var CYCLE_SECONDS2 = 120;
14846
15007
  var STALE_CYCLES = 3;
@@ -15002,7 +15163,7 @@ function usage() {
15002
15163
  crew install write and load this platform's scheduler unit
15003
15164
  crew uninstall unload and remove it
15004
15165
  --workspace-id ID --key K [--project NAME] [--area NAME] [--dir PATH] [--name N]
15005
- --json --ship-name N --agent-bin PATH --extra-path DIRS --host-passengers --no-wizard --choose KEY=VALUE (non-interactive: docs/commands.md)
15166
+ --json --machine-id ID --ship-name N --agent-bin PATH --extra-path DIRS --host-passengers --no-wizard --choose KEY=VALUE (non-interactive: docs/commands.md)
15006
15167
 
15007
15168
  Options:
15008
15169
  --dry-run decide everything, perform nothing
@@ -15109,13 +15270,19 @@ if (command === "passenger-sync-daemon") {
15109
15270
  if (!why) return false;
15110
15271
  emit.emit(`${why} \u2014 release skipped`, { step: "release" });
15111
15272
  return true;
15273
+ }, doctorMachineId = function(stateDir) {
15274
+ try {
15275
+ return ensureMachineId(stateDir, { override: value("machine-id") });
15276
+ } catch {
15277
+ return void 0;
15278
+ }
15112
15279
  }, baseHealth = function(r) {
15113
15280
  const { remote, base } = r.config.branch;
15114
15281
  const out = refreshBaseBranch(r.dir, remote, base, true);
15115
15282
  const unsafe = baseBranchUnsafe(out);
15116
15283
  return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
15117
15284
  };
15118
- requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, baseHealth2 = baseHealth;
15285
+ requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
15119
15286
  let cfg;
15120
15287
  let firstRun;
15121
15288
  let firstRunFromFlags = false;
@@ -15401,8 +15568,8 @@ if (command === "passenger-sync-daemon") {
15401
15568
  }
15402
15569
  if (host === "linux" && hasSystemd) {
15403
15570
  const label = labelFor(CREW_HOME, "run");
15404
- const staleTimer = join30(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15405
- if (existsSync30(staleTimer)) {
15571
+ const staleTimer = join31(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15572
+ if (existsSync31(staleTimer)) {
15406
15573
  process.stdout.write(
15407
15574
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
15408
15575
  `
@@ -15428,14 +15595,14 @@ if (command === "passenger-sync-daemon") {
15428
15595
  const tracker = new Tracker(c, cfg.ship);
15429
15596
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
15430
15597
  for (const r of await resolvedRepos(c)) {
15431
- if (!existsSync30(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15598
+ if (!existsSync31(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15432
15599
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
15433
15600
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
15434
15601
  }
15435
15602
  try {
15436
15603
  const ships = await tracker.shipRows();
15437
15604
  if (ships.length > 0) {
15438
- const mine = ships.filter((s2) => (s2.name ?? "").trim() === cfg.ship.name.trim());
15605
+ const mine = matchShipRows(ships, cfg.ship.name, doctorMachineId(cfg.ship.stateDir)).rows;
15439
15606
  if (mine.length !== 1) problems.push(`ship record: ${mine.length} rows named "${cfg.ship.name}"`);
15440
15607
  else if (mine[0].platform && mine[0].platform !== host) problems.push(`ship record: declares ${mine[0].platform}, this host is ${host}`);
15441
15608
  }
@@ -15446,7 +15613,7 @@ if (command === "passenger-sync-daemon") {
15446
15613
  }
15447
15614
  async function doctorFix(o) {
15448
15615
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
15449
- const unitExists = (p) => existsSync30(p);
15616
+ const unitExists = (p) => existsSync31(p);
15450
15617
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
15451
15618
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
15452
15619
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -15463,7 +15630,7 @@ if (command === "passenger-sync-daemon") {
15463
15630
  for (const a of plan.actions) {
15464
15631
  if (a.kind === "enable") {
15465
15632
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
15466
- writeFileSync17(cfg.configFile, planEnable(readFileSync23(cfg.configFile, "utf8"), a.route));
15633
+ writeFileSync18(cfg.configFile, planEnable(readFileSync24(cfg.configFile, "utf8"), a.route));
15467
15634
  enabledSound.add(a.route);
15468
15635
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
15469
15636
  } else {
@@ -16462,7 +16629,7 @@ ${changed2.map((c) => ` - ${c}
16462
16629
  process.exit(1);
16463
16630
  }
16464
16631
  requireArmed("run the daemon loop");
16465
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join30(CREW_HOME, "src"), join30(CREW_HOME, "dist"), join30(CREW_HOME, "bin")];
16632
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join31(CREW_HOME, "src"), join31(CREW_HOME, "dist"), join31(CREW_HOME, "bin")];
16466
16633
  const configPaths = daemonConfigPaths(cfg);
16467
16634
  watchPaths.push(...configPaths);
16468
16635
  noteDaemonStarted(cfg.ship.stateDir);
@@ -16663,10 +16830,10 @@ ${changed2.map((c) => ` - ${c}
16663
16830
  `);
16664
16831
  if (!dryRun) {
16665
16832
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16666
- const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
16833
+ const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync24(resolvedPath, "utf8")) : {};
16667
16834
  raw.agentSkills = result.agentSkills;
16668
- mkdirSync16(dirOf(resolvedPath), { recursive: true });
16669
- writeFileSync17(resolvedPath, `${JSON.stringify(raw, null, 2)}
16835
+ mkdirSync17(dirOf(resolvedPath), { recursive: true });
16836
+ writeFileSync18(resolvedPath, `${JSON.stringify(raw, null, 2)}
16670
16837
  `);
16671
16838
  }
16672
16839
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -16679,15 +16846,15 @@ ${changed2.map((c) => ` - ${c}
16679
16846
  process.exit(2);
16680
16847
  };
16681
16848
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
16682
- const configText = readFileSync23(cfg.configFile, "utf8");
16849
+ const configText = readFileSync24(cfg.configFile, "utf8");
16683
16850
  if (sub === "hooks") {
16684
16851
  try {
16685
16852
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
16686
- const dir = repoDirOf(configText, dirname17(cfg.configFile), a.route, a.name);
16687
- if (!existsSync30(dir)) throw new ReposError(`${dir} does not exist on disk`);
16688
- const files = { exists: (r) => existsSync30(resolvePath(dir, r)), read: (r) => {
16853
+ const dir = repoDirOf(configText, dirname18(cfg.configFile), a.route, a.name);
16854
+ if (!existsSync31(dir)) throw new ReposError(`${dir} does not exist on disk`);
16855
+ const files = { exists: (r) => existsSync31(resolvePath(dir, r)), read: (r) => {
16689
16856
  try {
16690
- return readFileSync23(resolvePath(dir, r), "utf8");
16857
+ return readFileSync24(resolvePath(dir, r), "utf8");
16691
16858
  } catch {
16692
16859
  return void 0;
16693
16860
  }
@@ -16699,13 +16866,13 @@ ${changed2.map((c) => ` - ${c}
16699
16866
  if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
16700
16867
  if (write === "route") {
16701
16868
  const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
16702
- writeFileSync17(cfg.configFile, plan.text);
16869
+ writeFileSync18(cfg.configFile, plan.text);
16703
16870
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
16704
16871
  } else if (write === "repo") {
16705
16872
  const have = existingRepoConfig(files);
16706
16873
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
16707
16874
  const file = resolvePath(dir, ".crew.yaml");
16708
- writeFileSync17(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16875
+ writeFileSync18(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16709
16876
  written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
16710
16877
  }
16711
16878
  if (flag("json")) {
@@ -16742,7 +16909,7 @@ ${changed2.map((c) => ` - ${c}
16742
16909
  if (sub === "list" && !routeName) usage2();
16743
16910
  if (sub === "add") {
16744
16911
  try {
16745
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync30(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16912
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync31(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16746
16913
  routeName = a.route;
16747
16914
  addPath = a.path;
16748
16915
  addName = a.name;
@@ -16771,15 +16938,15 @@ ${changed2.map((c) => ` - ${c}
16771
16938
  }
16772
16939
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
16773
16940
  let resolvedRemotes;
16774
- if (existsSync30(resolvedFile)) {
16941
+ if (existsSync31(resolvedFile)) {
16775
16942
  try {
16776
- resolvedRemotes = JSON.parse(readFileSync23(resolvedFile, "utf8"));
16943
+ resolvedRemotes = JSON.parse(readFileSync24(resolvedFile, "utf8"));
16777
16944
  } catch {
16778
16945
  }
16779
16946
  }
16780
16947
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
16781
16948
  const text = configText;
16782
- const base = dirname17(cfg.configFile);
16949
+ const base = dirname18(cfg.configFile);
16783
16950
  try {
16784
16951
  if (sub === "list") {
16785
16952
  const repos = configuredRepos(text, routeName, base);
@@ -16788,7 +16955,7 @@ ${changed2.map((c) => ` - ${c}
16788
16955
  for (const r of repos) {
16789
16956
  const origin2 = originOf(r.dir);
16790
16957
  const row = matchRepoName(origin2, resolvedRemotes);
16791
- const note = !existsSync30(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16958
+ const note = !existsSync31(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16792
16959
  process.stdout.write(`${r.name} ${r.dir} ${note}
16793
16960
  `);
16794
16961
  }
@@ -16800,9 +16967,9 @@ ${changed2.map((c) => ` - ${c}
16800
16967
  ` : `${human}
16801
16968
  `);
16802
16969
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
16803
- if (existsSync30(plan2.target) && (existsSync30(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
16970
+ if (existsSync31(plan2.target) && (existsSync31(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
16804
16971
  throw new ReposError(
16805
- existsSync30(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
16972
+ existsSync31(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
16806
16973
  );
16807
16974
  }
16808
16975
  if (dryRun) {
@@ -16820,12 +16987,12 @@ ${changed2.map((c) => ` - ${c}
16820
16987
  throw new ReposError(`${e.message}${authHint}
16821
16988
  crew.yaml was not changed`);
16822
16989
  }
16823
- writeFileSync17(cfg.configFile, plan2.text);
16990
+ writeFileSync18(cfg.configFile, plan2.text);
16824
16991
  say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
16825
16992
  break;
16826
16993
  }
16827
16994
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
16828
- const top = existsSync30(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16995
+ const top = existsSync31(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16829
16996
  if (!top) throw new ReposError(`${target} is not a git checkout`);
16830
16997
  const origin = originOf(top);
16831
16998
  const plan = planRepoAdd({
@@ -16874,15 +17041,15 @@ ${changed2.map((c) => ` - ${c}
16874
17041
  const created = await rowTracker.createRepoRow(rowPayload);
16875
17042
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
16876
17043
  `);
16877
- if (existsSync30(resolvedFile)) {
16878
- const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
17044
+ if (existsSync31(resolvedFile)) {
17045
+ const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
16879
17046
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
16880
17047
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
16881
- writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
17048
+ writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
16882
17049
  `);
16883
17050
  }
16884
17051
  }
16885
- writeFileSync17(cfg.configFile, plan.text);
17052
+ writeFileSync18(cfg.configFile, plan.text);
16886
17053
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
16887
17054
  `);
16888
17055
  } catch (e) {
@@ -17103,9 +17270,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17103
17270
  `);
17104
17271
  }
17105
17272
  }
17273
+ let machineId;
17274
+ try {
17275
+ machineId = ensureMachineId(cfg.ship.stateDir, { override: value("machine-id") });
17276
+ } catch (e) {
17277
+ process.stderr.write(`(no machine id \u2014 ${e.message})
17278
+ `);
17279
+ }
17106
17280
  const ship = {
17107
17281
  name: cfg.ship.name,
17108
17282
  platform: cfg.ship.platform,
17283
+ machineId,
17109
17284
  hostPassengers: firstRun?.answers.hostPassengers ?? existingRoute?.hostPassengers === true,
17110
17285
  sshPublicKey
17111
17286
  };
@@ -17202,10 +17377,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17202
17377
  }
17203
17378
  }
17204
17379
  let previous;
17205
- const hadPreviousFile = existsSync30(resolvedPath);
17380
+ const hadPreviousFile = existsSync31(resolvedPath);
17206
17381
  if (hadPreviousFile) {
17207
17382
  try {
17208
- previous = JSON.parse(readFileSync23(resolvedPath, "utf8"));
17383
+ previous = JSON.parse(readFileSync24(resolvedPath, "utf8"));
17209
17384
  } catch {
17210
17385
  }
17211
17386
  }
@@ -17314,10 +17489,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17314
17489
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
17315
17490
  `);
17316
17491
  } else {
17317
- mkdirSync16(dirname17(resolvedPath), { recursive: true });
17492
+ mkdirSync17(dirname18(resolvedPath), { recursive: true });
17318
17493
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
17319
17494
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
17320
- writeFileSync17(tmpPath, `${JSON.stringify(resolved, null, 2)}
17495
+ writeFileSync18(tmpPath, `${JSON.stringify(resolved, null, 2)}
17321
17496
  `);
17322
17497
  renameSync2(tmpPath, resolvedPath);
17323
17498
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -17352,8 +17527,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17352
17527
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
17353
17528
  `);
17354
17529
  } else {
17355
- mkdirSync16(dirname17(apiKeyFile), { recursive: true });
17356
- writeFileSync17(apiKeyFile, `${apiKeyVar}=${apiKey}
17530
+ mkdirSync17(dirname18(apiKeyFile), { recursive: true });
17531
+ writeFileSync18(apiKeyFile, `${apiKeyVar}=${apiKey}
17357
17532
  `, { mode: 384 });
17358
17533
  process.stderr.write(`API key written to ${apiKeyFile}
17359
17534
  `);
@@ -17362,7 +17537,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17362
17537
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
17363
17538
  const wizard = firstRun;
17364
17539
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
17365
- const routeDir = value("dir") ?? (writeFirstRun ? join30(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17540
+ const routeDir = value("dir") ?? (writeFirstRun ? join31(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17366
17541
  const routeBlock = renderConnection(
17367
17542
  found,
17368
17543
  route2,
@@ -18006,7 +18181,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18006
18181
  };
18007
18182
  const droppedRoutes = [];
18008
18183
  try {
18009
- const rawCfg = parseYaml2(readFileSync23(cfg.configFile, "utf8"));
18184
+ const rawCfg = parseYaml2(readFileSync24(cfg.configFile, "utf8"));
18010
18185
  for (const r of rawCfg?.routes ?? []) {
18011
18186
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
18012
18187
  try {
@@ -18048,8 +18223,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18048
18223
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
18049
18224
  break;
18050
18225
  }
18051
- mkdirSync16(cfg.ship.stateDir, { recursive: true });
18052
- writeFileSync17(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
18226
+ mkdirSync17(cfg.ship.stateDir, { recursive: true });
18227
+ writeFileSync18(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
18053
18228
  `);
18054
18229
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
18055
18230
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -18100,7 +18275,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18100
18275
  }
18101
18276
  case "log": {
18102
18277
  try {
18103
- const text = readFileSync23(cfg.ship.logFile, "utf8").split("\n");
18278
+ const text = readFileSync24(cfg.ship.logFile, "utf8").split("\n");
18104
18279
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
18105
18280
  `);
18106
18281
  } catch {
@@ -18123,8 +18298,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18123
18298
  };
18124
18299
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
18125
18300
  const tracker = new Tracker(route, cfg.ship);
18126
- const configText = readFileSync23(cfg.configFile, "utf8");
18127
- const cfgBase = dirname17(cfg.configFile);
18301
+ const configText = readFileSync24(cfg.configFile, "utf8");
18302
+ const cfgBase = dirname18(cfg.configFile);
18128
18303
  const configured = configuredRepos(configText, route.route, cfgBase);
18129
18304
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
18130
18305
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -18152,7 +18327,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18152
18327
  }
18153
18328
  if (configured.some((r) => r.dir === dir)) {
18154
18329
  try {
18155
- writeFileSync17(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
18330
+ writeFileSync18(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
18156
18331
  } catch (e) {
18157
18332
  if (e instanceof ReposError) fail(e.message);
18158
18333
  throw e;
@@ -18160,29 +18335,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18160
18335
  }
18161
18336
  if (row) {
18162
18337
  await tracker.deleteRepoRow(row.id);
18163
- if (existsSync30(resolvedFile)) {
18164
- const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
18338
+ if (existsSync31(resolvedFile)) {
18339
+ const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
18165
18340
  if (raw.repoNames) delete raw.repoNames[row.id];
18166
18341
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
18167
- writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
18342
+ writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
18168
18343
  `);
18169
18344
  }
18170
18345
  }
18171
- if (existsSync30(join30(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
18346
+ if (existsSync31(join31(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
18172
18347
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
18173
18348
  break;
18174
18349
  }
18175
18350
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
18176
- const fresh = !existsSync30(join30(dir, ".git"));
18177
- if (existsSync30(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
18351
+ const fresh = !existsSync31(join31(dir, ".git"));
18352
+ if (existsSync31(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
18178
18353
  if (dryRun) {
18179
18354
  say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
18180
18355
  break;
18181
18356
  }
18182
18357
  if (fresh) {
18183
- mkdirSync16(dir, { recursive: true });
18184
- writeFileSync17(join30(dir, "README.md"), SAMPLE_README);
18185
- writeFileSync17(join30(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
18358
+ mkdirSync17(dir, { recursive: true });
18359
+ writeFileSync18(join31(dir, "README.md"), SAMPLE_README);
18360
+ writeFileSync18(join31(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
18186
18361
  const g = (...a) => {
18187
18362
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
18188
18363
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -18200,17 +18375,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18200
18375
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
18201
18376
  });
18202
18377
  repoId = (await tracker.createRepoRow(payload)).id;
18203
- if (existsSync30(resolvedFile)) {
18204
- const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
18378
+ if (existsSync31(resolvedFile)) {
18379
+ const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
18205
18380
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
18206
18381
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
18207
- writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
18382
+ writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
18208
18383
  `);
18209
18384
  }
18210
18385
  }
18211
18386
  if (!configured.some((r) => r.dir === dir)) {
18212
18387
  try {
18213
- writeFileSync17(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
18388
+ writeFileSync18(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
18214
18389
  } catch (e) {
18215
18390
  if (e instanceof ReposError) fail(e.message);
18216
18391
  throw e;
@@ -18312,6 +18487,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18312
18487
  const issueLabelById = new Map(engagedTickets.map((t) => [t.id, t.issue_id]));
18313
18488
  const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route.resolved?.operator ?? "")).catch(() => []);
18314
18489
  const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
18490
+ const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
18315
18491
  if (flag("json")) {
18316
18492
  const repos = await resolvedRepos(route);
18317
18493
  const seatIds = route.resolved?.seats ?? {};
@@ -18371,6 +18547,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18371
18547
  watermark: state.watermark(),
18372
18548
  route: {
18373
18549
  name: route.route,
18550
+ // Additive (CREW-2248): the running version and the workspace's floor. `version` stays 1.
18551
+ crewVersion: sentVersion(cfg.ship),
18552
+ crewMinVersion,
18374
18553
  // Additive (CREW-1496): each project and its work mode. `version` stays 1.
18375
18554
  projects: projects.map((p) => ({ id: p.id, name: p.name, workMode: p.mode })),
18376
18555
  dir: route.dir,
@@ -18500,7 +18679,7 @@ watermark: ${state.watermark()}
18500
18679
  }
18501
18680
  for (const r of repos) {
18502
18681
  const remote = r.config.branch.remote;
18503
- if (existsSync30(r.dir)) {
18682
+ if (existsSync31(r.dir)) {
18504
18683
  pushAuth.push({
18505
18684
  route: c.route,
18506
18685
  repo: r.name,
@@ -18528,7 +18707,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18528
18707
  );
18529
18708
  try {
18530
18709
  const evFile = eventFileFor(cfg.ship.stateDir);
18531
- const evSize = existsSync30(evFile) ? statSync10(evFile).size : 0;
18710
+ const evSize = existsSync31(evFile) ? statSync10(evFile).size : 0;
18532
18711
  process.stdout.write(
18533
18712
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
18534
18713
  ` : "events log: ok\n"
@@ -18539,7 +18718,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18539
18718
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
18540
18719
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
18541
18720
  try {
18542
- return readFileSync23(p, "utf8");
18721
+ return readFileSync24(p, "utf8");
18543
18722
  } catch {
18544
18723
  return null;
18545
18724
  }
@@ -18561,6 +18740,19 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18561
18740
  }
18562
18741
  }
18563
18742
  const tracker = new Tracker(route, cfg.ship);
18743
+ {
18744
+ const have = sentVersion(cfg.ship);
18745
+ const floor = await readCrewFloor(route, cfg.ship);
18746
+ if (have && floor) {
18747
+ process.stdout.write(
18748
+ `crew version: ${belowFloor(have, floor) ? "WARN " : ""}${have} (workspace requires ${floor})${belowFloor(have, floor) ? " \u2014 update crew (the Crew app, or `npm install -g @tablation/crew`)" : ""}
18749
+ `
18750
+ );
18751
+ } else if (have) {
18752
+ process.stdout.write(`crew version: ${have}${floor === null ? " (workspace sets no minimum)" : ""}
18753
+ `);
18754
+ }
18755
+ }
18564
18756
  const contractProblems = validateContract(tracker.contract);
18565
18757
  process.stdout.write(
18566
18758
  contractProblems.length ? `contract: ${contractProblems.length} problem(s)
@@ -18640,7 +18832,12 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18640
18832
  if (ships.length === 0) {
18641
18833
  process.stdout.write("ship record: this workspace has no Ships table (fine)\n");
18642
18834
  } else {
18643
- const mine = ships.filter((s2) => (s2.name ?? "").trim() === cfg.ship.name.trim());
18835
+ const matched = matchShipRows(ships, cfg.ship.name, doctorMachineId(cfg.ship.stateDir));
18836
+ const mine = matched.rows;
18837
+ process.stdout.write(
18838
+ matched.hasColumn ? `ship row: ${mine.length === 1 ? `matched by ${matched.by === "machine_id" ? "machine id" : "name"}` : "not matched"}
18839
+ ` : "ship row: matched by name only \u2014 the Ships table has no machine_id column (update the Issues template)\n"
18840
+ );
18644
18841
  if (mine.length === 0) {
18645
18842
  process.stdout.write(
18646
18843
  `ship record: NO row named "${cfg.ship.name}" \u2014 have: ${ships.map((s2) => s2.name).join(", ")}
@@ -18923,4 +19120,5 @@ var passengerApiKeys2;
18923
19120
  var endpointApiFor2;
18924
19121
  var endpointReconcileDeps2;
18925
19122
  var releaseRefused2;
19123
+ var doctorMachineId2;
18926
19124
  var baseHealth2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.61.0",
3
+ "version": "0.63.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",