@tablation/crew 0.38.0 → 0.39.1

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/README.md CHANGED
@@ -41,12 +41,25 @@ agents prompt pair` prints its current persona text (a workspace admin's
41
41
  live edit if there is one, else the local default) for something like a
42
42
  `SessionStart` hook to feed into a session at start.
43
43
 
44
- `common.md` — the building lanes' shared policy (worktrees, one stateless
45
- invocation per cycle, the dev/design/QA hand-off) — is prefixed to the
46
- **dev**, **design** and **qa** briefs only. **triage** and **pair** are
47
- self-contained (`SELF_CONTAINED_ROLES` in `src/config.ts`): neither cuts a
48
- worktree, builds or hands anything to QA, so the policy would only
49
- misdescribe them. The prompts carry rules, not history; the incidents behind
44
+ The building lanes' shared policy (worktrees, one stateless invocation per
45
+ cycle, the dev/design/QA hand-off) ships as the `Crew Lane Agent` skill, and
46
+ the **dev**, **design** and **qa** briefs include it with a
47
+ `{{skill: Crew Lane Agent}}` marker on their first line. The Agents row keeps
48
+ the marker; the include is expanded when a run is assembled, from the
49
+ workspace's Agent Skills row if an admin edited it, else the local
50
+ `skills/*.md` file. So an admin edit to a lane's own sentences no longer forks
51
+ the shared policy. The marker is `{{skill:`, a skill name matched exactly after
52
+ trimming, `}}`, on its own line or inline. It is deliberately not a
53
+ `[[table:key]]` record link: personas and skills are not rows of a workspace
54
+ table. An unknown name refuses the run; a skill that includes itself (or a
55
+ cycle) is expanded once and the repeat left literal with a warning in the run
56
+ summary; nesting is capped at 3 levels and 200 KB of included text.
57
+ `crew agents sync` warns about a marker naming a skill the preset doesn't
58
+ ship. **triage** and **pair** are self-contained (`SELF_CONTAINED_ROLES` in
59
+ `src/config.ts`): neither cuts a worktree, builds or hands anything to QA, so
60
+ the policy would only misdescribe them. A preset forked before this change may
61
+ still ship `personas/common.md`, which keeps being prefixed to the building
62
+ briefs. The prompts carry rules, not history; the incidents behind
50
63
  each rule are kept in [`docs/prompt-rationale.md`](docs/prompt-rationale.md).
51
64
 
52
65
  Alongside personas, a prompt set can also ship **skills** —
@@ -64,7 +77,7 @@ diverged sync narrowed to skills alone, for when you've only touched
64
77
  grill-me.md` is one shipped example; the rest are the building lanes'
65
78
  situational procedures (`Worktree Setup`, `Running the Suite`, `Question and
66
79
  Investigation Tickets`, `Filing a Ticket`, `Ticket Attachments`, `Workspace
67
- Change Authorization`, `Design Pass`), which `common.md` names at the step
80
+ Change Authorization`, `Design Pass`), which the lane policy names at the step
68
81
  that needs each one — every run keeps the rule and the minimum inline and
69
82
  fetches the full procedure only when it applies, so a dev run that never
70
83
  files a ticket never pays for the filing procedure. `test/prompts.test.ts`
@@ -111,10 +124,10 @@ wants is a fact about that workspace, not about the machine running it.
111
124
  fit: copy a preset directory (`cp -r prompts/default ~/my-crew-policy`),
112
125
  edit its prose, and point `promptSet` at the copy (`~/my-crew-policy` or a
113
126
  relative path) — no fork of this repo required. A prompt set is a complete,
114
- self-contained fork of `personas/common.md` + one `personas/lane-<role>.md`
115
- per seat, plus whatever `skills/*.md` it wants, not a diff against another
116
- one; each persona file is read as plain prose (common.md ahead of a building
117
- lane's brief, a self-contained brief alone) and concatenated after the
127
+ self-contained fork of one `personas/lane-<role>.md` per seat
128
+ (and the shared-policy skill the building briefs include), plus whatever `skills/*.md` it wants, not a diff against another
129
+ one; each persona file is read as plain prose (`{{skill: Name}}` includes expanded
130
+ at run time) and concatenated after the
118
131
  per-run roster/environment sections and before the queue digest
119
132
  (`src/agent.ts`'s `assemblePrompt`), so there's no templating layer to
120
133
  learn. Keep the prose generic: `test/prompts.test.ts` fails a preset that
@@ -265,7 +278,7 @@ which only discovers names and remotes, never fetches anything itself.
265
278
  bin/crew the entry point
266
279
  src/ the implementation (Node, run directly via type stripping)
267
280
  test/ its tests, including a fake board for integration runs
268
- prompts/ prompt sets — prompts/<name>/personas/{common.md,lane-<role>.md}
281
+ prompts/ prompt sets — prompts/<name>/personas/lane-<role>.md
269
282
  + prompts/<name>/skills/*.md
270
283
  docs/ CONTRACT.md, REPO_SPEC.md, MIGRATION.md
271
284
  launchd/ timer templates (edit the paths before installing)
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 readFileSync8, existsSync as existsSync9, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6645
+ import { readFileSync as readFileSync9, existsSync as existsSync10, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6611
6646
  import { finished } from "stream/promises";
6612
- import { join as join10, dirname as dirname6 } from "path";
6647
+ import { join as join11, dirname as dirname6 } from "path";
6613
6648
  import { createHash as createHash2 } from "crypto";
6614
6649
 
6615
6650
  // src/environment.ts
@@ -6953,6 +6988,199 @@ async function reportAgentRun(target, run2) {
6953
6988
  return { agentLogId: entry.id };
6954
6989
  }
6955
6990
 
6991
+ // src/skill-include.ts
6992
+ var SkillIncludeError = class extends Error {
6993
+ };
6994
+ var MAX_INCLUDE_DEPTH = 3;
6995
+ var MAX_INCLUDED_BYTES = 2e5;
6996
+ var MARKER = /\{\{[ \t]*skill:[ \t]*([^{}\r\n]*?)[ \t]*\}\}/g;
6997
+ function skillMarkerNames(text) {
6998
+ const names = [];
6999
+ for (const m of text.matchAll(MARKER)) {
7000
+ const name = m[1].trim();
7001
+ if (name && !names.includes(name)) names.push(name);
7002
+ }
7003
+ return names;
7004
+ }
7005
+ function expandSkillMarkers(text, lookup, opts = {}) {
7006
+ const maxDepth = opts.maxDepth ?? MAX_INCLUDE_DEPTH;
7007
+ const maxBytes = opts.maxBytes ?? MAX_INCLUDED_BYTES;
7008
+ const warnings = [];
7009
+ let included = 0;
7010
+ const expand2 = (source, chain) => source.replace(MARKER, (literal, rawName) => {
7011
+ const name = rawName.trim();
7012
+ if (!name) return literal;
7013
+ if (chain.includes(name)) {
7014
+ warnings.push(`skill include "${name}" left literal: it includes itself (${[...chain, name].join(" \u2192 ")})`);
7015
+ return literal;
7016
+ }
7017
+ if (chain.length >= maxDepth) {
7018
+ warnings.push(`skill include "${name}" left literal: nested deeper than ${maxDepth} levels`);
7019
+ return literal;
7020
+ }
7021
+ const body = lookup(name);
7022
+ if (body === void 0) throw new SkillIncludeError(`no skill named "${name}" for the include ${literal}`);
7023
+ const trimmed = body.replace(/^\n+/, "").replace(/\n+$/, "");
7024
+ const bytes = Buffer.byteLength(trimmed, "utf8");
7025
+ if (included + bytes > maxBytes) {
7026
+ warnings.push(`skill include "${name}" left literal: it would exceed the ${maxBytes}-byte include budget`);
7027
+ return literal;
7028
+ }
7029
+ included += bytes;
7030
+ return expand2(trimmed, [...chain, name]);
7031
+ });
7032
+ return { text: expand2(text, []), warnings };
7033
+ }
7034
+
7035
+ // src/skills.ts
7036
+ import { readFileSync as readFileSync8, readdirSync as readdirSync2, existsSync as existsSync9 } from "fs";
7037
+ import { join as join10 } from "path";
7038
+ import { parse as parseYaml } from "yaml";
7039
+ import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
7040
+ var SkillSyncError = class extends Error {
7041
+ };
7042
+ function parseSkillFile(path) {
7043
+ const text = readFileSync8(path, "utf8");
7044
+ const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
7045
+ if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
7046
+ const frontmatter = match[1];
7047
+ const body = match[2];
7048
+ const meta = parseYaml(frontmatter);
7049
+ if (typeof meta.name !== "string" || !meta.name) throw new SkillSyncError(`${path}: frontmatter is missing "name"`);
7050
+ if (typeof meta.description !== "string" || !meta.description) {
7051
+ throw new SkillSyncError(`${path}: frontmatter is missing "description"`);
7052
+ }
7053
+ return { name: meta.name, description: meta.description, prompt: body, file: path };
7054
+ }
7055
+ function listLocalSkills(promptsDir) {
7056
+ const dir = join10(promptsDir, "skills");
7057
+ if (!existsSync9(dir)) return [];
7058
+ return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join10(dir, f)));
7059
+ }
7060
+ async function syncSkills(route, opts) {
7061
+ if (!route.resolved) {
7062
+ throw new SkillSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
7063
+ }
7064
+ const workspaceId = route.resolved.workspaceId;
7065
+ const client2 = new TablationClient2({
7066
+ baseUrl: `${route.baseUrl}/api`,
7067
+ apiKey: resolveApiKey(route),
7068
+ headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7069
+ });
7070
+ let skillsModel;
7071
+ try {
7072
+ skillsModel = await client2.dataModels.get("agent_skills", workspaceId);
7073
+ } catch (e) {
7074
+ if (e instanceof TablationApiError && e.status === 404) {
7075
+ throw new SkillSyncError(
7076
+ `workspace ${workspaceId} has no Agent Skills table yet \u2014 every workspace auto-provisions one; an older workspace may still need its one-time backfill`
7077
+ );
7078
+ }
7079
+ throw e;
7080
+ }
7081
+ const local = listLocalSkills(route.promptsDir);
7082
+ const rows = await client2.records.list(skillsModel.id, { limit: 200 });
7083
+ const cache = route.resolved.agentSkills ?? {};
7084
+ const outcomes = [];
7085
+ const agentSkills = { ...cache };
7086
+ for (const skill of local) {
7087
+ const row = rows.find((r) => r.name === skill.name);
7088
+ if (!row) {
7089
+ if (opts.dryRun) {
7090
+ outcomes.push({ name: skill.name, action: "created", skillId: "(dry run)" });
7091
+ continue;
7092
+ }
7093
+ const created = await client2.records.create(skillsModel.id, {
7094
+ name: skill.name,
7095
+ description: skill.description,
7096
+ prompt: skill.prompt
7097
+ });
7098
+ agentSkills[skill.name] = { skillId: created.id, lastSyncedUpdatedAt: created.updated_at };
7099
+ outcomes.push({ name: skill.name, action: "created", skillId: created.id });
7100
+ continue;
7101
+ }
7102
+ if (row.description === skill.description && row.prompt === skill.prompt) {
7103
+ agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: row.updated_at };
7104
+ outcomes.push({ name: skill.name, action: "unchanged", skillId: row.id });
7105
+ continue;
7106
+ }
7107
+ const knownBaseline = cache[skill.name]?.skillId === row.id ? cache[skill.name]?.lastSyncedUpdatedAt : void 0;
7108
+ if (!knownBaseline) {
7109
+ outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
7110
+ continue;
7111
+ }
7112
+ if (opts.dryRun) {
7113
+ outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
7114
+ continue;
7115
+ }
7116
+ try {
7117
+ const updated = await client2.records.update(
7118
+ skillsModel.id,
7119
+ row.id,
7120
+ { description: skill.description, prompt: skill.prompt },
7121
+ knownBaseline
7122
+ );
7123
+ agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
7124
+ outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
7125
+ } catch (e) {
7126
+ if (e instanceof StaleWriteError6) {
7127
+ outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
7128
+ continue;
7129
+ }
7130
+ throw e;
7131
+ }
7132
+ }
7133
+ return { outcomes, agentSkills };
7134
+ }
7135
+ function describeSkillSyncOutcome(o) {
7136
+ switch (o.action) {
7137
+ case "created":
7138
+ return `${o.name}: created (${o.skillId})`;
7139
+ case "updated":
7140
+ return `${o.name}: prompt updated from crew's local file`;
7141
+ case "unchanged":
7142
+ return `${o.name}: already in sync`;
7143
+ case "diverged":
7144
+ return `${o.name}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.skillId} in the app before syncing again`;
7145
+ }
7146
+ }
7147
+ async function fetchSkillOverrides(route, opts) {
7148
+ if (!route.resolved) return {};
7149
+ const client2 = new TablationClient2({
7150
+ baseUrl: `${route.baseUrl}/api`,
7151
+ apiKey: resolveApiKey(route),
7152
+ headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7153
+ });
7154
+ let rows;
7155
+ try {
7156
+ const model = await client2.dataModels.get("agent_skills", route.resolved.workspaceId);
7157
+ rows = await client2.records.list(model.id, { limit: 200 });
7158
+ } catch {
7159
+ return {};
7160
+ }
7161
+ const local = new Map(listLocalSkills(route.promptsDir).map((s) => [s.name, s]));
7162
+ const cache = route.resolved.agentSkills ?? {};
7163
+ const out = {};
7164
+ for (const row of rows) {
7165
+ const name = row.name;
7166
+ if (row.prompt === local.get(name)?.prompt) continue;
7167
+ const cached = cache[name];
7168
+ if (cached?.skillId === row.id && cached.lastSyncedUpdatedAt === row.updated_at) continue;
7169
+ out[name] = row.prompt;
7170
+ }
7171
+ return out;
7172
+ }
7173
+ function skillMarkerWarnings(promptsDir, personaPrompts) {
7174
+ const shipped = new Set(listLocalSkills(promptsDir).map((s) => s.name));
7175
+ const out = [];
7176
+ for (const [role, text] of Object.entries(personaPrompts)) {
7177
+ for (const name of skillMarkerNames(text ?? "")) {
7178
+ if (!shipped.has(name)) out.push(`${role}: brief includes {{skill: ${name}}} but this prompt set ships no skill with that name`);
7179
+ }
7180
+ }
7181
+ return out;
7182
+ }
7183
+
6956
7184
  // src/ticket-attribution.ts
6957
7185
  var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
6958
7186
  function ticketMentions(text) {
@@ -7007,35 +7235,45 @@ var AgentError = class extends Error {
7007
7235
  var AgentSpawnError = class extends AgentError {
7008
7236
  };
7009
7237
  function planAgentRun(o) {
7010
- const briefPath = join10(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7011
- if (!existsSync9(briefPath)) {
7238
+ const briefPath = join11(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7239
+ if (!existsSync10(briefPath)) {
7012
7240
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
7013
7241
  }
7014
- const commonPath = join10(o.route.promptsDir, "personas", "common.md");
7015
- const usesCommon = !SELF_CONTAINED_ROLES.has(o.role);
7016
- if (usesCommon && !existsSync9(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
7242
+ const commonPath = join11(o.route.promptsDir, "personas", "common.md");
7243
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync10(commonPath);
7017
7244
  const now = o.now ?? (() => Date.now());
7018
7245
  const routeLabel = routeSlug(o.route.route);
7019
- const digestPath2 = join10(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7246
+ const digestPath2 = join11(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7020
7247
  let digest;
7021
7248
  let ageSeconds;
7022
- if (existsSync9(digestPath2)) {
7249
+ if (existsSync10(digestPath2)) {
7023
7250
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
7024
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
7251
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync9(digestPath2, "utf8");
7025
7252
  }
7026
- const common = usesCommon ? readFileSync8(commonPath, "utf8") : "";
7027
- const brief = readFileSync8(briefPath, "utf8");
7253
+ const common = usesCommon ? readFileSync9(commonPath, "utf8") : "";
7254
+ const brief = readFileSync9(briefPath, "utf8");
7028
7255
  const effectiveCommon = o.divergedPrompt ? "" : common;
7029
7256
  const effectiveBrief = o.divergedPrompt ?? brief;
7257
+ const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
7258
+ let expanded;
7259
+ try {
7260
+ expanded = expandSkillMarkers(
7261
+ `${effectiveCommon}${effectiveBrief}`,
7262
+ (name) => o.skillOverrides?.[name] ?? localSkills.get(name)
7263
+ );
7264
+ } catch (e) {
7265
+ if (e instanceof SkillIncludeError) throw new AgentError(`${e.message} \u2014 refusing to run an unscoped session`);
7266
+ throw e;
7267
+ }
7030
7268
  const prompt = assemblePrompt({
7031
7269
  roster: o.roster,
7032
7270
  environment: o.environment,
7033
- common: effectiveCommon,
7034
- brief: effectiveBrief,
7271
+ common: "",
7272
+ brief: expanded.text,
7035
7273
  digest
7036
7274
  });
7037
- const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
7038
- const streamsDir = join10(o.stateDir, "streams");
7275
+ const promptSha = createHash2("sha256").update(expanded.text).digest("hex");
7276
+ const streamsDir = join11(o.stateDir, "streams");
7039
7277
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
7040
7278
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
7041
7279
  const { toolchain, model } = seat;
@@ -7071,7 +7309,7 @@ function planAgentRun(o) {
7071
7309
  harness: toolchain.harness,
7072
7310
  provider: toolchainProvider(toolchain),
7073
7311
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
7074
- warnings: seat.warnings,
7312
+ warnings: [...seat.warnings ?? [], ...expanded.warnings],
7075
7313
  prompt,
7076
7314
  promptBytes: Buffer.byteLength(prompt, "utf8"),
7077
7315
  unsetEnv: adapter.unsetEnv,
@@ -7091,8 +7329,8 @@ function planAgentRun(o) {
7091
7329
  digestAttached: digest !== void 0,
7092
7330
  digestAgeSeconds: ageSeconds,
7093
7331
  model,
7094
- streamPath: join10(streamsDir, `${base}.jsonl`),
7095
- eventsPath: join10(streamsDir, `${base}.events.jsonl`),
7332
+ streamPath: join11(streamsDir, `${base}.jsonl`),
7333
+ eventsPath: join11(streamsDir, `${base}.events.jsonl`),
7096
7334
  ticket: o.ticket,
7097
7335
  agentLog,
7098
7336
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -7298,13 +7536,13 @@ function spawnAgent(plan, emit) {
7298
7536
  }
7299
7537
 
7300
7538
  // src/run-worktree.ts
7301
- import { existsSync as existsSync11 } from "fs";
7302
- import { dirname as dirname7, join as join12 } from "path";
7539
+ import { existsSync as existsSync12 } from "fs";
7540
+ import { dirname as dirname7, join as join13 } from "path";
7303
7541
 
7304
7542
  // src/ports.ts
7305
7543
  import { execFileSync as execFileSync2 } from "child_process";
7306
- import { existsSync as existsSync10 } from "fs";
7307
- import { join as join11 } from "path";
7544
+ import { existsSync as existsSync11 } from "fs";
7545
+ import { join as join12 } from "path";
7308
7546
  var run = (cmd, args) => {
7309
7547
  try {
7310
7548
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7349,7 +7587,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7349
7587
  }
7350
7588
  function worktreeExists(parent, prefix, n) {
7351
7589
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7352
- if (existsSync10(join11(parent, name, ".git"))) return true;
7590
+ if (existsSync11(join12(parent, name, ".git"))) return true;
7353
7591
  }
7354
7592
  return false;
7355
7593
  }
@@ -7423,7 +7661,7 @@ function planRunWorktree(dir, cfg, t, role) {
7423
7661
  const existing = worktreeForNumber(dir, num);
7424
7662
  const located = locateBranchForTicket(dir, cfg, t, role);
7425
7663
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7426
- if (existing && !existing.prunable && existsSync11(existing.path)) {
7664
+ if (existing && !existing.prunable && existsSync12(existing.path)) {
7427
7665
  const branch = existing.branch;
7428
7666
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
7429
7667
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -7437,7 +7675,7 @@ function planRunWorktree(dir, cfg, t, role) {
7437
7675
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7438
7676
  return { action: "recut", path: existing.path, branch, base: baseRef };
7439
7677
  }
7440
- const path = join12(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7678
+ const path = join13(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7441
7679
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7442
7680
  return { action: "cut-remote", path, branch: located.branch };
7443
7681
  }
@@ -7484,7 +7722,7 @@ function worktreeHoldsUnpushed(path, remote) {
7484
7722
  }
7485
7723
  async function finishRunWorktree(dir, cfg, branch) {
7486
7724
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7487
- if (!wt || !existsSync11(wt.path)) return { kind: "none" };
7725
+ if (!wt || !existsSync12(wt.path)) return { kind: "none" };
7488
7726
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7489
7727
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7490
7728
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7496,7 +7734,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7496
7734
  if (pids.length) await killGently(pids);
7497
7735
  }
7498
7736
  gitOk(dir, ["worktree", "remove", wt.path]);
7499
- return existsSync11(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7737
+ return existsSync12(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7500
7738
  }
7501
7739
 
7502
7740
  // src/handoff-guard.ts
@@ -7645,13 +7883,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
7645
7883
  }
7646
7884
 
7647
7885
  // src/connect-wizard.ts
7648
- import { delimiter, dirname as dirname8, join as join13 } from "path";
7649
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync12, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
7886
+ import { delimiter, dirname as dirname8, join as join14 } from "path";
7887
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
7650
7888
  import { stringify } from "yaml";
7651
7889
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
7652
7890
  function shouldRunWizard(opts) {
7653
7891
  if (!opts.isTTY) return false;
7654
- const exists = opts.configExists ?? existsSync12;
7892
+ const exists = opts.configExists ?? existsSync13;
7655
7893
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
7656
7894
  }
7657
7895
  function firstRunConfigPath() {
@@ -7668,7 +7906,7 @@ function defaultIsExecutable(p) {
7668
7906
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
7669
7907
  for (const dir of pathEnv.split(delimiter)) {
7670
7908
  if (!dir) continue;
7671
- const candidate = join13(dir, name);
7909
+ const candidate = join14(dir, name);
7672
7910
  if (isExecutable(candidate)) return candidate;
7673
7911
  }
7674
7912
  return void 0;
@@ -7763,8 +8001,8 @@ Wrote ${o.configPath}.
7763
8001
  }
7764
8002
 
7765
8003
  // src/release-run.ts
7766
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync13, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
7767
- import { dirname as dirname9, join as join14 } from "path";
8004
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync14, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
8005
+ import { dirname as dirname9, join as join15 } from "path";
7768
8006
  import { tmpdir as tmpdir2 } from "os";
7769
8007
 
7770
8008
  // src/release.ts
@@ -8422,13 +8660,13 @@ async function bump(o, size) {
8422
8660
  }
8423
8661
  return printed;
8424
8662
  }
8425
- const files = o.repo.release.versionFiles.map((f) => join14(o.cwd, f));
8663
+ const files = o.repo.release.versionFiles.map((f) => join15(o.cwd, f));
8426
8664
  const first = files[0];
8427
- if (!existsSync13(first)) {
8665
+ if (!existsSync14(first)) {
8428
8666
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
8429
8667
  return null;
8430
8668
  }
8431
- const current = JSON.parse(readFileSync9(first, "utf8")).version;
8669
+ const current = JSON.parse(readFileSync10(first, "utf8")).version;
8432
8670
  if (!current) {
8433
8671
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
8434
8672
  return null;
@@ -8441,7 +8679,7 @@ async function bump(o, size) {
8441
8679
  return version;
8442
8680
  }
8443
8681
  for (const f of files) {
8444
- const src = readFileSync9(f, "utf8");
8682
+ const src = readFileSync10(f, "utf8");
8445
8683
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
8446
8684
  if (out === src) {
8447
8685
  o.emit.error(`no version field in ${f}`);
@@ -8454,13 +8692,13 @@ async function bump(o, size) {
8454
8692
  function writeChangelog(o, version, entries) {
8455
8693
  const file = o.repo.release.changelog;
8456
8694
  if (!file || entries.length === 0) return;
8457
- const path = join14(o.cwd, file);
8695
+ const path = join15(o.cwd, file);
8458
8696
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
8459
8697
  if (o.dryRun) {
8460
8698
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
8461
8699
  return;
8462
8700
  }
8463
- const existing = existsSync13(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
8701
+ const existing = existsSync14(path) ? readFileSync10(path, "utf8") : "# Changelog\n";
8464
8702
  writeFileSync9(path, insertChangelogSection(existing, section));
8465
8703
  }
8466
8704
  async function confirm(o, expected) {
@@ -8542,8 +8780,8 @@ async function confirmLastRelease(o, decision) {
8542
8780
  function openReleaseCheckout(o) {
8543
8781
  const { remote, base } = o.repo.branch;
8544
8782
  const branch = `crew/release-${Date.now()}`;
8545
- const root = mkdtempSync(join14(tmpdir2(), "crew-release-"));
8546
- const dir = join14(root, "checkout");
8783
+ const root = mkdtempSync(join15(tmpdir2(), "crew-release-"));
8784
+ const dir = join15(root, "checkout");
8547
8785
  const close = () => {
8548
8786
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
8549
8787
  rmSync6(root, { recursive: true, force: true });
@@ -8557,10 +8795,10 @@ function openReleaseCheckout(o) {
8557
8795
  throw e;
8558
8796
  }
8559
8797
  for (const f of o.repo.worktrees.copy) {
8560
- const src = join14(o.cwd, f);
8561
- if (!existsSync13(src)) continue;
8562
- mkdirSync8(dirname9(join14(dir, f)), { recursive: true });
8563
- copyFileSync(src, join14(dir, f));
8798
+ const src = join15(o.cwd, f);
8799
+ if (!existsSync14(src)) continue;
8800
+ mkdirSync8(dirname9(join15(dir, f)), { recursive: true });
8801
+ copyFileSync(src, join15(dir, f));
8564
8802
  }
8565
8803
  return { dir, branch, close };
8566
8804
  }
@@ -9029,7 +9267,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
9029
9267
  }
9030
9268
 
9031
9269
  // src/watch.ts
9032
- import { watch as watchFile, existsSync as existsSync14, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
9270
+ import { watch as watchFile, existsSync as existsSync15, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
9033
9271
  var E = "\x1B[";
9034
9272
  var dim = (s) => `${E}2m${s}${E}0m`;
9035
9273
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -9130,7 +9368,7 @@ function renderHeaders(states, now = Date.now()) {
9130
9368
  return states.map((s) => renderHeader(s, now)).join("");
9131
9369
  }
9132
9370
  function readFrom(file, from) {
9133
- if (!existsSync14(file)) return { events: [], offset: from };
9371
+ if (!existsSync15(file)) return { events: [], offset: from };
9134
9372
  const size = statSync6(file).size;
9135
9373
  if (size < from) return readFrom(file, 0);
9136
9374
  if (size === from) return { events: [], offset: from };
@@ -9370,9 +9608,9 @@ function nextRoles(decision, ran) {
9370
9608
  }
9371
9609
 
9372
9610
  // src/agents.ts
9373
- import { readFileSync as readFileSync10, existsSync as existsSync15 } from "fs";
9374
- import { join as join15 } from "path";
9375
- import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
9611
+ import { readFileSync as readFileSync11, existsSync as existsSync16 } from "fs";
9612
+ import { join as join16 } from "path";
9613
+ import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
9376
9614
  var PERSONA_NAME = {
9377
9615
  dev: "Developer",
9378
9616
  design: "Design",
@@ -9385,19 +9623,19 @@ var CREW_AGENT_COLUMN = "agent_id";
9385
9623
  var AgentsSyncError = class extends Error {
9386
9624
  };
9387
9625
  function personaDefaultPrompt(promptsDir, role) {
9388
- const briefPath = join15(promptsDir, "personas", `lane-${role}.md`);
9389
- if (!existsSync15(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
9390
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync10(briefPath, "utf8");
9391
- const commonPath = join15(promptsDir, "personas", "common.md");
9392
- if (!existsSync15(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
9393
- return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
9626
+ const briefPath = join16(promptsDir, "personas", `lane-${role}.md`);
9627
+ if (!existsSync16(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
9628
+ if (SELF_CONTAINED_ROLES.has(role)) return readFileSync11(briefPath, "utf8");
9629
+ const commonPath = join16(promptsDir, "personas", "common.md");
9630
+ if (!existsSync16(commonPath)) return readFileSync11(briefPath, "utf8");
9631
+ return `${readFileSync11(commonPath, "utf8")}${readFileSync11(briefPath, "utf8")}`;
9394
9632
  }
9395
9633
  async function resolveAgentId(route, role, opts) {
9396
9634
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
9397
9635
  if (cached) return cached;
9398
9636
  const seatId = route.resolved?.seats?.[role];
9399
9637
  if (!seatId || !route.resolved) return void 0;
9400
- const client2 = new TablationClient2({
9638
+ const client2 = new TablationClient3({
9401
9639
  baseUrl: `${route.baseUrl}/api`,
9402
9640
  apiKey: resolveApiKey(route),
9403
9641
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9412,7 +9650,7 @@ async function resolveAgentId(route, role, opts) {
9412
9650
  async function fetchDivergedPrompt(route, role, opts) {
9413
9651
  const agentId = await resolveAgentId(route, role, opts);
9414
9652
  if (!agentId || !route.resolved) return void 0;
9415
- const client2 = new TablationClient2({
9653
+ const client2 = new TablationClient3({
9416
9654
  baseUrl: `${route.baseUrl}/api`,
9417
9655
  apiKey: resolveApiKey(route),
9418
9656
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9450,7 +9688,7 @@ function choiceValue(v) {
9450
9688
  async function fetchSeatPersona(route, role, opts) {
9451
9689
  const agentId = await resolveAgentId(route, role, opts);
9452
9690
  if (!agentId || !route.resolved) return void 0;
9453
- const client2 = new TablationClient2({
9691
+ const client2 = new TablationClient3({
9454
9692
  baseUrl: `${route.baseUrl}/api`,
9455
9693
  apiKey: resolveApiKey(route),
9456
9694
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9498,7 +9736,7 @@ async function syncPersonas(route, opts) {
9498
9736
  throw new AgentsSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
9499
9737
  }
9500
9738
  const workspaceId = route.resolved.workspaceId;
9501
- const client2 = new TablationClient2({
9739
+ const client2 = new TablationClient3({
9502
9740
  baseUrl: `${route.baseUrl}/api`,
9503
9741
  apiKey: resolveApiKey(route),
9504
9742
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9507,7 +9745,7 @@ async function syncPersonas(route, opts) {
9507
9745
  try {
9508
9746
  agentsModel = await client2.dataModels.get("agents", workspaceId);
9509
9747
  } catch (e) {
9510
- if (e instanceof TablationApiError && e.status === 404) {
9748
+ if (e instanceof TablationApiError2 && e.status === 404) {
9511
9749
  throw new AgentsSyncError(
9512
9750
  `workspace ${workspaceId} has no Agents table yet \u2014 every workspace auto-provisions one (ISSUE-465); an older workspace may still need its one-time backfill`
9513
9751
  );
@@ -9581,7 +9819,7 @@ async function syncPersonas(route, opts) {
9581
9819
  agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
9582
9820
  outcomes.push({ role, action: "updated", agentId: row.id });
9583
9821
  } catch (e) {
9584
- if (e instanceof StaleWriteError6) {
9822
+ if (e instanceof StaleWriteError7) {
9585
9823
  outcomes.push({ role, action: "diverged", agentId: row.id });
9586
9824
  continue;
9587
9825
  }
@@ -10066,119 +10304,6 @@ import {
10066
10304
  DeviceLoginError
10067
10305
  } from "@tablation/client";
10068
10306
 
10069
- // src/skills.ts
10070
- import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync16 } from "fs";
10071
- import { join as join16 } from "path";
10072
- import { parse as parseYaml } from "yaml";
10073
- import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
10074
- var SkillSyncError = class extends Error {
10075
- };
10076
- function parseSkillFile(path) {
10077
- const text = readFileSync11(path, "utf8");
10078
- const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
10079
- if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
10080
- const frontmatter = match[1];
10081
- const body = match[2];
10082
- const meta = parseYaml(frontmatter);
10083
- if (typeof meta.name !== "string" || !meta.name) throw new SkillSyncError(`${path}: frontmatter is missing "name"`);
10084
- if (typeof meta.description !== "string" || !meta.description) {
10085
- throw new SkillSyncError(`${path}: frontmatter is missing "description"`);
10086
- }
10087
- return { name: meta.name, description: meta.description, prompt: body, file: path };
10088
- }
10089
- function listLocalSkills(promptsDir) {
10090
- const dir = join16(promptsDir, "skills");
10091
- if (!existsSync16(dir)) return [];
10092
- return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join16(dir, f)));
10093
- }
10094
- async function syncSkills(route, opts) {
10095
- if (!route.resolved) {
10096
- throw new SkillSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
10097
- }
10098
- const workspaceId = route.resolved.workspaceId;
10099
- const client2 = new TablationClient3({
10100
- baseUrl: `${route.baseUrl}/api`,
10101
- apiKey: resolveApiKey(route),
10102
- headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
10103
- });
10104
- let skillsModel;
10105
- try {
10106
- skillsModel = await client2.dataModels.get("agent_skills", workspaceId);
10107
- } catch (e) {
10108
- if (e instanceof TablationApiError2 && e.status === 404) {
10109
- throw new SkillSyncError(
10110
- `workspace ${workspaceId} has no Agent Skills table yet \u2014 every workspace auto-provisions one; an older workspace may still need its one-time backfill`
10111
- );
10112
- }
10113
- throw e;
10114
- }
10115
- const local = listLocalSkills(route.promptsDir);
10116
- const rows = await client2.records.list(skillsModel.id, { limit: 200 });
10117
- const cache = route.resolved.agentSkills ?? {};
10118
- const outcomes = [];
10119
- const agentSkills = { ...cache };
10120
- for (const skill of local) {
10121
- const row = rows.find((r) => r.name === skill.name);
10122
- if (!row) {
10123
- if (opts.dryRun) {
10124
- outcomes.push({ name: skill.name, action: "created", skillId: "(dry run)" });
10125
- continue;
10126
- }
10127
- const created = await client2.records.create(skillsModel.id, {
10128
- name: skill.name,
10129
- description: skill.description,
10130
- prompt: skill.prompt
10131
- });
10132
- agentSkills[skill.name] = { skillId: created.id, lastSyncedUpdatedAt: created.updated_at };
10133
- outcomes.push({ name: skill.name, action: "created", skillId: created.id });
10134
- continue;
10135
- }
10136
- if (row.description === skill.description && row.prompt === skill.prompt) {
10137
- agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: row.updated_at };
10138
- outcomes.push({ name: skill.name, action: "unchanged", skillId: row.id });
10139
- continue;
10140
- }
10141
- const knownBaseline = cache[skill.name]?.skillId === row.id ? cache[skill.name]?.lastSyncedUpdatedAt : void 0;
10142
- if (!knownBaseline) {
10143
- outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
10144
- continue;
10145
- }
10146
- if (opts.dryRun) {
10147
- outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
10148
- continue;
10149
- }
10150
- try {
10151
- const updated = await client2.records.update(
10152
- skillsModel.id,
10153
- row.id,
10154
- { description: skill.description, prompt: skill.prompt },
10155
- knownBaseline
10156
- );
10157
- agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
10158
- outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
10159
- } catch (e) {
10160
- if (e instanceof StaleWriteError7) {
10161
- outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
10162
- continue;
10163
- }
10164
- throw e;
10165
- }
10166
- }
10167
- return { outcomes, agentSkills };
10168
- }
10169
- function describeSkillSyncOutcome(o) {
10170
- switch (o.action) {
10171
- case "created":
10172
- return `${o.name}: created (${o.skillId})`;
10173
- case "updated":
10174
- return `${o.name}: prompt updated from crew's local file`;
10175
- case "unchanged":
10176
- return `${o.name}: already in sync`;
10177
- case "diverged":
10178
- 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`;
10179
- }
10180
- }
10181
-
10182
10307
  // src/logbook.ts
10183
10308
  import { createHash as createHash3 } from "crypto";
10184
10309
  import { TablationClient as TablationClient4 } from "@tablation/client";
@@ -11655,6 +11780,7 @@ async function runRoleAgent(role, decision, ctx) {
11655
11780
  ticket: ticketHint,
11656
11781
  repo: ticketRepo,
11657
11782
  divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
11783
+ skillOverrides: await fetchSkillOverrides(route, { userAgent: ship.userAgent }),
11658
11784
  agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
11659
11785
  resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
11660
11786
  });
@@ -11887,9 +12013,15 @@ var value = (name) => {
11887
12013
  const i = argv.indexOf(`--${name}`);
11888
12014
  return i >= 0 ? argv[i + 1] : void 0;
11889
12015
  };
11890
- var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
12016
+ var positional = positionalArgs(argv);
11891
12017
  var command = positional[0];
11892
12018
  if (!command) usage();
12019
+ function findSyncRoute(c, name) {
12020
+ if (!name && c.routes.length > 1) {
12021
+ throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
12022
+ }
12023
+ return findRoute(c, name);
12024
+ }
11893
12025
  function daemonConfigPaths(c) {
11894
12026
  const files = [c.configFile];
11895
12027
  for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
@@ -12749,6 +12881,7 @@ ${changed2.map((c) => ` - ${c}
12749
12881
  ticket: w.decision.actionable.top?.issue_id,
12750
12882
  repo: repoTargetFor(w.route, w.decision.actionable.top?.repo_id)?.name,
12751
12883
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12884
+ skillOverrides: await fetchSkillOverrides(w.route, { userAgent: cfg.ship.userAgent }),
12752
12885
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12753
12886
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
12754
12887
  });
@@ -12845,6 +12978,7 @@ ${changed2.map((c) => ` - ${c}
12845
12978
  ticket: fleetTicketHint,
12846
12979
  repo: fleetTicketRepo,
12847
12980
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12981
+ skillOverrides: await fetchSkillOverrides(w.route, { userAgent: cfg.ship.userAgent }),
12848
12982
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12849
12983
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
12850
12984
  });
@@ -13053,6 +13187,7 @@ ${changed2.map((c) => ` - ${c}
13053
13187
  ticket: ticketHint,
13054
13188
  repo: ticketRepo,
13055
13189
  divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
13190
+ skillOverrides: await fetchSkillOverrides(route, { userAgent: cfg.ship.userAgent }),
13056
13191
  agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
13057
13192
  resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
13058
13193
  });
@@ -13287,7 +13422,7 @@ ${changed2.map((c) => ` - ${c}
13287
13422
  }
13288
13423
  let target;
13289
13424
  try {
13290
- target = findRoute(cfg, positional[2]);
13425
+ target = findSyncRoute(cfg, positional[2]);
13291
13426
  } catch (e) {
13292
13427
  if (e instanceof ConfigError) {
13293
13428
  process.stderr.write(`crew: ${e.message}
@@ -13312,9 +13447,14 @@ ${changed2.map((c) => ` - ${c}
13312
13447
  }
13313
13448
  throw e;
13314
13449
  }
13450
+ process.stdout.write(`route: ${target.route}
13451
+ `);
13315
13452
  for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
13316
13453
  `);
13317
13454
  for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
13455
+ `);
13456
+ const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
13457
+ for (const w of skillMarkerWarnings(target.promptsDir, briefs)) process.stderr.write(`crew agents sync: warning: ${w}
13318
13458
  `);
13319
13459
  let skillResult;
13320
13460
  try {
@@ -13351,7 +13491,7 @@ ${changed2.map((c) => ` - ${c}
13351
13491
  }
13352
13492
  let target;
13353
13493
  try {
13354
- target = findRoute(cfg, positional[2]);
13494
+ target = findSyncRoute(cfg, positional[2]);
13355
13495
  } catch (e) {
13356
13496
  if (e instanceof ConfigError) {
13357
13497
  process.stderr.write(`crew: ${e.message}
@@ -13376,6 +13516,8 @@ ${changed2.map((c) => ` - ${c}
13376
13516
  }
13377
13517
  throw e;
13378
13518
  }
13519
+ process.stdout.write(`route: ${target.route}
13520
+ `);
13379
13521
  for (const o of result.outcomes) process.stdout.write(`${describeSkillSyncOutcome(o)}
13380
13522
  `);
13381
13523
  if (!dryRun) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.38.0",
3
+ "version": "0.39.1",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,3 +1,5 @@
1
+ {{skill: Crew Lane Agent}}
2
+
1
3
 
2
4
  ---
3
5
 
@@ -1,3 +1,5 @@
1
+ {{skill: Crew Lane Agent}}
2
+
1
3
 
2
4
  ---
3
5
 
@@ -1,3 +1,5 @@
1
+ {{skill: Crew Lane Agent}}
2
+
1
3
 
2
4
  ---
3
5
 
@@ -1,3 +1,7 @@
1
+ ---
2
+ name: Crew Lane Agent
3
+ description: The building lanes' shared policy (queue, worktrees, hand-off, release mechanics, stopping). Included at run time by the dev, design and QA briefs through a skill marker.
4
+ ---
1
5
  You are a ticket-implementation agent, running on the operator's machine on a
2
6
  schedule. You pick up approved tickets from the board and implement them in
3
7
  this repository, one at a time, each in its own **git worktree** cut from the
@@ -1,3 +1,5 @@
1
+ {{skill: Crew Lane Agent}}
2
+
1
3
 
2
4
  ---
3
5
 
@@ -1,3 +1,5 @@
1
+ {{skill: Crew Lane Agent}}
2
+
1
3
 
2
4
  ---
3
5
 
@@ -1,3 +1,5 @@
1
+ {{skill: Crew Lane Agent}}
2
+
1
3
 
2
4
  ---
3
5
 
@@ -1,3 +1,7 @@
1
+ ---
2
+ name: Crew Lane Agent
3
+ description: The building lanes' shared policy (queue, worktrees, hand-off, release mechanics, stopping). Included at run time by the dev, design and QA briefs through a skill marker.
4
+ ---
1
5
  You are a ticket-implementation agent, running on the operator's machine on a
2
6
  schedule. You pick up approved tickets from the board and implement them in
3
7
  this repository, one at a time, each in its own **git worktree** cut from the