@tablation/crew 0.39.0 → 0.40.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 +514 -428
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6,6 +6,41 @@ import { fileURLToPath } from "url";
6
6
  import { hostname as hostname2, homedir as homedir7 } from "os";
7
7
  import { spawnSync as spawnSync5 } from "child_process";
8
8
 
9
+ // src/argv.ts
10
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
11
+ "area",
12
+ "base-url",
13
+ "dir",
14
+ "harness",
15
+ "key",
16
+ "level",
17
+ "limit",
18
+ "max-roles",
19
+ "mcp",
20
+ "member",
21
+ "name",
22
+ "project",
23
+ "repo",
24
+ "role",
25
+ "route",
26
+ "tail",
27
+ "ticket",
28
+ "workspace",
29
+ "workspace-id"
30
+ ]);
31
+ function positionalArgs(argv2) {
32
+ const out = [];
33
+ for (let i = 0; i < argv2.length; i++) {
34
+ const a = argv2[i];
35
+ if (a.startsWith("--")) {
36
+ if (!a.includes("=") && VALUE_FLAGS.has(a.slice(2))) i++;
37
+ continue;
38
+ }
39
+ out.push(a);
40
+ }
41
+ return out;
42
+ }
43
+
9
44
  // src/config.ts
10
45
  import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
11
46
  import { resolve, dirname as dirname3, isAbsolute, join as join6, basename as basename3 } from "path";
@@ -6607,9 +6642,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
6607
6642
 
6608
6643
  // src/agent.ts
6609
6644
  import { spawn as spawn2 } from "child_process";
