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