@tablation/crew 0.37.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +38 -10
  2. package/dist/cli.js +274 -175
  3. package/package.json +1 -1
  4. package/prompts/default/personas/lane-design.md +63 -96
  5. package/prompts/default/personas/lane-dev.md +15 -15
  6. package/prompts/default/personas/lane-pair.md +64 -84
  7. package/prompts/default/personas/lane-qa.md +127 -158
  8. package/prompts/default/personas/lane-triage.md +79 -64
  9. package/prompts/default/skills/crew-lane-agent.md +479 -0
  10. package/prompts/default/skills/design-pass.md +53 -0
  11. package/prompts/default/skills/filing-a-ticket.md +56 -0
  12. package/prompts/default/skills/question-and-investigation.md +59 -0
  13. package/prompts/default/skills/running-the-suite.md +56 -0
  14. package/prompts/default/skills/ticket-attachments.md +34 -0
  15. package/prompts/default/skills/workspace-change-authorization.md +35 -0
  16. package/prompts/default/skills/worktree-setup.md +101 -0
  17. package/prompts/dev-qa/personas/lane-design.md +2 -0
  18. package/prompts/dev-qa/personas/lane-dev.md +15 -14
  19. package/prompts/dev-qa/personas/lane-pair.md +64 -84
  20. package/prompts/dev-qa/personas/lane-qa.md +126 -144
  21. package/prompts/dev-qa/personas/lane-triage.md +69 -56
  22. package/prompts/dev-qa/skills/crew-lane-agent.md +473 -0
  23. package/prompts/dev-qa/skills/filing-a-ticket.md +56 -0
  24. package/prompts/dev-qa/skills/question-and-investigation.md +59 -0
  25. package/prompts/dev-qa/skills/running-the-suite.md +56 -0
  26. package/prompts/dev-qa/skills/ticket-attachments.md +34 -0
  27. package/prompts/dev-qa/skills/workspace-change-authorization.md +35 -0
  28. package/prompts/dev-qa/skills/worktree-setup.md +101 -0
  29. package/prompts/default/personas/common.md +0 -903
  30. package/prompts/dev-qa/personas/common.md +0 -874
package/dist/cli.js CHANGED
@@ -1806,6 +1806,7 @@ var DEFAULT_BASE_URL = "https://app.tablation.com";
1806
1806
  var DEFAULT_MAX_THINKING_TOKENS = 4096;
1807
1807
  var DEFAULT_REPOS_BASE_PATH = "~/Crew";
1808
1808
  var ROLE_NAMES = ["dev", "design", "qa", "triage", "pair"];