6610
- import { readFileSync as readFileSync9, existsSync as existsSync10, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6645
+ import { readFileSync as readFileSync10, existsSync as existsSync11, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6611
6646
  import { finished } from "stream/promises";
6612
- import { join as join11, dirname as dirname6 } from "path";
6647
+ import { join as join12, dirname as dirname6 } from "path";
6613
6648
  import { createHash as createHash2 } from "crypto";
6614
6649
 
6615
6650
  // src/environment.ts
@@ -6998,33 +7033,144 @@ function expandSkillMarkers(text, lookup, opts = {}) {
6998
7033
  }
6999
7034
 
7000
7035
  // src/skills.ts
7001
- import { readFileSync as readFileSync8, readdirSync as readdirSync2, existsSync as existsSync9 } from "fs";
7002
- import { join as join10 } from "path";
7036
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync10 } from "fs";
7037
+ import { join as join11 } from "path";
7003
7038
  import { parse as parseYaml } from "yaml";
7039
+ import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
7040
+
7041
+ // src/agents.ts
7042
+ import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
7043
+ import { join as join10 } from "path";
7004
7044
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
7005
- var SkillSyncError = class extends Error {
7045
+ var PERSONA_NAME = {
7046
+ dev: "Developer",
7047
+ design: "Design",
7048
+ qa: "QA",
7049
+ triage: "Triage",
7050
+ pair: "Pair"
7006
7051
  };
7007
- function parseSkillFile(path) {
7008
- const text = readFileSync8(path, "utf8");
7009
- const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
7010
- if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
7011
- const frontmatter = match[1];
7012
- const body = match[2];
7013
- const meta = parseYaml(frontmatter);
7014
- if (typeof meta.name !== "string" || !meta.name) throw new SkillSyncError(`${path}: frontmatter is missing "name"`);
7015
- if (typeof meta.description !== "string" || !meta.description) {
7016
- throw new SkillSyncError(`${path}: frontmatter is missing "description"`);
7052
+ var CREW_AGENT_FIELD_NAME = "Agent";
7053
+ var CREW_AGENT_COLUMN = "agent_id";
7054
+ var AgentsSyncError = class extends Error {
7055
+ };
7056
+ function personaDefaultPrompt(promptsDir, role) {
7057
+ const briefPath = join10(promptsDir, "personas", `lane-${role}.md`);
7058
+ if (!existsSync9(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7059
+ if (SELF_CONTAINED_ROLES.has(role)) return readFileSync8(briefPath, "utf8");
7060
+ const commonPath = join10(promptsDir, "personas", "common.md");
7061
+ if (!existsSync9(commonPath)) return readFileSync8(briefPath, "utf8");
7062
+ return `${readFileSync8(commonPath, "utf8")}${readFileSync8(briefPath, "utf8")}`;
7063
+ }
7064
+ async function resolveAgentId(route, role, opts) {
7065
+ const cached = route.resolved?.agentPersonas?.[role]?.agentId;
7066
+ if (cached) return cached;
7067
+ const seatId = route.resolved?.seats?.[role];
7068
+ if (!seatId || !route.resolved) return void 0;
7069
+ const client2 = new TablationClient2({
7070
+ baseUrl: `${route.baseUrl}/api`,
7071
+ apiKey: resolveApiKey(route),
7072
+ headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7073
+ });
7074
+ try {
7075
+ const row = await client2.records.get(route.resolved.models.crew, seatId);
7076
+ return row.agent_id ?? void 0;
7077
+ } catch {
7078
+ return void 0;
7017
7079
  }
7018
- return { name: meta.name, description: meta.description, prompt: body, file: path };
7019
7080
  }
7020
- function listLocalSkills(promptsDir) {
7021
- const dir = join10(promptsDir, "skills");
7022
- if (!existsSync9(dir)) return [];
7023
- return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join10(dir, f)));
7081
+ async function fetchDivergedPrompt(route, role, opts) {
7082
+ const agentId = await resolveAgentId(route, role, opts);
7083
+ if (!agentId || !route.resolved) return void 0;
7084
+ const client2 = new TablationClient2({
7085
+ baseUrl: `${route.baseUrl}/api`,
7086
+ apiKey: resolveApiKey(route),
7087
+ headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7088
+ });
7089
+ let row;
7090
+ let agentsModelId;
7091
+ try {
7092
+ const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
7093
+ agentsModelId = agentsModel.id;
7094
+ row = await client2.records.get(agentsModelId, agentId);
7095
+ } catch {
7096
+ return void 0;
7097
+ }
7098
+ const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
7099
+ if (row.prompt === defaultPrompt) return void 0;
7100
+ const cached = route.resolved.agentPersonas?.[role];
7101
+ if (cached?.agentId === agentId && cached.historyId) {
7102
+ const lastSynced = await fetchLastSyncedPrompt(route, agentsModelId, agentId, cached.historyId, opts.userAgent);
7103
+ if (lastSynced !== void 0 && lastSynced === row.prompt) return void 0;
7104
+ }
7105
+ return row.prompt;
7024
7106
  }
7025
- async function syncSkills(route, opts) {
7107
+ async function fetchLastSyncedPrompt(route, agentsModelId, recordId, historyId, userAgent) {
7108
+ try {
7109
+ const entries = await fetchHistoryEntries(route, agentsModelId, recordId, userAgent);
7110
+ return entries.find((e) => e.history_id === historyId)?.prompt;
7111
+ } catch {
7112
+ return void 0;
7113
+ }
7114
+ }
7115
+ function choiceValue(v) {
7116
+ const raw = typeof v === "string" ? v : v && typeof v === "object" ? v.value ?? v.key : void 0;
7117
+ return typeof raw === "string" && raw.trim() ? raw.trim().toLowerCase() : void 0;
7118
+ }
7119
+ async function fetchSeatPersona(route, role, opts) {
7120
+ const agentId = await resolveAgentId(route, role, opts);
7121
+ if (!agentId || !route.resolved) return void 0;
7122
+ const client2 = new TablationClient2({
7123
+ baseUrl: `${route.baseUrl}/api`,
7124
+ apiKey: resolveApiKey(route),
7125
+ headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7126
+ });
7127
+ try {
7128
+ const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
7129
+ const row = await client2.records.get(agentsModel.id, agentId);
7130
+ const tier = choiceValue(row.tier);
7131
+ const vendor = choiceValue(row.vendor);
7132
+ const persona = {
7133
+ ...row.model ? { model: row.model } : {},
7134
+ ...tier && TIERS.includes(tier) ? { tier } : {},
7135
+ ...vendor ? { vendor } : {}
7136
+ };
7137
+ return Object.keys(persona).length > 0 ? persona : void 0;
7138
+ } catch {
7139
+ return void 0;
7140
+ }
7141
+ }
7142
+ async function currentPersonaPrompt(route, role, opts) {
7143
+ const diverged = await fetchDivergedPrompt(route, role, opts);
7144
+ return diverged ?? personaDefaultPrompt(route.promptsDir, role);
7145
+ }
7146
+ async function fetchHistoryEntries(route, agentsModelId, recordId, userAgent, limit = 50) {
7147
+ const url = `${route.baseUrl.replace(/\/+$/, "")}/api/data-models/${agentsModelId}/records/${recordId}/history?limit=${limit}`;
7148
+ const res = await fetch(url, {
7149
+ headers: {
7150
+ Authorization: `Bearer ${resolveApiKey(route)}`,
7151
+ "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
7152
+ }
7153
+ });
7154
+ if (!res.ok) throw new AgentsSyncError(`history lookup for ${recordId}: ${res.status} ${res.statusText}`);
7155
+ return await res.json();
7156
+ }
7157
+ async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
7158
+ try {
7159
+ const entries = await fetchHistoryEntries(route, agentsModelId, recordId, userAgent, 1);
7160
+ return entries[0]?.history_id;
7161
+ } catch {
7162
+ return void 0;
7163
+ }
7164
+ }
7165
+ function promptSourceFields(model, prompt) {
7166
+ return model.fields?.some((f) => f.columnName === "prompt_baseline") ? { prompt_source: "crew", prompt_baseline: prompt } : {};
7167
+ }
7168
+ function isPromptGateRejection(e) {
7169
+ return e instanceof TablationApiError && e.status === 422;
7170
+ }
7171
+ async function syncPersonas(route, opts) {
7026
7172
  if (!route.resolved) {
7027
- throw new SkillSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
7173
+ throw new AgentsSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
7028
7174
  }
7029
7175
  const workspaceId = route.resolved.workspaceId;
7030
7176
  const client2 = new TablationClient2({
@@ -7032,88 +7178,332 @@ async function syncSkills(route, opts) {
7032
7178
  apiKey: resolveApiKey(route),
7033
7179
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7034
7180
  });
7035
- let skillsModel;
7181
+ let agentsModel;
7036
7182
  try {
7037
- skillsModel = await client2.dataModels.get("agent_skills", workspaceId);
7183
+ agentsModel = await client2.dataModels.get("agents", workspaceId);
7038
7184
  } catch (e) {
7039
7185
  if (e instanceof TablationApiError && e.status === 404) {
7040
- throw new SkillSyncError(
7041
- `workspace ${workspaceId} has no Agent Skills table yet \u2014 every workspace auto-provisions one; an older workspace may still need its one-time backfill`
7186
+ throw new AgentsSyncError(
7187
+ `workspace ${workspaceId} has no Agents table yet \u2014 every workspace auto-provisions one (ISSUE-465); an older workspace may still need its one-time backfill`
7042
7188
  );
7043
7189
  }
7044
7190
  throw e;
7045
7191
  }
7046
- const local = listLocalSkills(route.promptsDir);
7047
- const rows = await client2.records.list(skillsModel.id, { limit: 200 });
7048
- const cache = route.resolved.agentSkills ?? {};
7192
+ const rows = await client2.records.list(agentsModel.id, { limit: 200 });
7193
+ const cache = route.resolved.agentPersonas ?? {};
7049
7194
  const outcomes = [];
7050
- const agentSkills = { ...cache };
7051
- for (const skill of local) {
7052
- const row = rows.find((r) => r.name === skill.name);
7195
+ const agentPersonas = { ...cache };
7196
+ for (const role of ROLE_NAMES) {
7197
+ const name = PERSONA_NAME[role];
7198
+ const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
7199
+ const row = rows.find((r) => r.name === name);
7053
7200
  if (!row) {
7054
7201
  if (opts.dryRun) {
7055
- outcomes.push({ name: skill.name, action: "created", skillId: "(dry run)" });
7202
+ outcomes.push({ role, action: "created", agentId: "(dry run)" });
7056
7203
  continue;
7057
7204
  }
7058
- const created = await client2.records.create(skillsModel.id, {
7059
- name: skill.name,
7060
- description: skill.description,
7061
- prompt: skill.prompt
7062
- });
7063
- agentSkills[skill.name] = { skillId: created.id, lastSyncedUpdatedAt: created.updated_at };
7064
- outcomes.push({ name: skill.name, action: "created", skillId: created.id });
7205
+ const created = await client2.records.create(agentsModel.id, { name, prompt: defaultPrompt, ...promptSourceFields(agentsModel, defaultPrompt) });
7206
+ const historyId = await fetchLatestHistoryId(route, agentsModel.id, created.id, opts.userAgent);
7207
+ agentPersonas[role] = { agentId: created.id, lastSyncedUpdatedAt: created.updated_at, ...historyId ? { historyId } : {} };
7208
+ outcomes.push({ role, action: "created", agentId: created.id });
7065
7209
  continue;
7066
7210
  }
7067
- if (row.description === skill.description && row.prompt === skill.prompt) {
7068
- agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: row.updated_at };
7069
- outcomes.push({ name: skill.name, action: "unchanged", skillId: row.id });
7211
+ if (row.prompt === defaultPrompt) {
7212
+ const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
7213
+ agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, ...historyId ? { historyId } : {} };
7214
+ outcomes.push({ role, action: "unchanged", agentId: row.id });
7070
7215
  continue;
7071
7216
  }
7072
- const knownBaseline = cache[skill.name]?.skillId === row.id ? cache[skill.name]?.lastSyncedUpdatedAt : void 0;
7073
- if (!knownBaseline) {
7074
- outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
7217
+ if (row.prompt_customized === true) {
7218
+ const prior = cache[role]?.agentId === row.id ? cache[role] : void 0;
7219
+ agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, ...prior?.historyId ? { historyId: prior.historyId } : {} };
7220
+ outcomes.push({ role, action: "overridden", agentId: row.id });
7221
+ continue;
7222
+ }
7223
+ if (opts.force) {
7224
+ if (opts.dryRun) {
7225
+ outcomes.push({ role, action: "forced", agentId: row.id, previousPrompt: row.prompt });
7226
+ continue;
7227
+ }
7228
+ let updated;
7229
+ try {
7230
+ updated = await client2.records.update(agentsModel.id, row.id, { prompt: defaultPrompt, ...promptSourceFields(agentsModel, defaultPrompt) });
7231
+ } catch (e) {
7232
+ if (!isPromptGateRejection(e)) throw e;
7233
+ outcomes.push({ role, action: "overridden", agentId: row.id });
7234
+ agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, ...cache[role]?.agentId === row.id && cache[role]?.historyId ? { historyId: cache[role].historyId } : {} };
7235
+ continue;
7236
+ }
7237
+ const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
7238
+ agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
7239
+ outcomes.push({ role, action: "forced", agentId: row.id, previousPrompt: row.prompt });
7240
+ continue;
7241
+ }
7242
+ const cached = cache[role]?.agentId === row.id ? cache[role] : void 0;
7243
+ if (!cached?.historyId) {
7244
+ outcomes.push({ role, action: "diverged", agentId: row.id });
7245
+ continue;
7246
+ }
7247
+ let lastSyncedPrompt;
7248
+ try {
7249
+ const entries = await fetchHistoryEntries(route, agentsModel.id, row.id, opts.userAgent);
7250
+ lastSyncedPrompt = entries.find((e) => e.history_id === cached.historyId)?.prompt;
7251
+ } catch {
7252
+ lastSyncedPrompt = void 0;
7253
+ }
7254
+ if (lastSyncedPrompt === void 0 || lastSyncedPrompt !== row.prompt) {
7255
+ outcomes.push({ role, action: "diverged", agentId: row.id });
7075
7256
  continue;
7076
7257
  }
7077
7258
  if (opts.dryRun) {
7078
- outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
7259
+ outcomes.push({ role, action: "updated", agentId: row.id });
7079
7260
  continue;
7080
7261
  }
7081
7262
  try {
7082
7263
  const updated = await client2.records.update(
7083
- skillsModel.id,
7264
+ agentsModel.id,
7084
7265
  row.id,
7085
- { description: skill.description, prompt: skill.prompt },
7086
- knownBaseline
7266
+ { prompt: defaultPrompt, ...promptSourceFields(agentsModel, defaultPrompt) },
7267
+ row.updated_at
7087
7268
  );
7088
- agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
7089
- outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
7269
+ const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
7270
+ agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
7271
+ outcomes.push({ role, action: "updated", agentId: row.id });
7090
7272
  } catch (e) {
7091
7273
  if (e instanceof StaleWriteError6) {
7092
- outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
7274
+ outcomes.push({ role, action: "diverged", agentId: row.id });
7275
+ continue;
7276
+ }
7277
+ if (isPromptGateRejection(e)) {
7278
+ agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, historyId: cached.historyId };
7279
+ outcomes.push({ role, action: "overridden", agentId: row.id });
7093
7280
  continue;
7094
7281
  }
7095
7282
  throw e;
7096
7283
  }
7097
7284
  }
7098
- return { outcomes, agentSkills };
7285
+ const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent);
7286
+ return { outcomes, agentPersonas, crewLinks };
7099
7287
  }
7100
- function describeSkillSyncOutcome(o) {
7101
- switch (o.action) {
7102
- case "created":
7103
- return `${o.name}: created (${o.skillId})`;
7104
- case "updated":
7105
- return `${o.name}: prompt updated from crew's local file`;
7106
- case "unchanged":
7107
- return `${o.name}: already in sync`;
7108
- case "diverged":
7109
- return `${o.name}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.skillId} in the app before syncing again`;
7288
+ async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent) {
7289
+ const seats2 = route.resolved.seats;
7290
+ const rolesWithSeats = ROLE_NAMES.filter((role) => seats2[role] && agentPersonas[role]);
7291
+ if (rolesWithSeats.length === 0) {
7292
+ return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "persona_diverged" : "no_seat" }));
7110
7293
  }
7111
- }
7112
- async function fetchSkillOverrides(route, opts) {
7113
- if (!route.resolved) return {};
7114
- const client2 = new TablationClient2({
7115
- baseUrl: `${route.baseUrl}/api`,
7116
- apiKey: resolveApiKey(route),
7294
+ const crewModelId = route.resolved.models.crew;
7295
+ const crewModel = await client2.dataModels.get(crewModelId);
7296
+ const columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent);
7297
+ const outcomes = [];
7298
+ for (const role of ROLE_NAMES) {
7299
+ const seatId = seats2[role];
7300
+ const agentId = agentPersonas[role]?.agentId;
7301
+ if (!seatId) {
7302
+ outcomes.push({ role, action: "no_seat" });
7303
+ continue;
7304
+ }
7305
+ if (!agentId) {
7306
+ outcomes.push({ role, action: "persona_diverged" });
7307
+ continue;
7308
+ }
7309
+ const seatRow = await client2.records.get(crewModelId, seatId);
7310
+ if (seatRow[CREW_AGENT_COLUMN] === agentId) {
7311
+ outcomes.push({ role, action: "already_linked" });
7312
+ continue;
7313
+ }
7314
+ await client2.records.update(crewModelId, seatId, { [columnName]: agentId });
7315
+ outcomes.push({ role, action: "linked" });
7316
+ }
7317
+ return outcomes;
7318
+ }
7319
+ async function ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent) {
7320
+ const existing = crewModel.fields.find(
7321
+ (f) => f.fieldType?.kind === "REFERENCE" && f.fieldType.targetModelId === agentsModelId && f.fieldType.isMultiple === false
7322
+ );
7323
+ if (existing) return existing.columnName;
7324
+ const workspaceId = route.resolved.workspaceId;
7325
+ const baseUrl = route.baseUrl.replace(/\/+$/, "");
7326
+ const apiKey = resolveApiKey(route);
7327
+ const headers = { Authorization: `Bearer ${apiKey}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" };
7328
+ const typesRes = await fetch(`${baseUrl}/api/workspaces/${workspaceId}/field-types`, { headers });
7329
+ if (!typesRes.ok) throw new AgentsSyncError(`field-types lookup failed: ${typesRes.status} ${typesRes.statusText}`);
7330
+ const types = await typesRes.json();
7331
+ const fieldType = types.find((t) => t.kind === "REFERENCE" && t.targetModelId === agentsModelId && t.isMultiple === false);
7332
+ if (!fieldType) {
7333
+ throw new AgentsSyncError(
7334
+ `workspace ${workspaceId} has no single-valued Agents-reference field type \u2014 every table auto-provisions one (see the pseudo-builtin reference types convention); an older Agents table may need its one-time backfill`
7335
+ );
7336
+ }
7337
+ const createRes = await fetch(`${baseUrl}/api/data-models/${crewModel.id}/fields`, {
7338
+ method: "POST",
7339
+ headers: { ...headers, "Content-Type": "application/json" },
7340
+ body: JSON.stringify({
7341
+ name: CREW_AGENT_FIELD_NAME,
7342
+ columnName: CREW_AGENT_COLUMN,
7343
+ fieldTypeId: fieldType.id,
7344
+ description: "The Agents-table row backing this seat's persona \u2014 set by `crew agents sync` (ISSUE-416), never edited by hand."
7345
+ })
7346
+ });
7347
+ if (!createRes.ok) throw new AgentsSyncError(`creating the Crew table's Agent field failed: ${createRes.status} ${createRes.statusText}`);
7348
+ const created = await createRes.json();
7349
+ return created.columnName;
7350
+ }
7351
+ function describeSyncOutcome(o) {
7352
+ const label = PERSONA_NAME[o.role];
7353
+ switch (o.action) {
7354
+ case "created":
7355
+ return `${label}: created (${o.agentId})`;
7356
+ case "updated":
7357
+ return `${label}: prompt updated from crew's local default`;
7358
+ case "unchanged":
7359
+ return `${label}: already in sync`;
7360
+ case "diverged":
7361
+ return `${label}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.agentId} in the app before syncing again`;
7362
+ case "overridden":
7363
+ return `${label}: override in place \u2014 prompt not synced (clear "Customize prompt" on the Agents row to resume)`;
7364
+ case "forced":
7365
+ return `${label}: --force overwrote local edits (previous prompt was: ${JSON.stringify(o.previousPrompt ?? "")})`;
7366
+ }
7367
+ }
7368
+ function describeCrewLink(o) {
7369
+ const label = PERSONA_NAME[o.role];
7370
+ switch (o.action) {
7371
+ case "linked":
7372
+ return `${label}: Crew row now references its Agents row`;
7373
+ case "already_linked":
7374
+ return `${label}: Crew row already references its Agents row`;
7375
+ case "no_seat":
7376
+ return `${label}: no seat configured for this role \u2014 skipped linking`;
7377
+ case "persona_diverged":
7378
+ return `${label}: persona has local edits and was not synced this pass \u2014 skipped linking until it's resolved`;
7379
+ }
7380
+ }
7381
+
7382
+ // src/skills.ts
7383
+ var SkillSyncError = class extends Error {
7384
+ };
7385
+ function parseSkillFile(path) {
7386
+ const text = readFileSync9(path, "utf8");
7387
+ const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
7388
+ if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
7389
+ const frontmatter = match[1];
7390
+ const body = match[2];
7391
+ const meta = parseYaml(frontmatter);
7392
+ if (typeof meta.name !== "string" || !meta.name) throw new SkillSyncError(`${path}: frontmatter is missing "name"`);
7393
+ if (typeof meta.description !== "string" || !meta.description) {
7394
+ throw new SkillSyncError(`${path}: frontmatter is missing "description"`);
7395
+ }
7396
+ return { name: meta.name, description: meta.description, prompt: body, file: path };
7397
+ }
7398
+ function listLocalSkills(promptsDir) {
7399
+ const dir = join11(promptsDir, "skills");
7400
+ if (!existsSync10(dir)) return [];
7401
+ return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join11(dir, f)));
7402
+ }
7403
+ async function syncSkills(route, opts) {
7404
+ if (!route.resolved) {
7405
+ throw new SkillSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
7406
+ }
7407
+ const workspaceId = route.resolved.workspaceId;
7408
+ const client2 = new TablationClient3({
7409
+ baseUrl: `${route.baseUrl}/api`,
7410
+ apiKey: resolveApiKey(route),
7411
+ headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7412
+ });
7413
+ let skillsModel;
7414
+ try {
7415
+ skillsModel = await client2.dataModels.get("agent_skills", workspaceId);
7416
+ } catch (e) {
7417
+ if (e instanceof TablationApiError2 && e.status === 404) {
7418
+ throw new SkillSyncError(
7419
+ `workspace ${workspaceId} has no Agent Skills table yet \u2014 every workspace auto-provisions one; an older workspace may still need its one-time backfill`
7420
+ );
7421
+ }
7422
+ throw e;
7423
+ }
7424
+ const local = listLocalSkills(route.promptsDir);
7425
+ const rows = await client2.records.list(skillsModel.id, { limit: 200 });
7426
+ const cache = route.resolved.agentSkills ?? {};
7427
+ const outcomes = [];
7428
+ const agentSkills = { ...cache };
7429
+ for (const skill of local) {
7430
+ const row = rows.find((r) => r.name === skill.name);
7431
+ if (!row) {
7432
+ if (opts.dryRun) {
7433
+ outcomes.push({ name: skill.name, action: "created", skillId: "(dry run)" });
7434
+ continue;
7435
+ }
7436
+ const created = await client2.records.create(skillsModel.id, {
7437
+ name: skill.name,
7438
+ description: skill.description,
7439
+ prompt: skill.prompt,
7440
+ ...promptSourceFields(skillsModel, skill.prompt)
7441
+ });
7442
+ agentSkills[skill.name] = { skillId: created.id, lastSyncedUpdatedAt: created.updated_at };
7443
+ outcomes.push({ name: skill.name, action: "created", skillId: created.id });
7444
+ continue;
7445
+ }
7446
+ if (row.description === skill.description && row.prompt === skill.prompt) {
7447
+ agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: row.updated_at };
7448
+ outcomes.push({ name: skill.name, action: "unchanged", skillId: row.id });
7449
+ continue;
7450
+ }
7451
+ if (row.prompt_customized === true) {
7452
+ agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: row.updated_at };
7453
+ outcomes.push({ name: skill.name, action: "overridden", skillId: row.id });
7454
+ continue;
7455
+ }
7456
+ const knownBaseline = cache[skill.name]?.skillId === row.id ? cache[skill.name]?.lastSyncedUpdatedAt : void 0;
7457
+ if (!knownBaseline) {
7458
+ outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
7459
+ continue;
7460
+ }
7461
+ if (opts.dryRun) {
7462
+ outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
7463
+ continue;
7464
+ }
7465
+ try {
7466
+ const updated = await client2.records.update(
7467
+ skillsModel.id,
7468
+ row.id,
7469
+ { description: skill.description, prompt: skill.prompt, ...promptSourceFields(skillsModel, skill.prompt) },
7470
+ knownBaseline
7471
+ );
7472
+ agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
7473
+ outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
7474
+ } catch (e) {
7475
+ if (e instanceof StaleWriteError7) {
7476
+ outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
7477
+ continue;
7478
+ }
7479
+ if (isPromptGateRejection(e)) {
7480
+ outcomes.push({ name: skill.name, action: "overridden", skillId: row.id });
7481
+ continue;
7482
+ }
7483
+ throw e;
7484
+ }
7485
+ }
7486
+ return { outcomes, agentSkills };
7487
+ }
7488
+ function describeSkillSyncOutcome(o) {
7489
+ switch (o.action) {
7490
+ case "created":
7491
+ return `${o.name}: created (${o.skillId})`;
7492
+ case "updated":
7493
+ return `${o.name}: prompt updated from crew's local file`;
7494
+ case "unchanged":
7495
+ return `${o.name}: already in sync`;
7496
+ case "overridden":
7497
+ return `${o.name}: override in place \u2014 prompt not synced (clear "Customize prompt" on the Agent Skills row to resume)`;
7498
+ case "diverged":
7499
+ return `${o.name}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.skillId} in the app before syncing again`;
7500
+ }
7501
+ }
7502
+ async function fetchSkillOverrides(route, opts) {
7503
+ if (!route.resolved) return {};
7504
+ const client2 = new TablationClient3({
7505
+ baseUrl: `${route.baseUrl}/api`,
7506
+ apiKey: resolveApiKey(route),
7117
7507
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7118
7508
  });
7119
7509
  let rows;
@@ -7200,23 +7590,23 @@ var AgentError = class extends Error {
7200
7590
  var AgentSpawnError = class extends AgentError {
7201
7591
  };
7202
7592
  function planAgentRun(o) {
7203
- const briefPath = join11(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7204
- if (!existsSync10(briefPath)) {
7593
+ const briefPath = join12(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7594
+ if (!existsSync11(briefPath)) {
7205
7595
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
7206
7596
  }
7207
- const commonPath = join11(o.route.promptsDir, "personas", "common.md");
7208
- const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync10(commonPath);
7597
+ const commonPath = join12(o.route.promptsDir, "personas", "common.md");
7598
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync11(commonPath);
7209
7599
  const now = o.now ?? (() => Date.now());
7210
7600
  const routeLabel = routeSlug(o.route.route);
7211
- const digestPath2 = join11(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7601
+ const digestPath2 = join12(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7212
7602
  let digest;
7213
7603
  let ageSeconds;
7214
- if (existsSync10(digestPath2)) {
7604
+ if (existsSync11(digestPath2)) {
7215
7605
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
7216
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync9(digestPath2, "utf8");
7606
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync10(digestPath2, "utf8");
7217
7607
  }
7218
- const common = usesCommon ? readFileSync9(commonPath, "utf8") : "";
7219
- const brief = readFileSync9(briefPath, "utf8");
7608
+ const common = usesCommon ? readFileSync10(commonPath, "utf8") : "";
7609
+ const brief = readFileSync10(briefPath, "utf8");
7220
7610
  const effectiveCommon = o.divergedPrompt ? "" : common;
7221
7611
  const effectiveBrief = o.divergedPrompt ?? brief;
7222
7612
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -7238,7 +7628,7 @@ function planAgentRun(o) {
7238
7628
  digest
7239
7629
  });
7240
7630
  const promptSha = createHash2("sha256").update(expanded.text).digest("hex");
7241
- const streamsDir = join11(o.stateDir, "streams");
7631
+ const streamsDir = join12(o.stateDir, "streams");
7242
7632
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
7243
7633
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
7244
7634
  const { toolchain, model } = seat;
@@ -7294,8 +7684,8 @@ function planAgentRun(o) {
7294
7684
  digestAttached: digest !== void 0,
7295
7685
  digestAgeSeconds: ageSeconds,
7296
7686
  model,
7297
- streamPath: join11(streamsDir, `${base}.jsonl`),
7298
- eventsPath: join11(streamsDir, `${base}.events.jsonl`),
7687
+ streamPath: join12(streamsDir, `${base}.jsonl`),
7688
+ eventsPath: join12(streamsDir, `${base}.events.jsonl`),
7299
7689
  ticket: o.ticket,
7300
7690
  agentLog,
7301
7691
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -7501,13 +7891,13 @@ function spawnAgent(plan, emit) {
7501
7891
  }
7502
7892
 
7503
7893
  // src/run-worktree.ts
7504
- import { existsSync as existsSync12 } from "fs";
7505
- import { dirname as dirname7, join as join13 } from "path";
7894
+ import { existsSync as existsSync13 } from "fs";
7895
+ import { dirname as dirname7, join as join14 } from "path";
7506
7896
 
7507
7897
  // src/ports.ts
7508
7898
  import { execFileSync as execFileSync2 } from "child_process";
7509
- import { existsSync as existsSync11 } from "fs";
7510
- import { join as join12 } from "path";
7899
+ import { existsSync as existsSync12 } from "fs";
7900
+ import { join as join13 } from "path";
7511
7901
  var run = (cmd, args) => {
7512
7902
  try {
7513
7903
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7552,7 +7942,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7552
7942
  }
7553
7943
  function worktreeExists(parent, prefix, n) {
7554
7944
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7555
- if (existsSync11(join12(parent, name, ".git"))) return true;
7945
+ if (existsSync12(join13(parent, name, ".git"))) return true;
7556
7946
  }
7557
7947
  return false;
7558
7948
  }
@@ -7626,7 +8016,7 @@ function planRunWorktree(dir, cfg, t, role) {
7626
8016
  const existing = worktreeForNumber(dir, num);
7627
8017
  const located = locateBranchForTicket(dir, cfg, t, role);
7628
8018
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7629
- if (existing && !existing.prunable && existsSync12(existing.path)) {
8019
+ if (existing && !existing.prunable && existsSync13(existing.path)) {
7630
8020
  const branch = existing.branch;
7631
8021
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
7632
8022
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -7640,7 +8030,7 @@ function planRunWorktree(dir, cfg, t, role) {
7640
8030
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7641
8031
  return { action: "recut", path: existing.path, branch, base: baseRef };
7642
8032
  }
7643
- const path = join13(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8033
+ const path = join14(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7644
8034
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7645
8035
  return { action: "cut-remote", path, branch: located.branch };
7646
8036
  }
@@ -7687,7 +8077,7 @@ function worktreeHoldsUnpushed(path, remote) {
7687
8077
  }
7688
8078
  async function finishRunWorktree(dir, cfg, branch) {
7689
8079
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7690
- if (!wt || !existsSync12(wt.path)) return { kind: "none" };
8080
+ if (!wt || !existsSync13(wt.path)) return { kind: "none" };
7691
8081
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7692
8082
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7693
8083
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7699,7 +8089,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7699
8089
  if (pids.length) await killGently(pids);
7700
8090
  }
7701
8091
  gitOk(dir, ["worktree", "remove", wt.path]);
7702
- return existsSync12(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
8092
+ return existsSync13(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7703
8093
  }
7704
8094
 
7705
8095
  // src/handoff-guard.ts
@@ -7848,13 +8238,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
7848
8238
  }
7849
8239
 
7850
8240
  // src/connect-wizard.ts
7851
- import { delimiter, dirname as dirname8, join as join14 } from "path";
7852
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
8241
+ import { delimiter, dirname as dirname8, join as join15 } from "path";
8242
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
7853
8243
  import { stringify } from "yaml";
7854
8244
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
7855
8245
  function shouldRunWizard(opts) {
7856
8246
  if (!opts.isTTY) return false;
7857
- const exists = opts.configExists ?? existsSync13;
8247
+ const exists = opts.configExists ?? existsSync14;
7858
8248
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
7859
8249
  }
7860
8250
  function firstRunConfigPath() {
@@ -7871,7 +8261,7 @@ function defaultIsExecutable(p) {
7871
8261
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
7872
8262
  for (const dir of pathEnv.split(delimiter)) {
7873
8263
  if (!dir) continue;
7874
- const candidate = join14(dir, name);
8264
+ const candidate = join15(dir, name);
7875
8265
  if (isExecutable(candidate)) return candidate;
7876
8266
  }
7877
8267
  return void 0;
@@ -7966,8 +8356,8 @@ Wrote ${o.configPath}.
7966
8356
  }
7967
8357
 
7968
8358
  // src/release-run.ts
7969
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync14, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
7970
- import { dirname as dirname9, join as join15 } from "path";
8359
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync15, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
8360
+ import { dirname as dirname9, join as join16 } from "path";
7971
8361
  import { tmpdir as tmpdir2 } from "os";
7972
8362
 
7973
8363
  // src/release.ts
@@ -8625,13 +9015,13 @@ async function bump(o, size) {
8625
9015
  }
8626
9016
  return printed;
8627
9017
  }
8628
- const files = o.repo.release.versionFiles.map((f) => join15(o.cwd, f));
9018
+ const files = o.repo.release.versionFiles.map((f) => join16(o.cwd, f));
8629
9019
  const first = files[0];
8630
- if (!existsSync14(first)) {
9020
+ if (!existsSync15(first)) {
8631
9021
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
8632
9022
  return null;
8633
9023
  }
8634
- const current = JSON.parse(readFileSync10(first, "utf8")).version;
9024
+ const current = JSON.parse(readFileSync11(first, "utf8")).version;
8635
9025
  if (!current) {
8636
9026
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
8637
9027
  return null;
@@ -8644,7 +9034,7 @@ async function bump(o, size) {
8644
9034
  return version;
8645
9035
  }
8646
9036
  for (const f of files) {
8647
- const src = readFileSync10(f, "utf8");
9037
+ const src = readFileSync11(f, "utf8");
8648
9038
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
8649
9039
  if (out === src) {
8650
9040
  o.emit.error(`no version field in ${f}`);
@@ -8657,13 +9047,13 @@ async function bump(o, size) {
8657
9047
  function writeChangelog(o, version, entries) {
8658
9048
  const file = o.repo.release.changelog;
8659
9049
  if (!file || entries.length === 0) return;
8660
- const path = join15(o.cwd, file);
9050
+ const path = join16(o.cwd, file);
8661
9051
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
8662
9052
  if (o.dryRun) {
8663
9053
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
8664
9054
  return;
8665
9055
  }
8666
- const existing = existsSync14(path) ? readFileSync10(path, "utf8") : "# Changelog\n";
9056
+ const existing = existsSync15(path) ? readFileSync11(path, "utf8") : "# Changelog\n";
8667
9057
  writeFileSync9(path, insertChangelogSection(existing, section));
8668
9058
  }
8669
9059
  async function confirm(o, expected) {
@@ -8745,8 +9135,8 @@ async function confirmLastRelease(o, decision) {
8745
9135
  function openReleaseCheckout(o) {
8746
9136
  const { remote, base } = o.repo.branch;
8747
9137
  const branch = `crew/release-${Date.now()}`;
8748
- const root = mkdtempSync(join15(tmpdir2(), "crew-release-"));
8749
- const dir = join15(root, "checkout");
9138
+ const root = mkdtempSync(join16(tmpdir2(), "crew-release-"));
9139
+ const dir = join16(root, "checkout");
8750
9140
  const close = () => {
8751
9141
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
8752
9142
  rmSync6(root, { recursive: true, force: true });
@@ -8760,10 +9150,10 @@ function openReleaseCheckout(o) {
8760
9150
  throw e;
8761
9151
  }
8762
9152
  for (const f of o.repo.worktrees.copy) {
8763
- const src = join15(o.cwd, f);
8764
- if (!existsSync14(src)) continue;
8765
- mkdirSync8(dirname9(join15(dir, f)), { recursive: true });
8766
- copyFileSync(src, join15(dir, f));
9153
+ const src = join16(o.cwd, f);
9154
+ if (!existsSync15(src)) continue;
9155
+ mkdirSync8(dirname9(join16(dir, f)), { recursive: true });
9156
+ copyFileSync(src, join16(dir, f));
8767
9157
  }
8768
9158
  return { dir, branch, close };
8769
9159
  }
@@ -9232,7 +9622,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
9232
9622
  }
9233
9623
 
9234
9624
  // src/watch.ts
9235
- import { watch as watchFile, existsSync as existsSync15, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
9625
+ import { watch as watchFile, existsSync as existsSync16, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
9236
9626
  var E = "\x1B[";
9237
9627
  var dim = (s) => `${E}2m${s}${E}0m`;
9238
9628
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -9333,7 +9723,7 @@ function renderHeaders(states, now = Date.now()) {
9333
9723
  return states.map((s) => renderHeader(s, now)).join("");
9334
9724
  }
9335
9725
  function readFrom(file, from) {
9336
- if (!existsSync15(file)) return { events: [], offset: from };
9726
+ if (!existsSync16(file)) return { events: [], offset: from };
9337
9727
  const size = statSync6(file).size;
9338
9728
  if (size < from) return readFrom(file, 0);
9339
9729
  if (size === from) return { events: [], offset: from };
@@ -9572,320 +9962,6 @@ function nextRoles(decision, ran) {
9572
9962
  return decision.selection.pending.filter((r) => !ran.includes(r)).sort((a, b) => (decision.selection.ranks[a] ?? 0) - (decision.selection.ranks[b] ?? 0));
9573
9963
  }
9574
9964
 
9575
- // src/agents.ts
9576
- import { readFileSync as readFileSync11, existsSync as existsSync16 } from "fs";
9577
- import { join as join16 } from "path";
9578
- import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
9579
- var PERSONA_NAME = {
9580
- dev: "Developer",
9581
- design: "Design",
9582
- qa: "QA",
9583
- triage: "Triage",
9584
- pair: "Pair"
9585
- };
9586
- var CREW_AGENT_FIELD_NAME = "Agent";
9587
- var CREW_AGENT_COLUMN = "agent_id";
9588
- var AgentsSyncError = class extends Error {
9589
- };
9590
- function personaDefaultPrompt(promptsDir, role) {
9591
- const briefPath = join16(promptsDir, "personas", `lane-${role}.md`);
9592
- if (!existsSync16(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
9593
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync11(briefPath, "utf8");
9594
- const commonPath = join16(promptsDir, "personas", "common.md");
9595
- if (!existsSync16(commonPath)) return readFileSync11(briefPath, "utf8");
9596
- return `${readFileSync11(commonPath, "utf8")}${readFileSync11(briefPath, "utf8")}`;
9597
- }
9598
- async function resolveAgentId(route, role, opts) {
9599
- const cached = route.resolved?.agentPersonas?.[role]?.agentId;
9600
- if (cached) return cached;
9601
- const seatId = route.resolved?.seats?.[role];
9602
- if (!seatId || !route.resolved) return void 0;
9603
- const client2 = new TablationClient3({
9604
- baseUrl: `${route.baseUrl}/api`,
9605
- apiKey: resolveApiKey(route),
9606
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9607
- });
9608
- try {
9609
- const row = await client2.records.get(route.resolved.models.crew, seatId);
9610
- return row.agent_id ?? void 0;
9611
- } catch {
9612
- return void 0;
9613
- }
9614
- }
9615
- async function fetchDivergedPrompt(route, role, opts) {
9616
- const agentId = await resolveAgentId(route, role, opts);
9617
- if (!agentId || !route.resolved) return void 0;
9618
- const client2 = new TablationClient3({
9619
- baseUrl: `${route.baseUrl}/api`,
9620
- apiKey: resolveApiKey(route),
9621
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9622
- });
9623
- let row;
9624
- let agentsModelId;
9625
- try {
9626
- const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
9627
- agentsModelId = agentsModel.id;
9628
- row = await client2.records.get(agentsModelId, agentId);
9629
- } catch {
9630
- return void 0;
9631
- }
9632
- const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
9633
- if (row.prompt === defaultPrompt) return void 0;
9634
- const cached = route.resolved.agentPersonas?.[role];
9635
- if (cached?.agentId === agentId && cached.historyId) {
9636
- const lastSynced = await fetchLastSyncedPrompt(route, agentsModelId, agentId, cached.historyId, opts.userAgent);
9637
- if (lastSynced !== void 0 && lastSynced === row.prompt) return void 0;
9638
- }
9639
- return row.prompt;
9640
- }
9641
- async function fetchLastSyncedPrompt(route, agentsModelId, recordId, historyId, userAgent) {
9642
- try {
9643
- const entries = await fetchHistoryEntries(route, agentsModelId, recordId, userAgent);
9644
- return entries.find((e) => e.history_id === historyId)?.prompt;
9645
- } catch {
9646
- return void 0;
9647
- }
9648
- }
9649
- function choiceValue(v) {
9650
- const raw = typeof v === "string" ? v : v && typeof v === "object" ? v.value ?? v.key : void 0;
9651
- return typeof raw === "string" && raw.trim() ? raw.trim().toLowerCase() : void 0;
9652
- }
9653
- async function fetchSeatPersona(route, role, opts) {
9654
- const agentId = await resolveAgentId(route, role, opts);
9655
- if (!agentId || !route.resolved) return void 0;
9656
- const client2 = new TablationClient3({
9657
- baseUrl: `${route.baseUrl}/api`,
9658
- apiKey: resolveApiKey(route),
9659
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9660
- });
9661
- try {
9662
- const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
9663
- const row = await client2.records.get(agentsModel.id, agentId);
9664
- const tier = choiceValue(row.tier);
9665
- const vendor = choiceValue(row.vendor);
9666
- const persona = {
9667
- ...row.model ? { model: row.model } : {},
9668
- ...tier && TIERS.includes(tier) ? { tier } : {},
9669
- ...vendor ? { vendor } : {}
9670
- };
9671
- return Object.keys(persona).length > 0 ? persona : void 0;
9672
- } catch {
9673
- return void 0;
9674
- }
9675
- }
9676
- async function currentPersonaPrompt(route, role, opts) {
9677
- const diverged = await fetchDivergedPrompt(route, role, opts);
9678
- return diverged ?? personaDefaultPrompt(route.promptsDir, role);
9679
- }
9680
- async function fetchHistoryEntries(route, agentsModelId, recordId, userAgent, limit = 50) {
9681
- const url = `${route.baseUrl.replace(/\/+$/, "")}/api/data-models/${agentsModelId}/records/${recordId}/history?limit=${limit}`;
9682
- const res = await fetch(url, {
9683
- headers: {
9684
- Authorization: `Bearer ${resolveApiKey(route)}`,
9685
- "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
9686
- }
9687
- });
9688
- if (!res.ok) throw new AgentsSyncError(`history lookup for ${recordId}: ${res.status} ${res.statusText}`);
9689
- return await res.json();
9690
- }
9691
- async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
9692
- try {
9693
- const entries = await fetchHistoryEntries(route, agentsModelId, recordId, userAgent, 1);
9694
- return entries[0]?.history_id;
9695
- } catch {
9696
- return void 0;
9697
- }
9698
- }
9699
- async function syncPersonas(route, opts) {
9700
- if (!route.resolved) {
9701
- throw new AgentsSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
9702
- }
9703
- const workspaceId = route.resolved.workspaceId;
9704
- const client2 = new TablationClient3({
9705
- baseUrl: `${route.baseUrl}/api`,
9706
- apiKey: resolveApiKey(route),
9707
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9708
- });
9709
- let agentsModel;
9710
- try {
9711
- agentsModel = await client2.dataModels.get("agents", workspaceId);
9712
- } catch (e) {
9713
- if (e instanceof TablationApiError2 && e.status === 404) {
9714
- throw new AgentsSyncError(
9715
- `workspace ${workspaceId} has no Agents table yet \u2014 every workspace auto-provisions one (ISSUE-465); an older workspace may still need its one-time backfill`
9716
- );
9717
- }
9718
- throw e;
9719
- }
9720
- const rows = await client2.records.list(agentsModel.id, { limit: 200 });
9721
- const cache = route.resolved.agentPersonas ?? {};
9722
- const outcomes = [];
9723
- const agentPersonas = { ...cache };
9724
- for (const role of ROLE_NAMES) {
9725
- const name = PERSONA_NAME[role];
9726
- const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
9727
- const row = rows.find((r) => r.name === name);
9728
- if (!row) {
9729
- if (opts.dryRun) {
9730
- outcomes.push({ role, action: "created", agentId: "(dry run)" });
9731
- continue;
9732
- }
9733
- const created = await client2.records.create(agentsModel.id, { name, prompt: defaultPrompt });
9734
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, created.id, opts.userAgent);
9735
- agentPersonas[role] = { agentId: created.id, lastSyncedUpdatedAt: created.updated_at, ...historyId ? { historyId } : {} };
9736
- outcomes.push({ role, action: "created", agentId: created.id });
9737
- continue;
9738
- }
9739
- if (row.prompt === defaultPrompt) {
9740
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
9741
- agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, ...historyId ? { historyId } : {} };
9742
- outcomes.push({ role, action: "unchanged", agentId: row.id });
9743
- continue;
9744
- }
9745
- if (opts.force) {
9746
- if (opts.dryRun) {
9747
- outcomes.push({ role, action: "forced", agentId: row.id, previousPrompt: row.prompt });
9748
- continue;
9749
- }
9750
- const updated = await client2.records.update(agentsModel.id, row.id, { prompt: defaultPrompt });
9751
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
9752
- agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
9753
- outcomes.push({ role, action: "forced", agentId: row.id, previousPrompt: row.prompt });
9754
- continue;
9755
- }
9756
- const cached = cache[role]?.agentId === row.id ? cache[role] : void 0;
9757
- if (!cached?.historyId) {
9758
- outcomes.push({ role, action: "diverged", agentId: row.id });
9759
- continue;
9760
- }
9761
- let lastSyncedPrompt;
9762
- try {
9763
- const entries = await fetchHistoryEntries(route, agentsModel.id, row.id, opts.userAgent);
9764
- lastSyncedPrompt = entries.find((e) => e.history_id === cached.historyId)?.prompt;
9765
- } catch {
9766
- lastSyncedPrompt = void 0;
9767
- }
9768
- if (lastSyncedPrompt === void 0 || lastSyncedPrompt !== row.prompt) {
9769
- outcomes.push({ role, action: "diverged", agentId: row.id });
9770
- continue;
9771
- }
9772
- if (opts.dryRun) {
9773
- outcomes.push({ role, action: "updated", agentId: row.id });
9774
- continue;
9775
- }
9776
- try {
9777
- const updated = await client2.records.update(
9778
- agentsModel.id,
9779
- row.id,
9780
- { prompt: defaultPrompt },
9781
- row.updated_at
9782
- );
9783
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
9784
- agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
9785
- outcomes.push({ role, action: "updated", agentId: row.id });
9786
- } catch (e) {
9787
- if (e instanceof StaleWriteError7) {
9788
- outcomes.push({ role, action: "diverged", agentId: row.id });
9789
- continue;
9790
- }
9791
- throw e;
9792
- }
9793
- }
9794
- const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent);
9795
- return { outcomes, agentPersonas, crewLinks };
9796
- }
9797
- async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent) {
9798
- const seats2 = route.resolved.seats;
9799
- const rolesWithSeats = ROLE_NAMES.filter((role) => seats2[role] && agentPersonas[role]);
9800
- if (rolesWithSeats.length === 0) {
9801
- return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "persona_diverged" : "no_seat" }));
9802
- }
9803
- const crewModelId = route.resolved.models.crew;
9804
- const crewModel = await client2.dataModels.get(crewModelId);
9805
- const columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent);
9806
- const outcomes = [];
9807
- for (const role of ROLE_NAMES) {
9808
- const seatId = seats2[role];
9809
- const agentId = agentPersonas[role]?.agentId;
9810
- if (!seatId) {
9811
- outcomes.push({ role, action: "no_seat" });
9812
- continue;
9813
- }
9814
- if (!agentId) {
9815
- outcomes.push({ role, action: "persona_diverged" });
9816
- continue;
9817
- }
9818
- const seatRow = await client2.records.get(crewModelId, seatId);
9819
- if (seatRow[CREW_AGENT_COLUMN] === agentId) {
9820
- outcomes.push({ role, action: "already_linked" });
9821
- continue;
9822
- }
9823
- await client2.records.update(crewModelId, seatId, { [columnName]: agentId });
9824
- outcomes.push({ role, action: "linked" });
9825
- }
9826
- return outcomes;
9827
- }
9828
- async function ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent) {
9829
- const existing = crewModel.fields.find(
9830
- (f) => f.fieldType?.kind === "REFERENCE" && f.fieldType.targetModelId === agentsModelId && f.fieldType.isMultiple === false
9831
- );
9832
- if (existing) return existing.columnName;
9833
- const workspaceId = route.resolved.workspaceId;
9834
- const baseUrl = route.baseUrl.replace(/\/+$/, "");
9835
- const apiKey = resolveApiKey(route);
9836
- const headers = { Authorization: `Bearer ${apiKey}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" };
9837
- const typesRes = await fetch(`${baseUrl}/api/workspaces/${workspaceId}/field-types`, { headers });
9838
- if (!typesRes.ok) throw new AgentsSyncError(`field-types lookup failed: ${typesRes.status} ${typesRes.statusText}`);
9839
- const types = await typesRes.json();
9840
- const fieldType = types.find((t) => t.kind === "REFERENCE" && t.targetModelId === agentsModelId && t.isMultiple === false);
9841
- if (!fieldType) {
9842
- throw new AgentsSyncError(
9843
- `workspace ${workspaceId} has no single-valued Agents-reference field type \u2014 every table auto-provisions one (see the pseudo-builtin reference types convention); an older Agents table may need its one-time backfill`
9844
- );
9845
- }
9846
- const createRes = await fetch(`${baseUrl}/api/data-models/${crewModel.id}/fields`, {
9847
- method: "POST",
9848
- headers: { ...headers, "Content-Type": "application/json" },
9849
- body: JSON.stringify({
9850
- name: CREW_AGENT_FIELD_NAME,
9851
- columnName: CREW_AGENT_COLUMN,
9852
- fieldTypeId: fieldType.id,
9853
- description: "The Agents-table row backing this seat's persona \u2014 set by `crew agents sync` (ISSUE-416), never edited by hand."
9854
- })
9855
- });
9856
- if (!createRes.ok) throw new AgentsSyncError(`creating the Crew table's Agent field failed: ${createRes.status} ${createRes.statusText}`);
9857
- const created = await createRes.json();
9858
- return created.columnName;
9859
- }
9860
- function describeSyncOutcome(o) {
9861
- const label = PERSONA_NAME[o.role];
9862
- switch (o.action) {
9863
- case "created":
9864
- return `${label}: created (${o.agentId})`;
9865
- case "updated":
9866
- return `${label}: prompt updated from crew's local default`;
9867
- case "unchanged":
9868
- return `${label}: already in sync`;
9869
- case "diverged":
9870
- return `${label}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.agentId} in the app before syncing again`;
9871
- case "forced":
9872
- return `${label}: --force overwrote local edits (previous prompt was: ${JSON.stringify(o.previousPrompt ?? "")})`;
9873
- }
9874
- }
9875
- function describeCrewLink(o) {
9876
- const label = PERSONA_NAME[o.role];
9877
- switch (o.action) {
9878
- case "linked":
9879
- return `${label}: Crew row now references its Agents row`;
9880
- case "already_linked":
9881
- return `${label}: Crew row already references its Agents row`;
9882
- case "no_seat":
9883
- return `${label}: no seat configured for this role \u2014 skipped linking`;
9884
- case "persona_diverged":
9885
- return `${label}: persona has local edits and was not synced this pass \u2014 skipped linking until it's resolved`;
9886
- }
9887
- }
9888
-
9889
9965
  // src/connect.ts
9890
9966
  var ConnectHttpError = class extends Error {
9891
9967
  status;
@@ -11978,9 +12054,15 @@ var value = (name) => {
11978
12054
  const i = argv.indexOf(`--${name}`);
11979
12055
  return i >= 0 ? argv[i + 1] : void 0;
11980
12056
  };
11981
- var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
12057
+ var positional = positionalArgs(argv);
11982
12058
  var command = positional[0];
11983
12059
  if (!command) usage();
12060
+ function findSyncRoute(c, name) {
12061
+ if (!name && c.routes.length > 1) {
12062
+ throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
12063
+ }
12064
+ return findRoute(c, name);
12065
+ }
11984
12066
  function daemonConfigPaths(c) {
11985
12067
  const files = [c.configFile];
11986
12068
  for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
@@ -13381,7 +13463,7 @@ ${changed2.map((c) => ` - ${c}
13381
13463
  }
13382
13464
  let target;
13383
13465
  try {
13384
- target = findRoute(cfg, positional[2]);
13466
+ target = findSyncRoute(cfg, positional[2]);
13385
13467
  } catch (e) {
13386
13468
  if (e instanceof ConfigError) {
13387
13469
  process.stderr.write(`crew: ${e.message}
@@ -13406,6 +13488,8 @@ ${changed2.map((c) => ` - ${c}
13406
13488
  }
13407
13489
  throw e;
13408
13490
  }
13491
+ process.stdout.write(`route: ${target.route}
13492
+ `);
13409
13493
  for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
13410
13494
  `);
13411
13495
  for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
@@ -13448,7 +13532,7 @@ ${changed2.map((c) => ` - ${c}
13448
13532
  }
13449
13533
  let target;
13450
13534
  try {
13451
- target = findRoute(cfg, positional[2]);
13535
+ target = findSyncRoute(cfg, positional[2]);
13452
13536
  } catch (e) {
13453
13537
  if (e instanceof ConfigError) {
13454
13538
  process.stderr.write(`crew: ${e.message}
@@ -13473,6 +13557,8 @@ ${changed2.map((c) => ` - ${c}
13473
13557
  }
13474
13558
  throw e;
13475
13559
  }
13560
+ process.stdout.write(`route: ${target.route}
13561
+ `);
13476
13562
  for (const o of result.outcomes) process.stdout.write(`${describeSkillSyncOutcome(o)}
13477
13563
  `);
13478
13564
  if (!dryRun) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.39.0",
3
+ "version": "0.40.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",