@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 +25 -12
- package/dist/cli.js +321 -179
- 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
|
@@ -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
|
|
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
|
|
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 =
|
|
7011
|
-
if (!
|
|
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 =
|
|
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 =
|
|
7246
|
+
const digestPath2 = join11(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
7020
7247
|
let digest;
|
|
7021
7248
|
let ageSeconds;
|
|
7022
|
-
if (
|
|
7249
|
+
if (existsSync10(digestPath2)) {
|
|
7023
7250
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
7024
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
7251
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync9(digestPath2, "utf8");
|
|
7025
7252
|
}
|
|
7026
|
-
const common = usesCommon ?
|
|
7027
|
-
const brief =
|
|
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:
|
|
7034
|
-
brief:
|
|
7271
|
+
common: "",
|
|
7272
|
+
brief: expanded.text,
|
|
7035
7273
|
digest
|
|
7036
7274
|
});
|
|
7037
|
-
const promptSha = createHash2("sha256").update(
|
|
7038
|
-
const streamsDir =
|
|
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:
|
|
7095
|
-
eventsPath:
|
|
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
|
|
7302
|
-
import { dirname as dirname7, join as
|
|
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
|
|
7307
|
-
import { join as
|
|
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 (
|
|
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 &&
|
|
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 =
|
|
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 || !
|
|
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
|
|
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
|
|
7649
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
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 ??
|
|
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 =
|
|
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
|
|
7767
|
-
import { dirname as dirname9, join as
|
|
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) =>
|
|
8663
|
+
const files = o.repo.release.versionFiles.map((f) => join15(o.cwd, f));
|
|
8426
8664
|
const first = files[0];
|
|
8427
|
-
if (!
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
8546
|
-
const dir =
|
|
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 =
|
|
8561
|
-
if (!
|
|
8562
|
-
mkdirSync8(dirname9(
|
|
8563
|
-
copyFileSync(src,
|
|
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
|
|
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 (!
|
|
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
|
|
9374
|
-
import { join as
|
|
9375
|
-
import { TablationClient as
|
|
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 =
|
|
9389
|
-
if (!
|
|
9390
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
9391
|
-
const commonPath =
|
|
9392
|
-
if (!
|
|
9393
|
-
return `${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,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
|