1809
+ var SELF_CONTAINED_ROLES = /* @__PURE__ */ new Set(["pair", "triage"]);
1809
1810
  var ROLE_LABEL = {
1810
1811
  dev: "Dev",
1811
1812
  design: "Design",
@@ -6606,9 +6607,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
6606
6607
 
6607
6608
  // src/agent.ts
6608
6609
  import { spawn as spawn2 } from "child_process";
6609
- import { readFileSync as readFileSync8, existsSync as existsSync9, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6610
+ import { readFileSync as readFileSync9, existsSync as existsSync10, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6610
6611
  import { finished } from "stream/promises";
6611
- import { join as join10, dirname as dirname6 } from "path";
6612
+ import { join as join11, dirname as dirname6 } from "path";
6612
6613
  import { createHash as createHash2 } from "crypto";
6613
6614
 
6614
6615
  // src/environment.ts
@@ -6952,6 +6953,199 @@ async function reportAgentRun(target, run2) {
6952
6953
  return { agentLogId: entry.id };
6953
6954
  }
6954
6955
 
6956
+ // src/skill-include.ts
6957
+ var SkillIncludeError = class extends Error {
6958
+ };
6959
+ var MAX_INCLUDE_DEPTH = 3;
6960
+ var MAX_INCLUDED_BYTES = 2e5;
6961
+ var MARKER = /\{\{[ \t]*skill:[ \t]*([^{}\r\n]*?)[ \t]*\}\}/g;
6962
+ function skillMarkerNames(text) {
6963
+ const names = [];
6964
+ for (const m of text.matchAll(MARKER)) {
6965
+ const name = m[1].trim();
6966
+ if (name && !names.includes(name)) names.push(name);
6967
+ }
6968
+ return names;
6969
+ }
6970
+ function expandSkillMarkers(text, lookup, opts = {}) {
6971
+ const maxDepth = opts.maxDepth ?? MAX_INCLUDE_DEPTH;
6972
+ const maxBytes = opts.maxBytes ?? MAX_INCLUDED_BYTES;
6973
+ const warnings = [];
6974
+ let included = 0;
6975
+ const expand2 = (source, chain) => source.replace(MARKER, (literal, rawName) => {
6976
+ const name = rawName.trim();
6977
+ if (!name) return literal;
6978
+ if (chain.includes(name)) {
6979
+ warnings.push(`skill include "${name}" left literal: it includes itself (${[...chain, name].join(" \u2192 ")})`);
6980
+ return literal;
6981
+ }
6982
+ if (chain.length >= maxDepth) {
6983
+ warnings.push(`skill include "${name}" left literal: nested deeper than ${maxDepth} levels`);
6984
+ return literal;
6985
+ }
6986
+ const body = lookup(name);
6987
+ if (body === void 0) throw new SkillIncludeError(`no skill named "${name}" for the include ${literal}`);
6988
+ const trimmed = body.replace(/^\n+/, "").replace(/\n+$/, "");
6989
+ const bytes = Buffer.byteLength(trimmed, "utf8");
6990
+ if (included + bytes > maxBytes) {
6991
+ warnings.push(`skill include "${name}" left literal: it would exceed the ${maxBytes}-byte include budget`);
6992
+ return literal;
6993
+ }
6994
+ included += bytes;
6995
+ return expand2(trimmed, [...chain, name]);
6996
+ });
6997
+ return { text: expand2(text, []), warnings };
6998
+ }
6999
+
7000
+ // src/skills.ts
7001
+ import { readFileSync as readFileSync8, readdirSync as readdirSync2, existsSync as existsSync9 } from "fs";
7002
+ import { join as join10 } from "path";
7003
+ import { parse as parseYaml } from "yaml";
7004
+ import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
7005
+ var SkillSyncError = class extends Error {
7006
+ };
7007
+ function parseSkillFile(path) {
7008
+ const text = readFileSync8(path, "utf8");
7009
+ const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
7010
+ if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
7011
+ const frontmatter = match[1];
7012
+ const body = match[2];
7013
+ const meta = parseYaml(frontmatter);
7014
+ if (typeof meta.name !== "string" || !meta.name) throw new SkillSyncError(`${path}: frontmatter is missing "name"`);
7015
+ if (typeof meta.description !== "string" || !meta.description) {
7016
+ throw new SkillSyncError(`${path}: frontmatter is missing "description"`);
7017
+ }
7018
+ return { name: meta.name, description: meta.description, prompt: body, file: path };
7019
+ }
7020
+ function listLocalSkills(promptsDir) {
7021
+ const dir = join10(promptsDir, "skills");
7022
+ if (!existsSync9(dir)) return [];
7023
+ return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join10(dir, f)));
7024
+ }
7025
+ async function syncSkills(route, opts) {
7026
+ if (!route.resolved) {
7027
+ throw new SkillSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
7028
+ }
7029
+ const workspaceId = route.resolved.workspaceId;
7030
+ const client2 = new TablationClient2({
7031
+ baseUrl: `${route.baseUrl}/api`,
7032
+ apiKey: resolveApiKey(route),
7033
+ headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7034
+ });
7035
+ let skillsModel;
7036
+ try {
7037
+ skillsModel = await client2.dataModels.get("agent_skills", workspaceId);
7038
+ } catch (e) {
7039
+ if (e instanceof TablationApiError && e.status === 404) {
7040
+ throw new SkillSyncError(
7041
+ `workspace ${workspaceId} has no Agent Skills table yet \u2014 every workspace auto-provisions one; an older workspace may still need its one-time backfill`
7042
+ );
7043
+ }
7044
+ throw e;
7045
+ }
7046
+ const local = listLocalSkills(route.promptsDir);
7047
+ const rows = await client2.records.list(skillsModel.id, { limit: 200 });
7048
+ const cache = route.resolved.agentSkills ?? {};
7049
+ const outcomes = [];
7050
+ const agentSkills = { ...cache };
7051
+ for (const skill of local) {
7052
+ const row = rows.find((r) => r.name === skill.name);
7053
+ if (!row) {
7054
+ if (opts.dryRun) {
7055
+ outcomes.push({ name: skill.name, action: "created", skillId: "(dry run)" });
7056
+ continue;
7057
+ }
7058
+ const created = await client2.records.create(skillsModel.id, {
7059
+ name: skill.name,
7060
+ description: skill.description,
7061
+ prompt: skill.prompt
7062
+ });
7063
+ agentSkills[skill.name] = { skillId: created.id, lastSyncedUpdatedAt: created.updated_at };
7064
+ outcomes.push({ name: skill.name, action: "created", skillId: created.id });
7065
+ continue;
7066
+ }
7067
+ if (row.description === skill.description && row.prompt === skill.prompt) {
7068
+ agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: row.updated_at };
7069
+ outcomes.push({ name: skill.name, action: "unchanged", skillId: row.id });
7070
+ continue;
7071
+ }
7072
+ const knownBaseline = cache[skill.name]?.skillId === row.id ? cache[skill.name]?.lastSyncedUpdatedAt : void 0;
7073
+ if (!knownBaseline) {
7074
+ outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
7075
+ continue;
7076
+ }
7077
+ if (opts.dryRun) {
7078
+ outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
7079
+ continue;
7080
+ }
7081
+ try {
7082
+ const updated = await client2.records.update(
7083
+ skillsModel.id,
7084
+ row.id,
7085
+ { description: skill.description, prompt: skill.prompt },
7086
+ knownBaseline
7087
+ );
7088
+ agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
7089
+ outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
7090
+ } catch (e) {
7091
+ if (e instanceof StaleWriteError6) {
7092
+ outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
7093
+ continue;
7094
+ }
7095
+ throw e;
7096
+ }
7097
+ }
7098
+ return { outcomes, agentSkills };
7099
+ }
7100
+ function describeSkillSyncOutcome(o) {
7101
+ switch (o.action) {
7102
+ case "created":
7103
+ return `${o.name}: created (${o.skillId})`;
7104
+ case "updated":
7105
+ return `${o.name}: prompt updated from crew's local file`;
7106
+ case "unchanged":
7107
+ return `${o.name}: already in sync`;
7108
+ case "diverged":
7109
+ return `${o.name}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.skillId} in the app before syncing again`;
7110
+ }
7111
+ }
7112
+ async function fetchSkillOverrides(route, opts) {
7113
+ if (!route.resolved) return {};
7114
+ const client2 = new TablationClient2({
7115
+ baseUrl: `${route.baseUrl}/api`,
7116
+ apiKey: resolveApiKey(route),
7117
+ headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7118
+ });
7119
+ let rows;
7120
+ try {
7121
+ const model = await client2.dataModels.get("agent_skills", route.resolved.workspaceId);
7122
+ rows = await client2.records.list(model.id, { limit: 200 });
7123
+ } catch {
7124
+ return {};
7125
+ }
7126
+ const local = new Map(listLocalSkills(route.promptsDir).map((s) => [s.name, s]));
7127
+ const cache = route.resolved.agentSkills ?? {};
7128
+ const out = {};
7129
+ for (const row of rows) {
7130
+ const name = row.name;
7131
+ if (row.prompt === local.get(name)?.prompt) continue;
7132
+ const cached = cache[name];
7133
+ if (cached?.skillId === row.id && cached.lastSyncedUpdatedAt === row.updated_at) continue;
7134
+ out[name] = row.prompt;
7135
+ }
7136
+ return out;
7137
+ }
7138
+ function skillMarkerWarnings(promptsDir, personaPrompts) {
7139
+ const shipped = new Set(listLocalSkills(promptsDir).map((s) => s.name));
7140
+ const out = [];
7141
+ for (const [role, text] of Object.entries(personaPrompts)) {
7142
+ for (const name of skillMarkerNames(text ?? "")) {
7143
+ if (!shipped.has(name)) out.push(`${role}: brief includes {{skill: ${name}}} but this prompt set ships no skill with that name`);
7144
+ }
7145
+ }
7146
+ return out;
7147
+ }
7148
+
6955
7149
  // src/ticket-attribution.ts
