@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 +25 -12
- package/dist/cli.js +273 -176
- package/package.json +1 -1
- package/prompts/default/personas/lane-design.md +2 -0
- package/prompts/default/personas/lane-dev.md +2 -0
- package/prompts/default/personas/lane-qa.md +2 -0
- package/prompts/default/{personas/common.md → skills/crew-lane-agent.md} +4 -0
- package/prompts/dev-qa/personas/lane-design.md +2 -0
- package/prompts/dev-qa/personas/lane-dev.md +2 -0
- package/prompts/dev-qa/personas/lane-qa.md +2 -0
- package/prompts/dev-qa/{personas/common.md → skills/crew-lane-agent.md} +4 -0
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
|
-
|
|
45
|
-
|
|
46
|
-
**dev**, **design** and **qa** briefs
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
115
|
-
|
|
116
|
-
one; each persona file is read as plain prose (
|
|
117
|
-
|
|
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/
|
|
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
|
|
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
|
|
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 =
|
|
7011
|
-
if (!
|
|
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 =
|
|
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 =
|
|
7211
|
+
const digestPath2 = join11(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
7020
7212
|
let digest;
|
|
7021
7213
|
let ageSeconds;
|
|
7022
|
-
if (
|
|
7214
|
+
if (existsSync10(digestPath2)) {
|
|
7023
7215
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
7024
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
7216
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync9(digestPath2, "utf8");
|
|
7025
7217
|
}
|
|
7026
|
-
const common = usesCommon ?
|
|
7027
|
-
const brief =
|
|
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:
|
|
7034
|
-
brief:
|
|
7236
|
+
common: "",
|
|
7237
|
+
brief: expanded.text,
|
|
7035
7238
|
digest
|
|
7036
7239
|
});
|
|
7037
|
-
const promptSha = createHash2("sha256").update(
|
|
7038
|
-
const streamsDir =
|
|
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:
|
|
7095
|
-
eventsPath:
|
|
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
|
|
7302
|
-
import { dirname as dirname7, join as
|
|
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
|
|
7307
|
-
import { join as
|
|
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 (
|
|
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 &&
|
|
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 =
|
|
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 || !
|
|
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
|
|
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
|
|
7649
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
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 ??
|
|
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 =
|
|
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
|
|
7767
|
-
import { dirname as dirname9, join as
|
|
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) =>
|
|
8628
|
+
const files = o.repo.release.versionFiles.map((f) => join15(o.cwd, f));
|
|
8426
8629
|
const first = files[0];
|
|
8427
|
-
if (!
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
8546
|
-
const dir =
|
|
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 =
|
|
8561
|
-
if (!
|
|
8562
|
-
mkdirSync8(dirname9(
|
|
8563
|
-
copyFileSync(src,
|
|
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
|
|
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 (!
|
|
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
|
|
9374
|
-
import { join as
|
|
9375
|
-
import { TablationClient as
|
|
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 =
|
|
9389
|
-
if (!
|
|
9390
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
9391
|
-
const commonPath =
|
|
9392
|
-
if (!
|
|
9393
|
-
return `${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,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,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
|