@tablation/crew 0.38.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.
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
@@ -6607,9 +6607,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
6607
6607
 
6608
6608
  // src/agent.ts
6609
6609
  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";
6610
+ import { readFileSync as readFileSync9, existsSync as existsSync10, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
6611
6611
  import { finished } from "stream/promises";
6612
- import { join as join10, dirname as dirname6 } from "path";
6612
+ import { join as join11, dirname as dirname6 } from "path";
6613
6613
  import { createHash as createHash2 } from "crypto";
6614
6614
 
6615
6615
  // src/environment.ts
@@ -6953,6 +6953,199 @@ async function reportAgentRun(target, run2) {
6953
6953
  return { agentLogId: entry.id };
6954
6954
  }
6955
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
+
6956
7149
  // src/ticket-attribution.ts
6957
7150
  var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
6958
7151
  function ticketMentions(text) {
@@ -7007,35 +7200,45 @@ var AgentError = class extends Error {
7007
7200
  var AgentSpawnError = class extends AgentError {
7008
7201
  };
7009
7202
  function planAgentRun(o) {
7010
- const briefPath = join10(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7011
- if (!existsSync9(briefPath)) {
7203
+ const briefPath = join11(o.route.promptsDir, "personas", `lane-${o.role}.md`);
7204
+ if (!existsSync10(briefPath)) {
7012
7205
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
7013
7206
  }
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}`);
7207
+ const commonPath = join11(o.route.promptsDir, "personas", "common.md");
7208
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync10(commonPath);
7017
7209
  const now = o.now ?? (() => Date.now());
7018
7210
  const routeLabel = routeSlug(o.route.route);
7019
- const digestPath2 = join10(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7211
+ const digestPath2 = join11(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
7020
7212
  let digest;
7021
7213
  let ageSeconds;
7022
- if (existsSync9(digestPath2)) {
7214
+ if (existsSync10(digestPath2)) {
7023
7215
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
7024
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
7216
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync9(digestPath2, "utf8");
7025
7217
  }
7026
- const common = usesCommon ? readFileSync8(commonPath, "utf8") : "";
7027
- const brief = readFileSync8(briefPath, "utf8");
7218
+ const common = usesCommon ? readFileSync9(commonPath, "utf8") : "";
7219
+ const brief = readFileSync9(briefPath, "utf8");
7028
7220
  const effectiveCommon = o.divergedPrompt ? "" : common;
7029
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
+ }
7030
7233
  const prompt = assemblePrompt({
7031
7234
  roster: o.roster,
7032
7235
  environment: o.environment,
7033
- common: effectiveCommon,
7034
- brief: effectiveBrief,
7236
+ common: "",
7237
+ brief: expanded.text,
7035
7238
  digest
7036
7239
  });
7037
- const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
7038
- const streamsDir = join10(o.stateDir, "streams");
7240
+ const promptSha = createHash2("sha256").update(expanded.text).digest("hex");
7241
+ const streamsDir = join11(o.stateDir, "streams");
7039
7242
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
7040
7243
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
7041
7244
  const { toolchain, model } = seat;
@@ -7071,7 +7274,7 @@ function planAgentRun(o) {
7071
7274
  harness: toolchain.harness,
7072
7275
  provider: toolchainProvider(toolchain),
7073
7276
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
7074
- warnings: seat.warnings,
7277
+ warnings: [...seat.warnings ?? [], ...expanded.warnings],
7075
7278
  prompt,
7076
7279
  promptBytes: Buffer.byteLength(prompt, "utf8"),
7077
7280
  unsetEnv: adapter.unsetEnv,
@@ -7091,8 +7294,8 @@ function planAgentRun(o) {
7091
7294
  digestAttached: digest !== void 0,
7092
7295
  digestAgeSeconds: ageSeconds,
7093
7296
  model,
7094
- streamPath: join10(streamsDir, `${base}.jsonl`),
7095
- eventsPath: join10(streamsDir, `${base}.events.jsonl`),
7297
+ streamPath: join11(streamsDir, `${base}.jsonl`),
7298
+ eventsPath: join11(streamsDir, `${base}.events.jsonl`),
7096
7299
  ticket: o.ticket,
7097
7300
  agentLog,
7098
7301
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -7298,13 +7501,13 @@ function spawnAgent(plan, emit) {
7298
7501
  }
7299
7502
 
7300
7503
  // src/run-worktree.ts
7301
- import { existsSync as existsSync11 } from "fs";
7302
- 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";
7303
7506
 
7304
7507
  // src/ports.ts
7305
7508
  import { execFileSync as execFileSync2 } from "child_process";
7306
- import { existsSync as existsSync10 } from "fs";
7307
- import { join as join11 } from "path";
7509
+ import { existsSync as existsSync11 } from "fs";
7510
+ import { join as join12 } from "path";
7308
7511
  var run = (cmd, args) => {
7309
7512
  try {
7310
7513
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7349,7 +7552,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7349
7552
  }
7350
7553
  function worktreeExists(parent, prefix, n) {
7351
7554
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7352
- if (existsSync10(join11(parent, name, ".git"))) return true;
7555
+ if (existsSync11(join12(parent, name, ".git"))) return true;
7353
7556
  }
7354
7557
  return false;
7355
7558
  }
@@ -7423,7 +7626,7 @@ function planRunWorktree(dir, cfg, t, role) {
7423
7626
  const existing = worktreeForNumber(dir, num);
7424
7627
  const located = locateBranchForTicket(dir, cfg, t, role);
7425
7628
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7426
- if (existing && !existing.prunable && existsSync11(existing.path)) {
7629
+ if (existing && !existing.prunable && existsSync12(existing.path)) {
7427
7630
  const branch = existing.branch;
7428
7631
  if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
7429
7632
  if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
@@ -7437,7 +7640,7 @@ function planRunWorktree(dir, cfg, t, role) {
7437
7640
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7438
7641
  return { action: "recut", path: existing.path, branch, base: baseRef };
7439
7642
  }
7440
- const path = join12(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7643
+ const path = join13(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7441
7644
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7442
7645
  return { action: "cut-remote", path, branch: located.branch };
7443
7646
  }
@@ -7484,7 +7687,7 @@ function worktreeHoldsUnpushed(path, remote) {
7484
7687
  }
7485
7688
  async function finishRunWorktree(dir, cfg, branch) {
7486
7689
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7487
- if (!wt || !existsSync11(wt.path)) return { kind: "none" };
7690
+ if (!wt || !existsSync12(wt.path)) return { kind: "none" };
7488
7691
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7489
7692
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7490
7693
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7496,7 +7699,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7496
7699
  if (pids.length) await killGently(pids);
7497
7700
  }
7498
7701
  gitOk(dir, ["worktree", "remove", wt.path]);
7499
- 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 };
7500
7703
  }
7501
7704
 
7502
7705
  // src/handoff-guard.ts
@@ -7645,13 +7848,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
7645
7848
  }
7646
7849
 
7647
7850
  // 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";
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";
7650
7853
  import { stringify } from "yaml";
7651
7854
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
7652
7855
  function shouldRunWizard(opts) {
7653
7856
  if (!opts.isTTY) return false;
7654
- const exists = opts.configExists ?? existsSync12;
7857
+ const exists = opts.configExists ?? existsSync13;
7655
7858
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
7656
7859
  }
7657
7860
  function firstRunConfigPath() {
@@ -7668,7 +7871,7 @@ function defaultIsExecutable(p) {
7668
7871
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
7669
7872
  for (const dir of pathEnv.split(delimiter)) {
7670
7873
  if (!dir) continue;
7671
- const candidate = join13(dir, name);
7874
+ const candidate = join14(dir, name);
7672
7875
  if (isExecutable(candidate)) return candidate;
7673
7876
  }
7674
7877
  return void 0;
@@ -7763,8 +7966,8 @@ Wrote ${o.configPath}.
7763
7966
  }
7764
7967
 
7765
7968
  // 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";
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";
7768
7971
  import { tmpdir as tmpdir2 } from "os";
7769
7972
 
7770
7973
  // src/release.ts
@@ -8422,13 +8625,13 @@ async function bump(o, size) {
8422
8625
  }
8423
8626
  return printed;
8424
8627
  }
8425
- 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));
8426
8629
  const first = files[0];
8427
- if (!existsSync13(first)) {
8630
+ if (!existsSync14(first)) {
8428
8631
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
8429
8632
  return null;
8430
8633
  }
8431
- const current = JSON.parse(readFileSync9(first, "utf8")).version;
8634
+ const current = JSON.parse(readFileSync10(first, "utf8")).version;
8432
8635
  if (!current) {
8433
8636
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
8434
8637
  return null;
@@ -8441,7 +8644,7 @@ async function bump(o, size) {
8441
8644
  return version;
8442
8645
  }
8443
8646
  for (const f of files) {
8444
- const src = readFileSync9(f, "utf8");
8647
+ const src = readFileSync10(f, "utf8");
8445
8648
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
8446
8649
  if (out === src) {
8447
8650
  o.emit.error(`no version field in ${f}`);
@@ -8454,13 +8657,13 @@ async function bump(o, size) {
8454
8657
  function writeChangelog(o, version, entries) {
8455
8658
  const file = o.repo.release.changelog;
8456
8659
  if (!file || entries.length === 0) return;
8457
- const path = join14(o.cwd, file);
8660
+ const path = join15(o.cwd, file);
8458
8661
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
8459
8662
  if (o.dryRun) {
8460
8663
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
8461
8664
  return;
8462
8665
  }
8463
- const existing = existsSync13(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
8666
+ const existing = existsSync14(path) ? readFileSync10(path, "utf8") : "# Changelog\n";
8464
8667
  writeFileSync9(path, insertChangelogSection(existing, section));
8465
8668
  }
8466
8669
  async function confirm(o, expected) {
@@ -8542,8 +8745,8 @@ async function confirmLastRelease(o, decision) {
8542
8745
  function openReleaseCheckout(o) {
8543
8746
  const { remote, base } = o.repo.branch;
8544
8747
  const branch = `crew/release-${Date.now()}`;
8545
- const root = mkdtempSync(join14(tmpdir2(), "crew-release-"));
8546
- const dir = join14(root, "checkout");
8748
+ const root = mkdtempSync(join15(tmpdir2(), "crew-release-"));
8749
+ const dir = join15(root, "checkout");
8547
8750
  const close = () => {
8548
8751
  gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
8549
8752
  rmSync6(root, { recursive: true, force: true });
@@ -8557,10 +8760,10 @@ function openReleaseCheckout(o) {
8557
8760
  throw e;
8558
8761
  }
8559
8762
  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));
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));
8564
8767
  }
8565
8768
  return { dir, branch, close };
8566
8769
  }
@@ -9029,7 +9232,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
9029
9232
  }
9030
9233
 
9031
9234
  // src/watch.ts
9032
- 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";
9033
9236
  var E = "\x1B[";
9034
9237
  var dim = (s) => `${E}2m${s}${E}0m`;
9035
9238
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -9130,7 +9333,7 @@ function renderHeaders(states, now = Date.now()) {
9130
9333
  return states.map((s) => renderHeader(s, now)).join("");
9131
9334
  }
9132
9335
  function readFrom(file, from) {
9133
- if (!existsSync14(file)) return { events: [], offset: from };
9336
+ if (!existsSync15(file)) return { events: [], offset: from };
9134
9337
  const size = statSync6(file).size;
9135
9338
  if (size < from) return readFrom(file, 0);
9136
9339
  if (size === from) return { events: [], offset: from };
@@ -9370,9 +9573,9 @@ function nextRoles(decision, ran) {
9370
9573
  }
9371
9574
 
9372
9575
  // 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";
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";
9376
9579
  var PERSONA_NAME = {
9377
9580
  dev: "Developer",
9378
9581
  design: "Design",
@@ -9385,19 +9588,19 @@ var CREW_AGENT_COLUMN = "agent_id";
9385
9588
  var AgentsSyncError = class extends Error {
9386
9589
  };
9387
9590
  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")}`;
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")}`;
9394
9597
  }
9395
9598
  async function resolveAgentId(route, role, opts) {
9396
9599
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
9397
9600
  if (cached) return cached;
9398
9601
  const seatId = route.resolved?.seats?.[role];
9399
9602
  if (!seatId || !route.resolved) return void 0;
9400
- const client2 = new TablationClient2({
9603
+ const client2 = new TablationClient3({
9401
9604
  baseUrl: `${route.baseUrl}/api`,
9402
9605
  apiKey: resolveApiKey(route),
9403
9606
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9412,7 +9615,7 @@ async function resolveAgentId(route, role, opts) {
9412
9615
  async function fetchDivergedPrompt(route, role, opts) {
9413
9616
  const agentId = await resolveAgentId(route, role, opts);
9414
9617
  if (!agentId || !route.resolved) return void 0;
9415
- const client2 = new TablationClient2({
9618
+ const client2 = new TablationClient3({
9416
9619
  baseUrl: `${route.baseUrl}/api`,
9417
9620
  apiKey: resolveApiKey(route),
9418
9621
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9450,7 +9653,7 @@ function choiceValue(v) {
9450
9653
  async function fetchSeatPersona(route, role, opts) {
9451
9654
  const agentId = await resolveAgentId(route, role, opts);
9452
9655
  if (!agentId || !route.resolved) return void 0;
9453
- const client2 = new TablationClient2({
9656
+ const client2 = new TablationClient3({
9454
9657
  baseUrl: `${route.baseUrl}/api`,
9455
9658
  apiKey: resolveApiKey(route),
9456
9659
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9498,7 +9701,7 @@ async function syncPersonas(route, opts) {
9498
9701
  throw new AgentsSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
9499
9702
  }
9500
9703
  const workspaceId = route.resolved.workspaceId;
9501
- const client2 = new TablationClient2({
9704
+ const client2 = new TablationClient3({
9502
9705
  baseUrl: `${route.baseUrl}/api`,
9503
9706
  apiKey: resolveApiKey(route),
9504
9707
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -9507,7 +9710,7 @@ async function syncPersonas(route, opts) {
9507
9710
  try {
9508
9711
  agentsModel = await client2.dataModels.get("agents", workspaceId);
9509
9712
  } catch (e) {
9510
- if (e instanceof TablationApiError && e.status === 404) {
9713
+ if (e instanceof TablationApiError2 && e.status === 404) {
9511
9714
  throw new AgentsSyncError(
9512
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`
9513
9716
  );
@@ -9581,7 +9784,7 @@ async function syncPersonas(route, opts) {
9581
9784
  agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
9582
9785
  outcomes.push({ role, action: "updated", agentId: row.id });
9583
9786
  } catch (e) {
9584
- if (e instanceof StaleWriteError6) {
9787
+ if (e instanceof StaleWriteError7) {
9585
9788
  outcomes.push({ role, action: "diverged", agentId: row.id });
9586
9789
  continue;
9587
9790
  }
@@ -10066,119 +10269,6 @@ import {
10066
10269
  DeviceLoginError
10067
10270
  } from "@tablation/client";
10068
10271
 
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
10272
  // src/logbook.ts
10183
10273
  import { createHash as createHash3 } from "crypto";
10184
10274
  import { TablationClient as TablationClient4 } from "@tablation/client";
@@ -11655,6 +11745,7 @@ async function runRoleAgent(role, decision, ctx) {
11655
11745
  ticket: ticketHint,
11656
11746
  repo: ticketRepo,
11657
11747
  divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
11748
+ skillOverrides: await fetchSkillOverrides(route, { userAgent: ship.userAgent }),
11658
11749
  agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
11659
11750
  resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
11660
11751
  });
@@ -12749,6 +12840,7 @@ ${changed2.map((c) => ` - ${c}
12749
12840
  ticket: w.decision.actionable.top?.issue_id,
12750
12841
  repo: repoTargetFor(w.route, w.decision.actionable.top?.repo_id)?.name,
12751
12842
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12843
+ skillOverrides: await fetchSkillOverrides(w.route, { userAgent: cfg.ship.userAgent }),
12752
12844
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12753
12845
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
12754
12846
  });
@@ -12845,6 +12937,7 @@ ${changed2.map((c) => ` - ${c}
12845
12937
  ticket: fleetTicketHint,
12846
12938
  repo: fleetTicketRepo,
12847
12939
  divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12940
+ skillOverrides: await fetchSkillOverrides(w.route, { userAgent: cfg.ship.userAgent }),
12848
12941
  agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
12849
12942
  resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
12850
12943
  });
@@ -13053,6 +13146,7 @@ ${changed2.map((c) => ` - ${c}
13053
13146
  ticket: ticketHint,
13054
13147
  repo: ticketRepo,
13055
13148
  divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
13149
+ skillOverrides: await fetchSkillOverrides(route, { userAgent: cfg.ship.userAgent }),
13056
13150
  agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
13057
13151
  resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
13058
13152
  });
@@ -13315,6 +13409,9 @@ ${changed2.map((c) => ` - ${c}
13315
13409
  for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
13316
13410
  `);
13317
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}
13318
13415
  `);
13319
13416
  let skillResult;
13320
13417
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
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