6956
7150
  var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
6957
7151
  function ticketMentions(text) {
@@ -7006,34 +7200,45 @@ var AgentError = class extends Error {
7006
7200
  var AgentSpawnError = class extends AgentError {
7007
7201
  };
7008
7202
  function planAgentRun(o) {
7009
- const briefPath = join10(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7010
- if (!existsSync9(briefPath)) {
7203
+ const briefPath = join11(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7204
+ if (!existsSync10(briefPath)) {
7011
7205
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
7012
7206
  }
7013
- const commonPath = join10(o.route.promptsDir, "personas", "common.md");
7014
- if (!existsSync9(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
7207
+ const commonPath = join11(o.route.promptsDir, "personas", "common.md");
7208
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync10(commonPath);
7015
7209
  const now = o.now ?? (() => Date.now());
7016
7210
  const routeLabel = routeSlug(o.route.route);
7017
- const digestPath2 = join10(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7211
+ const digestPath2 = join11(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7018
7212
  let digest;
7019
7213
  let ageSeconds;
7020
- if (existsSync9(digestPath2)) {
7214
+ if (existsSync10(digestPath2)) {
7021
7215
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
7022
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
7216
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync9(digestPath2, "utf8");
7023
7217
  }
7024
- const common = readFileSync8(commonPath, "utf8");
7025
- const brief = readFileSync8(briefPath, "utf8");
7218
+ const common = usesCommon ? readFileSync9(commonPath, "utf8") : "";
7219
+ const brief = readFileSync9(briefPath, "utf8");
7026
7220
  const effectiveCommon = o.divergedPrompt ? "" : common;
7027
7221
  const effectiveBrief = o.divergedPrompt ?? brief;
7222
+ const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
7223
+ let expanded;
7224
+ try {
7225
+ expanded = expandSkillMarkers(
7226
+ `${effectiveCommon}${effectiveBrief}`,
7227
+ (name) => o.skillOverrides?.[name] ?? localSkills.get(name)
7228
+ );
7229
+ } catch (e) {
7230
+ if (e instanceof SkillIncludeError) throw new AgentError(`${e.message} \u2014 refusing to run an unscoped session`);
7231
+ throw e;
7232
+ }
7028
7233
  const prompt = assemblePrompt({
7029
7234
  roster: o.roster,
7030
7235
  environment: o.environment,
7031
- common: effectiveCommon,
7032
- brief: effectiveBrief,
7236
+ common: "",
7237
+ brief: expanded.text,
7033
7238
  digest
7034
7239
  });
7035
- const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
7036
- const streamsDir = join10(o.stateDir, "streams");
7240
+ const promptSha = createHash2("sha256").update(expanded.text).digest("hex");
7241
+ const streamsDir = join11(o.stateDir, "streams");
7037
7242
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
7038
7243
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
7039
7244
  const { toolchain, model } = seat;
@@ -7069,7 +7274,7 @@ function planAgentRun(o) {
7069
7274
  harness: toolchain.harness,
7070
7275
  provider: toolchainProvider(toolchain),
7071
7276
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
7072
- warnings: seat.warnings,
7277
+ warnings: [...seat.warnings ?? [], ...expanded.warnings],
7073
7278
  prompt,
7074
7279
  promptBytes: Buffer.byteLength(prompt, "utf8"),
7075
7280
  unsetEnv: adapter.unsetEnv,
@@ -7089,8 +7294,8 @@ function planAgentRun(o) {
7089
7294
  digestAttached: digest !== void 0,
7090
7295
  digestAgeSeconds: ageSeconds,
7091
7296
  model,
7092
- streamPath: join10(streamsDir, `${base}.jsonl`),
7093
- eventsPath: join10(streamsDir, `${base}.events.jsonl`),
7297
+ streamPath: join11(streamsDir, `${base}.jsonl`),
7298
+ eventsPath: join11(streamsDir, `${base}.events.jsonl`),
7094
7299
  ticket: o.ticket,
7095
7300
  agentLog,
7096
7301
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -7296,13 +7501,13 @@ function spawnAgent(plan, emit) {
7296
7501
  }
7297
7502
 
7298
7503
  // src/run-worktree.ts
7299
- import { existsSync as existsSync11 } from "fs";
7300
- import { dirname as dirname7, join as join12 } from "path";
7504
+ import { existsSync as existsSync12 } from "fs";
7505
+ import { dirname as dirname7, join as join13 } from "path";
7301
7506
 
7302
7507
  // src/ports.ts
7303
7508
  import { execFileSync as execFileSync2 } from "child_process";
7304
- import { existsSync as existsSync10 } from "fs";
7305
- import { join as join11 } from "path";
7509
+ import { existsSync as existsSync11 } from "fs";
7510
+ import { join as join12 } from "path";
7306
7511
  var run = (cmd, args) => {
7307
7512
  try {
7308
7513
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7347,7 +7552,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7347
7552
  }
7348
7553
  function worktreeExists(parent, prefix, n) {
7349
7554
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7350
- if (existsSync10(join11(parent, name, ".git"))) return true;
7555
+ if (existsSync11(join12(parent, name, ".git"))) return true;
7351
7556
  }
7352
7557
  return false;
7353
7558
  }
@@ -7421,7 +7626,7 @@ function planRunWorktree(dir, cfg, t, role) {
7421
7626
  const existing = worktreeForNumber(dir, num);
7422
7627
  const located = locateBranchForTicket(dir, cfg, t, role);
7423
7628
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7424
- if (existing && !existing.prunable && existsSync11(existing.path)) {
7629
+ if (existing && !existing.prunable && existsSync12(existing.path)) {
7425
7630
  const branch = existing.branch;
7426
7631
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
7427
7632
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -7435,7 +7640,7 @@ function planRunWorktree(dir, cfg, t, role) {
7435
7640
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7436
7641
  return { action: "recut", path: existing.path, branch, base: baseRef };
7437
7642
  }
7438
- const path = join12(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7643
+ const path = join13(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7439
7644
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7440
7645
  return { action: "cut-remote", path, branch: located.branch };
7441
7646
  }
@@ -7482,7 +7687,7 @@ function worktreeHoldsUnpushed(path, remote) {
7482
7687
  }
7483
7688
  async function finishRunWorktree(dir, cfg, branch) {
7484
7689
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7485
- if (!wt || !existsSync11(wt.path)) return { kind: "none" };
7690
+ if (!wt || !existsSync12(wt.path)) return { kind: "none" };
7486
7691
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7487
7692
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7488
7693
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7494,7 +7699,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7494
7699
  if (pids.length) await killGently(pids);
7495
7700
  }
7496
7701
  gitOk(dir, ["worktree", "remove", wt.path]);
7497
- return existsSync11(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7702
+ return existsSync12(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7498
7703
  }
7499
7704
 
7500
7705
  // src/handoff-guard.ts
@@ -7643,13 +7848,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
7643
7848
  }
7644
7849
 
7645
7850
  // src/connect-wizard.ts
7646
- import { delimiter, dirname as dirname8, join as join13 } from "path";
7647
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync12, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
7851
+ import { delimiter, dirname as dirname8, join as join14 } from "path";
7852
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
7648
7853
  import { stringify } from "yaml";
7649
7854
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
7650
7855
  function shouldRunWizard(opts) {
7651
7856
  if (!opts.isTTY) return false;
7652
- const exists = opts.configExists ?? existsSync12;
7857
+ const exists = opts.configExists ?? existsSync13;
7653
7858
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
7654
7859
  }
7655
7860
  function firstRunConfigPath() {
@@ -7666,7 +7871,7 @@ function defaultIsExecutable(p) {
7666
7871
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
7667
7872
  for (const dir of pathEnv.split(delimiter)) {
7668
7873
  if (!dir) continue;
7669
- const candidate = join13(dir, name);
7874
+ const candidate = join14(dir, name);
7670
7875
  if (isExecutable(candidate)) return candidate;
7671
7876
  }
7672
7877
  return void 0;
@@ -7761,8 +7966,8 @@ Wrote ${o.configPath}.
7761
7966
  }
7762
7967
 
7763
7968
  // src/release-run.ts
7764
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync13, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
7765
- import { dirname as dirname9, join as join14 } from "path";
7969
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync14, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
7970
+ import { dirname as dirname9, join as join15 } from "path";
7766
7971
  import { tmpdir as tmpdir2 } from "os";
7767
7972
 
7768
7973
  // src/release.ts
@@ -8420,13 +8625,13 @@ async function bump(o, size) {
8420
8625
  }
8421
8626
  return printed;
8422
8627
  }
8423
- const files = o.repo.release.versionFiles.map((f) => join14(o.cwd, f));
8628
+ const files = o.repo.release.versionFiles.map((f) => join15(o.cwd, f));
8424
8629
  const first = files[0];
8425
- if (!existsSync13(first)) {
8630
+ if (!existsSync14(first)) {
8426
8631
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
8427
8632
  return null;
8428
8633
  }
8429
- const current = JSON.parse(readFileSync9(first, "utf8")).version;
8634
+ const current = JSON.parse(readFileSync10(first, "utf8")).version;
8430
8635
  if (!current) {
8431
8636
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
8432
8637
  return null;
@@ -8439,7 +8644,7 @@ async function bump(o, size) {
8439
8644
  return version;
8440
8645
  }
8441
8646
  for (const f of files) {
8442
- const src = readFileSync9(f, "utf8");
8647
+ const src = readFileSync10(f, "utf8");
8443
8648
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
8444
8649
  if (out === src) {
8445
8650
  o.emit.error(`no version field in ${f}`);
@@ -8452,13 +8657,13 @@ async function bump(o, size) {
8452
8657
  function writeChangelog(o, version, entries) {
8453
8658
  const file = o.repo.release.changelog;
8454
8659
  if (!file || entries.length === 0) return;
8455
- const path = join14(o.cwd, file);
8660
+ const path = join15(o.cwd, file);
8456
8661
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
8457
8662
  if (o.dryRun) {
8458
8663
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
8459
8664
  return;
8460
8665
  }
8461
- const existing = existsSync13(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
8666
+ const existing = existsSync14(path) ? readFileSync10(path, "utf8") : "# Changelog\n";
8462
8667
  writeFileSync9(path, insertChangelogSection(existing, section));
8463
8668
  }
8464
8669
  async function confirm(o, expected) {
@@ -8540,8 +8745,8 @@ async function confirmLastRelease(o, decision) {
8540
8745
  function openReleaseCheckout(o) {
8541
8746
  const { remote, base } = o.repo.branch;
8542
8747
  const branch = `crew/release-${Date.now()}`;
8543
- const root = mkdtempSync(join14(tmpdir2(), "crew-release-"));
8544
- const dir = join14(root, "checkout");
8748
+ const root = mkdtempSync(join15(tmpdir2(), "crew-release-"));
8749
+ const dir = join15(root, "checkout");
8545
8750
  const close = () => {
8546
8751
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
8547
8752
  rmSync6(root, { recursive: true, force: true });
@@ -8555,10 +8760,10 @@ function openReleaseCheckout(o) {
8555
8760
  throw e;
8556
8761
  }
8557
8762
  for (const f of o.repo.worktrees.copy) {
8558
- const src = join14(o.cwd, f);
8559
- if (!existsSync13(src)) continue;
8560
- mkdirSync8(dirname9(join14(dir, f)), { recursive: true });
8561
- copyFileSync(src, join14(dir, f));
8763
+ const src = join15(o.cwd, f);
8764
+ if (!existsSync14(src)) continue;
8765
+ mkdirSync8(dirname9(join15(dir, f)), { recursive: true });
8766
+ copyFileSync(src, join15(dir, f));
8562
8767
  }
8563
8768
  return { dir, branch, close };
8564
8769
  }
@@ -9027,7 +9232,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
9027
9232
  }
9028
9233
 
9029
9234
  // src/watch.ts
9030
- import { watch as watchFile, existsSync as existsSync14, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
9235
+ import { watch as watchFile, existsSync as existsSync15, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
9031
9236
  var E = "\x1B[";
9032
9237
  var dim = (s) => `${E}2m${s}${E}0m`;
9033
9238
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -9128,7 +9333,7 @@ function renderHeaders(states, now = Date.now()) {
9128
9333
  return states.map((s) => renderHeader(s, now)).join("");
9129
9334
  }
9130
9335
  function readFrom(file, from) {
9131
- if (!existsSync14(file)) return { events: [], offset: from };
9336
+ if (!existsSync15(file)) return { events: [], offset: from };
9132
9337
  const size = statSync6(file).size;
9133
9338
  if (size < from) return readFrom(file, 0);
9134
9339
  if (size === from) return { events: [], offset: from };
@@ -9368,9 +9573,9 @@ function nextRoles(decision, ran) {
9368
9573
  }
9369
9574
 
9370
9575
  // src/agents.ts
9371
- import { readFileSync as readFileSync10, existsSync as existsSync15 } from "fs";
9372
- import { join as join15 } from "path";
9373
- import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
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";
9374
9579
  var PERSONA_NAME = {
9375
9580
  dev: "Developer",
9376
9581
  design: "Design",
@@ -9383,19 +9588,19 @@ var CREW_AGENT_COLUMN = "agent_id";
9383
9588
  var AgentsSyncError = class extends Error {
9384
9589
  };
9385
9590
  function personaDefaultPrompt(promptsDir, role) {
9386
- const briefPath = join15(promptsDir, "personas", `lane-${role}.md`);
9387
- if (!existsSync15(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
9388
- if (role === "pair") return readFileSync10(briefPath, "utf8");
9389
- const commonPath = join15(promptsDir, "personas", "common.md");
9390
- if (!existsSync15(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
9391
- return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
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")}`;
9392
9597
  }
9393
9598
  async function resolveAgentId(route, role, opts) {
9394
9599
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
9395
9600
  if (cached) return cached;
9396
9601
  const seatId = route.resolved?.seats?.[role];
9397
9602
  if (!seatId || !route.resolved) return void 0;
9398
- const client2 = new TablationClient2({
9603
+ const client2 = new TablationClient3({
9399
9604
  baseUrl: `${route.baseUrl}/api`,
9400
9605
  apiKey: resolveApiKey(route),
9401
9606
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9410,7 +9615,7 @@ async function resolveAgentId(route, role, opts) {
9410
9615
  async function fetchDivergedPrompt(route, role, opts) {
9411
9616
  const agentId = await resolveAgentId(route, role, opts);
9412
9617
  if (!agentId || !route.resolved) return void 0;
9413
- const client2 = new TablationClient2({
9618
+ const client2 = new TablationClient3({
9414
9619
  baseUrl: `${route.baseUrl}/api`,
9415
9620
  apiKey: resolveApiKey(route),
9416
9621
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9448,7 +9653,7 @@ function choiceValue(v) {
9448
9653
  async function fetchSeatPersona(route, role, opts) {
9449
9654
  const agentId = await resolveAgentId(route, role, opts);
9450
9655
  if (!agentId || !route.resolved) return void 0;
9451
- const client2 = new TablationClient2({
9656
+ const client2 = new TablationClient3({
9452
9657
  baseUrl: `${route.baseUrl}/api`,
9453
9658
  apiKey: resolveApiKey(route),
9454
9659
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9496,7 +9701,7 @@ async function syncPersonas(route, opts) {
9496
9701
  throw new AgentsSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
9497
9702
  }
9498
9703
  const workspaceId = route.resolved.workspaceId;
9499
- const client2 = new TablationClient2({
9704
+ const client2 = new TablationClient3({
9500
9705
  baseUrl: `${route.baseUrl}/api`,
9501
9706
  apiKey: resolveApiKey(route),
9502
9707
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9505,7 +9710,7 @@ async function syncPersonas(route, opts) {
9505
9710
  try {
9506
9711
  agentsModel = await client2.dataModels.get("agents", workspaceId);
9507
9712
  } catch (e) {
9508
- if (e instanceof TablationApiError && e.status === 404) {
9713
+ if (e instanceof TablationApiError2 && e.status === 404) {
9509
9714
  throw new AgentsSyncError(
9510
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`
9511
9716
  );
@@ -9579,7 +9784,7 @@ async function syncPersonas(route, opts) {
9579
9784
  agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
9580
9785
  outcomes.push({ role, action: "updated", agentId: row.id });
9581
9786
  } catch (e) {
9582
- if (e instanceof StaleWriteError6) {
9787
+ if (e instanceof StaleWriteError7) {
9583
9788
  outcomes.push({ role, action: "diverged", agentId: row.id });
9584
9789
  continue;
9585
9790
  }
@@ -10064,119 +10269,6 @@ import {
10064
10269
  DeviceLoginError
10065
10270
  } from "@tablation/client";
10066
10271
 
10067
- // src/skills.ts
10068
- import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync16 } from "fs";
10069
- import { join as join16 } from "path";
10070
- import { parse as parseYaml } from "yaml";
10071
- import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
10072
- var SkillSyncError = class extends Error {
10073
- };
10074
- function parseSkillFile(path) {
10075
- const text = readFileSync11(path, "utf8");
10076
- const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
10077
- if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
10078
- const frontmatter = match[1];
10079
- const body = match[2];
10080
- const meta = parseYaml(frontmatter);
10081
- if (typeof meta.name !== "string" || !meta.name) throw new SkillSyncError(`${path}: frontmatter is missing "name"`);
10082
- if (typeof meta.description !== "string" || !meta.description) {
10083
- throw new SkillSyncError(`${path}: frontmatter is missing "description"`);
10084
- }
10085
- return { name: meta.name, description: meta.description, prompt: body, file: path };
10086
- }
10087
- function listLocalSkills(promptsDir) {
10088
- const dir = join16(promptsDir, "skills");
10089
- if (!existsSync16(dir)) return [];
10090
- return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join16(dir, f)));
10091
- }
10092
- async function syncSkills(route, opts) {
10093
- if (!route.resolved) {
10094
- throw new SkillSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
10095
- }
10096
- const workspaceId = route.resolved.workspaceId;
10097
- const client2 = new TablationClient3({
10098
- baseUrl: `${route.baseUrl}/api`,
10099
- apiKey: resolveApiKey(route),
10100
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
10101
- });
10102
- let skillsModel;
10103
- try {
10104
- skillsModel = await client2.dataModels.get("agent_skills", workspaceId);
10105
- } catch (e) {
10106
- if (e instanceof TablationApiError2 && e.status === 404) {
10107
- throw new SkillSyncError(
10108
- `workspace ${workspaceId} has no Agent Skills table yet \u2014 every workspace auto-provisions one; an older workspace may still need its one-time backfill`
10109
- );
10110
- }
10111
- throw e;
10112
- }
10113
- const local = listLocalSkills(route.promptsDir);
10114
- const rows = await client2.records.list(skillsModel.id, { limit: 200 });
10115
- const cache = route.resolved.agentSkills ?? {};
10116
- const outcomes = [];
10117
- const agentSkills = { ...cache };
10118
- for (const skill of local) {
10119
- const row = rows.find((r) => r.name === skill.name);
10120
- if (!row) {
10121
- if (opts.dryRun) {
10122
- outcomes.push({ name: skill.name, action: "created", skillId: "(dry run)" });
10123
- continue;
10124
- }
10125
- const created = await client2.records.create(skillsModel.id, {
10126
- name: skill.name,
10127
- description: skill.description,
10128
- prompt: skill.prompt
10129
- });
10130
- agentSkills[skill.name] = { skillId: created.id, lastSyncedUpdatedAt: created.updated_at };
10131
- outcomes.push({ name: skill.name, action: "created", skillId: created.id });
10132
- continue;
10133
- }
10134
- if (row.description === skill.description && row.prompt === skill.prompt) {
10135
- agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: row.updated_at };
10136
- outcomes.push({ name: skill.name, action: "unchanged", skillId: row.id });
10137
- continue;
10138
- }
10139
- const knownBaseline = cache[skill.name]?.skillId === row.id ? cache[skill.name]?.lastSyncedUpdatedAt : void 0;
10140
- if (!knownBaseline) {
10141
- outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
10142
- continue;
10143
- }
10144
- if (opts.dryRun) {
10145
- outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
10146
- continue;
10147
- }
10148
- try {
10149
- const updated = await client2.records.update(
10150
- skillsModel.id,
10151
- row.id,
10152
- { description: skill.description, prompt: skill.prompt },
10153
- knownBaseline
10154
- );
10155
- agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
10156
- outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
10157
- } catch (e) {
10158
- if (e instanceof StaleWriteError7) {
10159
- outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
10160
- continue;
10161
- }
10162
- throw e;
10163
- }
10164
- }
10165
- return { outcomes, agentSkills };
10166
- }
10167
- function describeSkillSyncOutcome(o) {
10168
- switch (o.action) {
10169
- case "created":
10170
- return `${o.name}: created (${o.skillId})`;
10171
- case "updated":
10172
- return `${o.name}: prompt updated from crew's local file`;
10173
- case "unchanged":
10174
- return `${o.name}: already in sync`;
10175
- case "diverged":
10176
- 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`;
10177
- }
10178
- }
10179
-
10180
10272
  // src/logbook.ts
10181
10273
  import { createHash as createHash3 } from "crypto";
10182
10274
  import { TablationClient as TablationClient4 } from "@tablation/client";
@@ -11653,6 +11745,7 @@ async function runRoleAgent(role, decision, ctx) {
11653
11745
  ticket: ticketHint,
11654
11746
  repo: ticketRepo,
11655
11747
  divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
11748
+ skillOverrides: await fetchSkillOverrides(route, { userAgent: ship.userAgent }),
11656
11749
  agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
11657
11750
  resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
11658
11751
  });
@@ -12747,6 +12840,7 @@ ${changed2.map((c) => ` - ${c}
12747
12840
  ticket: w.decision.actionable.top?.issue_id,
12748
12841
  repo: repoTargetFor(w.route, w.decision.actionable.top?.repo_id)?.name,
12749
12842
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12843
+ skillOverrides: await fetchSkillOverrides(w.route, { userAgent: cfg.ship.userAgent }),
12750
12844
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12751
12845
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
12752
12846
  });
@@ -12843,6 +12937,7 @@ ${changed2.map((c) => ` - ${c}
12843
12937
  ticket: fleetTicketHint,
12844
12938
  repo: fleetTicketRepo,
12845
12939
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12940
+ skillOverrides: await fetchSkillOverrides(w.route, { userAgent: cfg.ship.userAgent }),
12846
12941
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12847
12942
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
12848
12943
  });
@@ -13051,6 +13146,7 @@ ${changed2.map((c) => ` - ${c}
13051
13146
  ticket: ticketHint,
13052
13147
  repo: ticketRepo,
13053
13148
  divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
13149
+ skillOverrides: await fetchSkillOverrides(route, { userAgent: cfg.ship.userAgent }),
13054
13150
  agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
13055
13151
  resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
13056
13152
  });
@@ -13313,6 +13409,9 @@ ${changed2.map((c) => ` - ${c}
13313
13409
  for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
13314
13410
  `);
13315
13411
  for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
13412
+ `);
13413
+ const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
13414
+ for (const w of skillMarkerWarnings(target.promptsDir, briefs)) process.stderr.write(`crew agents sync: warning: ${w}
13316
13415
  `);
13317
13416
  let skillResult;
13318
13417
  try {