@tablation/crew 0.63.2 → 0.64.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 +1 -1
- package/dist/cli.js +296 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -263,7 +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 hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo writes them, --json)
|
|
266
|
+
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo writes them, --recipes, --agent, --json)
|
|
267
267
|
crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
|
|
268
268
|
crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
|
|
269
269
|
```
|
package/dist/cli.js
CHANGED
|
@@ -5209,6 +5209,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
5209
5209
|
"repo",
|
|
5210
5210
|
"role",
|
|
5211
5211
|
"route",
|
|
5212
|
+
"recipes",
|
|
5212
5213
|
"ship-name",
|
|
5213
5214
|
"tail",
|
|
5214
5215
|
"ticket",
|
|
@@ -13204,17 +13205,62 @@ var DETECTORS = [
|
|
|
13204
13205
|
{ source: "pyproject.toml", detect: fromPyproject },
|
|
13205
13206
|
{ source: "Makefile", detect: fromMakefile }
|
|
13206
13207
|
];
|
|
13208
|
+
var WORKFLOW_RE = /\.ya?ml$/;
|
|
13209
|
+
function detectRecipes(f) {
|
|
13210
|
+
const workflows = (f.list?.(".github/workflows") ?? []).filter((n) => WORKFLOW_RE.test(n)).sort();
|
|
13211
|
+
if (workflows.length === 0) return [];
|
|
13212
|
+
const evidence = workflows.map((n) => `.github/workflows/${n}`);
|
|
13213
|
+
return [
|
|
13214
|
+
{
|
|
13215
|
+
id: "github-actions-release",
|
|
13216
|
+
title: "Releases run in GitHub Actions",
|
|
13217
|
+
description: "CI publishes the release after the crew merges; the crew watches `gh release view` for the live tag.",
|
|
13218
|
+
evidence,
|
|
13219
|
+
// `ltrimstr` because `verify.match: version` compares the bare version, not `v1.2.3`.
|
|
13220
|
+
hooks: { released: `gh release view --json tagName -q '.tagName | ltrimstr("v")'` },
|
|
13221
|
+
release: { mode: "ci_auto", ci: "github", verifyMatch: "version" },
|
|
13222
|
+
writable: true,
|
|
13223
|
+
defaultOn: true
|
|
13224
|
+
},
|
|
13225
|
+
{
|
|
13226
|
+
id: "github-actions-ci-status",
|
|
13227
|
+
title: "Check CI status before hand-off",
|
|
13228
|
+
description: "Ask GitHub whether the latest run on the branch passed before a ticket is handed to QA. The repo spec has no hook for this yet, so it is reported but cannot be written.",
|
|
13229
|
+
evidence,
|
|
13230
|
+
hooks: {},
|
|
13231
|
+
check: `gh run list --branch "$CREW_BRANCH" --limit 1 --json conclusion -q '.[0].conclusion == "success"'`,
|
|
13232
|
+
writable: false,
|
|
13233
|
+
defaultOn: false
|
|
13234
|
+
}
|
|
13235
|
+
];
|
|
13236
|
+
}
|
|
13207
13237
|
function suggestHooks(f) {
|
|
13238
|
+
const recipes = detectRecipes(f);
|
|
13208
13239
|
const found = DETECTORS.flatMap((d) => {
|
|
13209
13240
|
const hooks = d.detect(f);
|
|
13210
13241
|
return hooks && Object.keys(hooks).length > 0 ? [{ source: d.source, hooks }] : [];
|
|
13211
13242
|
});
|
|
13212
13243
|
if (found.length === 0) {
|
|
13213
|
-
return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"] };
|
|
13244
|
+
return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"], recipes };
|
|
13214
13245
|
}
|
|
13215
13246
|
const [first, ...others] = found;
|
|
13216
13247
|
const notes = others.length > 0 ? [`also found ${others.map((o) => o.source).join(", ")}; proposing from ${first.source} only`] : [];
|
|
13217
|
-
return { source: first.source, hooks: first.hooks, notes };
|
|
13248
|
+
return { source: first.source, hooks: first.hooks, notes, recipes };
|
|
13249
|
+
}
|
|
13250
|
+
function selectRecipes(s, ids) {
|
|
13251
|
+
const chosen = ids.map((id) => {
|
|
13252
|
+
const r = s.recipes.find((x) => x.id === id);
|
|
13253
|
+
if (!r) throw new ReposError(`no recipe "${id}" here (have: ${s.recipes.map((x) => x.id).join(", ") || "none"})`);
|
|
13254
|
+
if (!r.writable) throw new ReposError(`recipe "${id}" cannot be written: ${r.description}`);
|
|
13255
|
+
return r;
|
|
13256
|
+
});
|
|
13257
|
+
let hooks = { ...s.hooks };
|
|
13258
|
+
let release;
|
|
13259
|
+
for (const r of chosen) {
|
|
13260
|
+
hooks = { ...r.hooks, ...hooks };
|
|
13261
|
+
release ??= r.release;
|
|
13262
|
+
}
|
|
13263
|
+
return { hooks, ...release ? { release } : {} };
|
|
13218
13264
|
}
|
|
13219
13265
|
function parseHooksArgs(args, routes) {
|
|
13220
13266
|
const rest = [...args];
|
|
@@ -13224,7 +13270,7 @@ function parseHooksArgs(args, routes) {
|
|
|
13224
13270
|
if (routes.length === 1) route = routes[0];
|
|
13225
13271
|
else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
|
|
13226
13272
|
}
|
|
13227
|
-
if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--write route|repo] [--json]");
|
|
13273
|
+
if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--json]");
|
|
13228
13274
|
return { route, name: rest[0] };
|
|
13229
13275
|
}
|
|
13230
13276
|
function repoDirOf(text, base, route, name) {
|
|
@@ -13270,7 +13316,7 @@ function planRouteHooks(text, route, name, hooks) {
|
|
|
13270
13316
|
}
|
|
13271
13317
|
return { text: String(doc), written, kept };
|
|
13272
13318
|
}
|
|
13273
|
-
function starterRepoConfig(hooks) {
|
|
13319
|
+
function starterRepoConfig(hooks, release) {
|
|
13274
13320
|
const lines = [
|
|
13275
13321
|
"# Starter .crew.yaml proposed by `crew repos hooks --write repo`.",
|
|
13276
13322
|
"# Review the hooks, then commit this file; it is the durable home for them",
|
|
@@ -13283,7 +13329,12 @@ function starterRepoConfig(hooks) {
|
|
|
13283
13329
|
const entries = Object.entries(hooks).filter(([, v]) => v !== void 0);
|
|
13284
13330
|
if (entries.length === 0) lines.push(' # test: "..."', ' # build: "..."');
|
|
13285
13331
|
for (const [k, v] of entries) lines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
13286
|
-
|
|
13332
|
+
if (release) {
|
|
13333
|
+
lines.push("", "release:", ` mode: ${release.mode}`);
|
|
13334
|
+
if (release.ci) lines.push(` ci: { provider: ${release.ci} }`);
|
|
13335
|
+
if (release.verifyMatch) lines.push(" verify:", ` match: ${release.verifyMatch}`);
|
|
13336
|
+
lines.push("");
|
|
13337
|
+
} else lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
|
|
13287
13338
|
const text = lines.join("\n");
|
|
13288
13339
|
parseRepoConfig(text, ".crew.yaml");
|
|
13289
13340
|
return text;
|
|
@@ -13292,6 +13343,178 @@ function existingRepoConfig(f) {
|
|
|
13292
13343
|
return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
|
|
13293
13344
|
}
|
|
13294
13345
|
|
|
13346
|
+
// src/repos-hooks-agent.ts
|
|
13347
|
+
import { spawn as spawn4, execFileSync as execFileSync4 } from "child_process";
|
|
13348
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
13349
|
+
import { isAbsolute as isAbsolute2, relative } from "path";
|
|
13350
|
+
var AgentSuggestError = class extends Error {
|
|
13351
|
+
reason;
|
|
13352
|
+
constructor(reason, message) {
|
|
13353
|
+
super(message);
|
|
13354
|
+
this.reason = reason;
|
|
13355
|
+
}
|
|
13356
|
+
};
|
|
13357
|
+
var HOOK_KEYS = ["test", "build", "version", "bump", "released"];
|
|
13358
|
+
var READ_ONLY_TOOLS = ["Read", "Grep", "Glob"];
|
|
13359
|
+
var WRITING_TOOLS = ["Bash", "Edit", "Write", "NotebookEdit", "WebFetch", "WebSearch", "Task", "Agent"];
|
|
13360
|
+
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
13361
|
+
function buildAgentPrompt(sniff) {
|
|
13362
|
+
return [
|
|
13363
|
+
"You are reviewing a git checkout (the current directory) to work out how to test and build it.",
|
|
13364
|
+
"Read README*, CLAUDE.md, AGENTS.md, GEMINI.md, package/build manifests and .github/workflows/* as needed.",
|
|
13365
|
+
"You may only read files. Do not modify anything and do not run commands.",
|
|
13366
|
+
"",
|
|
13367
|
+
`A file sniff already proposed: ${JSON.stringify(sniff.hooks)}.`,
|
|
13368
|
+
"Propose the commands a contributor should run, using the repo's own documented ones over guesses.",
|
|
13369
|
+
"Omit any hook you cannot support from a file you read.",
|
|
13370
|
+
"",
|
|
13371
|
+
"Answer with ONLY one JSON object, no prose and no code fence:",
|
|
13372
|
+
'{"hooks": {"test": "...", "build": "...", "version": "...", "bump": "...", "released": "..."}, "notes": ["..."]}',
|
|
13373
|
+
"`version` prints the bare version, `bump` bumps by $CREW_BUMP (patch|minor|major) and prints the new version,",
|
|
13374
|
+
"`released` prints what is live (a version or sha). Keys you cannot support are left out."
|
|
13375
|
+
].join("\n");
|
|
13376
|
+
}
|
|
13377
|
+
function agentInvocation(spec, model) {
|
|
13378
|
+
if (spec.harness !== "claude") {
|
|
13379
|
+
throw new AgentSuggestError("unsupported_harness", `--agent supports the claude harness only (toolchain "${spec.name}" is ${spec.harness})`);
|
|
13380
|
+
}
|
|
13381
|
+
return wrapInvocation(spec, {
|
|
13382
|
+
model,
|
|
13383
|
+
args: [
|
|
13384
|
+
"-p",
|
|
13385
|
+
"--allowedTools",
|
|
13386
|
+
...READ_ONLY_TOOLS,
|
|
13387
|
+
"--disallowedTools",
|
|
13388
|
+
...WRITING_TOOLS,
|
|
13389
|
+
"--model",
|
|
13390
|
+
model,
|
|
13391
|
+
"--output-format",
|
|
13392
|
+
"stream-json",
|
|
13393
|
+
"--verbose",
|
|
13394
|
+
...spec.args
|
|
13395
|
+
]
|
|
13396
|
+
});
|
|
13397
|
+
}
|
|
13398
|
+
function parseAgentStream(out, dir) {
|
|
13399
|
+
let text = "";
|
|
13400
|
+
let isError = false;
|
|
13401
|
+
const files = /* @__PURE__ */ new Set();
|
|
13402
|
+
let base = dir;
|
|
13403
|
+
try {
|
|
13404
|
+
base = realpathSync2(dir);
|
|
13405
|
+
} catch {
|
|
13406
|
+
}
|
|
13407
|
+
for (const line of out.split("\n")) {
|
|
13408
|
+
if (!line.trim()) continue;
|
|
13409
|
+
let ev;
|
|
13410
|
+
try {
|
|
13411
|
+
ev = JSON.parse(line);
|
|
13412
|
+
} catch {
|
|
13413
|
+
continue;
|
|
13414
|
+
}
|
|
13415
|
+
if (ev?.type === "assistant" && Array.isArray(ev.message?.content)) {
|
|
13416
|
+
for (const b of ev.message.content) {
|
|
13417
|
+
const p = b?.type === "tool_use" && b.name === "Read" ? b.input?.file_path : void 0;
|
|
13418
|
+
if (typeof p === "string") files.add(isAbsolute2(p) ? relative(base, p) : p);
|
|
13419
|
+
}
|
|
13420
|
+
} else if (ev?.type === "result") {
|
|
13421
|
+
text = typeof ev.result === "string" ? ev.result : "";
|
|
13422
|
+
isError = ev.is_error === true;
|
|
13423
|
+
}
|
|
13424
|
+
}
|
|
13425
|
+
return { text, isError, filesRead: [...files].sort() };
|
|
13426
|
+
}
|
|
13427
|
+
function parseAgentHooks(text) {
|
|
13428
|
+
const start = text.indexOf("{");
|
|
13429
|
+
const end = text.lastIndexOf("}");
|
|
13430
|
+
if (start < 0 || end <= start) throw new AgentSuggestError("bad_output", "the toolchain did not answer with a JSON object");
|
|
13431
|
+
let obj;
|
|
13432
|
+
try {
|
|
13433
|
+
obj = JSON.parse(text.slice(start, end + 1));
|
|
13434
|
+
} catch {
|
|
13435
|
+
throw new AgentSuggestError("bad_output", "the toolchain's answer was not valid JSON");
|
|
13436
|
+
}
|
|
13437
|
+
const raw = obj && typeof obj === "object" && obj.hooks && typeof obj.hooks === "object" ? obj.hooks : {};
|
|
13438
|
+
const hooks = {};
|
|
13439
|
+
for (const k of HOOK_KEYS) if (typeof raw[k] === "string" && raw[k].trim()) hooks[k] = raw[k].trim();
|
|
13440
|
+
const notes = Array.isArray(obj.notes) ? obj.notes.filter((n) => typeof n === "string") : [];
|
|
13441
|
+
return { hooks, notes };
|
|
13442
|
+
}
|
|
13443
|
+
var SIGNED_OUT = /not logged in|\/login|invalid api key|authentication|unauthori[sz]ed|\b401\b|credit balance/i;
|
|
13444
|
+
function classifyFailure(o) {
|
|
13445
|
+
if (o.spawnError?.code === "ENOENT") return "not_installed";
|
|
13446
|
+
return SIGNED_OUT.test(o.output) ? "not_signed_in" : "failed";
|
|
13447
|
+
}
|
|
13448
|
+
function porcelain(dir) {
|
|
13449
|
+
try {
|
|
13450
|
+
return execFileSync4("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
|
|
13451
|
+
} catch {
|
|
13452
|
+
return void 0;
|
|
13453
|
+
}
|
|
13454
|
+
}
|
|
13455
|
+
async function runAgentSuggest(o) {
|
|
13456
|
+
const model = o.model ?? o.spec.model;
|
|
13457
|
+
const { bin, args } = agentInvocation(o.spec, model);
|
|
13458
|
+
const adapter = adapterFor(o.spec.harness);
|
|
13459
|
+
const env = { ...process.env, ...adapter.runEnv(o.spec) };
|
|
13460
|
+
for (const v of adapter.unsetEnv) delete env[v];
|
|
13461
|
+
delete env.CREW_API_KEY;
|
|
13462
|
+
const before = porcelain(o.dir);
|
|
13463
|
+
const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
13464
|
+
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
13465
|
+
const run2 = await new Promise((resolve6) => {
|
|
13466
|
+
let out = "";
|
|
13467
|
+
let err = "";
|
|
13468
|
+
const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
13469
|
+
let settled = false;
|
|
13470
|
+
const settle = (r) => {
|
|
13471
|
+
if (settled) return;
|
|
13472
|
+
settled = true;
|
|
13473
|
+
signal.removeEventListener("abort", onAbort);
|
|
13474
|
+
resolve6(r);
|
|
13475
|
+
};
|
|
13476
|
+
const onAbort = () => {
|
|
13477
|
+
try {
|
|
13478
|
+
if (child.pid) process.kill(-child.pid, "SIGTERM");
|
|
13479
|
+
} catch {
|
|
13480
|
+
child.kill("SIGTERM");
|
|
13481
|
+
}
|
|
13482
|
+
child.stdout.destroy();
|
|
13483
|
+
child.stderr.destroy();
|
|
13484
|
+
settle({ code: null, out, err, aborted: true });
|
|
13485
|
+
};
|
|
13486
|
+
if (signal.aborted) onAbort();
|
|
13487
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
13488
|
+
child.stdout.on("data", (d) => {
|
|
13489
|
+
out += d.toString();
|
|
13490
|
+
});
|
|
13491
|
+
child.stderr.on("data", (d) => {
|
|
13492
|
+
err += d.toString();
|
|
13493
|
+
});
|
|
13494
|
+
child.on("error", (e) => settle({ code: null, out, err, spawnError: e, aborted: false }));
|
|
13495
|
+
child.on("close", (code) => settle({ code, out, err, aborted: false }));
|
|
13496
|
+
child.stdin.on("error", () => {
|
|
13497
|
+
});
|
|
13498
|
+
child.stdin.end(buildAgentPrompt(o.sniff));
|
|
13499
|
+
});
|
|
13500
|
+
if (run2.aborted || signal.aborted) {
|
|
13501
|
+
throw new AgentSuggestError(o.signal?.aborted ? "cancelled" : "timeout", o.signal?.aborted ? "cancelled" : "the toolchain did not answer in time");
|
|
13502
|
+
}
|
|
13503
|
+
const after = porcelain(o.dir);
|
|
13504
|
+
if (before !== void 0 && after !== before) {
|
|
13505
|
+
throw new AgentSuggestError("modified_checkout", "the checkout changed during --agent; inspect it with `git status` (the agent result was discarded)");
|
|
13506
|
+
}
|
|
13507
|
+
const parsed = parseAgentStream(run2.out, o.dir);
|
|
13508
|
+
if (run2.spawnError || run2.code !== 0 || parsed.isError) {
|
|
13509
|
+
const reason = classifyFailure({ code: run2.code, ...run2.spawnError ? { spawnError: run2.spawnError } : {}, output: `${parsed.text}
|
|
13510
|
+
${run2.err}` });
|
|
13511
|
+
throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text || run2.err).trim().slice(0, 300) || `exited ${run2.code}`);
|
|
13512
|
+
}
|
|
13513
|
+
const { hooks, notes } = parseAgentHooks(parsed.text);
|
|
13514
|
+
const differsFromSniff = HOOK_KEYS.filter((k) => hooks[k] !== void 0 && hooks[k] !== o.sniff.hooks[k]);
|
|
13515
|
+
return { source: "agent", hooks, filesRead: parsed.filesRead, differsFromSniff, notes, toolchain: o.spec.name, model };
|
|
13516
|
+
}
|
|
13517
|
+
|
|
13295
13518
|
// src/sample-ticket.ts
|
|
13296
13519
|
import { basename as basename8, dirname as dirname15, join as join23 } from "path";
|
|
13297
13520
|
var SAMPLE_REPO = "crew-sample";
|
|
@@ -13719,12 +13942,12 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
13719
13942
|
}
|
|
13720
13943
|
|
|
13721
13944
|
// src/cli.ts
|
|
13722
|
-
import { realpathSync as
|
|
13945
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync25, existsSync as existsSync31, mkdirSync as mkdirSync18, writeFileSync as writeFileSync19, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
|
|
13723
13946
|
import { createInterface } from "readline/promises";
|
|
13724
13947
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
13725
13948
|
|
|
13726
13949
|
// src/ssh-keys.ts
|
|
13727
|
-
import { execFileSync as
|
|
13950
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
13728
13951
|
import { existsSync as existsSync25, mkdirSync as mkdirSync13, readFileSync as readFileSync19, chmodSync } from "fs";
|
|
13729
13952
|
import { dirname as dirname17, join as join26 } from "path";
|
|
13730
13953
|
var SshKeyError = class extends Error {
|
|
@@ -13738,7 +13961,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
13738
13961
|
if (!existsSync25(privateKeyPath)) {
|
|
13739
13962
|
mkdirSync13(dirname17(privateKeyPath), { recursive: true });
|
|
13740
13963
|
try {
|
|
13741
|
-
|
|
13964
|
+
execFileSync5("ssh-keygen", [
|
|
13742
13965
|
"-t",
|
|
13743
13966
|
"ed25519",
|
|
13744
13967
|
"-N",
|
|
@@ -13764,7 +13987,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
13764
13987
|
}
|
|
13765
13988
|
function sshKeygenAvailable() {
|
|
13766
13989
|
try {
|
|
13767
|
-
|
|
13990
|
+
execFileSync5("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
13768
13991
|
return true;
|
|
13769
13992
|
} catch (e) {
|
|
13770
13993
|
return e.code !== "ENOENT";
|
|
@@ -13772,7 +13995,7 @@ function sshKeygenAvailable() {
|
|
|
13772
13995
|
}
|
|
13773
13996
|
|
|
13774
13997
|
// src/machine-id.ts
|
|
13775
|
-
import { execFileSync as
|
|
13998
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
13776
13999
|
import { createHash as createHash4 } from "crypto";
|
|
13777
14000
|
import { existsSync as existsSync26, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15 } from "fs";
|
|
13778
14001
|
import { dirname as dirname18, join as join27 } from "path";
|
|
@@ -13783,7 +14006,7 @@ var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
|
13783
14006
|
function readRawMachineId(platform2 = process.platform) {
|
|
13784
14007
|
try {
|
|
13785
14008
|
if (platform2 === "darwin") {
|
|
13786
|
-
const out =
|
|
14009
|
+
const out = execFileSync6("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
13787
14010
|
encoding: "utf8",
|
|
13788
14011
|
stdio: ["ignore", "pipe", "ignore"]
|
|
13789
14012
|
});
|
|
@@ -13822,11 +14045,11 @@ function matchShipRows(ships, shipName2, machineId) {
|
|
|
13822
14045
|
}
|
|
13823
14046
|
|
|
13824
14047
|
// src/passenger-containers.ts
|
|
13825
|
-
import { execFileSync as
|
|
14048
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
13826
14049
|
import { createHash as createHash5 } from "crypto";
|
|
13827
14050
|
var realExec = (cmd, args) => {
|
|
13828
14051
|
try {
|
|
13829
|
-
return
|
|
14052
|
+
return execFileSync7(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
13830
14053
|
} catch (e) {
|
|
13831
14054
|
const err = e;
|
|
13832
14055
|
const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
@@ -14024,12 +14247,12 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
14024
14247
|
}
|
|
14025
14248
|
|
|
14026
14249
|
// src/tunnel.ts
|
|
14027
|
-
import { spawn as
|
|
14250
|
+
import { spawn as spawn5 } from "child_process";
|
|
14028
14251
|
import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
|
|
14029
14252
|
import { randomInt } from "crypto";
|
|
14030
14253
|
import { join as join28 } from "path";
|
|
14031
14254
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
14032
|
-
var defaultSpawn = (cmd, args) =>
|
|
14255
|
+
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
14033
14256
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
14034
14257
|
var SLUG_LENGTH = 12;
|
|
14035
14258
|
function generateTunnelSlug() {
|
|
@@ -14325,7 +14548,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
14325
14548
|
|
|
14326
14549
|
// src/passenger-sync-daemon.ts
|
|
14327
14550
|
import { createServer as createHttpServer } from "http";
|
|
14328
|
-
import { spawn as
|
|
14551
|
+
import { spawn as spawn6 } from "child_process";
|
|
14329
14552
|
import { randomBytes } from "crypto";
|
|
14330
14553
|
import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
|
|
14331
14554
|
import { join as join29 } from "path";
|
|
@@ -14412,7 +14635,7 @@ function runSyncDaemonFromEnv(env = process.env) {
|
|
|
14412
14635
|
return server;
|
|
14413
14636
|
}
|
|
14414
14637
|
var defaultSpawn2 = (cmd, args, env) => {
|
|
14415
|
-
const child =
|
|
14638
|
+
const child = spawn6(cmd, args, { detached: true, stdio: "ignore", env });
|
|
14416
14639
|
child.unref();
|
|
14417
14640
|
return child;
|
|
14418
14641
|
};
|
|
@@ -14732,7 +14955,7 @@ async function runOnePassUnguarded(o) {
|
|
|
14732
14955
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
14733
14956
|
const emit = o.newEmitter();
|
|
14734
14957
|
const decide = o.decide ?? decideCycle;
|
|
14735
|
-
const
|
|
14958
|
+
const spawn7 = o.spawnRoleAgent ?? runRoleAgent;
|
|
14736
14959
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
14737
14960
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
14738
14961
|
try {
|
|
@@ -14793,7 +15016,7 @@ async function runOnePassUnguarded(o) {
|
|
|
14793
15016
|
continue;
|
|
14794
15017
|
}
|
|
14795
15018
|
started.push(role);
|
|
14796
|
-
const run2 =
|
|
15019
|
+
const run2 = spawn7(role, decision, ctx).then(() => {
|
|
14797
15020
|
breaker.recordSuccess(role);
|
|
14798
15021
|
clearShipAttention(o.state, `role_parked:${role}`);
|
|
14799
15022
|
}).catch(async (e) => {
|
|
@@ -15192,7 +15415,7 @@ function usage() {
|
|
|
15192
15415
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
15193
15416
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
15194
15417
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
15195
|
-
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo, --json)
|
|
15418
|
+
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
|
|
15196
15419
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
|
|
15197
15420
|
crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
|
|
15198
15421
|
crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
|
|
@@ -16121,10 +16344,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16121
16344
|
lock?.release();
|
|
16122
16345
|
lock = void 0;
|
|
16123
16346
|
};
|
|
16347
|
+
let sigintOwnedByCommand = false;
|
|
16124
16348
|
if (!dryRun) {
|
|
16125
16349
|
process.on("exit", dropLock);
|
|
16126
16350
|
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
16127
16351
|
process.on(sig, () => {
|
|
16352
|
+
if (sig === "SIGINT" && sigintOwnedByCommand) return;
|
|
16128
16353
|
dropLock();
|
|
16129
16354
|
process.exit(0);
|
|
16130
16355
|
});
|
|
@@ -16879,7 +17104,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16879
17104
|
case "repos": {
|
|
16880
17105
|
const sub = positional[1];
|
|
16881
17106
|
const usage2 = () => {
|
|
16882
|
-
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>\ncrew repos hooks [workspace/project] <name> [--suggest] [--write route|repo] [--json]\n");
|
|
17107
|
+
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>\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--json]\n");
|
|
16883
17108
|
process.exit(2);
|
|
16884
17109
|
};
|
|
16885
17110
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
@@ -16889,7 +17114,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16889
17114
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
16890
17115
|
const dir = repoDirOf(configText, dirname19(cfg.configFile), a.route, a.name);
|
|
16891
17116
|
if (!existsSync31(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
16892
|
-
const files = { exists: (r) => existsSync31(resolvePath(dir, r)),
|
|
17117
|
+
const files = { exists: (r) => existsSync31(resolvePath(dir, r)), list: (r) => {
|
|
17118
|
+
try {
|
|
17119
|
+
return readdirSync12(resolvePath(dir, r));
|
|
17120
|
+
} catch {
|
|
17121
|
+
return [];
|
|
17122
|
+
}
|
|
17123
|
+
}, read: (r) => {
|
|
16893
17124
|
try {
|
|
16894
17125
|
return readFileSync25(resolvePath(dir, r), "utf8");
|
|
16895
17126
|
} catch {
|
|
@@ -16899,21 +17130,43 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16899
17130
|
const suggestion = suggestHooks(files);
|
|
16900
17131
|
const write = value("write");
|
|
16901
17132
|
if (write !== void 0 && write !== "route" && write !== "repo") throw new ReposError(`--write takes "route" or "repo" (got "${write}")`);
|
|
17133
|
+
const recipeIds = (value("recipes") ?? "").split(",").map((x) => x.trim()).filter((x) => x && x !== "none");
|
|
17134
|
+
if (recipeIds.length > 0 && !write) throw new ReposError("--recipes only applies with --write route|repo");
|
|
17135
|
+
if (flag("agent") && write) throw new ReposError("--agent is read-only; drop --write (write the sniffed hooks, then re-run --agent to compare)");
|
|
17136
|
+
let agent = null;
|
|
17137
|
+
let agentError = null;
|
|
17138
|
+
if (flag("agent")) {
|
|
17139
|
+
const ac = new AbortController();
|
|
17140
|
+
const onSigint = () => ac.abort();
|
|
17141
|
+
process.once("SIGINT", onSigint);
|
|
17142
|
+
sigintOwnedByCommand = true;
|
|
17143
|
+
try {
|
|
17144
|
+
const routeCfg = cfg.routes.find((r) => r.route === a.route);
|
|
17145
|
+
agent = await runAgentSuggest({ spec: resolveToolchain(cfg.ship, routeCfg, a.name), dir, sniff: suggestion, signal: ac.signal });
|
|
17146
|
+
} catch (e) {
|
|
17147
|
+
if (!(e instanceof AgentSuggestError)) throw e;
|
|
17148
|
+
agentError = { reason: e.reason, message: e.message };
|
|
17149
|
+
} finally {
|
|
17150
|
+
process.off("SIGINT", onSigint);
|
|
17151
|
+
sigintOwnedByCommand = false;
|
|
17152
|
+
}
|
|
17153
|
+
}
|
|
17154
|
+
const picked = selectRecipes(suggestion, recipeIds);
|
|
16902
17155
|
let written;
|
|
16903
|
-
if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
17156
|
+
if (write && suggestion.source === null && recipeIds.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
16904
17157
|
if (write === "route") {
|
|
16905
|
-
const plan = planRouteHooks(configText, a.route, a.name,
|
|
17158
|
+
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
|
|
16906
17159
|
writeFileSync19(cfg.configFile, plan.text);
|
|
16907
17160
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
16908
17161
|
} else if (write === "repo") {
|
|
16909
17162
|
const have = existingRepoConfig(files);
|
|
16910
17163
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
16911
17164
|
const file = resolvePath(dir, ".crew.yaml");
|
|
16912
|
-
writeFileSync19(file, starterRepoConfig(
|
|
16913
|
-
written = { target: "repo", file, hooks: Object.keys(
|
|
17165
|
+
writeFileSync19(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
17166
|
+
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [] };
|
|
16914
17167
|
}
|
|
16915
17168
|
if (flag("json")) {
|
|
16916
|
-
process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, written: written ?? null })}
|
|
17169
|
+
process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, agent, agentError, written: written ?? null })}
|
|
16917
17170
|
`);
|
|
16918
17171
|
} else {
|
|
16919
17172
|
if (suggestion.source) process.stdout.write(`${a.name}: proposed from ${suggestion.source}
|
|
@@ -16921,6 +17174,18 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16921
17174
|
for (const [k, v] of Object.entries(suggestion.hooks)) process.stdout.write(` ${k}: ${v}
|
|
16922
17175
|
`);
|
|
16923
17176
|
for (const n of suggestion.notes) process.stdout.write(`note: ${n}
|
|
17177
|
+
`);
|
|
17178
|
+
for (const r of suggestion.recipes) process.stdout.write(`recipe ${r.id}${r.defaultOn ? " (default on)" : ""}${r.writable ? "" : " (advisory)"}: ${r.title} [${r.evidence.join(", ")}]
|
|
17179
|
+
`);
|
|
17180
|
+
if (agent) {
|
|
17181
|
+
process.stdout.write(`agent (${agent.toolchain}, ${agent.model}) read ${agent.filesRead.join(", ") || "no files"}
|
|
17182
|
+
`);
|
|
17183
|
+
for (const [k, v] of Object.entries(agent.hooks)) process.stdout.write(` ${k}: ${v}${agent.differsFromSniff.includes(k) ? " (differs from file sniff)" : ""}
|
|
17184
|
+
`);
|
|
17185
|
+
for (const n of agent.notes) process.stdout.write(`note: ${n}
|
|
17186
|
+
`);
|
|
17187
|
+
}
|
|
17188
|
+
if (agentError) process.stderr.write(`crew repos: --agent failed (${agentError.reason}): ${agentError.message}
|
|
16924
17189
|
`);
|
|
16925
17190
|
if (written) {
|
|
16926
17191
|
process.stdout.write(`wrote ${written.hooks.join(", ") || "nothing new"} to ${written.file}
|
|
@@ -16928,8 +17193,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16928
17193
|
if (written.kept.length) process.stdout.write(`kept existing route hooks: ${written.kept.join(", ")}
|
|
16929
17194
|
`);
|
|
16930
17195
|
if (written.target === "repo") process.stdout.write("review it and commit it to the repository\n");
|
|
17196
|
+
if (written.target === "route" && picked.release) process.stdout.write(`note: release.mode ${picked.release.mode} is repo-level; use --write repo to set it
|
|
17197
|
+
`);
|
|
16931
17198
|
}
|
|
16932
17199
|
}
|
|
17200
|
+
if (agentError) process.exitCode = 1;
|
|
16933
17201
|
} catch (e) {
|
|
16934
17202
|
if (e instanceof ReposError) {
|
|
16935
17203
|
process.stderr.write(`crew repos: ${e.message}
|
|
@@ -18239,7 +18507,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18239
18507
|
},
|
|
18240
18508
|
inspectCheckout(dir) {
|
|
18241
18509
|
const dirty = status(dir).map((c) => c.path);
|
|
18242
|
-
const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) =>
|
|
18510
|
+
const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync3(p) !== realpathSync3(dir));
|
|
18243
18511
|
return { dirty, linkedWorktrees };
|
|
18244
18512
|
}
|
|
18245
18513
|
};
|
package/package.json
CHANGED