@tablation/crew 0.39.1 → 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 +478 -437
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6642,9 +6642,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
6642
6642
 
6643
6643
  // src/agent.ts
6644
6644
  import { spawn as spawn2 } from "child_process";
6645
- 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";
6646
6646
  import { finished } from "stream/promises";
6647
- import { join as join11, dirname as dirname6 } from "path";
6647
+ import { join as join12, dirname as dirname6 } from "path";
6648
6648
  import { createHash as createHash2 } from "crypto";
6649
6649
 
6650
6650
  // src/environment.ts
@@ -7033,33 +7033,144 @@ function expandSkillMarkers(text, lookup, opts = {}) {
7033
7033
  }
7034
7034
 
7035
7035
  // src/skills.ts
7036
- import { readFileSync as readFileSync8, readdirSync as readdirSync2, existsSync as existsSync9 } from "fs";
7037
- 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";
7038
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";
7039
7044
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
7040
- var SkillSyncError = class extends Error {
7045
+ var PERSONA_NAME = {
7046
+ dev: "Developer",
7047
+ design: "Design",
7048
+ qa: "QA",
7049
+ triage: "Triage",
7050
+ pair: "Pair"
7041
7051
  };
7042
- function parseSkillFile(path) {
7043
- const text = readFileSync8(path, "utf8");
7044
- const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
7045
- if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
7046
- const frontmatter = match[1];
7047
- const body = match[2];
7048
- const meta = parseYaml(frontmatter);
7049
- if (typeof meta.name !== "string" || !meta.name) throw new SkillSyncError(`${path}: frontmatter is missing "name"`);
7050
- if (typeof meta.description !== "string" || !meta.description) {
7051
- 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;
7052
7079
  }
7053
- return { name: meta.name, description: meta.description, prompt: body, file: path };
7054
7080
  }
7055
- function listLocalSkills(promptsDir) {
7056
- const dir = join10(promptsDir, "skills");
7057
- if (!existsSync9(dir)) return [];
7058
- 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;
7059
7106
  }
7060
- 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) {
7061
7172
  if (!route.resolved) {
7062
- 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`);
7063
7174
  }
7064
7175
  const workspaceId = route.resolved.workspaceId;
7065
7176
  const client2 = new TablationClient2({
@@ -7067,100 +7178,344 @@ async function syncSkills(route, opts) {
7067
7178
  apiKey: resolveApiKey(route),
7068
7179
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7069
7180
  });
7070
- let skillsModel;
7181
+ let agentsModel;
7071
7182
  try {
7072
- skillsModel = await client2.dataModels.get("agent_skills", workspaceId);
7183
+ agentsModel = await client2.dataModels.get("agents", workspaceId);
7073
7184
  } catch (e) {
7074
7185
  if (e instanceof TablationApiError && e.status === 404) {
7075
- throw new SkillSyncError(
7076
- `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`
7077
7188
  );
7078
7189
  }
7079
7190
  throw e;
7080
7191
  }
7081
- const local = listLocalSkills(route.promptsDir);
7082
- const rows = await client2.records.list(skillsModel.id, { limit: 200 });
7083
- const cache = route.resolved.agentSkills ?? {};
7192
+ const rows = await client2.records.list(agentsModel.id, { limit: 200 });
7193
+ const cache = route.resolved.agentPersonas ?? {};
7084
7194
  const outcomes = [];
7085
- const agentSkills = { ...cache };
7086
- for (const skill of local) {
7087
- 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);
7088
7200
  if (!row) {
7089
7201
  if (opts.dryRun) {
7090
- outcomes.push({ name: skill.name, action: "created", skillId: "(dry run)" });
7202
+ outcomes.push({ role, action: "created", agentId: "(dry run)" });
7091
7203
  continue;
7092
7204
  }
7093
- const created = await client2.records.create(skillsModel.id, {
7094
- name: skill.name,
7095
- description: skill.description,
7096
- prompt: skill.prompt
7097
- });
7098
- agentSkills[skill.name] = { skillId: created.id, lastSyncedUpdatedAt: created.updated_at };
7099
- 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 });
7100
7209
  continue;
7101
7210
  }
7102
- if (row.description === skill.description && row.prompt === skill.prompt) {
7103
- agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: row.updated_at };
7104
- 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 });
7105
7215
  continue;
7106
7216
  }
7107
- const knownBaseline = cache[skill.name]?.skillId === row.id ? cache[skill.name]?.lastSyncedUpdatedAt : void 0;
7108
- if (!knownBaseline) {
7109
- 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 });
7110
7256
  continue;
7111
7257
  }
7112
7258
  if (opts.dryRun) {
7113
- outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
7259
+ outcomes.push({ role, action: "updated", agentId: row.id });
7114
7260
  continue;
7115
7261
  }
7116
7262
  try {
7117
7263
  const updated = await client2.records.update(
7118
- skillsModel.id,
7264
+ agentsModel.id,
7119
7265
  row.id,
7120
- { description: skill.description, prompt: skill.prompt },
7121
- knownBaseline
7266
+ { prompt: defaultPrompt, ...promptSourceFields(agentsModel, defaultPrompt) },
7267
+ row.updated_at
7122
7268
  );
7123
- agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
7124
- 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 });
7125
7272
  } catch (e) {
7126
7273
  if (e instanceof StaleWriteError6) {
7127
- 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 });
7128
7280
  continue;
7129
7281
  }
7130
7282
  throw e;
7131
7283
  }
7132
7284
  }
7133
- return { outcomes, agentSkills };
7134
- }
7135
- function describeSkillSyncOutcome(o) {
7136
- switch (o.action) {
7137
- case "created":
7138
- return `${o.name}: created (${o.skillId})`;
7139
- case "updated":
7140
- return `${o.name}: prompt updated from crew's local file`;
7141
- case "unchanged":
7142
- return `${o.name}: already in sync`;
7143
- case "diverged":
7144
- 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`;
7145
- }
7285
+ const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent);
7286
+ return { outcomes, agentPersonas, crewLinks };
7146
7287
  }
7147
- async function fetchSkillOverrides(route, opts) {
7148
- if (!route.resolved) return {};
7149
- const client2 = new TablationClient2({
7150
- baseUrl: `${route.baseUrl}/api`,
7151
- apiKey: resolveApiKey(route),
7152
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7153
- });
7154
- let rows;
7155
- try {
7156
- const model = await client2.dataModels.get("agent_skills", route.resolved.workspaceId);
7157
- rows = await client2.records.list(model.id, { limit: 200 });
7158
- } catch {
7159
- return {};
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" }));
7160
7293
  }
7161
- const local = new Map(listLocalSkills(route.promptsDir).map((s) => [s.name, s]));
7162
- const cache = route.resolved.agentSkills ?? {};
7163
- const out = {};
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),
7507
+ headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7508
+ });
7509
+ let rows;
7510
+ try {
7511
+ const model = await client2.dataModels.get("agent_skills", route.resolved.workspaceId);
7512
+ rows = await client2.records.list(model.id, { limit: 200 });
7513
+ } catch {
7514
+ return {};
7515
+ }
7516
+ const local = new Map(listLocalSkills(route.promptsDir).map((s) => [s.name, s]));
7517
+ const cache = route.resolved.agentSkills ?? {};
7518
+ const out = {};
7164
7519
  for (const row of rows) {
7165
7520
  const name = row.name;
7166
7521
  if (row.prompt === local.get(name)?.prompt) continue;
@@ -7235,23 +7590,23 @@ var AgentError = class extends Error {
7235
7590
  var AgentSpawnError = class extends AgentError {
7236
7591
  };
7237
7592
  function planAgentRun(o) {
7238
- const briefPath = join11(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7239
- if (!existsSync10(briefPath)) {
7593
+ const briefPath = join12(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7594
+ if (!existsSync11(briefPath)) {
7240
7595
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
7241
7596
  }
7242
- const commonPath = join11(o.route.promptsDir, "personas", "common.md");
7243
- 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);
7244
7599
  const now = o.now ?? (() => Date.now());
7245
7600
  const routeLabel = routeSlug(o.route.route);
7246
- const digestPath2 = join11(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7601
+ const digestPath2 = join12(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7247
7602
  let digest;
7248
7603
  let ageSeconds;
7249
- if (existsSync10(digestPath2)) {
7604
+ if (existsSync11(digestPath2)) {
7250
7605
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
7251
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync9(digestPath2, "utf8");
7606
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync10(digestPath2, "utf8");
7252
7607
  }
7253
- const common = usesCommon ? readFileSync9(commonPath, "utf8") : "";
7254
- const brief = readFileSync9(briefPath, "utf8");
7608
+ const common = usesCommon ? readFileSync10(commonPath, "utf8") : "";
7609
+ const brief = readFileSync10(briefPath, "utf8");
7255
7610
  const effectiveCommon = o.divergedPrompt ? "" : common;
7256
7611
  const effectiveBrief = o.divergedPrompt ?? brief;
7257
7612
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -7273,7 +7628,7 @@ function planAgentRun(o) {
7273
7628
  digest
7274
7629
  });
7275
7630
  const promptSha = createHash2("sha256").update(expanded.text).digest("hex");
7276
- const streamsDir = join11(o.stateDir, "streams");
7631
+ const streamsDir = join12(o.stateDir, "streams");
7277
7632
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
7278
7633
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
7279
7634
  const { toolchain, model } = seat;
@@ -7329,8 +7684,8 @@ function planAgentRun(o) {
7329
7684
  digestAttached: digest !== void 0,
7330
7685
  digestAgeSeconds: ageSeconds,
7331
7686
  model,
7332
- streamPath: join11(streamsDir, `${base}.jsonl`),
7333
- eventsPath: join11(streamsDir, `${base}.events.jsonl`),
7687
+ streamPath: join12(streamsDir, `${base}.jsonl`),
7688
+ eventsPath: join12(streamsDir, `${base}.events.jsonl`),
7334
7689
  ticket: o.ticket,
7335
7690
  agentLog,
7336
7691
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -7536,13 +7891,13 @@ function spawnAgent(plan, emit) {
7536
7891
  }
7537
7892
 
7538
7893
  // src/run-worktree.ts
7539
- import { existsSync as existsSync12 } from "fs";
7540
- 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";
7541
7896
 
7542
7897
  // src/ports.ts
7543
7898
  import { execFileSync as execFileSync2 } from "child_process";
7544
- import { existsSync as existsSync11 } from "fs";
7545
- import { join as join12 } from "path";
7899
+ import { existsSync as existsSync12 } from "fs";
7900
+ import { join as join13 } from "path";
7546
7901
  var run = (cmd, args) => {
7547
7902
  try {
7548
7903
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7587,7 +7942,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7587
7942
  }
7588
7943
  function worktreeExists(parent, prefix, n) {
7589
7944
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7590
- if (existsSync11(join12(parent, name, ".git"))) return true;
7945
+ if (existsSync12(join13(parent, name, ".git"))) return true;
7591
7946
  }
7592
7947
  return false;
7593
7948
  }
@@ -7661,7 +8016,7 @@ function planRunWorktree(dir, cfg, t, role) {
7661
8016
  const existing = worktreeForNumber(dir, num);
7662
8017
  const located = locateBranchForTicket(dir, cfg, t, role);
7663
8018
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7664
- if (existing && !existing.prunable && existsSync12(existing.path)) {
8019
+ if (existing && !existing.prunable && existsSync13(existing.path)) {
7665
8020
  const branch = existing.branch;
7666
8021
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
7667
8022
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -7675,7 +8030,7 @@ function planRunWorktree(dir, cfg, t, role) {
7675
8030
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7676
8031
  return { action: "recut", path: existing.path, branch, base: baseRef };
7677
8032
  }
7678
- const path = join13(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8033
+ const path = join14(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7679
8034
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7680
8035
  return { action: "cut-remote", path, branch: located.branch };
7681
8036
  }
@@ -7722,7 +8077,7 @@ function worktreeHoldsUnpushed(path, remote) {
7722
8077
  }
7723
8078
  async function finishRunWorktree(dir, cfg, branch) {
7724
8079
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7725
- if (!wt || !existsSync12(wt.path)) return { kind: "none" };
8080
+ if (!wt || !existsSync13(wt.path)) return { kind: "none" };
7726
8081
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7727
8082
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7728
8083
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7734,7 +8089,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7734
8089
  if (pids.length) await killGently(pids);
7735
8090
  }
7736
8091
  gitOk(dir, ["worktree", "remove", wt.path]);
7737
- 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 };
7738
8093
  }
7739
8094
 
7740
8095
  // src/handoff-guard.ts
@@ -7883,13 +8238,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
7883
8238
  }
7884
8239
 
7885
8240
  // src/connect-wizard.ts
7886
- import { delimiter, dirname as dirname8, join as join14 } from "path";
7887
- 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";
7888
8243
  import { stringify } from "yaml";
7889
8244
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
7890
8245
  function shouldRunWizard(opts) {
7891
8246
  if (!opts.isTTY) return false;
7892
- const exists = opts.configExists ?? existsSync13;
8247
+ const exists = opts.configExists ?? existsSync14;
7893
8248
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
7894
8249
  }
7895
8250
  function firstRunConfigPath() {
@@ -7906,7 +8261,7 @@ function defaultIsExecutable(p) {
7906
8261
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
7907
8262
  for (const dir of pathEnv.split(delimiter)) {
7908
8263
  if (!dir) continue;
7909
- const candidate = join14(dir, name);
8264
+ const candidate = join15(dir, name);
7910
8265
  if (isExecutable(candidate)) return candidate;
7911
8266
  }
7912
8267
  return void 0;
@@ -8001,8 +8356,8 @@ Wrote ${o.configPath}.
8001
8356
  }
8002
8357
 
8003
8358
  // src/release-run.ts
8004
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync14, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
8005
- 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";
8006
8361
  import { tmpdir as tmpdir2 } from "os";
8007
8362
 
8008
8363
  // src/release.ts
@@ -8660,13 +9015,13 @@ async function bump(o, size) {
8660
9015
  }
8661
9016
  return printed;
8662
9017
  }
8663
- 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));
8664
9019
  const first = files[0];
8665
- if (!existsSync14(first)) {
9020
+ if (!existsSync15(first)) {
8666
9021
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
8667
9022
  return null;
8668
9023
  }
8669
- const current = JSON.parse(readFileSync10(first, "utf8")).version;
9024
+ const current = JSON.parse(readFileSync11(first, "utf8")).version;
8670
9025
  if (!current) {
8671
9026
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
8672
9027
  return null;
@@ -8679,7 +9034,7 @@ async function bump(o, size) {
8679
9034
  return version;
8680
9035
  }
8681
9036
  for (const f of files) {
8682
- const src = readFileSync10(f, "utf8");
9037
+ const src = readFileSync11(f, "utf8");
8683
9038
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
8684
9039
  if (out === src) {
8685
9040
  o.emit.error(`no version field in ${f}`);
@@ -8692,13 +9047,13 @@ async function bump(o, size) {
8692
9047
  function writeChangelog(o, version, entries) {
8693
9048
  const file = o.repo.release.changelog;
8694
9049
  if (!file || entries.length === 0) return;
8695
- const path = join15(o.cwd, file);
9050
+ const path = join16(o.cwd, file);
8696
9051
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
8697
9052
  if (o.dryRun) {
8698
9053
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
8699
9054
  return;
8700
9055
  }
8701
- const existing = existsSync14(path) ? readFileSync10(path, "utf8") : "# Changelog\n";
9056
+ const existing = existsSync15(path) ? readFileSync11(path, "utf8") : "# Changelog\n";
8702
9057
  writeFileSync9(path, insertChangelogSection(existing, section));
8703
9058
  }
8704
9059
  async function confirm(o, expected) {
@@ -8780,8 +9135,8 @@ async function confirmLastRelease(o, decision) {
8780
9135
  function openReleaseCheckout(o) {
8781
9136
  const { remote, base } = o.repo.branch;
8782
9137
  const branch = `crew/release-${Date.now()}`;
8783
- const root = mkdtempSync(join15(tmpdir2(), "crew-release-"));
8784
- const dir = join15(root, "checkout");
9138
+ const root = mkdtempSync(join16(tmpdir2(), "crew-release-"));
9139
+ const dir = join16(root, "checkout");
8785
9140
  const close = () => {
8786
9141
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
8787
9142
  rmSync6(root, { recursive: true, force: true });
@@ -8795,10 +9150,10 @@ function openReleaseCheckout(o) {
8795
9150
  throw e;
8796
9151
  }
8797
9152
  for (const f of o.repo.worktrees.copy) {
8798
- const src = join15(o.cwd, f);
8799
- if (!existsSync14(src)) continue;
8800
- mkdirSync8(dirname9(join15(dir, f)), { recursive: true });
8801
- 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));
8802
9157
  }
8803
9158
  return { dir, branch, close };
8804
9159
  }
@@ -9267,7 +9622,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
9267
9622
  }
9268
9623
 
9269
9624
  // src/watch.ts
9270
- 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";
9271
9626
  var E = "\x1B[";
9272
9627
  var dim = (s) => `${E}2m${s}${E}0m`;
9273
9628
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -9368,7 +9723,7 @@ function renderHeaders(states, now = Date.now()) {
9368
9723
  return states.map((s) => renderHeader(s, now)).join("");
9369
9724
  }
9370
9725
  function readFrom(file, from) {
9371
- if (!existsSync15(file)) return { events: [], offset: from };
9726
+ if (!existsSync16(file)) return { events: [], offset: from };
9372
9727
  const size = statSync6(file).size;
9373
9728
  if (size < from) return readFrom(file, 0);
9374
9729
  if (size === from) return { events: [], offset: from };
@@ -9607,320 +9962,6 @@ function nextRoles(decision, ran) {
9607
9962
  return decision.selection.pending.filter((r) => !ran.includes(r)).sort((a, b) => (decision.selection.ranks[a] ?? 0) - (decision.selection.ranks[b] ?? 0));
9608
9963
  }
9609
9964
 
9610
- // src/agents.ts
9611
- import { readFileSync as readFileSync11, existsSync as existsSync16 } from "fs";
9612
- import { join as join16 } from "path";
9613
- import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
9614
- var PERSONA_NAME = {
9615
- dev: "Developer",
9616
- design: "Design",
9617
- qa: "QA",
9618
- triage: "Triage",
9619
- pair: "Pair"
9620
- };
9621
- var CREW_AGENT_FIELD_NAME = "Agent";
9622
- var CREW_AGENT_COLUMN = "agent_id";
9623
- var AgentsSyncError = class extends Error {
9624
- };
9625
- function personaDefaultPrompt(promptsDir, role) {
9626
- const briefPath = join16(promptsDir, "personas", `lane-${role}.md`);
9627
- if (!existsSync16(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
9628
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync11(briefPath, "utf8");
9629
- const commonPath = join16(promptsDir, "personas", "common.md");
9630
- if (!existsSync16(commonPath)) return readFileSync11(briefPath, "utf8");
9631
- return `${readFileSync11(commonPath, "utf8")}${readFileSync11(briefPath, "utf8")}`;
9632
- }
9633
- async function resolveAgentId(route, role, opts) {
9634
- const cached = route.resolved?.agentPersonas?.[role]?.agentId;
9635
- if (cached) return cached;
9636
- const seatId = route.resolved?.seats?.[role];
9637
- if (!seatId || !route.resolved) return void 0;
9638
- const client2 = new TablationClient3({
9639
- baseUrl: `${route.baseUrl}/api`,
9640
- apiKey: resolveApiKey(route),
9641
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9642
- });
9643
- try {
9644
- const row = await client2.records.get(route.resolved.models.crew, seatId);
9645
- return row.agent_id ?? void 0;
9646
- } catch {
9647
- return void 0;
9648
- }
9649
- }
9650
- async function fetchDivergedPrompt(route, role, opts) {
9651
- const agentId = await resolveAgentId(route, role, opts);
9652
- if (!agentId || !route.resolved) return void 0;
9653
- const client2 = new TablationClient3({
9654
- baseUrl: `${route.baseUrl}/api`,
9655
- apiKey: resolveApiKey(route),
9656
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9657
- });
9658
- let row;
9659
- let agentsModelId;
9660
- try {
9661
- const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
9662
- agentsModelId = agentsModel.id;
9663
- row = await client2.records.get(agentsModelId, agentId);
9664
- } catch {
9665
- return void 0;
9666
- }
9667
- const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
9668
- if (row.prompt === defaultPrompt) return void 0;
9669
- const cached = route.resolved.agentPersonas?.[role];
9670
- if (cached?.agentId === agentId && cached.historyId) {
9671
- const lastSynced = await fetchLastSyncedPrompt(route, agentsModelId, agentId, cached.historyId, opts.userAgent);
9672
- if (lastSynced !== void 0 && lastSynced === row.prompt) return void 0;
9673
- }
9674
- return row.prompt;
9675
- }
9676
- async function fetchLastSyncedPrompt(route, agentsModelId, recordId, historyId, userAgent) {
9677
- try {
9678
- const entries = await fetchHistoryEntries(route, agentsModelId, recordId, userAgent);
9679
- return entries.find((e) => e.history_id === historyId)?.prompt;
9680
- } catch {
9681
- return void 0;
9682
- }
9683
- }
9684
- function choiceValue(v) {
9685
- const raw = typeof v === "string" ? v : v && typeof v === "object" ? v.value ?? v.key : void 0;
9686
- return typeof raw === "string" && raw.trim() ? raw.trim().toLowerCase() : void 0;
9687
- }
9688
- async function fetchSeatPersona(route, role, opts) {
9689
- const agentId = await resolveAgentId(route, role, opts);
9690
- if (!agentId || !route.resolved) return void 0;
9691
- const client2 = new TablationClient3({
9692
- baseUrl: `${route.baseUrl}/api`,
9693
- apiKey: resolveApiKey(route),
9694
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9695
- });
9696
- try {
9697
- const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
9698
- const row = await client2.records.get(agentsModel.id, agentId);
9699
- const tier = choiceValue(row.tier);
9700
- const vendor = choiceValue(row.vendor);
9701
- const persona = {
9702
- ...row.model ? { model: row.model } : {},
9703
- ...tier && TIERS.includes(tier) ? { tier } : {},
9704
- ...vendor ? { vendor } : {}
9705
- };
9706
- return Object.keys(persona).length > 0 ? persona : void 0;
9707
- } catch {
9708
- return void 0;
9709
- }
9710
- }
9711
- async function currentPersonaPrompt(route, role, opts) {
9712
- const diverged = await fetchDivergedPrompt(route, role, opts);
9713
- return diverged ?? personaDefaultPrompt(route.promptsDir, role);
9714
- }
9715
- async function fetchHistoryEntries(route, agentsModelId, recordId, userAgent, limit = 50) {
9716
- const url = `${route.baseUrl.replace(/\/+$/, "")}/api/data-models/${agentsModelId}/records/${recordId}/history?limit=${limit}`;
9717
- const res = await fetch(url, {
9718
- headers: {
9719
- Authorization: `Bearer ${resolveApiKey(route)}`,
9720
- "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
9721
- }
9722
- });
9723
- if (!res.ok) throw new AgentsSyncError(`history lookup for ${recordId}: ${res.status} ${res.statusText}`);
9724
- return await res.json();
9725
- }
9726
- async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
9727
- try {
9728
- const entries = await fetchHistoryEntries(route, agentsModelId, recordId, userAgent, 1);
9729
- return entries[0]?.history_id;
9730
- } catch {
9731
- return void 0;
9732
- }
9733
- }
9734
- async function syncPersonas(route, opts) {
9735
- if (!route.resolved) {
9736
- throw new AgentsSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
9737
- }
9738
- const workspaceId = route.resolved.workspaceId;
9739
- const client2 = new TablationClient3({
9740
- baseUrl: `${route.baseUrl}/api`,
9741
- apiKey: resolveApiKey(route),
9742
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9743
- });
9744
- let agentsModel;
9745
- try {
9746
- agentsModel = await client2.dataModels.get("agents", workspaceId);
9747
- } catch (e) {
9748
- if (e instanceof TablationApiError2 && e.status === 404) {
9749
- throw new AgentsSyncError(
9750
- `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`
9751
- );
9752
- }
9753
- throw e;
9754
- }
9755
- const rows = await client2.records.list(agentsModel.id, { limit: 200 });
9756
- const cache = route.resolved.agentPersonas ?? {};
9757
- const outcomes = [];
9758
- const agentPersonas = { ...cache };
9759
- for (const role of ROLE_NAMES) {
9760
- const name = PERSONA_NAME[role];
9761
- const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
9762
- const row = rows.find((r) => r.name === name);
9763
- if (!row) {
9764
- if (opts.dryRun) {
9765
- outcomes.push({ role, action: "created", agentId: "(dry run)" });
9766
- continue;
9767
- }
9768
- const created = await client2.records.create(agentsModel.id, { name, prompt: defaultPrompt });
9769
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, created.id, opts.userAgent);
9770
- agentPersonas[role] = { agentId: created.id, lastSyncedUpdatedAt: created.updated_at, ...historyId ? { historyId } : {} };
9771
- outcomes.push({ role, action: "created", agentId: created.id });
9772
- continue;
9773
- }
9774
- if (row.prompt === defaultPrompt) {
9775
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
9776
- agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, ...historyId ? { historyId } : {} };
9777
- outcomes.push({ role, action: "unchanged", agentId: row.id });
9778
- continue;
9779
- }
9780
- if (opts.force) {
9781
- if (opts.dryRun) {
9782
- outcomes.push({ role, action: "forced", agentId: row.id, previousPrompt: row.prompt });
9783
- continue;
9784
- }
9785
- const updated = await client2.records.update(agentsModel.id, row.id, { prompt: defaultPrompt });
9786
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
9787
- agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
9788
- outcomes.push({ role, action: "forced", agentId: row.id, previousPrompt: row.prompt });
9789
- continue;
9790
- }
9791
- const cached = cache[role]?.agentId === row.id ? cache[role] : void 0;
9792
- if (!cached?.historyId) {
9793
- outcomes.push({ role, action: "diverged", agentId: row.id });
9794
- continue;
9795
- }
9796
- let lastSyncedPrompt;
9797
- try {
9798
- const entries = await fetchHistoryEntries(route, agentsModel.id, row.id, opts.userAgent);
9799
- lastSyncedPrompt = entries.find((e) => e.history_id === cached.historyId)?.prompt;
9800
- } catch {
9801
- lastSyncedPrompt = void 0;
9802
- }
9803
- if (lastSyncedPrompt === void 0 || lastSyncedPrompt !== row.prompt) {
9804
- outcomes.push({ role, action: "diverged", agentId: row.id });
9805
- continue;
9806
- }
9807
- if (opts.dryRun) {
9808
- outcomes.push({ role, action: "updated", agentId: row.id });
9809
- continue;
9810
- }
9811
- try {
9812
- const updated = await client2.records.update(
9813
- agentsModel.id,
9814
- row.id,
9815
- { prompt: defaultPrompt },
9816
- row.updated_at
9817
- );
9818
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
9819
- agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
9820
- outcomes.push({ role, action: "updated", agentId: row.id });
9821
- } catch (e) {
9822
- if (e instanceof StaleWriteError7) {
9823
- outcomes.push({ role, action: "diverged", agentId: row.id });
9824
- continue;
9825
- }
9826
- throw e;
9827
- }
9828
- }
9829
- const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent);
9830
- return { outcomes, agentPersonas, crewLinks };
9831
- }
9832
- async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent) {
9833
- const seats2 = route.resolved.seats;
9834
- const rolesWithSeats = ROLE_NAMES.filter((role) => seats2[role] && agentPersonas[role]);
9835
- if (rolesWithSeats.length === 0) {
9836
- return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "persona_diverged" : "no_seat" }));
9837
- }
9838
- const crewModelId = route.resolved.models.crew;
9839
- const crewModel = await client2.dataModels.get(crewModelId);
9840
- const columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent);
9841
- const outcomes = [];
9842
- for (const role of ROLE_NAMES) {
9843
- const seatId = seats2[role];
9844
- const agentId = agentPersonas[role]?.agentId;
9845
- if (!seatId) {
9846
- outcomes.push({ role, action: "no_seat" });
9847
- continue;
9848
- }
9849
- if (!agentId) {
9850
- outcomes.push({ role, action: "persona_diverged" });
9851
- continue;
9852
- }
9853
- const seatRow = await client2.records.get(crewModelId, seatId);
9854
- if (seatRow[CREW_AGENT_COLUMN] === agentId) {
9855
- outcomes.push({ role, action: "already_linked" });
9856
- continue;
9857
- }
9858
- await client2.records.update(crewModelId, seatId, { [columnName]: agentId });
9859
- outcomes.push({ role, action: "linked" });
9860
- }
9861
- return outcomes;
9862
- }
9863
- async function ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent) {
9864
- const existing = crewModel.fields.find(
9865
- (f) => f.fieldType?.kind === "REFERENCE" && f.fieldType.targetModelId === agentsModelId && f.fieldType.isMultiple === false
9866
- );
9867
- if (existing) return existing.columnName;
9868
- const workspaceId = route.resolved.workspaceId;
9869
- const baseUrl = route.baseUrl.replace(/\/+$/, "");
9870
- const apiKey = resolveApiKey(route);
9871
- const headers = { Authorization: `Bearer ${apiKey}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" };
9872
- const typesRes = await fetch(`${baseUrl}/api/workspaces/${workspaceId}/field-types`, { headers });
9873
- if (!typesRes.ok) throw new AgentsSyncError(`field-types lookup failed: ${typesRes.status} ${typesRes.statusText}`);
9874
- const types = await typesRes.json();
9875
- const fieldType = types.find((t) => t.kind === "REFERENCE" && t.targetModelId === agentsModelId && t.isMultiple === false);
9876
- if (!fieldType) {
9877
- throw new AgentsSyncError(
9878
- `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`
9879
- );
9880
- }
9881
- const createRes = await fetch(`${baseUrl}/api/data-models/${crewModel.id}/fields`, {
9882
- method: "POST",
9883
- headers: { ...headers, "Content-Type": "application/json" },
9884
- body: JSON.stringify({
9885
- name: CREW_AGENT_FIELD_NAME,
9886
- columnName: CREW_AGENT_COLUMN,
9887
- fieldTypeId: fieldType.id,
9888
- description: "The Agents-table row backing this seat's persona \u2014 set by `crew agents sync` (ISSUE-416), never edited by hand."
9889
- })
9890
- });
9891
- if (!createRes.ok) throw new AgentsSyncError(`creating the Crew table's Agent field failed: ${createRes.status} ${createRes.statusText}`);
9892
- const created = await createRes.json();
9893
- return created.columnName;
9894
- }
9895
- function describeSyncOutcome(o) {
9896
- const label = PERSONA_NAME[o.role];
9897
- switch (o.action) {
9898
- case "created":
9899
- return `${label}: created (${o.agentId})`;
9900
- case "updated":
9901
- return `${label}: prompt updated from crew's local default`;
9902
- case "unchanged":
9903
- return `${label}: already in sync`;
9904
- case "diverged":
9905
- return `${label}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.agentId} in the app before syncing again`;
9906
- case "forced":
9907
- return `${label}: --force overwrote local edits (previous prompt was: ${JSON.stringify(o.previousPrompt ?? "")})`;
9908
- }
9909
- }
9910
- function describeCrewLink(o) {
9911
- const label = PERSONA_NAME[o.role];
9912
- switch (o.action) {
9913
- case "linked":
9914
- return `${label}: Crew row now references its Agents row`;
9915
- case "already_linked":
9916
- return `${label}: Crew row already references its Agents row`;
9917
- case "no_seat":
9918
- return `${label}: no seat configured for this role \u2014 skipped linking`;
9919
- case "persona_diverged":
9920
- return `${label}: persona has local edits and was not synced this pass \u2014 skipped linking until it's resolved`;
9921
- }
9922
- }
9923
-
9924
9965
  // src/connect.ts
9925
9966
  var ConnectHttpError = class extends Error {
9926
9967
  status;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.39.1",
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",