@tablation/crew 0.50.2 → 0.51.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 +222 -51
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2288,6 +2288,7 @@ function parseResolved(raw, m, where) {
|
|
|
2288
2288
|
contract: raw.contract ?? void 0,
|
|
2289
2289
|
...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
|
|
2290
2290
|
...typeof raw.releaseAgentId === "string" && raw.releaseAgentId ? { releaseAgentId: raw.releaseAgentId } : {},
|
|
2291
|
+
...typeof raw.role === "string" && raw.role ? { role: raw.role } : {},
|
|
2291
2292
|
...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
|
|
2292
2293
|
...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
|
|
2293
2294
|
};
|
|
@@ -7724,6 +7725,25 @@ async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
|
|
|
7724
7725
|
return void 0;
|
|
7725
7726
|
}
|
|
7726
7727
|
}
|
|
7728
|
+
function isAdminRole(role) {
|
|
7729
|
+
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
7730
|
+
}
|
|
7731
|
+
async function fetchKeyRole(route, userAgent) {
|
|
7732
|
+
if (!route.resolved) return void 0;
|
|
7733
|
+
try {
|
|
7734
|
+
const res = await fetch(`${route.baseUrl.replace(/\/+$/, "")}/api/auth/me?workspaceId=${route.resolved.workspaceId}`, {
|
|
7735
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
7736
|
+
});
|
|
7737
|
+
if (!res.ok) return void 0;
|
|
7738
|
+
const me = await res.json();
|
|
7739
|
+
return typeof me.role === "string" ? me.role : void 0;
|
|
7740
|
+
} catch {
|
|
7741
|
+
return void 0;
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7744
|
+
function isForbidden(e) {
|
|
7745
|
+
return e instanceof TablationApiError && e.status === 403;
|
|
7746
|
+
}
|
|
7727
7747
|
function promptSourceFields(model, prompt) {
|
|
7728
7748
|
return model.fields?.some((f) => f.columnName === "prompt_baseline") ? { prompt_source: "crew", prompt_baseline: prompt } : {};
|
|
7729
7749
|
}
|
|
@@ -7767,6 +7787,16 @@ async function syncPersonas(route, opts) {
|
|
|
7767
7787
|
const name = PERSONA_NAME[role];
|
|
7768
7788
|
const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
|
|
7769
7789
|
const row = rows.find((r) => r.name === name);
|
|
7790
|
+
if (opts.readOnly) {
|
|
7791
|
+
if (!row) {
|
|
7792
|
+
outcomes.push({ role, action: "missing", agentId: "" });
|
|
7793
|
+
continue;
|
|
7794
|
+
}
|
|
7795
|
+
const prior = cache[role]?.agentId === row.id ? cache[role] : void 0;
|
|
7796
|
+
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: prior?.lastSyncedUpdatedAt ?? row.updated_at, ...prior?.historyId ? { historyId: prior.historyId } : {} };
|
|
7797
|
+
outcomes.push({ role, action: "adopted", agentId: row.id });
|
|
7798
|
+
continue;
|
|
7799
|
+
}
|
|
7770
7800
|
if (!row) {
|
|
7771
7801
|
if (opts.dryRun) {
|
|
7772
7802
|
outcomes.push({ role, action: "created", agentId: "(dry run)" });
|
|
@@ -7868,35 +7898,70 @@ async function syncPersonas(route, opts) {
|
|
|
7868
7898
|
throw e;
|
|
7869
7899
|
}
|
|
7870
7900
|
}
|
|
7871
|
-
const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent);
|
|
7872
|
-
const
|
|
7873
|
-
return {
|
|
7901
|
+
const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent, opts.readOnly === true);
|
|
7902
|
+
const release = opts.dryRun ? { agentId: route.resolved.releaseAgentId } : await linkReleaseSeat(route, client2, agentsModel, opts.userAgent, opts.readOnly === true);
|
|
7903
|
+
return {
|
|
7904
|
+
outcomes,
|
|
7905
|
+
agentPersonas,
|
|
7906
|
+
crewLinks,
|
|
7907
|
+
...release.agentId ? { releaseAgentId: release.agentId } : {},
|
|
7908
|
+
...release.note ? { releaseNote: release.note } : {},
|
|
7909
|
+
...opts.readOnly ? { readOnly: true } : {}
|
|
7910
|
+
};
|
|
7874
7911
|
}
|
|
7875
|
-
async function linkReleaseSeat(route, client2, agentsModel, userAgent) {
|
|
7912
|
+
async function linkReleaseSeat(route, client2, agentsModel, userAgent, readOnly) {
|
|
7876
7913
|
const seatId = route.resolved.releaseSeat;
|
|
7877
|
-
if (!seatId) return
|
|
7914
|
+
if (!seatId) return {};
|
|
7878
7915
|
const rows = await client2.records.list(agentsModel.id, { limit: 200 });
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7916
|
+
let agentId = rows.find((r) => r.name === RELEASE_AGENT_NAME)?.id;
|
|
7917
|
+
if (!agentId) {
|
|
7918
|
+
if (readOnly) return { note: `Release: no "${RELEASE_AGENT_NAME}" Agents row in this workspace \u2014 writes as the ship until a workspace admin runs \`crew agents sync\`` };
|
|
7919
|
+
try {
|
|
7920
|
+
agentId = (await client2.records.create(agentsModel.id, {
|
|
7921
|
+
name: RELEASE_AGENT_NAME,
|
|
7922
|
+
prompt: RELEASE_AGENT_PROMPT,
|
|
7923
|
+
...promptSourceFields(agentsModel, RELEASE_AGENT_PROMPT)
|
|
7924
|
+
})).id;
|
|
7925
|
+
} catch (e) {
|
|
7926
|
+
if (isForbidden(e)) return { note: `Release: could not create the "${RELEASE_AGENT_NAME}" Agents row (${NO_WRITE_NOTE}) \u2014 writes as the ship` };
|
|
7927
|
+
throw e;
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7884
7930
|
const crewModelId = route.resolved.models.crew;
|
|
7885
7931
|
const crewModel = await client2.dataModels.get(crewModelId);
|
|
7886
|
-
|
|
7932
|
+
let columnName;
|
|
7933
|
+
try {
|
|
7934
|
+
columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModel.id, userAgent);
|
|
7935
|
+
} catch (e) {
|
|
7936
|
+
if (e instanceof CrewAgentFieldMissing) return { note: `Release: ${CREW_FIELD_MISSING_NOTE}` };
|
|
7937
|
+
throw e;
|
|
7938
|
+
}
|
|
7887
7939
|
const seatRow = await client2.records.get(crewModelId, seatId);
|
|
7888
7940
|
if (seatRow[CREW_AGENT_COLUMN] !== agentId) await client2.records.update(crewModelId, seatId, { [columnName]: agentId });
|
|
7889
|
-
return agentId;
|
|
7941
|
+
return { agentId };
|
|
7890
7942
|
}
|
|
7891
|
-
|
|
7943
|
+
var NO_WRITE_NOTE = "this key has no write access to the Agents table";
|
|
7944
|
+
var CREW_FIELD_MISSING_NOTE = "Crew table has no Agent field yet \u2014 a workspace admin's `crew agents sync` adds it";
|
|
7945
|
+
var CrewAgentFieldMissing = class extends Error {
|
|
7946
|
+
};
|
|
7947
|
+
async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent, readOnly) {
|
|
7892
7948
|
const seats2 = route.resolved.seats;
|
|
7893
7949
|
const rolesWithSeats = ROLE_NAMES.filter((role) => seats2[role] && agentPersonas[role]);
|
|
7894
7950
|
if (rolesWithSeats.length === 0) {
|
|
7951
|
+
if (readOnly) return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agents_row" : "no_seat" }));
|
|
7895
7952
|
return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "persona_diverged" : "no_seat" }));
|
|
7896
7953
|
}
|
|
7897
7954
|
const crewModelId = route.resolved.models.crew;
|
|
7898
7955
|
const crewModel = await client2.dataModels.get(crewModelId);
|
|
7899
|
-
|
|
7956
|
+
let columnName;
|
|
7957
|
+
try {
|
|
7958
|
+
columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent);
|
|
7959
|
+
} catch (e) {
|
|
7960
|
+
if (e instanceof CrewAgentFieldMissing) {
|
|
7961
|
+
return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agent_field" : "no_seat" }));
|
|
7962
|
+
}
|
|
7963
|
+
throw e;
|
|
7964
|
+
}
|
|
7900
7965
|
const outcomes = [];
|
|
7901
7966
|
for (const role of ROLE_NAMES) {
|
|
7902
7967
|
const seatId = seats2[role];
|
|
@@ -7906,7 +7971,7 @@ async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userA
|
|
|
7906
7971
|
continue;
|
|
7907
7972
|
}
|
|
7908
7973
|
if (!agentId) {
|
|
7909
|
-
outcomes.push({ role, action: "persona_diverged" });
|
|
7974
|
+
outcomes.push({ role, action: readOnly ? "no_agents_row" : "persona_diverged" });
|
|
7910
7975
|
continue;
|
|
7911
7976
|
}
|
|
7912
7977
|
const seatRow = await client2.records.get(crewModelId, seatId);
|
|
@@ -7947,6 +8012,7 @@ async function ensureCrewAgentField(route, client2, crewModel, agentsModelId, us
|
|
|
7947
8012
|
description: "The Agents-table row backing this seat's persona \u2014 set by `crew agents sync` (ISSUE-416), never edited by hand."
|
|
7948
8013
|
})
|
|
7949
8014
|
});
|
|
8015
|
+
if (createRes.status === 403) throw new CrewAgentFieldMissing();
|
|
7950
8016
|
if (!createRes.ok) throw new AgentsSyncError(`creating the Crew table's Agent field failed: ${createRes.status} ${createRes.statusText}`);
|
|
7951
8017
|
const created = await createRes.json();
|
|
7952
8018
|
return created.columnName;
|
|
@@ -7964,6 +8030,10 @@ function describeSyncOutcome(o) {
|
|
|
7964
8030
|
return `${label}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.agentId} in the app before syncing again`;
|
|
7965
8031
|
case "overridden":
|
|
7966
8032
|
return `${label}: override in place \u2014 prompt not synced (clear "Customize prompt" on the Agents row to resume)`;
|
|
8033
|
+
case "adopted":
|
|
8034
|
+
return `${label}: using the workspace's existing Agents row (${o.agentId})`;
|
|
8035
|
+
case "missing":
|
|
8036
|
+
return `${label}: no Agents row in this workspace \u2014 writes as the ship until a workspace admin runs \`crew agents sync\``;
|
|
7967
8037
|
case "forced":
|
|
7968
8038
|
return `${label}: --force overwrote local edits (previous prompt was: ${JSON.stringify(o.previousPrompt ?? "")})`;
|
|
7969
8039
|
}
|
|
@@ -7977,6 +8047,10 @@ function describeCrewLink(o) {
|
|
|
7977
8047
|
return `${label}: Crew row already references its Agents row`;
|
|
7978
8048
|
case "no_seat":
|
|
7979
8049
|
return `${label}: no seat configured for this role \u2014 skipped linking`;
|
|
8050
|
+
case "no_agent_field":
|
|
8051
|
+
return `${label}: ${CREW_FIELD_MISSING_NOTE}`;
|
|
8052
|
+
case "no_agents_row":
|
|
8053
|
+
return `${label}: no Agents row to link yet \u2014 a workspace admin's \`crew agents sync\` creates it; run it here afterwards to link this seat`;
|
|
7980
8054
|
case "persona_diverged":
|
|
7981
8055
|
return `${label}: persona has local edits and was not synced this pass \u2014 skipped linking until it's resolved`;
|
|
7982
8056
|
}
|
|
@@ -11207,6 +11281,7 @@ async function discover(o) {
|
|
|
11207
11281
|
out.role = me.role;
|
|
11208
11282
|
out.meEmail = me.email;
|
|
11209
11283
|
out.meId = me.id;
|
|
11284
|
+
out.meName = me.name;
|
|
11210
11285
|
} catch {
|
|
11211
11286
|
}
|
|
11212
11287
|
if (o.project) {
|
|
@@ -11430,18 +11505,72 @@ async function discover(o) {
|
|
|
11430
11505
|
}
|
|
11431
11506
|
}
|
|
11432
11507
|
const seatIds = /* @__PURE__ */ new Set([...Object.values(out.seats), ...out.releaseSeat ? [out.releaseSeat] : []]);
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
|
|
11508
|
+
const holdRows = rows.filter((r) => !seatIds.has(r.id));
|
|
11509
|
+
out.holds = holdRows.map((r) => ({ id: r.id, name: r.name ?? "", email: r.email, user_id: r.user_id }));
|
|
11510
|
+
out.crewModelId = out.models.crew;
|
|
11511
|
+
if (!out.meId) {
|
|
11512
|
+
if (out.holds.length === 0) problems.push("no non-seat Crew rows \u2014 which row is the operator?");
|
|
11513
|
+
else {
|
|
11514
|
+
const byEmail = out.meEmail ? holdRows.filter((r) => eq(r.email, out.meEmail)) : [];
|
|
11515
|
+
if (byEmail.length === 1) out.operator = byEmail[0].id;
|
|
11516
|
+
else if (out.holds.length === 1) out.operator = out.holds[0].id;
|
|
11517
|
+
}
|
|
11518
|
+
} else {
|
|
11519
|
+
const candidates = holdRows.filter((r) => !r.user_id || r.user_id === out.meId);
|
|
11520
|
+
const mine = holdRows.find((r) => r.user_id === out.meId);
|
|
11521
|
+
const byEmail = out.meEmail ? candidates.filter((r) => eq(r.email, out.meEmail)) : [];
|
|
11522
|
+
const only = holdRows.length === 1 && candidates.length === 1 ? candidates[0] : void 0;
|
|
11523
|
+
const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
|
|
11524
|
+
if (pick) {
|
|
11525
|
+
out.operator = pick.id;
|
|
11526
|
+
if (pick.user_id !== out.meId && o.ship) await linkOperatorRow(o, out, pick);
|
|
11527
|
+
} else if (o.ship && (!o.interactive || candidates.length === 0)) {
|
|
11528
|
+
await createOperatorRow(o, out);
|
|
11529
|
+
}
|
|
11440
11530
|
}
|
|
11441
|
-
if (!out.operator && out.holds.length === 1) out.operator = out.holds[0].id;
|
|
11442
11531
|
}
|
|
11443
11532
|
return out;
|
|
11444
11533
|
}
|
|
11534
|
+
async function linkOperatorRow(o, found, row) {
|
|
11535
|
+
const name = row.name || row.id;
|
|
11536
|
+
if (!found.meId || !found.crewModelId) return;
|
|
11537
|
+
if (o.dryRun) {
|
|
11538
|
+
found.provisioning.push(`would link Crew row "${name}" to your account`);
|
|
11539
|
+
return;
|
|
11540
|
+
}
|
|
11541
|
+
const body = { user_id: found.meId };
|
|
11542
|
+
if (!row.email && found.meEmail) body.email = found.meEmail;
|
|
11543
|
+
try {
|
|
11544
|
+
await patch(o, `/data-models/${found.crewModelId}/records/${row.id}`, body);
|
|
11545
|
+
found.provisioning.push(`linked Crew row "${name}" to your account`);
|
|
11546
|
+
} catch (e) {
|
|
11547
|
+
if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
|
|
11548
|
+
found.provisioning.push(
|
|
11549
|
+
`could not link your Crew row (no write access) \u2014 ask a workspace admin to set user_id on "${name}"`
|
|
11550
|
+
);
|
|
11551
|
+
}
|
|
11552
|
+
}
|
|
11553
|
+
async function createOperatorRow(o, found) {
|
|
11554
|
+
if (!found.meId || !found.crewModelId) return;
|
|
11555
|
+
const name = found.meName || found.meEmail || "Operator";
|
|
11556
|
+
if (o.dryRun) {
|
|
11557
|
+
found.provisioning.push(`would create Crew row "${name}" for you`);
|
|
11558
|
+
return;
|
|
11559
|
+
}
|
|
11560
|
+
try {
|
|
11561
|
+
const created = await post2(o, `/data-models/${found.crewModelId}/records`, {
|
|
11562
|
+
name,
|
|
11563
|
+
...found.meEmail ? { email: found.meEmail } : {},
|
|
11564
|
+
user_id: found.meId
|
|
11565
|
+
});
|
|
11566
|
+
found.operator = created.id;
|
|
11567
|
+
found.holds.push({ id: created.id, name, email: found.meEmail, user_id: found.meId });
|
|
11568
|
+
found.provisioning.push(`created Crew row "${name}" for you`);
|
|
11569
|
+
} catch (e) {
|
|
11570
|
+
if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
|
|
11571
|
+
found.provisioning.push("could not create your Crew row (no write access) \u2014 ask a workspace admin to add one with your user_id");
|
|
11572
|
+
}
|
|
11573
|
+
}
|
|
11445
11574
|
function renderConnection(d, route, dir, opts = {}) {
|
|
11446
11575
|
const apiKeyFileLine = opts.keychainBacked ? ` # apiKey: resolved automatically from the OS keychain (crew connect) \u2014 no apiKeyFile needed` : opts.apiKeyFile ? ` apiKeyFile: "${opts.apiKeyFile}"` : ` apiKeyFile: "REPLACE \u2014 a file holding this workspace's key"`;
|
|
11447
11576
|
const apiKeyVarLine = opts.keychainBacked ? "" : opts.apiKeyVar ? `
|
|
@@ -11474,6 +11603,12 @@ import {
|
|
|
11474
11603
|
// src/seat-keys.ts
|
|
11475
11604
|
import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
11476
11605
|
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
11606
|
+
function keyIsAdmin(route) {
|
|
11607
|
+
const role = route.resolved?.role;
|
|
11608
|
+
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
11609
|
+
}
|
|
11610
|
+
var MEMBER_NO_ROW = "has no linked Agents row \u2014 a workspace admin's `crew agents sync` creates the persona rows; run it here afterwards to link this ship's seats";
|
|
11611
|
+
var MEMBER_NO_KEY = "no agent key for this seat \u2014 minting needs a workspace admin key";
|
|
11477
11612
|
function seatKeySlug(workspaceSlug, agentId) {
|
|
11478
11613
|
return `${workspaceSlug}#agent:${agentId}`;
|
|
11479
11614
|
}
|
|
@@ -11487,11 +11622,11 @@ function seatAgentId(route, role) {
|
|
|
11487
11622
|
async function resolveReleaseKey(route, getStore = getSessionStore2) {
|
|
11488
11623
|
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
|
|
11489
11624
|
const agentId = route.resolved?.releaseAgentId;
|
|
11490
|
-
if (!agentId) return ship("release seat has no linked Agents row (run `crew agents sync`)");
|
|
11625
|
+
if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
11491
11626
|
try {
|
|
11492
11627
|
const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
|
|
11493
11628
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
11494
|
-
return ship("no agent key stored for the release seat (run `crew agents sync`)");
|
|
11629
|
+
return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
11495
11630
|
} catch {
|
|
11496
11631
|
return ship("OS keychain unavailable");
|
|
11497
11632
|
}
|
|
@@ -11500,11 +11635,11 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
|
|
|
11500
11635
|
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
|
|
11501
11636
|
if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
|
|
11502
11637
|
const agentId = seatAgentId(route, role);
|
|
11503
|
-
if (!agentId) return ship("seat has no linked Agents row (run `crew agents sync`)");
|
|
11638
|
+
if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
11504
11639
|
try {
|
|
11505
11640
|
const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
|
|
11506
11641
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
11507
|
-
return ship("no agent key stored for this seat (run `crew agents sync`)");
|
|
11642
|
+
return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
11508
11643
|
} catch {
|
|
11509
11644
|
return ship("OS keychain unavailable");
|
|
11510
11645
|
}
|
|
@@ -13386,20 +13521,25 @@ function terminalPrompter() {
|
|
|
13386
13521
|
}
|
|
13387
13522
|
};
|
|
13388
13523
|
}
|
|
13389
|
-
async function pickOperator(holds2) {
|
|
13524
|
+
async function pickOperator(holds2, me) {
|
|
13525
|
+
const choices = holds2.filter((h) => !h.user_id || h.user_id === me.id);
|
|
13526
|
+
const canCreate = !!me.id;
|
|
13390
13527
|
process.stderr.write("\nWhich of these Crew rows is you?\n");
|
|
13391
|
-
|
|
13528
|
+
choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || h.id}
|
|
13392
13529
|
`));
|
|
13530
|
+
if (canCreate) process.stderr.write(` 0. Create a new Crew row for ${me.name ?? ""} ${me.email ?? ""}`.trimEnd() + "\n");
|
|
13393
13531
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
13394
13532
|
try {
|
|
13395
|
-
const
|
|
13533
|
+
const fallback = canCreate && choices.length === 0 ? "0" : "";
|
|
13534
|
+
const answer = (await rl.question(`Enter a number (blank to ${fallback ? "create one" : "skip"}): `)).trim() || fallback;
|
|
13396
13535
|
const n = Number(answer);
|
|
13397
|
-
if (!answer || !Number.isInteger(n) || n <
|
|
13398
|
-
return
|
|
13536
|
+
if (!answer || !Number.isInteger(n) || n < 0 || n > choices.length) return void 0;
|
|
13537
|
+
return n === 0 ? canCreate ? NEW_OPERATOR_ROW : void 0 : choices[n - 1].id;
|
|
13399
13538
|
} finally {
|
|
13400
13539
|
rl.close();
|
|
13401
13540
|
}
|
|
13402
13541
|
}
|
|
13542
|
+
var NEW_OPERATOR_ROW = "new";
|
|
13403
13543
|
async function pickResolvedStatuses(unrecognized) {
|
|
13404
13544
|
process.stderr.write(
|
|
13405
13545
|
"\nThis workspace has status values the default contract doesn't know the meaning of.\nFor each: does it mean the ticket is DONE \u2014 resolved/terminal (shipped, closed, won't-fix) \u2014 so a ticket blocked by it should stop being blocked?\n"
|
|
@@ -15024,9 +15164,18 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15024
15164
|
`);
|
|
15025
15165
|
process.exit(2);
|
|
15026
15166
|
}
|
|
15167
|
+
const keyRole = target.resolved.role ?? await fetchKeyRole(target, cfg.ship.userAgent);
|
|
15168
|
+
let readOnly = !isAdminRole(keyRole);
|
|
15027
15169
|
let personaResult;
|
|
15170
|
+
const runPersonas = (ro) => syncPersonas(target, { userAgent: cfg.ship.userAgent, dryRun, force, readOnly: ro });
|
|
15028
15171
|
try {
|
|
15029
|
-
|
|
15172
|
+
try {
|
|
15173
|
+
personaResult = await runPersonas(readOnly);
|
|
15174
|
+
} catch (e) {
|
|
15175
|
+
if (readOnly || !isForbidden(e)) throw e;
|
|
15176
|
+
readOnly = true;
|
|
15177
|
+
personaResult = await runPersonas(true);
|
|
15178
|
+
}
|
|
15030
15179
|
} catch (e) {
|
|
15031
15180
|
if (e instanceof AgentsSyncError) {
|
|
15032
15181
|
process.stderr.write(`crew agents sync: ${e.message}
|
|
@@ -15040,28 +15189,32 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15040
15189
|
for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
|
|
15041
15190
|
`);
|
|
15042
15191
|
for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
|
|
15192
|
+
`);
|
|
15193
|
+
if (personaResult.releaseNote) process.stdout.write(`${personaResult.releaseNote}
|
|
15043
15194
|
`);
|
|
15044
15195
|
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
15045
15196
|
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) process.stderr.write(`crew agents sync: warning: ${w}
|
|
15046
15197
|
`);
|
|
15047
15198
|
let skillResult;
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15199
|
+
if (!readOnly) {
|
|
15200
|
+
try {
|
|
15201
|
+
skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun, force });
|
|
15202
|
+
} catch (e) {
|
|
15203
|
+
if (isForbidden(e)) readOnly = true;
|
|
15204
|
+
else if (e instanceof SkillSyncError) {
|
|
15205
|
+
process.stderr.write(`crew agents sync: ${e.message}
|
|
15053
15206
|
`);
|
|
15054
|
-
|
|
15207
|
+
process.exit(2);
|
|
15208
|
+
} else throw e;
|
|
15055
15209
|
}
|
|
15056
|
-
throw e;
|
|
15057
15210
|
}
|
|
15058
|
-
for (const o of skillResult
|
|
15211
|
+
for (const o of skillResult?.outcomes ?? []) process.stdout.write(`${describeSkillSyncOutcome(o)}
|
|
15059
15212
|
`);
|
|
15060
15213
|
if (!dryRun) {
|
|
15061
15214
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15062
15215
|
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
|
|
15063
15216
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
15064
|
-
raw.agentSkills = skillResult.agentSkills;
|
|
15217
|
+
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15065
15218
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
15066
15219
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
15067
15220
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15071,11 +15224,16 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15071
15224
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
15072
15225
|
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
15073
15226
|
}
|
|
15074
|
-
|
|
15075
|
-
process.stdout.write(
|
|
15227
|
+
if (readOnly) {
|
|
15228
|
+
process.stdout.write("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)\n");
|
|
15229
|
+
process.stdout.write("seat keys: not minted (needs a workspace admin key)\n");
|
|
15230
|
+
} else {
|
|
15231
|
+
for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
|
|
15232
|
+
process.stdout.write(`${describeMintOutcome(o)}
|
|
15076
15233
|
`);
|
|
15234
|
+
}
|
|
15077
15235
|
}
|
|
15078
|
-
if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult
|
|
15236
|
+
if (personaResult.outcomes.some((o) => o.action === "diverged") || (skillResult?.outcomes.some((o) => o.action === "diverged") ?? false)) {
|
|
15079
15237
|
process.exitCode = 1;
|
|
15080
15238
|
}
|
|
15081
15239
|
break;
|
|
@@ -15444,11 +15602,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15444
15602
|
sshPublicKey
|
|
15445
15603
|
};
|
|
15446
15604
|
const runConnect = async (ws, proj) => {
|
|
15447
|
-
let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun });
|
|
15605
|
+
let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun, interactive: !!(process.stdin.isTTY && process.stdout.isTTY) });
|
|
15448
15606
|
if (!found.projectId && found.offerTemplateInstall && process.stdin.isTTY && process.stdout.isTTY) {
|
|
15449
15607
|
const installedSlug = await offerTemplateInstall(authOpts, found.workspaceId);
|
|
15450
15608
|
if (installedSlug) {
|
|
15451
|
-
found = await discover({ ...authOpts, workspace: ws, project: installedSlug, area: value("area"), ship, dryRun });
|
|
15609
|
+
found = await discover({ ...authOpts, workspace: ws, project: installedSlug, area: value("area"), ship, dryRun, interactive: !!(process.stdin.isTTY && process.stdout.isTTY) });
|
|
15452
15610
|
}
|
|
15453
15611
|
}
|
|
15454
15612
|
if (!found.projectId) {
|
|
@@ -15474,7 +15632,15 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15474
15632
|
}
|
|
15475
15633
|
let operator = found.operator;
|
|
15476
15634
|
if (!operator && found.holds.length > 0 && process.stdin.isTTY && process.stdout.isTTY) {
|
|
15477
|
-
|
|
15635
|
+
const picked = await pickOperator(found.holds, { id: found.meId, name: found.meName, email: found.meEmail });
|
|
15636
|
+
if (picked === NEW_OPERATOR_ROW) {
|
|
15637
|
+
await createOperatorRow({ ...authOpts, dryRun }, found);
|
|
15638
|
+
operator = found.operator;
|
|
15639
|
+
} else if (picked) {
|
|
15640
|
+
operator = picked;
|
|
15641
|
+
const row = found.holds.find((h) => h.id === picked);
|
|
15642
|
+
if (row && found.meId && row.user_id !== found.meId) await linkOperatorRow({ ...authOpts, dryRun }, found, row);
|
|
15643
|
+
}
|
|
15478
15644
|
}
|
|
15479
15645
|
let previous;
|
|
15480
15646
|
const hadPreviousFile = existsSync26(resolvedPath);
|
|
@@ -15524,6 +15690,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15524
15690
|
seats: found.seats,
|
|
15525
15691
|
holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
|
|
15526
15692
|
...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
|
|
15693
|
+
...found.role ?? previous?.role ? { role: found.role ?? previous?.role } : {},
|
|
15527
15694
|
...operator ? { operator } : {},
|
|
15528
15695
|
...contract ? { contract } : {},
|
|
15529
15696
|
...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
|
|
@@ -16461,16 +16628,20 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16461
16628
|
process.stdout.write(
|
|
16462
16629
|
route.resolved?.releaseSeat ? "release seat: this ship has its own Release Crew row\n" : "release seat: none \u2014 release comments are credited to the dev seat; run `crew connect` once the Crew table has this ship's row\n"
|
|
16463
16630
|
);
|
|
16631
|
+
const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
|
|
16632
|
+
const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
|
|
16633
|
+
process.stdout.write(`api key role: ${keyRole ?? "unknown"}
|
|
16634
|
+
`);
|
|
16464
16635
|
for (const role of SEAT_KEY_ROLES) {
|
|
16465
16636
|
if (!route.resolved?.seats[role]) continue;
|
|
16466
|
-
const id = await resolveSeatKey(
|
|
16637
|
+
const id = await resolveSeatKey(roleRoute, role);
|
|
16467
16638
|
process.stdout.write(
|
|
16468
16639
|
`seat identity: ${role} writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
|
|
16469
16640
|
`
|
|
16470
16641
|
);
|
|
16471
16642
|
}
|
|
16472
16643
|
if (route.resolved?.releaseSeat) {
|
|
16473
|
-
const id = await resolveReleaseKey(
|
|
16644
|
+
const id = await resolveReleaseKey(roleRoute);
|
|
16474
16645
|
process.stdout.write(
|
|
16475
16646
|
`seat identity: release writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
|
|
16476
16647
|
`
|
|
@@ -16511,7 +16682,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
16511
16682
|
process.exitCode = 1;
|
|
16512
16683
|
} else if (!row.user_id) {
|
|
16513
16684
|
process.stdout.write(
|
|
16514
|
-
`operator identity: ERROR Crew row "${row.name ?? op}" has no user_id \u2014 their UI comments will read "(no identity)";
|
|
16685
|
+
`operator identity: ERROR Crew row "${row.name ?? op}" has no user_id \u2014 their UI comments will read "(no identity)"; re-run \`crew connect\` to link it
|
|
16515
16686
|
`
|
|
16516
16687
|
);
|
|
16517
16688
|
process.exitCode = 1;
|
package/package.json
CHANGED