@tablation/crew 0.77.0 → 0.78.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 +1 -0
- package/dist/cli.js +268 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -263,6 +263,7 @@ crew inbox [--member NAME] your tickets across every workspace
|
|
|
263
263
|
crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
|
|
264
264
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
|
|
265
265
|
crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
|
|
266
|
+
crew repos check [PATH] validate a checkout's .crew.yaml (parse, release readiness, hook syntax) without attaching it (--route ROUTE, --strict, --json)
|
|
266
267
|
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo [--replace] writes them, --clear route removes them, --recipes, --hook KEY=CMD, --agent, --json)
|
|
267
268
|
crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
|
|
268
269
|
crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
|
package/dist/cli.js
CHANGED
|
@@ -230,6 +230,13 @@ function extractTurnTokens(line) {
|
|
|
230
230
|
if (!usage2) return void 0;
|
|
231
231
|
return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
|
|
232
232
|
}
|
|
233
|
+
function extractPrefixTokens(line) {
|
|
234
|
+
const obj = line;
|
|
235
|
+
if (!obj || obj.type !== "assistant") return void 0;
|
|
236
|
+
const usage2 = obj.message?.usage;
|
|
237
|
+
if (!usage2) return void 0;
|
|
238
|
+
return (usage2.input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
|
|
239
|
+
}
|
|
233
240
|
function extractResult(line) {
|
|
234
241
|
const obj = line;
|
|
235
242
|
if (!obj || obj.type !== "result") return void 0;
|
|
@@ -580,6 +587,7 @@ var claudeAdapter = {
|
|
|
580
587
|
mapStreamLine,
|
|
581
588
|
extractResult,
|
|
582
589
|
extractTurnTokens,
|
|
590
|
+
extractPrefixTokens,
|
|
583
591
|
inspectMcp(o) {
|
|
584
592
|
const s = inspectClaudeMcp(o);
|
|
585
593
|
return s.state === "no-claude" ? { state: "no-binary" } : s;
|
|
@@ -5114,6 +5122,8 @@ async function reportAgentRun(target, run2) {
|
|
|
5114
5122
|
cacheReadTokens: run2.cacheReadTokens,
|
|
5115
5123
|
cacheWriteTokens: run2.cacheWriteTokens,
|
|
5116
5124
|
costUsd: run2.costUsd,
|
|
5125
|
+
numTurns: run2.numTurns,
|
|
5126
|
+
prefixTokens: run2.prefixTokens,
|
|
5117
5127
|
promptVersion: run2.promptVersion,
|
|
5118
5128
|
promptSha: run2.promptSha
|
|
5119
5129
|
};
|
|
@@ -5271,24 +5281,31 @@ function ticketMentions(text) {
|
|
|
5271
5281
|
}
|
|
5272
5282
|
var TicketAttributionTracker = class {
|
|
5273
5283
|
#tokensByTicket = /* @__PURE__ */ new Map();
|
|
5284
|
+
#turns = 0;
|
|
5274
5285
|
#current;
|
|
5275
5286
|
/** Tracker record uuid → ticket key, learned when a write names both (a claim resolves the key from the last mention). */
|
|
5276
5287
|
#keyByRecord = /* @__PURE__ */ new Map();
|
|
5277
5288
|
#lastBy = {};
|
|
5278
|
-
/**
|
|
5289
|
+
/**
|
|
5290
|
+
* The ticket key most recently named in the transcript, or undefined before any.
|
|
5291
|
+
* Only the FIRST key in a turn counts: a closing summary names its own
|
|
5292
|
+
* ticket first and then lists others ("Other tickets", "Blocked by", "See
|
|
5293
|
+
* also"), and those trailing keys are not what the run worked (CREW-2598).
|
|
5294
|
+
*/
|
|
5279
5295
|
get current() {
|
|
5280
5296
|
return this.#current;
|
|
5281
5297
|
}
|
|
5282
5298
|
/** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
|
|
5283
5299
|
note(text) {
|
|
5284
5300
|
const mentions = ticketMentions(text);
|
|
5285
|
-
if (mentions.length > 0) this.#current = mentions[
|
|
5301
|
+
if (mentions.length > 0) this.#current = mentions[0];
|
|
5286
5302
|
}
|
|
5287
5303
|
/** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
|
|
5288
5304
|
add(text, tokens) {
|
|
5289
5305
|
const mentions = ticketMentions(text);
|
|
5290
|
-
if (mentions.length > 0) this.#current = mentions[
|
|
5306
|
+
if (mentions.length > 0) this.#current = mentions[0];
|
|
5291
5307
|
if (this.#current === void 0 || tokens <= 0) return;
|
|
5308
|
+
this.#turns++;
|
|
5292
5309
|
this.#tokensByTicket.set(this.#current, (this.#tokensByTicket.get(this.#current) ?? 0) + tokens);
|
|
5293
5310
|
}
|
|
5294
5311
|
/**
|
|
@@ -5310,16 +5327,31 @@ var TicketAttributionTracker = class {
|
|
|
5310
5327
|
/**
|
|
5311
5328
|
* How the attribution is decided: a hand-off write beats a claim, a claim
|
|
5312
5329
|
* beats a comment (so a follow-up ticket filed or commented on while
|
|
5313
|
-
* working never displaces the claimed one), and any write beats
|
|
5314
|
-
*
|
|
5315
|
-
*
|
|
5330
|
+
* working never displaces the claimed one), and any write beats everything
|
|
5331
|
+
* below. Next, `hintMoved` — the post-run read-back showed the hinted
|
|
5332
|
+
* ticket changed status or assignee during the run — is a hand-off signal
|
|
5333
|
+
* for a run that wrote through MCP tools crew cannot see. Then the
|
|
5334
|
+
* mention-token heuristic, which with a hint may override it only when a
|
|
5335
|
+
* different ticket holds a strict majority of the credited tokens (a single
|
|
5336
|
+
* credited turn, like a lone end-of-run summary naming another ticket,
|
|
5337
|
+
* must not). A run with no hint
|
|
5338
|
+
* keeps the plain winner. The pre-run hint is last.
|
|
5316
5339
|
*/
|
|
5317
|
-
decide(hint) {
|
|
5340
|
+
decide(hint, opts = {}) {
|
|
5318
5341
|
for (const by of ["handoff", "claim", "comment"]) {
|
|
5319
5342
|
const ticket = this.#lastBy[by];
|
|
5320
5343
|
if (ticket) return { ticket, by };
|
|
5321
5344
|
}
|
|
5322
|
-
if (
|
|
5345
|
+
if (hint && opts.hintMoved) return { ticket: hint, by: "readback" };
|
|
5346
|
+
if (this.#tokensByTicket.size > 0) {
|
|
5347
|
+
if (!hint) return { ticket: this.winner(), by: "mentions" };
|
|
5348
|
+
const winner = this.winner(hint);
|
|
5349
|
+
if (winner === hint) return { ticket: hint, by: "mentions" };
|
|
5350
|
+
let total = 0;
|
|
5351
|
+
for (const n of this.#tokensByTicket.values()) total += n;
|
|
5352
|
+
const share = (this.#tokensByTicket.get(winner) ?? 0) / total;
|
|
5353
|
+
return share > 0.5 && this.#turns > 1 ? { ticket: winner, by: "mentions" } : { ticket: hint, by: "hint" };
|
|
5354
|
+
}
|
|
5323
5355
|
return { ticket: hint, by: "hint" };
|
|
5324
5356
|
}
|
|
5325
5357
|
/**
|
|
@@ -8467,6 +8499,8 @@ var RunObserver = class {
|
|
|
8467
8499
|
/** Hand-off writes and their outcomes, to tell a run whose hand-off never landed (CREW-2363). */
|
|
8468
8500
|
handoff = new HandoffTranscriptTracker();
|
|
8469
8501
|
result;
|
|
8502
|
+
/** Prefix tokens of the first assistant call that carried usage; kept once. */
|
|
8503
|
+
prefixTokens;
|
|
8470
8504
|
/** Set once the stream carries the harness's final result (turn count or cost). */
|
|
8471
8505
|
sawResult = false;
|
|
8472
8506
|
muted = false;
|
|
@@ -8593,6 +8627,7 @@ var RunObserver = class {
|
|
|
8593
8627
|
}
|
|
8594
8628
|
}
|
|
8595
8629
|
}
|
|
8630
|
+
if (this.prefixTokens === void 0 && parsed !== void 0) this.prefixTokens = this.adapter.extractPrefixTokens?.(parsed);
|
|
8596
8631
|
const turnTokens = parsed === void 0 ? void 0 : this.adapter.extractTurnTokens(parsed);
|
|
8597
8632
|
if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
|
|
8598
8633
|
}
|
|
@@ -8616,8 +8651,15 @@ function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
|
8616
8651
|
} catch {
|
|
8617
8652
|
}
|
|
8618
8653
|
}
|
|
8619
|
-
async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
8620
|
-
|
|
8654
|
+
async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved) {
|
|
8655
|
+
let moved = false;
|
|
8656
|
+
if (hintMoved) {
|
|
8657
|
+
try {
|
|
8658
|
+
moved = await hintMoved();
|
|
8659
|
+
} catch {
|
|
8660
|
+
}
|
|
8661
|
+
}
|
|
8662
|
+
const { ticket: attributedTicket, by: attributedBy } = obs.attribution.decide(obs.liveTicket, { hintMoved: moved });
|
|
8621
8663
|
const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
|
|
8622
8664
|
const { result } = obs;
|
|
8623
8665
|
const pending = end.interrupted ? null : obs.handoff.pending();
|
|
@@ -8649,6 +8691,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
|
8649
8691
|
...pending ? { handoff: "pending" } : {},
|
|
8650
8692
|
sessionId: result?.sessionId ?? null,
|
|
8651
8693
|
numTurns: result?.numTurns ?? null,
|
|
8694
|
+
prefixTokens: obs.prefixTokens ?? null,
|
|
8652
8695
|
totalCostUsd: result?.costUsd ?? null
|
|
8653
8696
|
});
|
|
8654
8697
|
} catch {
|
|
@@ -8661,6 +8704,8 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
|
8661
8704
|
...pending ? { handoff: "pending" } : {},
|
|
8662
8705
|
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
8663
8706
|
...identity,
|
|
8707
|
+
attributedTicket: attributedTicket ?? null,
|
|
8708
|
+
attributedBy,
|
|
8664
8709
|
...result?.sessionId ? { sessionId: result.sessionId } : {},
|
|
8665
8710
|
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
8666
8711
|
...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
|
|
@@ -8679,6 +8724,9 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
|
8679
8724
|
cacheReadTokens: result?.cacheReadTokens,
|
|
8680
8725
|
cacheWriteTokens: result?.cacheWriteTokens,
|
|
8681
8726
|
costUsd: result?.costUsd,
|
|
8727
|
+
// Both come from a finished run: without the result line (interrupted) neither is trustworthy.
|
|
8728
|
+
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
8729
|
+
...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
|
|
8682
8730
|
harness: identity.harness,
|
|
8683
8731
|
provider: identity.provider,
|
|
8684
8732
|
promptVersion: id.promptVersion,
|
|
@@ -8723,7 +8771,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
|
8723
8771
|
}
|
|
8724
8772
|
return { ms: ms2, ticket: attributedTicket, handoffPending: !!pending };
|
|
8725
8773
|
}
|
|
8726
|
-
function spawnAgent(plan, emit) {
|
|
8774
|
+
function spawnAgent(plan, emit, hintMoved) {
|
|
8727
8775
|
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
8728
8776
|
const started = Date.now();
|
|
8729
8777
|
const startedAt = new Date(started).toISOString();
|
|
@@ -8788,7 +8836,7 @@ function spawnAgent(plan, emit) {
|
|
|
8788
8836
|
obs.end();
|
|
8789
8837
|
await endSink(eventsSink);
|
|
8790
8838
|
if (spawnFailed) return;
|
|
8791
|
-
const { ms: ms2, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey);
|
|
8839
|
+
const { ms: ms2, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey, hintMoved);
|
|
8792
8840
|
resolve6({ code: code ?? 1, ms: ms2, ...handoffPending ? { handoffPending } : {} });
|
|
8793
8841
|
});
|
|
8794
8842
|
child.stdin?.on("error", (err) => {
|
|
@@ -9041,10 +9089,10 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
9041
9089
|
}
|
|
9042
9090
|
|
|
9043
9091
|
// src/cli.ts
|
|
9044
|
-
import { resolve as resolve5, dirname as dirname20, join as join37, basename as
|
|
9092
|
+
import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename12 } from "path";
|
|
9045
9093
|
import { fileURLToPath } from "url";
|
|
9046
9094
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
9047
|
-
import { spawnSync as
|
|
9095
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
9048
9096
|
import { parse as parseYaml2 } from "yaml";
|
|
9049
9097
|
|
|
9050
9098
|
// src/argv.ts
|
|
@@ -14393,12 +14441,107 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
14393
14441
|
};
|
|
14394
14442
|
}
|
|
14395
14443
|
|
|
14444
|
+
// src/repos-check.ts
|
|
14445
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
14446
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
14447
|
+
import { basename as basename8 } from "path";
|
|
14448
|
+
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
14449
|
+
function checkHookSyntax(script, shell) {
|
|
14450
|
+
const spec = resolveShell(shell);
|
|
14451
|
+
const name = basename8(spec.bin).replace(/\.exe$/i, "").toLowerCase();
|
|
14452
|
+
let run2;
|
|
14453
|
+
if (POSIX_SHELL.test(name)) {
|
|
14454
|
+
run2 = spawnSync6(spec.bin, ["-n"], { input: script, encoding: "utf8" });
|
|
14455
|
+
} else if (/^(pwsh|powershell)$/.test(name)) {
|
|
14456
|
+
const parse3 = "$e=$null;[void][System.Management.Automation.Language.Parser]::ParseInput($env:CREW_CHECK_SCRIPT,[ref]$null,[ref]$e);if($e){$e|ForEach-Object{[Console]::Error.WriteLine($_.Message)};exit 1}";
|
|
14457
|
+
run2 = spawnSync6(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
|
|
14458
|
+
encoding: "utf8",
|
|
14459
|
+
env: { ...process.env, CREW_CHECK_SCRIPT: script }
|
|
14460
|
+
});
|
|
14461
|
+
} else {
|
|
14462
|
+
return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
|
|
14463
|
+
}
|
|
14464
|
+
if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
|
|
14465
|
+
if (run2.status === 0) return { status: "ok" };
|
|
14466
|
+
const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
|
|
14467
|
+
return { status: "error", message };
|
|
14468
|
+
}
|
|
14469
|
+
function renderSamples(cfg, dir) {
|
|
14470
|
+
const checkout = basename8(dir.replace(/[/\\]+$/, ""));
|
|
14471
|
+
const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
|
|
14472
|
+
const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
|
|
14473
|
+
return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
|
|
14474
|
+
const branch = renderBranchName(effectiveBranchTemplate(cfg, ctx.prefix), ctx);
|
|
14475
|
+
const worktree = effectiveWorktreeDirName(cfg, dir, branch, "123", (t) => renderWorktreeName(t, { ...ctx, dir: checkout }));
|
|
14476
|
+
return { case: label, branch, worktree };
|
|
14477
|
+
});
|
|
14478
|
+
}
|
|
14479
|
+
function checkRepoSpec(dir, opts = {}) {
|
|
14480
|
+
const empty = { parsed: null, effective: null, readiness: [], hooks: [], shadowed: [], samples: [] };
|
|
14481
|
+
const file = findRepoConfig(dir);
|
|
14482
|
+
if (!file) {
|
|
14483
|
+
return { ...empty, file: null, error: `no .crew.yaml, .crew.yml or .crew.json in ${dir}`, exitCode: 1 };
|
|
14484
|
+
}
|
|
14485
|
+
let parsed;
|
|
14486
|
+
try {
|
|
14487
|
+
parsed = parseRepoConfig(readFileSync24(file, "utf8"), file);
|
|
14488
|
+
} catch (e) {
|
|
14489
|
+
if (!(e instanceof RepoConfigError)) throw e;
|
|
14490
|
+
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
14491
|
+
}
|
|
14492
|
+
const effective = resolveRepoConfig(parsed, opts.ship, dir);
|
|
14493
|
+
const readiness = validateEffective(effective);
|
|
14494
|
+
const hooks = Object.keys(effective.hooks).map((name) => ({
|
|
14495
|
+
name,
|
|
14496
|
+
label: hookLabel(effective, name),
|
|
14497
|
+
syntax: checkHookSyntax(effective.hooks[name], effective.shell)
|
|
14498
|
+
}));
|
|
14499
|
+
const failed = hooks.some((h) => h.syntax.status === "error");
|
|
14500
|
+
return {
|
|
14501
|
+
file,
|
|
14502
|
+
parsed,
|
|
14503
|
+
effective,
|
|
14504
|
+
readiness,
|
|
14505
|
+
hooks,
|
|
14506
|
+
shadowed: effective.shadowed,
|
|
14507
|
+
samples: renderSamples(effective, dir),
|
|
14508
|
+
error: null,
|
|
14509
|
+
exitCode: failed || opts.strict && readiness.length > 0 ? 1 : 0
|
|
14510
|
+
};
|
|
14511
|
+
}
|
|
14512
|
+
function renderRepoCheck(r) {
|
|
14513
|
+
if (r.error) return `${r.file ?? "crew repos check"}: ${r.error}
|
|
14514
|
+
`;
|
|
14515
|
+
const e = r.effective;
|
|
14516
|
+
const lines = [
|
|
14517
|
+
`${r.file}`,
|
|
14518
|
+
` platform ${e.platform}`,
|
|
14519
|
+
` release.mode ${e.release.mode}`,
|
|
14520
|
+
` release.versioning ${e.release.versioning}`,
|
|
14521
|
+
` branch.base ${e.branch.base}`,
|
|
14522
|
+
` branch.name ${e.branch.name}`,
|
|
14523
|
+
` branch.push ${e.branch.push}`
|
|
14524
|
+
];
|
|
14525
|
+
for (const s of r.samples) lines.push(` ${s.case}: branch ${s.branch}, worktree ../${s.worktree}`);
|
|
14526
|
+
lines.push(r.hooks.length ? " hooks" : " hooks none");
|
|
14527
|
+
for (const h of r.hooks) {
|
|
14528
|
+
const named = h.label !== e.hooks[h.name] ? ` (${h.label})` : "";
|
|
14529
|
+
const syntax = h.syntax.status === "ok" ? "syntax ok" : h.syntax.status === "unchecked" ? `syntax not checked: ${h.syntax.message}` : "syntax ERROR";
|
|
14530
|
+
lines.push(` ${h.name}${named}: ${syntax}`);
|
|
14531
|
+
if (h.syntax.status === "error") lines.push(...(h.syntax.message ?? "").split("\n").map((l) => ` ${l}`));
|
|
14532
|
+
}
|
|
14533
|
+
if (r.shadowed.length) lines.push(` this ship declares ${r.shadowed.join(", ")}, and the file overrides it \u2014 the file wins`);
|
|
14534
|
+
for (const p of r.readiness) lines.push(` - release readiness: ${p}`);
|
|
14535
|
+
return `${lines.join("\n")}
|
|
14536
|
+
`;
|
|
14537
|
+
}
|
|
14538
|
+
|
|
14396
14539
|
// src/repos-hooks.ts
|
|
14397
14540
|
import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
|
|
14398
14541
|
|
|
14399
14542
|
// src/repos-cmd.ts
|
|
14400
14543
|
import { homedir as homedir9 } from "os";
|
|
14401
|
-
import { basename as
|
|
14544
|
+
import { basename as basename9, join as join28, resolve as resolve3 } from "path";
|
|
14402
14545
|
import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
|
|
14403
14546
|
var ReposError = class extends Error {
|
|
14404
14547
|
};
|
|
@@ -14470,7 +14613,7 @@ function configuredRepos(text, route, base) {
|
|
|
14470
14613
|
});
|
|
14471
14614
|
}
|
|
14472
14615
|
const dir = m.get("dir");
|
|
14473
|
-
return dir ? [{ name:
|
|
14616
|
+
return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
14474
14617
|
}
|
|
14475
14618
|
function cloneBaseOf(text, route, base, isCheckout) {
|
|
14476
14619
|
const m = routeMapOf(parseDocument2(text), route);
|
|
@@ -14559,7 +14702,7 @@ function planRepoAdd(i) {
|
|
|
14559
14702
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
14560
14703
|
}
|
|
14561
14704
|
const matched = matchRepoName(i.origin, i.resolved);
|
|
14562
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ??
|
|
14705
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
|
|
14563
14706
|
let warning;
|
|
14564
14707
|
if (!matched) {
|
|
14565
14708
|
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
@@ -15075,7 +15218,7 @@ ${run2.err}` });
|
|
|
15075
15218
|
}
|
|
15076
15219
|
|
|
15077
15220
|
// src/sample-ticket.ts
|
|
15078
|
-
import { basename as
|
|
15221
|
+
import { basename as basename10, dirname as dirname17, join as join29 } from "path";
|
|
15079
15222
|
var SAMPLE_REPO = "crew-sample";
|
|
15080
15223
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
15081
15224
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -15191,8 +15334,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
15191
15334
|
}
|
|
15192
15335
|
|
|
15193
15336
|
// src/worktree-sweep.ts
|
|
15194
|
-
import { existsSync as existsSync27, readFileSync as
|
|
15195
|
-
import { basename as
|
|
15337
|
+
import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync8 } from "fs";
|
|
15338
|
+
import { basename as basename11, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
|
|
15196
15339
|
function planWorktreeSweep(target, tickets, contract) {
|
|
15197
15340
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
15198
15341
|
const verified = contract.statuses.verified;
|
|
@@ -15223,7 +15366,7 @@ function ticketForWorktree(w, byNumber, contract) {
|
|
|
15223
15366
|
const t = num ? byNumber.get(num) : void 0;
|
|
15224
15367
|
if (t && ticketStems(t).has(stem)) return t;
|
|
15225
15368
|
}
|
|
15226
|
-
const dir =
|
|
15369
|
+
const dir = basename11(w.path);
|
|
15227
15370
|
const m = DIR_NUMBER.exec(dir);
|
|
15228
15371
|
if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
|
|
15229
15372
|
if (!w.branch) {
|
|
@@ -15239,7 +15382,7 @@ function worktreeTicketNumbers(target) {
|
|
|
15239
15382
|
const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
|
|
15240
15383
|
if (n) nums.add(n);
|
|
15241
15384
|
}
|
|
15242
|
-
const m = DIR_NUMBER_SUFFIXED.exec(
|
|
15385
|
+
const m = DIR_NUMBER_SUFFIXED.exec(basename11(w.path));
|
|
15243
15386
|
if (m) nums.add(m[1]);
|
|
15244
15387
|
}
|
|
15245
15388
|
for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
|
|
@@ -15247,7 +15390,7 @@ function worktreeTicketNumbers(target) {
|
|
|
15247
15390
|
}
|
|
15248
15391
|
function siblingWorktreeDirs(target) {
|
|
15249
15392
|
const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
|
|
15250
|
-
const stem = `${
|
|
15393
|
+
const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
15251
15394
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
15252
15395
|
let names;
|
|
15253
15396
|
try {
|
|
@@ -15272,7 +15415,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
|
|
|
15272
15415
|
}
|
|
15273
15416
|
function warnOrphanedWorktreeDirs(paths, log, held) {
|
|
15274
15417
|
for (const p of paths) {
|
|
15275
|
-
const number = DIR_NUMBER_SUFFIXED.exec(
|
|
15418
|
+
const number = DIR_NUMBER_SUFFIXED.exec(basename11(p))?.[1];
|
|
15276
15419
|
const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
|
|
15277
15420
|
if (when) {
|
|
15278
15421
|
log.warn(`orphaned directory ${p}: its ticket is deployed but it is not a registered worktree; the sweep will not delete it \u2014 remove it by hand`, { step: "worktree" });
|
|
@@ -15362,7 +15505,7 @@ function isHandBackMerge(path, base) {
|
|
|
15362
15505
|
if (!msgPath) return false;
|
|
15363
15506
|
let first;
|
|
15364
15507
|
try {
|
|
15365
|
-
first =
|
|
15508
|
+
first = readFileSync25(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
15366
15509
|
} catch {
|
|
15367
15510
|
return false;
|
|
15368
15511
|
}
|
|
@@ -15416,7 +15559,7 @@ function isTicketBranch(name, base, stems) {
|
|
|
15416
15559
|
}
|
|
15417
15560
|
function sweepCandidatesExist(cwd, base) {
|
|
15418
15561
|
const linked = worktrees(cwd).slice(1);
|
|
15419
|
-
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(
|
|
15562
|
+
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename11(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
|
|
15420
15563
|
if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
|
|
15421
15564
|
return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
|
|
15422
15565
|
}
|
|
@@ -15512,13 +15655,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
15512
15655
|
}
|
|
15513
15656
|
|
|
15514
15657
|
// src/cli.ts
|
|
15515
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
15658
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync31, existsSync as existsSync34, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
|
|
15516
15659
|
import { createInterface } from "readline/promises";
|
|
15517
15660
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
15518
15661
|
|
|
15519
15662
|
// src/ssh-keys.ts
|
|
15520
15663
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
15521
|
-
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as
|
|
15664
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync26, chmodSync } from "fs";
|
|
15522
15665
|
import { dirname as dirname19, join as join32 } from "path";
|
|
15523
15666
|
var SshKeyError = class extends Error {
|
|
15524
15667
|
};
|
|
@@ -15552,7 +15695,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
15552
15695
|
if (!existsSync29(publicKeyPath)) {
|
|
15553
15696
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
15554
15697
|
}
|
|
15555
|
-
const publicKey =
|
|
15698
|
+
const publicKey = readFileSync26(publicKeyPath, "utf8").trim();
|
|
15556
15699
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
15557
15700
|
}
|
|
15558
15701
|
function sshKeygenAvailable() {
|
|
@@ -15769,7 +15912,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
15769
15912
|
|
|
15770
15913
|
// src/tunnel.ts
|
|
15771
15914
|
import { spawn as spawn5 } from "child_process";
|
|
15772
|
-
import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as
|
|
15915
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync27, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
|
|
15773
15916
|
import { randomInt } from "crypto";
|
|
15774
15917
|
import { join as join33 } from "path";
|
|
15775
15918
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -15844,7 +15987,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
15844
15987
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
15845
15988
|
if (!existsSync30(path)) return void 0;
|
|
15846
15989
|
try {
|
|
15847
|
-
return JSON.parse(
|
|
15990
|
+
return JSON.parse(readFileSync27(path, "utf8"));
|
|
15848
15991
|
} catch {
|
|
15849
15992
|
return void 0;
|
|
15850
15993
|
}
|
|
@@ -16071,7 +16214,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
16071
16214
|
import { createServer as createHttpServer } from "http";
|
|
16072
16215
|
import { spawn as spawn6 } from "child_process";
|
|
16073
16216
|
import { randomBytes } from "crypto";
|
|
16074
|
-
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as
|
|
16217
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
|
|
16075
16218
|
import { join as join34 } from "path";
|
|
16076
16219
|
|
|
16077
16220
|
// src/secret-compare.ts
|
|
@@ -16167,7 +16310,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
16167
16310
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
16168
16311
|
if (!existsSync31(path)) return void 0;
|
|
16169
16312
|
try {
|
|
16170
|
-
return JSON.parse(
|
|
16313
|
+
return JSON.parse(readFileSync28(path, "utf8"));
|
|
16171
16314
|
} catch {
|
|
16172
16315
|
return void 0;
|
|
16173
16316
|
}
|
|
@@ -16284,11 +16427,11 @@ function describeCause(err) {
|
|
|
16284
16427
|
}
|
|
16285
16428
|
|
|
16286
16429
|
// src/daemon.ts
|
|
16287
|
-
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as
|
|
16430
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
|
|
16288
16431
|
import { join as join36 } from "path";
|
|
16289
16432
|
|
|
16290
16433
|
// src/interrupted.ts
|
|
16291
|
-
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as
|
|
16434
|
+
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync29 } from "fs";
|
|
16292
16435
|
import { join as join35 } from "path";
|
|
16293
16436
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
16294
16437
|
function interruptedComment(b) {
|
|
@@ -16310,7 +16453,7 @@ function runFinished(streamPath) {
|
|
|
16310
16453
|
const path = `${streamPath}.meta.json`;
|
|
16311
16454
|
if (!existsSync32(path)) return false;
|
|
16312
16455
|
try {
|
|
16313
|
-
return JSON.parse(
|
|
16456
|
+
return JSON.parse(readFileSync29(path, "utf8")).running !== true;
|
|
16314
16457
|
} catch {
|
|
16315
16458
|
return true;
|
|
16316
16459
|
}
|
|
@@ -16467,7 +16610,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
16467
16610
|
}
|
|
16468
16611
|
function readDaemonStarted(stateDir) {
|
|
16469
16612
|
try {
|
|
16470
|
-
const v = JSON.parse(
|
|
16613
|
+
const v = JSON.parse(readFileSync30(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
16471
16614
|
return typeof v === "number" ? v : null;
|
|
16472
16615
|
} catch {
|
|
16473
16616
|
return null;
|
|
@@ -16819,7 +16962,19 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
16819
16962
|
}
|
|
16820
16963
|
try {
|
|
16821
16964
|
const runStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
16822
|
-
|
|
16965
|
+
let hintMoved;
|
|
16966
|
+
if (workingId) {
|
|
16967
|
+
try {
|
|
16968
|
+
const before = await tracker.ticket(workingId);
|
|
16969
|
+
const col = tracker.contract.columns;
|
|
16970
|
+
hintMoved = async () => {
|
|
16971
|
+
const after = await tracker.ticket(workingId);
|
|
16972
|
+
return after[col.status] !== before[col.status] || after[col.assignee] !== before[col.assignee];
|
|
16973
|
+
};
|
|
16974
|
+
} catch {
|
|
16975
|
+
}
|
|
16976
|
+
}
|
|
16977
|
+
await spawnAgent(plan, emit, hintMoved);
|
|
16823
16978
|
await guardRunHandoff(route, ship, tracker, role, workingId, emit, runStart);
|
|
16824
16979
|
} finally {
|
|
16825
16980
|
if (memberId) {
|
|
@@ -17018,6 +17173,7 @@ function usage(problem, requested = false) {
|
|
|
17018
17173
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
17019
17174
|
crew ship rename NAME [--json] rename this ship in crew.yaml and on every workspace's Ships row
|
|
17020
17175
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
17176
|
+
crew repos check [PATH] validate a checkout's .crew.yaml without attaching it (--route ROUTE, --strict, --json)
|
|
17021
17177
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
17022
17178
|
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
|
|
17023
17179
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches (--json: every Repos row too)
|
|
@@ -17116,6 +17272,22 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
17116
17272
|
"worktree"
|
|
17117
17273
|
]);
|
|
17118
17274
|
if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
|
|
17275
|
+
function runReposCheck(dir, ship) {
|
|
17276
|
+
const report = checkRepoSpec(dir, { ship, strict: flag("strict") });
|
|
17277
|
+
if (flag("json")) {
|
|
17278
|
+
const { exitCode: _exit, ...rest } = report;
|
|
17279
|
+
process.stdout.write(`${JSON.stringify(rest)}
|
|
17280
|
+
`);
|
|
17281
|
+
} else {
|
|
17282
|
+
process.stdout.write(renderRepoCheck(report));
|
|
17283
|
+
}
|
|
17284
|
+
process.exitCode = report.exitCode;
|
|
17285
|
+
}
|
|
17286
|
+
var reposCheckBare = command === "repos" && positional[1] === "check" && !value("route") && positional.length < 4;
|
|
17287
|
+
if (reposCheckBare) {
|
|
17288
|
+
runReposCheck(resolvePath(positional[2] ?? "."));
|
|
17289
|
+
process.exit(process.exitCode ?? 0);
|
|
17290
|
+
}
|
|
17119
17291
|
function findSyncRoute(c, name) {
|
|
17120
17292
|
if (!name && c.routes.length > 1) {
|
|
17121
17293
|
throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
|
|
@@ -17502,7 +17674,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17502
17674
|
`
|
|
17503
17675
|
);
|
|
17504
17676
|
if (!dryRun) {
|
|
17505
|
-
|
|
17677
|
+
spawnSync7("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
17506
17678
|
try {
|
|
17507
17679
|
unlinkSync5(staleTimer);
|
|
17508
17680
|
} catch {
|
|
@@ -17542,7 +17714,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17542
17714
|
async function doctorFix(o) {
|
|
17543
17715
|
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))));
|
|
17544
17716
|
const unitExists = (p) => existsSync34(p);
|
|
17545
|
-
const crontab = o.host === "linux" ?
|
|
17717
|
+
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
17546
17718
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
17547
17719
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
17548
17720
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -17558,7 +17730,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17558
17730
|
for (const a of plan.actions) {
|
|
17559
17731
|
if (a.kind === "enable") {
|
|
17560
17732
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
17561
|
-
writeFileSync23(cfg.configFile, planEnable(
|
|
17733
|
+
writeFileSync23(cfg.configFile, planEnable(readFileSync31(cfg.configFile, "utf8"), a.route));
|
|
17562
17734
|
enabledSound.add(a.route);
|
|
17563
17735
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
17564
17736
|
} else {
|
|
@@ -18559,7 +18731,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18559
18731
|
}
|
|
18560
18732
|
let failed = false;
|
|
18561
18733
|
for (const cmd2 of commands) {
|
|
18562
|
-
const res2 =
|
|
18734
|
+
const res2 = spawnSync7(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
18563
18735
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
18564
18736
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
18565
18737
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -18577,7 +18749,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18577
18749
|
`);
|
|
18578
18750
|
break;
|
|
18579
18751
|
}
|
|
18580
|
-
const res =
|
|
18752
|
+
const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
18581
18753
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
18582
18754
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
18583
18755
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -18828,7 +19000,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18828
19000
|
`);
|
|
18829
19001
|
if (!dryRun) {
|
|
18830
19002
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
18831
|
-
const raw = existsSync34(resolvedPath) ? JSON.parse(
|
|
19003
|
+
const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
|
|
18832
19004
|
raw.agentSkills = result.agentSkills;
|
|
18833
19005
|
mkdirSync22(dirOf(resolvedPath), { recursive: true });
|
|
18834
19006
|
writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -18854,7 +19026,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18854
19026
|
throw e;
|
|
18855
19027
|
}
|
|
18856
19028
|
const oldName = cfg.ship.name;
|
|
18857
|
-
writeFileSync23(cfg.configFile, setShipNameInYaml(
|
|
19029
|
+
writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), newName));
|
|
18858
19030
|
cfg.ship.name = newName;
|
|
18859
19031
|
let renameMachineId;
|
|
18860
19032
|
try {
|
|
@@ -18897,11 +19069,37 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18897
19069
|
case "repos": {
|
|
18898
19070
|
const sub = positional[1];
|
|
18899
19071
|
const usage2 = () => {
|
|
18900
|
-
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo [--replace]] [--clear route] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
|
|
19072
|
+
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos check [PATH | --route ROUTE [PATH] | ROUTE NAME] [--strict] [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo [--replace]] [--clear route] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
|
|
18901
19073
|
process.exit(2);
|
|
18902
19074
|
};
|
|
18903
|
-
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
18904
|
-
const configText =
|
|
19075
|
+
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
19076
|
+
const configText = readFileSync31(cfg.configFile, "utf8");
|
|
19077
|
+
if (sub === "check") {
|
|
19078
|
+
try {
|
|
19079
|
+
const routeArg = value("route") ?? positional[2];
|
|
19080
|
+
const found = cfg.routes.find((r) => r.route === routeArg);
|
|
19081
|
+
if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
|
|
19082
|
+
const named2 = value("route") ? void 0 : positional[3];
|
|
19083
|
+
const dir = named2 ? repoDirOf(configText, dirname20(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
|
|
19084
|
+
const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
|
|
19085
|
+
const o = target ? found.repoOverrides[target.name] : void 0;
|
|
19086
|
+
runReposCheck(dir, {
|
|
19087
|
+
hooks: { ...found.hooks, ...o?.hooks },
|
|
19088
|
+
labels: { ...found.labels, ...o?.labels },
|
|
19089
|
+
release: mergeRouteRelease(found.release, o?.release),
|
|
19090
|
+
branch: { ...found.branch, ...o?.branch },
|
|
19091
|
+
worktrees: { prefix: shipWorktreePrefixFor(found), nameOverride: o?.worktreeName }
|
|
19092
|
+
});
|
|
19093
|
+
} catch (e) {
|
|
19094
|
+
if (e instanceof ReposError) {
|
|
19095
|
+
process.stderr.write(`crew repos: ${e.message}
|
|
19096
|
+
`);
|
|
19097
|
+
process.exit(2);
|
|
19098
|
+
}
|
|
19099
|
+
throw e;
|
|
19100
|
+
}
|
|
19101
|
+
break;
|
|
19102
|
+
}
|
|
18905
19103
|
if (sub === "hooks") {
|
|
18906
19104
|
try {
|
|
18907
19105
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
@@ -18915,7 +19113,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18915
19113
|
}
|
|
18916
19114
|
}, read: (r) => {
|
|
18917
19115
|
try {
|
|
18918
|
-
return
|
|
19116
|
+
return readFileSync31(resolvePath(dir, r), "utf8");
|
|
18919
19117
|
} catch {
|
|
18920
19118
|
return void 0;
|
|
18921
19119
|
}
|
|
@@ -19056,7 +19254,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
19056
19254
|
let resolvedRemotes;
|
|
19057
19255
|
if (existsSync34(resolvedFile)) {
|
|
19058
19256
|
try {
|
|
19059
|
-
resolvedRemotes = JSON.parse(
|
|
19257
|
+
resolvedRemotes = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
19060
19258
|
} catch {
|
|
19061
19259
|
}
|
|
19062
19260
|
}
|
|
@@ -19077,7 +19275,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
19077
19275
|
isCheckout,
|
|
19078
19276
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
19079
19277
|
try {
|
|
19080
|
-
return Object.keys(parseRepoConfig(
|
|
19278
|
+
return Object.keys(parseRepoConfig(readFileSync31(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
19081
19279
|
} catch {
|
|
19082
19280
|
return false;
|
|
19083
19281
|
}
|
|
@@ -19186,7 +19384,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
19186
19384
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
19187
19385
|
`);
|
|
19188
19386
|
if (existsSync34(resolvedFile)) {
|
|
19189
|
-
const raw = JSON.parse(
|
|
19387
|
+
const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
19190
19388
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
19191
19389
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
19192
19390
|
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -19245,7 +19443,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
19245
19443
|
}
|
|
19246
19444
|
for (const e of entries) {
|
|
19247
19445
|
process.stdout.write(
|
|
19248
|
-
`${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}
|
|
19446
|
+
`${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}${e.num_turns != null ? ` turns ${e.num_turns}` : ""}${e.prefix_tokens != null ? ` prefix ${e.prefix_tokens}` : ""}
|
|
19249
19447
|
`
|
|
19250
19448
|
);
|
|
19251
19449
|
}
|
|
@@ -19263,7 +19461,9 @@ ticket: ${entry.ticket_reference ?? "(none)"}
|
|
|
19263
19461
|
model: ${entry.model ?? "?"}
|
|
19264
19462
|
harness: ${entry.harness ?? "?"}
|
|
19265
19463
|
provider: ${entry.provider ?? "?"}
|
|
19266
|
-
`
|
|
19464
|
+
` + (entry.num_turns != null ? `turns: ${entry.num_turns}
|
|
19465
|
+
` : "") + (entry.prefix_tokens != null ? `prefix: ${entry.prefix_tokens} tokens
|
|
19466
|
+
` : "")
|
|
19267
19467
|
);
|
|
19268
19468
|
if (flag("prompt")) {
|
|
19269
19469
|
process.stdout.write(`
|
|
@@ -19544,7 +19744,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19544
19744
|
process.exit(2);
|
|
19545
19745
|
}
|
|
19546
19746
|
if (found.shipNameAdopted && !dryRun) {
|
|
19547
|
-
writeFileSync23(cfg.configFile, setShipNameInYaml(
|
|
19747
|
+
writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
19548
19748
|
cfg.ship.name = found.shipNameAdopted;
|
|
19549
19749
|
}
|
|
19550
19750
|
if (found.shipRow) {
|
|
@@ -19590,7 +19790,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19590
19790
|
const hadPreviousFile = existsSync34(resolvedPath);
|
|
19591
19791
|
if (hadPreviousFile) {
|
|
19592
19792
|
try {
|
|
19593
|
-
previous = JSON.parse(
|
|
19793
|
+
previous = JSON.parse(readFileSync31(resolvedPath, "utf8"));
|
|
19594
19794
|
} catch {
|
|
19595
19795
|
}
|
|
19596
19796
|
}
|
|
@@ -20475,7 +20675,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20475
20675
|
};
|
|
20476
20676
|
const droppedRoutes = [];
|
|
20477
20677
|
try {
|
|
20478
|
-
const rawCfg = parseYaml2(
|
|
20678
|
+
const rawCfg = parseYaml2(readFileSync31(cfg.configFile, "utf8"));
|
|
20479
20679
|
for (const r of rawCfg?.routes ?? []) {
|
|
20480
20680
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
20481
20681
|
try {
|
|
@@ -20511,7 +20711,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20511
20711
|
}
|
|
20512
20712
|
const host = hostPlatform();
|
|
20513
20713
|
const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
|
|
20514
|
-
const unitStatus = control?.statusCommand ?
|
|
20714
|
+
const unitStatus = control?.statusCommand ? spawnSync7(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
|
|
20515
20715
|
const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
|
|
20516
20716
|
if (!unitRunning) {
|
|
20517
20717
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
@@ -20523,7 +20723,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20523
20723
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
20524
20724
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
20525
20725
|
for (const cmd of [control.stopCommand, control.startCommand]) {
|
|
20526
|
-
const res =
|
|
20726
|
+
const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
20527
20727
|
if (cmd === control.startCommand && (res.error || res.status !== 0)) {
|
|
20528
20728
|
process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
|
|
20529
20729
|
`);
|
|
@@ -20569,7 +20769,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20569
20769
|
}
|
|
20570
20770
|
case "log": {
|
|
20571
20771
|
try {
|
|
20572
|
-
const text =
|
|
20772
|
+
const text = readFileSync31(cfg.ship.logFile, "utf8").split("\n");
|
|
20573
20773
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
20574
20774
|
`);
|
|
20575
20775
|
} catch {
|
|
@@ -20592,7 +20792,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20592
20792
|
};
|
|
20593
20793
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
20594
20794
|
const tracker = new Tracker(route, cfg.ship);
|
|
20595
|
-
const configText =
|
|
20795
|
+
const configText = readFileSync31(cfg.configFile, "utf8");
|
|
20596
20796
|
const cfgBase = dirname20(cfg.configFile);
|
|
20597
20797
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
20598
20798
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
@@ -20630,14 +20830,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20630
20830
|
if (row) {
|
|
20631
20831
|
await tracker.deleteRepoRow(row.id);
|
|
20632
20832
|
if (existsSync34(resolvedFile)) {
|
|
20633
|
-
const raw = JSON.parse(
|
|
20833
|
+
const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
20634
20834
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
20635
20835
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
20636
20836
|
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
20637
20837
|
`);
|
|
20638
20838
|
}
|
|
20639
20839
|
}
|
|
20640
|
-
if (existsSync34(join37(dir, ".git")) &&
|
|
20840
|
+
if (existsSync34(join37(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
|
|
20641
20841
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
20642
20842
|
break;
|
|
20643
20843
|
}
|
|
@@ -20653,7 +20853,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20653
20853
|
writeFileSync23(join37(dir, "README.md"), SAMPLE_README);
|
|
20654
20854
|
writeFileSync23(join37(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
20655
20855
|
const g = (...a) => {
|
|
20656
|
-
const r =
|
|
20856
|
+
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
20657
20857
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
20658
20858
|
};
|
|
20659
20859
|
g("init", "-b", "main");
|
|
@@ -20670,7 +20870,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20670
20870
|
});
|
|
20671
20871
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
20672
20872
|
if (existsSync34(resolvedFile)) {
|
|
20673
|
-
const raw = JSON.parse(
|
|
20873
|
+
const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
20674
20874
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
20675
20875
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
20676
20876
|
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -20901,7 +21101,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20901
21101
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
20902
21102
|
// config-provenance display; this is the field that actually
|
|
20903
21103
|
// describes where a worktree lands by default.
|
|
20904
|
-
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${
|
|
21104
|
+
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
|
|
20905
21105
|
worktreeParent: resolvePath(r.dir, "..")
|
|
20906
21106
|
})),
|
|
20907
21107
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -21068,7 +21268,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
21068
21268
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
21069
21269
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
21070
21270
|
try {
|
|
21071
|
-
return
|
|
21271
|
+
return readFileSync31(p, "utf8");
|
|
21072
21272
|
} catch {
|
|
21073
21273
|
return null;
|
|
21074
21274
|
}
|
|
@@ -21111,7 +21311,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
21111
21311
|
);
|
|
21112
21312
|
for (const r of await resolvedRepos(route)) {
|
|
21113
21313
|
const { config } = r;
|
|
21114
|
-
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${
|
|
21314
|
+
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
|
|
21115
21315
|
process.stdout.write(
|
|
21116
21316
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
21117
21317
|
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
package/package.json
CHANGED