@tablation/crew 0.36.0 → 0.38.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 +22 -7
- package/dist/cli.js +123 -39
- package/package.json +1 -1
- package/prompts/default/personas/common.md +425 -853
- package/prompts/default/personas/lane-design.md +61 -96
- package/prompts/default/personas/lane-dev.md +13 -15
- package/prompts/default/personas/lane-pair.md +64 -84
- package/prompts/default/personas/lane-qa.md +125 -158
- package/prompts/default/personas/lane-triage.md +79 -64
- package/prompts/default/skills/design-pass.md +53 -0
- package/prompts/default/skills/filing-a-ticket.md +56 -0
- package/prompts/default/skills/question-and-investigation.md +59 -0
- package/prompts/default/skills/running-the-suite.md +56 -0
- package/prompts/default/skills/ticket-attachments.md +34 -0
- package/prompts/default/skills/workspace-change-authorization.md +35 -0
- package/prompts/default/skills/worktree-setup.md +101 -0
- package/prompts/dev-qa/personas/common.md +416 -821
- package/prompts/dev-qa/personas/lane-dev.md +13 -14
- package/prompts/dev-qa/personas/lane-pair.md +64 -84
- package/prompts/dev-qa/personas/lane-qa.md +124 -144
- package/prompts/dev-qa/personas/lane-triage.md +69 -56
- package/prompts/dev-qa/skills/filing-a-ticket.md +56 -0
- package/prompts/dev-qa/skills/question-and-investigation.md +59 -0
- package/prompts/dev-qa/skills/running-the-suite.md +56 -0
- package/prompts/dev-qa/skills/ticket-attachments.md +34 -0
- package/prompts/dev-qa/skills/workspace-change-authorization.md +35 -0
- package/prompts/dev-qa/skills/worktree-setup.md +101 -0
package/README.md
CHANGED
|
@@ -36,13 +36,19 @@ prompt set (the three-lane dev/design/QA policy this repo ships with — see
|
|
|
36
36
|
A fifth persona, **pair**, rides `crew agents sync` alongside these four but
|
|
37
37
|
is not polled: it is the identity a live, interactive session (an IDE
|
|
38
38
|
conversation, not a scheduled run) uses when it touches the tracker. It has
|
|
39
|
-
no Crew-table seat
|
|
40
|
-
brief (`prompts/default/personas/lane-pair.md`) is not prefixed with
|
|
41
|
-
`common.md` — none of the polling loop's shared policy describes it. `crew
|
|
39
|
+
no Crew-table seat and is never assigned a ticket by the poll loop. `crew
|
|
42
40
|
agents prompt pair` prints its current persona text (a workspace admin's
|
|
43
41
|
live edit if there is one, else the local default) for something like a
|
|
44
42
|
`SessionStart` hook to feed into a session at start.
|
|
45
43
|
|
|
44
|
+
`common.md` — the building lanes' shared policy (worktrees, one stateless
|
|
45
|
+
invocation per cycle, the dev/design/QA hand-off) — is prefixed to the
|
|
46
|
+
**dev**, **design** and **qa** briefs only. **triage** and **pair** are
|
|
47
|
+
self-contained (`SELF_CONTAINED_ROLES` in `src/config.ts`): neither cuts a
|
|
48
|
+
worktree, builds or hands anything to QA, so the policy would only
|
|
49
|
+
misdescribe them. The prompts carry rules, not history; the incidents behind
|
|
50
|
+
each rule are kept in [`docs/prompt-rationale.md`](docs/prompt-rationale.md).
|
|
51
|
+
|
|
46
52
|
Alongside personas, a prompt set can also ship **skills** —
|
|
47
53
|
`prompts/<promptSet>/skills/*.md`, each one YAML frontmatter (`name`,
|
|
48
54
|
`description`) plus a markdown prompt body. A skill isn't a seat: it's not
|
|
@@ -55,7 +61,14 @@ diverged sync narrowed to skills alone, for when you've only touched
|
|
|
55
61
|
`skills/`. Example use — a Pair session invoked via an Epic's
|
|
56
62
|
`grill_link` looks up the `Grill-Me` skill through the Tablation MCP tool
|
|
57
63
|
`agent_skills_controller_get_skill` and follows it. `prompts/default/skills/
|
|
58
|
-
grill-me.md` is
|
|
64
|
+
grill-me.md` is one shipped example; the rest are the building lanes'
|
|
65
|
+
situational procedures (`Worktree Setup`, `Running the Suite`, `Question and
|
|
66
|
+
Investigation Tickets`, `Filing a Ticket`, `Ticket Attachments`, `Workspace
|
|
67
|
+
Change Authorization`, `Design Pass`), which `common.md` names at the step
|
|
68
|
+
that needs each one — every run keeps the rule and the minimum inline and
|
|
69
|
+
fetches the full procedure only when it applies, so a dev run that never
|
|
70
|
+
files a ticket never pays for the filing procedure. `test/prompts.test.ts`
|
|
71
|
+
checks that every skill a persona names ships in the same preset.
|
|
59
72
|
|
|
60
73
|
One role runs per cycle, whichever holds the most urgent actionable ticket
|
|
61
74
|
(`src/priority.ts` decides, and the same module sorts the digest that agent is
|
|
@@ -100,10 +113,12 @@ edit its prose, and point `promptSet` at the copy (`~/my-crew-policy` or a
|
|
|
100
113
|
relative path) — no fork of this repo required. A prompt set is a complete,
|
|
101
114
|
self-contained fork of `personas/common.md` + one `personas/lane-<role>.md`
|
|
102
115
|
per seat, plus whatever `skills/*.md` it wants, not a diff against another
|
|
103
|
-
one; each persona file is read as plain prose
|
|
104
|
-
|
|
116
|
+
one; each persona file is read as plain prose (common.md ahead of a building
|
|
117
|
+
lane's brief, a self-contained brief alone) and concatenated after the
|
|
118
|
+
per-run roster/environment sections and before the queue digest
|
|
105
119
|
(`src/agent.ts`'s `assemblePrompt`), so there's no templating layer to
|
|
106
|
-
learn.
|
|
120
|
+
learn. Keep the prose generic: `test/prompts.test.ts` fails a preset that
|
|
121
|
+
names a path, a command, an id, a ticket reference or a date. `prompts/dev-qa/` is a worked example of trimming a lane out of
|
|
107
122
|
`default` cleanly, including the unused `lane-design.md` stub every
|
|
108
123
|
preset still needs today — see the next paragraph for why.
|
|
109
124
|
|
package/dist/cli.js
CHANGED
|
@@ -1806,6 +1806,7 @@ var DEFAULT_BASE_URL = "https://app.tablation.com";
|
|
|
1806
1806
|
var DEFAULT_MAX_THINKING_TOKENS = 4096;
|
|
1807
1807
|
var DEFAULT_REPOS_BASE_PATH = "~/Crew";
|
|
1808
1808
|
var ROLE_NAMES = ["dev", "design", "qa", "triage", "pair"];
|
|
1809
|
+
var SELF_CONTAINED_ROLES = /* @__PURE__ */ new Set(["pair", "triage"]);
|
|
1809
1810
|
var ROLE_LABEL = {
|
|
1810
1811
|
dev: "Dev",
|
|
1811
1812
|
design: "Design",
|
|
@@ -7011,7 +7012,8 @@ function planAgentRun(o) {
|
|
|
7011
7012
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
7012
7013
|
}
|
|
7013
7014
|
const commonPath = join10(o.route.promptsDir, "personas", "common.md");
|
|
7014
|
-
|
|
7015
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role);
|
|
7016
|
+
if (usesCommon && !existsSync9(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
|
|
7015
7017
|
const now = o.now ?? (() => Date.now());
|
|
7016
7018
|
const routeLabel = routeSlug(o.route.route);
|
|
7017
7019
|
const digestPath2 = join10(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
@@ -7021,7 +7023,7 @@ function planAgentRun(o) {
|
|
|
7021
7023
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
7022
7024
|
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
|
|
7023
7025
|
}
|
|
7024
|
-
const common = readFileSync8(commonPath, "utf8");
|
|
7026
|
+
const common = usesCommon ? readFileSync8(commonPath, "utf8") : "";
|
|
7025
7027
|
const brief = readFileSync8(briefPath, "utf8");
|
|
7026
7028
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
7027
7029
|
const effectiveBrief = o.divergedPrompt ?? brief;
|
|
@@ -9385,7 +9387,7 @@ var AgentsSyncError = class extends Error {
|
|
|
9385
9387
|
function personaDefaultPrompt(promptsDir, role) {
|
|
9386
9388
|
const briefPath = join15(promptsDir, "personas", `lane-${role}.md`);
|
|
9387
9389
|
if (!existsSync15(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
9388
|
-
if (role
|
|
9390
|
+
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync10(briefPath, "utf8");
|
|
9389
9391
|
const commonPath = join15(promptsDir, "personas", "common.md");
|
|
9390
9392
|
if (!existsSync15(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
|
|
9391
9393
|
return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
|
|
@@ -10616,7 +10618,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
10616
10618
|
}
|
|
10617
10619
|
|
|
10618
10620
|
// src/cli.ts
|
|
10619
|
-
import { readFileSync as
|
|
10621
|
+
import { readFileSync as readFileSync16, existsSync as existsSync23, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
10620
10622
|
import { createInterface } from "readline/promises";
|
|
10621
10623
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
10622
10624
|
|
|
@@ -11354,7 +11356,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
11354
11356
|
}
|
|
11355
11357
|
|
|
11356
11358
|
// src/daemon.ts
|
|
11357
|
-
import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
11359
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync15, readdirSync as readdirSync6, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
|
|
11358
11360
|
import { join as join21 } from "path";
|
|
11359
11361
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
11360
11362
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -11401,9 +11403,38 @@ function latestMtime(path) {
|
|
|
11401
11403
|
function computeTreeSignature(paths) {
|
|
11402
11404
|
return paths.map((p) => String(latestMtime(p))).join(":");
|
|
11403
11405
|
}
|
|
11404
|
-
function makeStaleChecker(paths) {
|
|
11406
|
+
function makeStaleChecker(paths, opts = {}) {
|
|
11405
11407
|
const initial = computeTreeSignature(paths);
|
|
11406
|
-
return () => computeTreeSignature(paths) !== initial;
|
|
11408
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync22(opts.reloadSentinel);
|
|
11409
|
+
}
|
|
11410
|
+
function reloadSentinelFor(stateDir) {
|
|
11411
|
+
return join21(stateDir, "reload-requested");
|
|
11412
|
+
}
|
|
11413
|
+
function daemonStampFor(stateDir) {
|
|
11414
|
+
return join21(stateDir, "daemon.started");
|
|
11415
|
+
}
|
|
11416
|
+
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
11417
|
+
mkdirSync12(stateDir, { recursive: true });
|
|
11418
|
+
rmSync8(reloadSentinelFor(stateDir), { force: true });
|
|
11419
|
+
writeFileSync12(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
11420
|
+
`);
|
|
11421
|
+
}
|
|
11422
|
+
function readDaemonStarted(stateDir) {
|
|
11423
|
+
try {
|
|
11424
|
+
const v = JSON.parse(readFileSync15(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
11425
|
+
return typeof v === "number" ? v : null;
|
|
11426
|
+
} catch {
|
|
11427
|
+
return null;
|
|
11428
|
+
}
|
|
11429
|
+
}
|
|
11430
|
+
function configChangedAfter(files, startedAt) {
|
|
11431
|
+
for (const f of files) {
|
|
11432
|
+
try {
|
|
11433
|
+
if (statSync8(f).mtimeMs > startedAt) return f;
|
|
11434
|
+
} catch {
|
|
11435
|
+
}
|
|
11436
|
+
}
|
|
11437
|
+
return null;
|
|
11407
11438
|
}
|
|
11408
11439
|
var SPAWN_PARK_AFTER = 3;
|
|
11409
11440
|
var SpawnBreaker = class {
|
|
@@ -11804,6 +11835,7 @@ function usage() {
|
|
|
11804
11835
|
crew watch [route] live view of what the crew is doing
|
|
11805
11836
|
crew status [route] [--json] paused/running state; --json for a machine reader
|
|
11806
11837
|
crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
|
|
11838
|
+
crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
|
|
11807
11839
|
crew doctor [route] [--fix] preflight; --fix enables clean routes and offers crew install
|
|
11808
11840
|
crew ports [route] which checkout owns which ports, and what is up
|
|
11809
11841
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
@@ -11858,7 +11890,12 @@ var value = (name) => {
|
|
|
11858
11890
|
var positional = argv.filter((a, i) => !a.startsWith("--") && !argv[i - 1]?.startsWith("--"));
|
|
11859
11891
|
var command = positional[0];
|
|
11860
11892
|
if (!command) usage();
|
|
11861
|
-
|
|
11893
|
+
function daemonConfigPaths(c) {
|
|
11894
|
+
const files = [c.configFile];
|
|
11895
|
+
for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
|
|
11896
|
+
return files;
|
|
11897
|
+
}
|
|
11898
|
+
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
|
|
11862
11899
|
var STATE_WRITING_SUBCOMMANDS = {
|
|
11863
11900
|
agents: /* @__PURE__ */ new Set(["sync"]),
|
|
11864
11901
|
skills: /* @__PURE__ */ new Set(["sync"])
|
|
@@ -11968,7 +12005,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
11968
12005
|
const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
|
|
11969
12006
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
11970
12007
|
const named = command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
11971
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
|
|
12008
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
|
|
11972
12009
|
let route;
|
|
11973
12010
|
try {
|
|
11974
12011
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -12175,7 +12212,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12175
12212
|
if (host === "linux" && hasSystemd) {
|
|
12176
12213
|
const label = labelFor(CREW_HOME, "run");
|
|
12177
12214
|
const staleTimer = join22(homedir7(), ".config", "systemd", "user", `${label}.timer`);
|
|
12178
|
-
if (
|
|
12215
|
+
if (existsSync23(staleTimer)) {
|
|
12179
12216
|
process.stdout.write(
|
|
12180
12217
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
12181
12218
|
`
|
|
@@ -12201,7 +12238,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12201
12238
|
const tracker = new Tracker(c, cfg.ship);
|
|
12202
12239
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
12203
12240
|
for (const r of await resolvedRepos(c)) {
|
|
12204
|
-
if (!
|
|
12241
|
+
if (!existsSync23(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
12205
12242
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
12206
12243
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
12207
12244
|
}
|
|
@@ -12219,7 +12256,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12219
12256
|
}
|
|
12220
12257
|
async function doctorFix(o) {
|
|
12221
12258
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
12222
|
-
const unitExists = (p) =>
|
|
12259
|
+
const unitExists = (p) => existsSync23(p);
|
|
12223
12260
|
const crontab = o.host === "linux" ? spawnSync5("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
12224
12261
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
12225
12262
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -12236,7 +12273,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12236
12273
|
for (const a of plan.actions) {
|
|
12237
12274
|
if (a.kind === "enable") {
|
|
12238
12275
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
12239
|
-
|
|
12276
|
+
writeFileSync13(cfg.configFile, planEnable(readFileSync16(cfg.configFile, "utf8"), a.route));
|
|
12240
12277
|
enabledSound.add(a.route);
|
|
12241
12278
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
12242
12279
|
} else {
|
|
@@ -13141,6 +13178,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13141
13178
|
}
|
|
13142
13179
|
requireArmed("run the daemon loop");
|
|
13143
13180
|
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join22(CREW_HOME, "src"), join22(CREW_HOME, "dist"), join22(CREW_HOME, "bin")];
|
|
13181
|
+
const configPaths = daemonConfigPaths(cfg);
|
|
13182
|
+
watchPaths.push(...configPaths);
|
|
13183
|
+
noteDaemonStarted(cfg.ship.stateDir);
|
|
13144
13184
|
const spawnBreaker = new SpawnBreaker();
|
|
13145
13185
|
const stopReason = await runDaemonLoop({
|
|
13146
13186
|
runPass: () => runOnePass({
|
|
@@ -13158,14 +13198,16 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13158
13198
|
`)
|
|
13159
13199
|
})
|
|
13160
13200
|
}),
|
|
13161
|
-
checkStale: makeStaleChecker(watchPaths),
|
|
13201
|
+
checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
|
|
13162
13202
|
onPass: (result) => {
|
|
13163
13203
|
if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
|
|
13164
13204
|
for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
|
|
13165
13205
|
}
|
|
13166
13206
|
});
|
|
13167
13207
|
if (stopReason === "stale") {
|
|
13168
|
-
emit.emit(
|
|
13208
|
+
emit.emit(
|
|
13209
|
+
"crew daemon: installed tree, crew.yaml or a reload request changed on disk \u2014 exiting cleanly so the OS-level restart relaunches with the update"
|
|
13210
|
+
);
|
|
13169
13211
|
process.exit(0);
|
|
13170
13212
|
}
|
|
13171
13213
|
break;
|
|
@@ -13289,11 +13331,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13289
13331
|
`);
|
|
13290
13332
|
if (!dryRun) {
|
|
13291
13333
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13292
|
-
const raw =
|
|
13334
|
+
const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
|
|
13293
13335
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
13294
13336
|
raw.agentSkills = skillResult.agentSkills;
|
|
13295
|
-
|
|
13296
|
-
|
|
13337
|
+
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
13338
|
+
writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
13297
13339
|
`);
|
|
13298
13340
|
}
|
|
13299
13341
|
if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
|
|
@@ -13338,10 +13380,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13338
13380
|
`);
|
|
13339
13381
|
if (!dryRun) {
|
|
13340
13382
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13341
|
-
const raw =
|
|
13383
|
+
const raw = existsSync23(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
|
|
13342
13384
|
raw.agentSkills = result.agentSkills;
|
|
13343
|
-
|
|
13344
|
-
|
|
13385
|
+
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
13386
|
+
writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
13345
13387
|
`);
|
|
13346
13388
|
}
|
|
13347
13389
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -13354,14 +13396,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13354
13396
|
process.exit(2);
|
|
13355
13397
|
};
|
|
13356
13398
|
if (sub !== "add" && sub !== "list") usage2();
|
|
13357
|
-
const configText =
|
|
13399
|
+
const configText = readFileSync16(cfg.configFile, "utf8");
|
|
13358
13400
|
let routeName = positional[2];
|
|
13359
13401
|
let addPath = ".";
|
|
13360
13402
|
let addName;
|
|
13361
13403
|
if (sub === "list" && !routeName) usage2();
|
|
13362
13404
|
if (sub === "add") {
|
|
13363
13405
|
try {
|
|
13364
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
13406
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync23(resolvePath(p.replace(/^~(?=\/|$)/, homedir7()))));
|
|
13365
13407
|
routeName = a.route;
|
|
13366
13408
|
addPath = a.path;
|
|
13367
13409
|
addName = a.name;
|
|
@@ -13376,9 +13418,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13376
13418
|
}
|
|
13377
13419
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
13378
13420
|
let resolvedRemotes;
|
|
13379
|
-
if (
|
|
13421
|
+
if (existsSync23(resolvedFile)) {
|
|
13380
13422
|
try {
|
|
13381
|
-
resolvedRemotes = JSON.parse(
|
|
13423
|
+
resolvedRemotes = JSON.parse(readFileSync16(resolvedFile, "utf8"));
|
|
13382
13424
|
} catch {
|
|
13383
13425
|
}
|
|
13384
13426
|
}
|
|
@@ -13393,14 +13435,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13393
13435
|
for (const r of repos) {
|
|
13394
13436
|
const origin2 = originOf(r.dir);
|
|
13395
13437
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
13396
|
-
const note = !
|
|
13438
|
+
const note = !existsSync23(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
13397
13439
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
13398
13440
|
`);
|
|
13399
13441
|
}
|
|
13400
13442
|
break;
|
|
13401
13443
|
}
|
|
13402
13444
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir7()));
|
|
13403
|
-
const top =
|
|
13445
|
+
const top = existsSync23(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
13404
13446
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
13405
13447
|
const origin = originOf(top);
|
|
13406
13448
|
const plan = planRepoAdd({
|
|
@@ -13449,15 +13491,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13449
13491
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
13450
13492
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
13451
13493
|
`);
|
|
13452
|
-
if (
|
|
13453
|
-
const raw = JSON.parse(
|
|
13494
|
+
if (existsSync23(resolvedFile)) {
|
|
13495
|
+
const raw = JSON.parse(readFileSync16(resolvedFile, "utf8"));
|
|
13454
13496
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
13455
13497
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
13456
|
-
|
|
13498
|
+
writeFileSync13(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
13457
13499
|
`);
|
|
13458
13500
|
}
|
|
13459
13501
|
}
|
|
13460
|
-
|
|
13502
|
+
writeFileSync13(cfg.configFile, plan.text);
|
|
13461
13503
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
13462
13504
|
`);
|
|
13463
13505
|
} catch (e) {
|
|
@@ -13696,10 +13738,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13696
13738
|
operator = await pickOperator(found.holds);
|
|
13697
13739
|
}
|
|
13698
13740
|
let previous;
|
|
13699
|
-
const hadPreviousFile =
|
|
13741
|
+
const hadPreviousFile = existsSync23(resolvedPath);
|
|
13700
13742
|
if (hadPreviousFile) {
|
|
13701
13743
|
try {
|
|
13702
|
-
previous = JSON.parse(
|
|
13744
|
+
previous = JSON.parse(readFileSync16(resolvedPath, "utf8"));
|
|
13703
13745
|
} catch {
|
|
13704
13746
|
}
|
|
13705
13747
|
}
|
|
@@ -13760,10 +13802,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13760
13802
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
13761
13803
|
`);
|
|
13762
13804
|
} else {
|
|
13763
|
-
|
|
13805
|
+
mkdirSync13(dirname12(resolvedPath), { recursive: true });
|
|
13764
13806
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
13765
13807
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
13766
|
-
|
|
13808
|
+
writeFileSync13(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
13767
13809
|
`);
|
|
13768
13810
|
renameSync2(tmpPath, resolvedPath);
|
|
13769
13811
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -13777,8 +13819,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13777
13819
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
13778
13820
|
`);
|
|
13779
13821
|
} else {
|
|
13780
|
-
|
|
13781
|
-
|
|
13822
|
+
mkdirSync13(dirname12(apiKeyFile), { recursive: true });
|
|
13823
|
+
writeFileSync13(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
13782
13824
|
`, { mode: 384 });
|
|
13783
13825
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
13784
13826
|
`);
|
|
@@ -14285,6 +14327,38 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14285
14327
|
if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
|
|
14286
14328
|
break;
|
|
14287
14329
|
}
|
|
14330
|
+
case "reload": {
|
|
14331
|
+
const sentinel = reloadSentinelFor(cfg.ship.stateDir);
|
|
14332
|
+
if (dryRun) {
|
|
14333
|
+
process.stdout.write(`would write ${sentinel}
|
|
14334
|
+
`);
|
|
14335
|
+
break;
|
|
14336
|
+
}
|
|
14337
|
+
const host = hostPlatform();
|
|
14338
|
+
const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
|
|
14339
|
+
const unitStatus = control?.statusCommand ? spawnSync5(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
|
|
14340
|
+
const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
|
|
14341
|
+
if (!unitRunning) {
|
|
14342
|
+
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
14343
|
+
break;
|
|
14344
|
+
}
|
|
14345
|
+
mkdirSync13(cfg.ship.stateDir, { recursive: true });
|
|
14346
|
+
writeFileSync13(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
14347
|
+
`);
|
|
14348
|
+
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
14349
|
+
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
14350
|
+
for (const cmd of [control.stopCommand, control.startCommand]) {
|
|
14351
|
+
const res = spawnSync5(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
14352
|
+
if (cmd === control.startCommand && (res.error || res.status !== 0)) {
|
|
14353
|
+
process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
|
|
14354
|
+
`);
|
|
14355
|
+
process.exitCode = 1;
|
|
14356
|
+
}
|
|
14357
|
+
}
|
|
14358
|
+
process.stdout.write("crew reload: restarted the daemon unit now\n");
|
|
14359
|
+
}
|
|
14360
|
+
break;
|
|
14361
|
+
}
|
|
14288
14362
|
case "uninstall": {
|
|
14289
14363
|
const host = hostPlatform();
|
|
14290
14364
|
if (host === "windows") {
|
|
@@ -14320,7 +14394,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14320
14394
|
}
|
|
14321
14395
|
case "log": {
|
|
14322
14396
|
try {
|
|
14323
|
-
const text =
|
|
14397
|
+
const text = readFileSync16(cfg.ship.logFile, "utf8").split("\n");
|
|
14324
14398
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
14325
14399
|
`);
|
|
14326
14400
|
} catch {
|
|
@@ -14530,13 +14604,23 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
14530
14604
|
);
|
|
14531
14605
|
try {
|
|
14532
14606
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
14533
|
-
const evSize =
|
|
14607
|
+
const evSize = existsSync23(evFile) ? statSync9(evFile).size : 0;
|
|
14534
14608
|
process.stdout.write(
|
|
14535
14609
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
14536
14610
|
` : "events log: ok\n"
|
|
14537
14611
|
);
|
|
14538
14612
|
} catch {
|
|
14539
14613
|
}
|
|
14614
|
+
const daemonStartedAt = readDaemonStarted(cfg.ship.stateDir);
|
|
14615
|
+
if (daemonStartedAt !== null) {
|
|
14616
|
+
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
|
14617
|
+
if (changed2) {
|
|
14618
|
+
process.stdout.write(
|
|
14619
|
+
`daemon config: WARN ${changed2} changed at ${new Date(statSync9(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
|
|
14620
|
+
`
|
|
14621
|
+
);
|
|
14622
|
+
}
|
|
14623
|
+
}
|
|
14540
14624
|
const tracker = new Tracker(route, cfg.ship);
|
|
14541
14625
|
const contractProblems = validateContract(tracker.contract);
|
|
14542
14626
|
process.stdout.write(
|
package/package.json
CHANGED