@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.
- package/dist/cli.js +478 -437
- 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
|
|
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
|
|
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
|
|
7037
|
-
import { join as
|
|
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
|
|
7045
|
+
var PERSONA_NAME = {
|
|
7046
|
+
dev: "Developer",
|
|
7047
|
+
design: "Design",
|
|
7048
|
+
qa: "QA",
|
|
7049
|
+
triage: "Triage",
|
|
7050
|
+
pair: "Pair"
|
|
7041
7051
|
};
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
const
|
|
7048
|
-
|
|
7049
|
-
if (
|
|
7050
|
-
|
|
7051
|
-
|
|
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
|
|
7056
|
-
const
|
|
7057
|
-
if (!
|
|
7058
|
-
|
|
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
|
|
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
|
|
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
|
|
7181
|
+
let agentsModel;
|
|
7071
7182
|
try {
|
|
7072
|
-
|
|
7183
|
+
agentsModel = await client2.dataModels.get("agents", workspaceId);
|
|
7073
7184
|
} catch (e) {
|
|
7074
7185
|
if (e instanceof TablationApiError && e.status === 404) {
|
|
7075
|
-
throw new
|
|
7076
|
-
`workspace ${workspaceId} has no
|
|
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
|
|
7082
|
-
const
|
|
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
|
|
7086
|
-
for (const
|
|
7087
|
-
const
|
|
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({
|
|
7202
|
+
outcomes.push({ role, action: "created", agentId: "(dry run)" });
|
|
7091
7203
|
continue;
|
|
7092
7204
|
}
|
|
7093
|
-
const created = await client2.records.create(
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
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.
|
|
7103
|
-
|
|
7104
|
-
|
|
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
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
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({
|
|
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
|
-
|
|
7264
|
+
agentsModel.id,
|
|
7119
7265
|
row.id,
|
|
7120
|
-
{
|
|
7121
|
-
|
|
7266
|
+
{ prompt: defaultPrompt, ...promptSourceFields(agentsModel, defaultPrompt) },
|
|
7267
|
+
row.updated_at
|
|
7122
7268
|
);
|
|
7123
|
-
|
|
7124
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
|
7148
|
-
|
|
7149
|
-
const
|
|
7150
|
-
|
|
7151
|
-
|
|
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
|
|
7162
|
-
const
|
|
7163
|
-
const
|
|
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 =
|
|
7239
|
-
if (!
|
|
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 =
|
|
7243
|
-
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) &&
|
|
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 =
|
|
7601
|
+
const digestPath2 = join12(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
7247
7602
|
let digest;
|
|
7248
7603
|
let ageSeconds;
|
|
7249
|
-
if (
|
|
7604
|
+
if (existsSync11(digestPath2)) {
|
|
7250
7605
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
7251
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
7606
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync10(digestPath2, "utf8");
|
|
7252
7607
|
}
|
|
7253
|
-
const common = usesCommon ?
|
|
7254
|
-
const brief =
|
|
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 =
|
|
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:
|
|
7333
|
-
eventsPath:
|
|
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
|
|
7540
|
-
import { dirname as dirname7, join as
|
|
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
|
|
7545
|
-
import { join as
|
|
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 (
|
|
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 &&
|
|
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 =
|
|
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 || !
|
|
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
|
|
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
|
|
7887
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
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 ??
|
|
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 =
|
|
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
|
|
8005
|
-
import { dirname as dirname9, join as
|
|
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) =>
|
|
9018
|
+
const files = o.repo.release.versionFiles.map((f) => join16(o.cwd, f));
|
|
8664
9019
|
const first = files[0];
|
|
8665
|
-
if (!
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
8784
|
-
const dir =
|
|
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 =
|
|
8799
|
-
if (!
|
|
8800
|
-
mkdirSync8(dirname9(
|
|
8801
|
-
copyFileSync(src,
|
|
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
|
|
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 (!
|
|
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