@4pm/cli 1.8.4 → 1.9.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.
Files changed (2) hide show
  1. package/dist/index.js +541 -423
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -33965,6 +33965,14 @@ var issueProvisioningTokenRequestSchema = external_exports.object({
33965
33965
  /** TTL seconds (0/absent ⇒ a default short TTL applied server-side). */
33966
33966
  ttlSec: external_exports.number().int().min(0).max(2592e3).optional()
33967
33967
  });
33968
+ var provisionRequestSchema = external_exports.object({
33969
+ mode: external_exports.enum(["sync", "force"]).optional().default("sync")
33970
+ });
33971
+ var provisionForUserRequestSchema = external_exports.object({
33972
+ userId: external_exports.string().uuid(),
33973
+ projectId: external_exports.string().uuid(),
33974
+ mode: external_exports.enum(["sync", "force"]).optional().default("sync")
33975
+ });
33968
33976
  var listWorkersQuerySchema = baseRequestSchema.extend({
33969
33977
  status: external_exports.enum(["online", "offline"]).optional(),
33970
33978
  /** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
@@ -34240,7 +34248,13 @@ var repoSpecSchema = external_exports.object({
34240
34248
  /** Clone url — `https` or `ssh` (required; ADR-0172). */
34241
34249
  url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
34242
34250
  /** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
34243
- subdir: external_exports.string().max(120).optional().default("")
34251
+ subdir: external_exports.string().max(120).optional().default(""),
34252
+ /**
34253
+ * Primary branch to clone / check out (ADR-0292). Empty ⇒ the repo's default branch. The
34254
+ * worker clones with `-b <branch>` and the on-demand "update repos" action checks it out +
34255
+ * fast-forward pulls it.
34256
+ */
34257
+ branch: external_exports.string().max(200).optional().default("")
34244
34258
  });
34245
34259
  function repoName(url) {
34246
34260
  const last = (url || "").trim().replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
@@ -34367,9 +34381,282 @@ var activityHeartbeatSchema = external_exports.object({
34367
34381
  projectId: external_exports.string().uuid().optional()
34368
34382
  });
34369
34383
 
34384
+ // src/core/scaffold.ts
34385
+ import { cp, mkdir, readdir, rename, rm, writeFile } from "fs/promises";
34386
+ import { existsSync as existsSync4 } from "fs";
34387
+ import { execFile as execFile2 } from "child_process";
34388
+ import { dirname as dirname3, join as join6, resolve } from "path";
34389
+ import { fileURLToPath as fileURLToPath3 } from "url";
34390
+ import { promisify as promisify2 } from "util";
34391
+
34392
+ // src/core/ai-assist.ts
34393
+ import { execFile } from "child_process";
34394
+ import { promisify } from "util";
34395
+ var run = promisify(execFile);
34396
+ function aiCli() {
34397
+ return process.env.AI_CLI || "claude";
34398
+ }
34399
+ function aiArgs(cli, prompt) {
34400
+ if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
34401
+ if (cli === "codex") return ["exec", "--json", prompt];
34402
+ return ["-p", prompt];
34403
+ }
34404
+ function estimateTokens(prompt, output) {
34405
+ return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
34406
+ }
34407
+ function parseAi(cli, stdout, prompt) {
34408
+ if (cli === "claude") {
34409
+ try {
34410
+ const j = JSON.parse(stdout);
34411
+ const text2 = (j.result ?? "").trim();
34412
+ const tokens = (j.usage?.input_tokens ?? 0) + (j.usage?.output_tokens ?? 0);
34413
+ if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens(prompt, text2) };
34414
+ } catch {
34415
+ }
34416
+ }
34417
+ if (cli === "codex") {
34418
+ try {
34419
+ let text2 = "";
34420
+ let tokens = 0;
34421
+ for (const line of stdout.split("\n").filter(Boolean)) {
34422
+ const obj = JSON.parse(line);
34423
+ if (obj.text) text2 = obj.text;
34424
+ else if (obj.message) text2 = obj.message;
34425
+ if (obj.usage) {
34426
+ tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
34427
+ }
34428
+ }
34429
+ if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens(prompt, text2) };
34430
+ } catch {
34431
+ }
34432
+ }
34433
+ const text = stdout.trim();
34434
+ return { text, tokens: estimateTokens(prompt, text) };
34435
+ }
34436
+ async function runAi(prompt) {
34437
+ const cli = aiCli();
34438
+ const { stdout } = await run(cli, aiArgs(cli, prompt), {
34439
+ timeout: 12e4,
34440
+ maxBuffer: 8 * 1024 * 1024
34441
+ });
34442
+ return parseAi(cli, stdout, prompt);
34443
+ }
34444
+ async function aiGenerate(prompt) {
34445
+ const { text } = await runAi(prompt);
34446
+ return text;
34447
+ }
34448
+ async function aiOutboundReview(input, allowedRepos, projectPath) {
34449
+ const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
34450
+ ${input}`;
34451
+ const { text, tokens } = await runAi(prompt);
34452
+ const start = text.indexOf("{");
34453
+ const end = text.lastIndexOf("}");
34454
+ if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
34455
+ try {
34456
+ const j = JSON.parse(text.slice(start, end + 1));
34457
+ const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
34458
+ return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
34459
+ } catch {
34460
+ return { ok: false, reasons: ["ai_parse"], tokens };
34461
+ }
34462
+ }
34463
+
34464
+ // src/core/scaffold.ts
34465
+ var run2 = promisify2(execFile2);
34466
+ function reposOf(spec) {
34467
+ const repos = spec?.repos;
34468
+ return Array.isArray(repos) ? repos : [];
34469
+ }
34470
+ function subDirName(repo) {
34471
+ const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
34472
+ return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
34473
+ }
34474
+ function cloneArgs(url, dest, branch) {
34475
+ const b = (branch ?? "").trim();
34476
+ return b ? ["clone", "-b", b, url, dest] : ["clone", url, dest];
34477
+ }
34478
+ async function updateRepo(dir2, label, branch, emit) {
34479
+ const b = (branch ?? "").trim();
34480
+ emit("git", `Updating ${label} (fetch + ${b ? `checkout ${b} + ` : ""}fast-forward pull)\u2026`);
34481
+ await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
34482
+ if (b) await run2("git", ["checkout", b], { cwd: dir2, timeout: 6e4 });
34483
+ await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
34484
+ }
34485
+ async function provisionRepos(target, repos, emit, opts = {}) {
34486
+ const mode = opts.mode ?? "clone";
34487
+ const subDirsToKeep = new Set(repos.filter((r) => !r.primary && r.url).map((r) => subDirName(r)));
34488
+ for (const repo of repos) {
34489
+ if (repo.primary) {
34490
+ if (opts.clonePrimary && repo.url) {
34491
+ if (existsSync4(join6(target, ".git"))) {
34492
+ if (mode === "sync") {
34493
+ await updateRepo(target, "primary repo", repo.branch, emit);
34494
+ } else if (mode === "force") {
34495
+ emit("git", `Re-cloning ${repo.url} (force)\u2026`);
34496
+ const tmp = `${target}.4pm-reclone`;
34497
+ await rm(tmp, { recursive: true, force: true });
34498
+ await run2("git", cloneArgs(repo.url, tmp, repo.branch), { timeout: 12e4 });
34499
+ for (const entry of await readdir(target)) {
34500
+ if (subDirsToKeep.has(entry)) continue;
34501
+ await rm(join6(target, entry), { recursive: true, force: true });
34502
+ }
34503
+ for (const entry of await readdir(tmp)) {
34504
+ if (subDirsToKeep.has(entry)) continue;
34505
+ await rename(join6(tmp, entry), join6(target, entry));
34506
+ }
34507
+ await rm(tmp, { recursive: true, force: true });
34508
+ } else {
34509
+ emit("git", `Primary repo already present \u2014 skipping clone.`);
34510
+ }
34511
+ continue;
34512
+ }
34513
+ emit("git", `Cloning ${repo.url}\u2026`);
34514
+ await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
34515
+ continue;
34516
+ }
34517
+ emit("git", "Initializing the primary repo\u2026");
34518
+ await run2("git", ["init"], { cwd: target, timeout: 2e4 });
34519
+ if (repo.url) {
34520
+ await run2("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
34521
+ () => void 0
34522
+ );
34523
+ }
34524
+ continue;
34525
+ }
34526
+ const dir2 = join6(target, subDirName(repo));
34527
+ if (repo.url) {
34528
+ if (existsSync4(join6(dir2, ".git"))) {
34529
+ if (mode === "sync") {
34530
+ await updateRepo(dir2, `sub-repo ${subDirName(repo)}`, repo.branch, emit);
34531
+ } else if (mode === "force") {
34532
+ emit("git", `Re-cloning sub-repo ${repo.url} (force)\u2026`);
34533
+ await rm(dir2, { recursive: true, force: true });
34534
+ await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34535
+ } else {
34536
+ emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
34537
+ }
34538
+ continue;
34539
+ }
34540
+ emit("git", `Cloning sub-repo ${repo.url}\u2026`);
34541
+ await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34542
+ } else {
34543
+ emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
34544
+ await mkdir(dir2, { recursive: true });
34545
+ await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
34546
+ }
34547
+ }
34548
+ }
34549
+ async function ensureReposCloned(physicRoot, repos, emit) {
34550
+ if (repos.length === 0) return;
34551
+ await provisionRepos(physicRoot, repos, emit ?? (() => void 0), { clonePrimary: true });
34552
+ }
34553
+ function sampleDir() {
34554
+ if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
34555
+ const here = dirname3(fileURLToPath3(import.meta.url));
34556
+ const bundled = resolve(here, "project-sample");
34557
+ for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
34558
+ if (existsSync4(candidate)) return candidate;
34559
+ }
34560
+ return bundled;
34561
+ }
34562
+ async function scaffoldProject(payload, profileDir2, onProgress) {
34563
+ let lastStep = "start";
34564
+ const emit = (step, message) => {
34565
+ lastStep = step;
34566
+ onProgress?.({ projectId: payload.projectId, step, message });
34567
+ };
34568
+ try {
34569
+ const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
34570
+ const target = join6(root, payload.projectName);
34571
+ await mkdir(target, { recursive: true });
34572
+ emit("copy", "Copying the sample template\u2026");
34573
+ await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
34574
+ if (payload.spec) {
34575
+ emit("spec", "Writing project.spec.json\u2026");
34576
+ await writeFile(
34577
+ join6(target, "project.spec.json"),
34578
+ JSON.stringify(payload.spec, null, 2),
34579
+ "utf8"
34580
+ );
34581
+ }
34582
+ const repos = reposOf(payload.spec);
34583
+ if (repos.length > 0) await provisionRepos(target, repos, emit);
34584
+ if (payload.spec) await aiInit(target, payload.spec, emit);
34585
+ emit("version", "Writing .4pm/.4pm.json\u2026");
34586
+ await writeTemplateMarker(target);
34587
+ onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
34588
+ return { ok: true, path: target };
34589
+ } catch (err) {
34590
+ return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
34591
+ }
34592
+ }
34593
+ async function writeTemplateMarker(target) {
34594
+ const dir2 = join6(target, ".4pm");
34595
+ await mkdir(dir2, { recursive: true });
34596
+ const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
34597
+ await writeFile(join6(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
34598
+ }
34599
+ async function aiInit(target, spec, emit) {
34600
+ emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
34601
+ const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
34602
+ if (subagents.length > 0) {
34603
+ await mkdir(join6(target, ".claude", "agents"), { recursive: true });
34604
+ for (const sa of subagents) {
34605
+ const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
34606
+ if (!name) continue;
34607
+ const body = `---
34608
+ name: ${name}
34609
+ ---
34610
+
34611
+ ${(sa.description || "").trim()}
34612
+ `;
34613
+ await writeFile(join6(target, ".claude", "agents", `${name}.md`), body, "utf8");
34614
+ }
34615
+ }
34616
+ const specJson = JSON.stringify(spec);
34617
+ await generateFile(
34618
+ join6(target, "README.md"),
34619
+ `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
34620
+ ${specJson}`
34621
+ );
34622
+ await generateFile(
34623
+ join6(target, "CLAUDE.md"),
34624
+ `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
34625
+ ${specJson}`
34626
+ );
34627
+ }
34628
+ async function generateFile(path, prompt) {
34629
+ try {
34630
+ const text = await aiGenerate(prompt);
34631
+ if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
34632
+ } catch {
34633
+ }
34634
+ }
34635
+ async function addProject(payload, profileDir2, onProgress) {
34636
+ let lastStep = "start";
34637
+ const emit = (step, message) => {
34638
+ lastStep = step;
34639
+ onProgress?.({ projectId: payload.projectId, step, message });
34640
+ };
34641
+ try {
34642
+ const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
34643
+ const target = join6(root, payload.projectName);
34644
+ await mkdir(target, { recursive: true });
34645
+ const repos = payload.repos ?? [];
34646
+ const mode = payload.mode ?? "clone";
34647
+ if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true, mode });
34648
+ onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
34649
+ return { ok: true, path: target };
34650
+ } catch (err) {
34651
+ const message = err instanceof Error ? err.message : String(err);
34652
+ onProgress?.({ projectId: payload.projectId, step: "error", message: `Failed: ${message}`, done: true });
34653
+ return { ok: false, error: message, step: lastStep };
34654
+ }
34655
+ }
34656
+
34370
34657
  // src/core/worker-metrics.ts
34371
34658
  import { statfs, readFile } from "fs/promises";
34372
- import { existsSync as existsSync4 } from "fs";
34659
+ import { existsSync as existsSync5 } from "fs";
34373
34660
  import { totalmem, freemem, cpus, platform, arch } from "os";
34374
34661
  var CGROUP = "/sys/fs/cgroup";
34375
34662
  var MB = 1024 * 1024;
@@ -34419,7 +34706,7 @@ function parseMaybeMax(text) {
34419
34706
  return Number.isFinite(n) ? n : null;
34420
34707
  }
34421
34708
  function createWorkerMetricsSampler(diskPath) {
34422
- const cgroupV2 = existsSync4(`${CGROUP}/cpu.stat`) && existsSync4(`${CGROUP}/memory.current`);
34709
+ const cgroupV2 = existsSync5(`${CGROUP}/cpu.stat`) && existsSync5(`${CGROUP}/memory.current`);
34423
34710
  let cgPrev = null;
34424
34711
  let osPrev = null;
34425
34712
  async function cgroupCpuCount() {
@@ -34734,6 +35021,10 @@ var WsChannels = {
34734
35021
  // serving cli so a change (e.g. aiRunTimeoutSec) applies on the next run, not only after a
34735
35022
  // reconnect. `ws_token` seeds these on connect; this channel only carries updates.
34736
35023
  PROJECT_TOKENS: "project.tokens",
35024
+ // server → cli (ADR-0289): update the cli to the latest version now (idle-aware self-update +
35025
+ // re-exec). Sent by the manual operator "Update" action and the update-on-connect hook when the
35026
+ // connecting cli is outdated. No payload; honours the machine's local autoUpdate opt-out.
35027
+ CLI_UPDATE: "cli.update",
34737
35028
  // command lifecycle
34738
35029
  COMMAND_ANNOUNCE: "command.announce",
34739
35030
  // console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
@@ -34847,9 +35138,9 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
34847
35138
  import { spawn as spawn2 } from "child_process";
34848
35139
 
34849
35140
  // src/common/logger/logger.ts
34850
- import { appendFile, mkdir, readdir, unlink } from "fs/promises";
35141
+ import { appendFile, mkdir as mkdir2, readdir as readdir2, unlink } from "fs/promises";
34851
35142
  import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
34852
- import { join as join6 } from "path";
35143
+ import { join as join7 } from "path";
34853
35144
  var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
34854
35145
  var DAY_MS = 24 * 60 * 60 * 1e3;
34855
35146
  var DEFAULT_RETENTION_DAYS = 14;
@@ -34898,8 +35189,8 @@ var CliLogger = class {
34898
35189
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
34899
35190
  const entry = { timestamp, level, ...this.base, event, ...fields };
34900
35191
  const line = JSON.stringify(entry) + "\n";
34901
- const path = join6(dir2, `cli-${timestamp.slice(0, 10)}.jsonl`);
34902
- this.queue = this.queue.then(() => mkdir(dir2, { recursive: true })).then(() => appendFile(path, line, "utf8")).catch((err) => {
35192
+ const path = join7(dir2, `cli-${timestamp.slice(0, 10)}.jsonl`);
35193
+ this.queue = this.queue.then(() => mkdir2(dir2, { recursive: true })).then(() => appendFile(path, line, "utf8")).catch((err) => {
34903
35194
  console.warn(`Failed to write cli log: ${err}`);
34904
35195
  });
34905
35196
  }
@@ -34907,13 +35198,13 @@ var CliLogger = class {
34907
35198
  async cleanupOldLogs() {
34908
35199
  const dir2 = this.dir;
34909
35200
  if (!dir2) return;
34910
- const files = await readdir(dir2).catch(() => []);
35201
+ const files = await readdir2(dir2).catch(() => []);
34911
35202
  const cutoff = Date.now() - this.retentionDays * DAY_MS;
34912
35203
  for (const file of files) {
34913
35204
  const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
34914
35205
  if (!match) continue;
34915
35206
  if ((/* @__PURE__ */ new Date(`${match[1]}T00:00:00Z`)).getTime() < cutoff) {
34916
- await unlink(join6(dir2, file)).catch(() => void 0);
35207
+ await unlink(join7(dir2, file)).catch(() => void 0);
34917
35208
  }
34918
35209
  }
34919
35210
  }
@@ -34924,7 +35215,7 @@ function readRecentLogLines(dir2, limit) {
34924
35215
  const files = readdirSync2(dir2).filter((f) => /^cli-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort();
34925
35216
  const latest = files[files.length - 1];
34926
35217
  if (!latest) return [];
34927
- const lines = readFileSync6(join6(dir2, latest), "utf8").split("\n").filter(Boolean);
35218
+ const lines = readFileSync6(join7(dir2, latest), "utf8").split("\n").filter(Boolean);
34928
35219
  return lines.slice(-limit);
34929
35220
  } catch {
34930
35221
  return [];
@@ -34937,8 +35228,8 @@ function readLogUpload(dir2) {
34937
35228
  const latest = files[files.length - 1];
34938
35229
  if (!latest) return null;
34939
35230
  let totalBytes = 0;
34940
- for (const f of files) totalBytes += statSync(join6(dir2, f)).size;
34941
- let content = readFileSync6(join6(dir2, latest), "utf8");
35231
+ for (const f of files) totalBytes += statSync(join7(dir2, f)).size;
35232
+ let content = readFileSync6(join7(dir2, latest), "utf8");
34942
35233
  if (content.length > LOG_UPLOAD_CONTENT_MAX) {
34943
35234
  content = `\u2026[truncated]
34944
35235
  ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
@@ -34981,7 +35272,7 @@ function classifyToolFailure(code, out) {
34981
35272
  }
34982
35273
  return { reason: "other", retryable: false };
34983
35274
  }
34984
- function run(cmd, args, onLine, timeoutMs = 18e4) {
35275
+ function run3(cmd, args, onLine, timeoutMs = 18e4) {
34985
35276
  return new Promise((resolve10) => {
34986
35277
  let out = "";
34987
35278
  let buf = "";
@@ -35021,13 +35312,13 @@ function run(cmd, args, onLine, timeoutMs = 18e4) {
35021
35312
  });
35022
35313
  }
35023
35314
  async function detectOne(cmd, versionArg) {
35024
- const { code, out } = await run(cmd, [versionArg], void 0, 15e3);
35315
+ const { code, out } = await run3(cmd, [versionArg], void 0, 15e3);
35025
35316
  if (code !== 0) return null;
35026
35317
  const first = out.split("\n").map((l) => l.trim()).find(Boolean);
35027
35318
  return first ?? "";
35028
35319
  }
35029
35320
  async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
35030
- const { out } = await run("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
35321
+ const { out } = await run3("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
35031
35322
  if (!out.trim()) return [];
35032
35323
  let deps = {};
35033
35324
  try {
@@ -35103,7 +35394,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
35103
35394
  return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
35104
35395
  }
35105
35396
  onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
35106
- const { code } = await run(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
35397
+ const { code } = await run3(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
35107
35398
  return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
35108
35399
  }
35109
35400
  function setToolAutoUpdate(profileDir2, name, enabled) {
@@ -35134,7 +35425,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
35134
35425
  for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
35135
35426
  const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
35136
35427
  onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
35137
- const { code, out } = await run(manager, args, onLine, timeoutMs);
35428
+ const { code, out } = await run3(manager, args, onLine, timeoutMs);
35138
35429
  if (code === 0) return { ok: true };
35139
35430
  const { reason, retryable } = classifyToolFailure(code, out);
35140
35431
  if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
@@ -35200,6 +35491,9 @@ var UpdateScheduler = class {
35200
35491
  lastDoneDateKey = null;
35201
35492
  /** Date key marked due but deferred because the cli was busy (stays set past the hour). */
35202
35493
  pendingDateKey = null;
35494
+ /** A push/connect-triggered update requested now (ADR-0289) — runs when idle, independent of the
35495
+ * daily policy; cleared once it runs. */
35496
+ forcedPending = false;
35203
35497
  /** Guards against a second update while one is in flight. */
35204
35498
  updating = false;
35205
35499
  /** Apply the latest policy (called on each ws_token) and start the tick timer once. */
@@ -35210,6 +35504,25 @@ var UpdateScheduler = class {
35210
35504
  this.timer.unref?.();
35211
35505
  }
35212
35506
  }
35507
+ /**
35508
+ * Request an update to latest NOW (ADR-0289) — the server's `cli.update` push or the
35509
+ * update-on-connect hook. Idle-aware like the daily tick (defers while a command runs) and
35510
+ * honours the local `autoUpdate:false` opt-out, but is independent of the daily org policy. Also
35511
+ * ensures the tick timer is running so the request is served even before the first ws_token.
35512
+ */
35513
+ updateNow() {
35514
+ if (this.optedOutLocally()) {
35515
+ logger.info("update.push.optedOut");
35516
+ return;
35517
+ }
35518
+ if (CLI_VERSION.startsWith("0.0.0")) return;
35519
+ this.forcedPending = true;
35520
+ if (!this.timer) {
35521
+ this.timer = setInterval(() => void this.tick(), TICK_MS);
35522
+ this.timer.unref?.();
35523
+ }
35524
+ void this.tick();
35525
+ }
35213
35526
  /** Stop the scheduler when the session ends (logout / replaced / stopped). */
35214
35527
  stop() {
35215
35528
  if (this.timer) {
@@ -35242,12 +35555,19 @@ var UpdateScheduler = class {
35242
35555
  return null;
35243
35556
  }
35244
35557
  }
35245
- /** One evaluation: mark the update due at the target hour, then update once idle. */
35558
+ /** One evaluation: run a forced (push/connect) update if pending, else the daily-scheduled one. */
35246
35559
  async tick() {
35247
- const p = this.policy;
35248
- if (!p || !p.autoUpdateDaily || this.updating) return;
35560
+ if (this.updating) return;
35249
35561
  if (CLI_VERSION.startsWith("0.0.0")) return;
35250
35562
  if (this.optedOutLocally()) return;
35563
+ if (this.forcedPending) {
35564
+ if (this.bus.busy !== null) return;
35565
+ this.forcedPending = false;
35566
+ await this.runUpdate();
35567
+ return;
35568
+ }
35569
+ const p = this.policy;
35570
+ if (!p || !p.autoUpdateDaily) return;
35251
35571
  const clock = this.orgClock();
35252
35572
  if (!clock) return;
35253
35573
  if (clock.dateKey === this.lastDoneDateKey) return;
@@ -35305,14 +35625,14 @@ var UpdateScheduler = class {
35305
35625
  };
35306
35626
 
35307
35627
  // src/core/ai-profile-state.ts
35308
- import { existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
35309
- import { join as join7 } from "path";
35628
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
35629
+ import { join as join8 } from "path";
35310
35630
  function statePath(profileDir2) {
35311
- return join7(profileDir2, "ai-profiles.json");
35631
+ return join8(profileDir2, "ai-profiles.json");
35312
35632
  }
35313
35633
  function readState(profileDir2) {
35314
35634
  const path = statePath(profileDir2);
35315
- if (!existsSync5(path)) return {};
35635
+ if (!existsSync6(path)) return {};
35316
35636
  try {
35317
35637
  return JSON.parse(readFileSync7(path, "utf8"));
35318
35638
  } catch {
@@ -35370,7 +35690,7 @@ function clearPinnedCredential(profileDir2) {
35370
35690
  // src/utils/ai-cli.ts
35371
35691
  import { readFileSync as readFileSync8 } from "fs";
35372
35692
  import { homedir as homedir3 } from "os";
35373
- import { basename, isAbsolute, join as join8 } from "path";
35693
+ import { basename, isAbsolute, join as join9 } from "path";
35374
35694
  var REQUIRED_AI_ARGS = {
35375
35695
  claude: ["-p", "--output-format", "stream-json", "--verbose"],
35376
35696
  codex: ["exec", "--json"],
@@ -35507,9 +35827,9 @@ function claudeCredentialKeys(config) {
35507
35827
  return claudeProfilesOf(config).filter(isUsableProfile).map((p) => credentialKey("claude", resolveHomePath(p.profile)));
35508
35828
  }
35509
35829
  function resolveHomePath(value) {
35510
- if (value.startsWith("~")) return join8(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
35830
+ if (value.startsWith("~")) return join9(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
35511
35831
  if (isAbsolute(value)) return value;
35512
- return join8(homedir3(), value);
35832
+ return join9(homedir3(), value);
35513
35833
  }
35514
35834
  function profileDirs(profiles) {
35515
35835
  if (!profiles) return [];
@@ -35555,11 +35875,11 @@ function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnl
35555
35875
  }
35556
35876
  function claudeHomeDirs(config) {
35557
35877
  const dirs = profileDirs(claudeProfilesOf(config));
35558
- return dirs.length > 0 ? dirs : [join8(homedir3(), ".claude")];
35878
+ return dirs.length > 0 ? dirs : [join9(homedir3(), ".claude")];
35559
35879
  }
35560
35880
  function profileDisplayLabel(dir2) {
35561
35881
  try {
35562
- const raw = JSON.parse(readFileSync8(join8(dir2, ".claude.json"), "utf8"));
35882
+ const raw = JSON.parse(readFileSync8(join9(dir2, ".claude.json"), "utf8"));
35563
35883
  const email = raw.oauthAccount?.emailAddress;
35564
35884
  if (email && email.trim()) return email;
35565
35885
  } catch {
@@ -35661,11 +35981,11 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
35661
35981
  // src/core/claude-usage.ts
35662
35982
  import { spawn as spawn3 } from "child_process";
35663
35983
  import { readFileSync as readFileSync9 } from "fs";
35664
- import { join as join9 } from "path";
35984
+ import { join as join10 } from "path";
35665
35985
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
35666
35986
  function readCreds(dir2) {
35667
35987
  try {
35668
- const raw = JSON.parse(readFileSync9(join9(dir2, ".credentials.json"), "utf8"));
35988
+ const raw = JSON.parse(readFileSync9(join10(dir2, ".credentials.json"), "utf8"));
35669
35989
  const oauth = raw.claudeAiOauth ?? raw;
35670
35990
  return oauth.accessToken ? oauth : null;
35671
35991
  } catch {
@@ -35774,8 +36094,8 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
35774
36094
  }
35775
36095
 
35776
36096
  // src/core/command-history.ts
35777
- import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
35778
- import { join as join10 } from "path";
36097
+ import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
36098
+ import { join as join11 } from "path";
35779
36099
  var MAX_ENTRIES = 1e3;
35780
36100
  var filePath = null;
35781
36101
  var entries = [];
@@ -35837,8 +36157,8 @@ async function uploadHistory(cfg) {
35837
36157
  }
35838
36158
  }
35839
36159
  function initCommandHistory(profileDir2, profileName, uploadMinutes = 10) {
35840
- filePath = join10(profileDir2, "command-history.json");
35841
- if (existsSync6(filePath)) {
36160
+ filePath = join11(profileDir2, "command-history.json");
36161
+ if (existsSync7(filePath)) {
35842
36162
  try {
35843
36163
  entries = JSON.parse(readFileSync10(filePath, "utf8"));
35844
36164
  } catch {
@@ -35894,16 +36214,16 @@ function finishCommand(commandId, exitCode, usage) {
35894
36214
  }
35895
36215
 
35896
36216
  // src/core/command-images.ts
35897
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
35898
- import { join as join11 } from "path";
36217
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
36218
+ import { join as join12 } from "path";
35899
36219
  function attachmentsBaseDir(physicRoot) {
35900
- return join11(physicRoot, ".4pm", "attachments");
36220
+ return join12(physicRoot, ".4pm", "attachments");
35901
36221
  }
35902
36222
  var ATTACHMENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
35903
36223
  async function materializeImages(physicRoot, commandId, images, fetch2) {
35904
36224
  const map = /* @__PURE__ */ new Map();
35905
36225
  if (!images.length) return map;
35906
- const dir2 = join11(attachmentsBaseDir(physicRoot), commandId);
36226
+ const dir2 = join12(attachmentsBaseDir(physicRoot), commandId);
35907
36227
  mkdirSync3(dir2, { recursive: true });
35908
36228
  let n = 0;
35909
36229
  for (const img of images) {
@@ -35915,7 +36235,7 @@ async function materializeImages(physicRoot, commandId, images, fetch2) {
35915
36235
  continue;
35916
36236
  }
35917
36237
  const ext = commandImageExt(reply.mime ?? img.mime);
35918
- const file = join11(dir2, `img-${n}.${ext}`);
36238
+ const file = join12(dir2, `img-${n}.${ext}`);
35919
36239
  writeFileSync6(file, Buffer.from(reply.dataBase64, "base64"));
35920
36240
  map.set(img.placeholder, file);
35921
36241
  } catch (err) {
@@ -35937,12 +36257,12 @@ function rewriteImagePlaceholders(prompt, paths) {
35937
36257
  }
35938
36258
  function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
35939
36259
  const base = attachmentsBaseDir(physicRoot);
35940
- if (!existsSync7(base)) return 0;
36260
+ if (!existsSync8(base)) return 0;
35941
36261
  const cutoff = Date.now() - maxAgeMs;
35942
36262
  let removed = 0;
35943
36263
  try {
35944
36264
  for (const entry of readdirSync3(base)) {
35945
- const dir2 = join11(base, entry);
36265
+ const dir2 = join12(base, entry);
35946
36266
  try {
35947
36267
  if (statSync2(dir2).mtimeMs < cutoff) {
35948
36268
  rmSync3(dir2, { recursive: true, force: true });
@@ -35959,18 +36279,18 @@ function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
35959
36279
  // src/core/command-output-store.ts
35960
36280
  import {
35961
36281
  appendFileSync,
35962
- existsSync as existsSync8,
36282
+ existsSync as existsSync9,
35963
36283
  mkdirSync as mkdirSync4,
35964
36284
  readFileSync as readFileSync11,
35965
36285
  readdirSync as readdirSync4,
35966
36286
  rmSync as rmSync4,
35967
36287
  statSync as statSync3
35968
36288
  } from "fs";
35969
- import { join as join12 } from "path";
36289
+ import { join as join13 } from "path";
35970
36290
  var KEEP_FILES = 200;
35971
36291
  var dir = null;
35972
36292
  function initCommandOutput(profileDir2) {
35973
- dir = join12(profileDir2, "command-output");
36293
+ dir = join13(profileDir2, "command-output");
35974
36294
  try {
35975
36295
  mkdirSync4(dir, { recursive: true });
35976
36296
  prune();
@@ -35980,14 +36300,14 @@ function initCommandOutput(profileDir2) {
35980
36300
  function appendCommandOutput(commandId, text) {
35981
36301
  if (!dir || !text) return;
35982
36302
  try {
35983
- appendFileSync(join12(dir, `${commandId}.log`), text, "utf8");
36303
+ appendFileSync(join13(dir, `${commandId}.log`), text, "utf8");
35984
36304
  } catch {
35985
36305
  }
35986
36306
  }
35987
36307
  function readCommandOutput(commandId) {
35988
36308
  if (!dir) return null;
35989
- const path = join12(dir, `${commandId}.log`);
35990
- if (!existsSync8(path)) return null;
36309
+ const path = join13(dir, `${commandId}.log`);
36310
+ if (!existsSync9(path)) return null;
35991
36311
  try {
35992
36312
  return readFileSync11(path, "utf8");
35993
36313
  } catch {
@@ -35997,8 +36317,8 @@ function readCommandOutput(commandId) {
35997
36317
  function prune() {
35998
36318
  if (!dir) return;
35999
36319
  try {
36000
- const files = readdirSync4(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync3(join12(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
36001
- for (const { f } of files.slice(KEEP_FILES)) rmSync4(join12(dir, f), { force: true });
36320
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync3(join13(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
36321
+ for (const { f } of files.slice(KEEP_FILES)) rmSync4(join13(dir, f), { force: true });
36002
36322
  } catch {
36003
36323
  }
36004
36324
  }
@@ -36007,7 +36327,7 @@ function pruneCommandOutputByAge(days) {
36007
36327
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
36008
36328
  try {
36009
36329
  for (const f of readdirSync4(dir).filter((n) => n.endsWith(".log"))) {
36010
- const p = join12(dir, f);
36330
+ const p = join13(dir, f);
36011
36331
  if (statSync3(p).mtimeMs < cutoff) rmSync4(p, { force: true });
36012
36332
  }
36013
36333
  } catch {
@@ -36016,12 +36336,12 @@ function pruneCommandOutputByAge(days) {
36016
36336
 
36017
36337
  // src/core/support-answer.ts
36018
36338
  import { spawn as spawn4 } from "child_process";
36019
- import { execFile } from "child_process";
36339
+ import { execFile as execFile3 } from "child_process";
36020
36340
  import { createHash as createHash3 } from "crypto";
36021
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
36341
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
36022
36342
  import { randomUUID } from "crypto";
36023
- import { join as join13 } from "path";
36024
- import { promisify } from "util";
36343
+ import { join as join14 } from "path";
36344
+ import { promisify as promisify3 } from "util";
36025
36345
 
36026
36346
  // src/core/ai-stream.ts
36027
36347
  function assistantText(ev) {
@@ -36116,22 +36436,22 @@ function createAiStreamParser(cli) {
36116
36436
  apiError: () => ({ ...apiError })
36117
36437
  };
36118
36438
  }
36119
- function estimateTokens(text) {
36439
+ function estimateTokens2(text) {
36120
36440
  return Math.max(1, Math.ceil(text.length / 4));
36121
36441
  }
36122
36442
 
36123
36443
  // src/core/support-answer.ts
36124
36444
  var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
36125
- var execFileAsync = promisify(execFile);
36445
+ var execFileAsync = promisify3(execFile3);
36126
36446
  var MAX_CONTEXT_CHARS = 12e4;
36127
36447
  var ANSWER_TIMEOUT_MS = 12e4;
36128
36448
  var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
36129
36449
  function kbRoot(profileDir2) {
36130
- return join13(profileDir2, "support-kb");
36450
+ return join14(profileDir2, "support-kb");
36131
36451
  }
36132
36452
  function repoDir(profileDir2, url, branch) {
36133
36453
  const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
36134
- return join13(kbRoot(profileDir2), key);
36454
+ return join14(kbRoot(profileDir2), key);
36135
36455
  }
36136
36456
  function authedUrl(url, token) {
36137
36457
  if (!token) return url;
@@ -36140,7 +36460,7 @@ function authedUrl(url, token) {
36140
36460
  }
36141
36461
  async function ensureRepo(profileDir2, repo) {
36142
36462
  const dir2 = repoDir(profileDir2, repo.url, repo.branch);
36143
- if (existsSync9(join13(dir2, ".git"))) return dir2;
36463
+ if (existsSync10(join14(dir2, ".git"))) return dir2;
36144
36464
  const url = authedUrl(repo.url, repo.token);
36145
36465
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
36146
36466
  mkdirSync5(kbRoot(profileDir2), { recursive: true });
@@ -36151,11 +36471,11 @@ async function ensureRepo(profileDir2, repo) {
36151
36471
  }
36152
36472
  async function refreshSupportKb(profileDir2) {
36153
36473
  const root = kbRoot(profileDir2);
36154
- if (!existsSync9(root)) return;
36474
+ if (!existsSync10(root)) return;
36155
36475
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
36156
36476
  for (const entry of readdirSync5(root)) {
36157
- const dir2 = join13(root, entry);
36158
- if (!existsSync9(join13(dir2, ".git"))) continue;
36477
+ const dir2 = join14(root, entry);
36478
+ if (!existsSync10(join14(dir2, ".git"))) continue;
36159
36479
  try {
36160
36480
  await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
36161
36481
  } catch (err) {
@@ -36170,7 +36490,7 @@ function collectDocs(dir2) {
36170
36490
  if (total >= MAX_CONTEXT_CHARS) return;
36171
36491
  for (const entry of readdirSync5(current2)) {
36172
36492
  if (entry === ".git" || entry === "node_modules") continue;
36173
- const full = join13(current2, entry);
36493
+ const full = join14(current2, entry);
36174
36494
  const st = statSync4(full);
36175
36495
  if (st.isDirectory()) {
36176
36496
  walk2(full);
@@ -36235,11 +36555,11 @@ function imageExt(mime) {
36235
36555
  }
36236
36556
  }
36237
36557
  function materializeImages2(profileDir2, images, question) {
36238
- const dir2 = join13(profileDir2, "help-images", randomUUID());
36558
+ const dir2 = join14(profileDir2, "help-images", randomUUID());
36239
36559
  mkdirSync5(dir2, { recursive: true });
36240
36560
  let rewritten = question;
36241
36561
  images.forEach((img, i) => {
36242
- const file = join13(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
36562
+ const file = join14(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
36243
36563
  writeFileSync7(file, Buffer.from(img.dataBase64, "base64"));
36244
36564
  if (img.placeholder) rewritten = rewritten.split(img.placeholder).join(file);
36245
36565
  });
@@ -36334,7 +36654,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
36334
36654
  if (!text) return { body: "", error: "empty answer" };
36335
36655
  const { body, moderation } = parseModeratedAnswer(text);
36336
36656
  if (!body) return { body: "", error: "empty answer" };
36337
- const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens(body) };
36657
+ const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
36338
36658
  return {
36339
36659
  body,
36340
36660
  ...moderation ? { moderation } : {},
@@ -36381,12 +36701,12 @@ function reportToolResult(tool, ok, message) {
36381
36701
  }
36382
36702
 
36383
36703
  // src/core/autonomous.ts
36384
- import { readFile as readFile2, writeFile, chmod } from "fs/promises";
36704
+ import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
36385
36705
  import { readFileSync as readFileSync13 } from "fs";
36386
- import { execFile as execFile2, spawn as spawn5 } from "child_process";
36387
- import { join as join14 } from "path";
36388
- import { promisify as promisify2 } from "util";
36389
- var run2 = promisify2(execFile2);
36706
+ import { execFile as execFile4, spawn as spawn5 } from "child_process";
36707
+ import { join as join15 } from "path";
36708
+ import { promisify as promisify4 } from "util";
36709
+ var run4 = promisify4(execFile4);
36390
36710
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
36391
36711
  var HISTORIES_REL = ".claude/.autonomous.histories.json";
36392
36712
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
@@ -36419,11 +36739,11 @@ function tableCell(value) {
36419
36739
  return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
36420
36740
  }
36421
36741
  function tickScript(root) {
36422
- return join14(root, TICK_REL);
36742
+ return join15(root, TICK_REL);
36423
36743
  }
36424
36744
  async function crontabList() {
36425
36745
  try {
36426
- const { stdout } = await run2("crontab", ["-l"], { timeout: 1e4 });
36746
+ const { stdout } = await run4("crontab", ["-l"], { timeout: 1e4 });
36427
36747
  return stdout;
36428
36748
  } catch {
36429
36749
  return "";
@@ -36442,8 +36762,8 @@ async function cronInstalled(root) {
36442
36762
  return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
36443
36763
  }
36444
36764
  async function getAutonomousStatus(root) {
36445
- const settings = parseJson(await readText(join14(root, SETTINGS_REL))) ?? {};
36446
- const hist = parseJson(await readText(join14(root, HISTORIES_REL))) ?? {};
36765
+ const settings = parseJson(await readText(join15(root, SETTINGS_REL))) ?? {};
36766
+ const hist = parseJson(await readText(join15(root, HISTORIES_REL))) ?? {};
36447
36767
  const records = Array.isArray(hist.records) ? hist.records : [];
36448
36768
  const last = records[records.length - 1];
36449
36769
  const ticks = hist.ticks ?? {};
@@ -36460,9 +36780,9 @@ async function getAutonomousStatus(root) {
36460
36780
  }
36461
36781
  function isAutonomousRunning(root) {
36462
36782
  try {
36463
- const s = JSON.parse(readFileSync13(join14(root, SETTINGS_REL), "utf8"));
36783
+ const s = JSON.parse(readFileSync13(join15(root, SETTINGS_REL), "utf8"));
36464
36784
  if (s.paused === true) return false;
36465
- const h = JSON.parse(readFileSync13(join14(root, HISTORIES_REL), "utf8"));
36785
+ const h = JSON.parse(readFileSync13(join15(root, HISTORIES_REL), "utf8"));
36466
36786
  const ts = h.records?.[h.records.length - 1]?.ts;
36467
36787
  if (!ts) return false;
36468
36788
  const t2 = Date.parse(ts.replace(" ", "T"));
@@ -36473,10 +36793,10 @@ function isAutonomousRunning(root) {
36473
36793
  }
36474
36794
  async function readAutonomous(root) {
36475
36795
  const [settings, approvals, status, ...books] = await Promise.all([
36476
- readText(join14(root, SETTINGS_REL)),
36477
- readText(join14(root, APPROVALS_REL), "{}"),
36796
+ readText(join15(root, SETTINGS_REL)),
36797
+ readText(join15(root, APPROVALS_REL), "{}"),
36478
36798
  getAutonomousStatus(root),
36479
- ...Object.values(BOOK_FILES).map((f) => readText(join14(root, f)))
36799
+ ...Object.values(BOOK_FILES).map((f) => readText(join15(root, f)))
36480
36800
  ]);
36481
36801
  const keys = Object.keys(BOOK_FILES);
36482
36802
  const bookMap = {};
@@ -36485,7 +36805,7 @@ async function readAutonomous(root) {
36485
36805
  }
36486
36806
  async function readAutonomousLogs(root, date) {
36487
36807
  const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
36488
- const text = await readText(join14(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
36808
+ const text = await readText(join15(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
36489
36809
  const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
36490
36810
  return { date: day, lines };
36491
36811
  }
@@ -36509,22 +36829,22 @@ async function writeAutonomous(root, req, by) {
36509
36829
  switch (req.kind) {
36510
36830
  case "settings": {
36511
36831
  if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
36512
- await writeFile(join14(root, SETTINGS_REL), req.settings, "utf8");
36832
+ await writeFile2(join15(root, SETTINGS_REL), req.settings, "utf8");
36513
36833
  break;
36514
36834
  }
36515
36835
  case "approvals": {
36516
- const map = parseJson(await readText(join14(root, APPROVALS_REL), "{}")) ?? {};
36836
+ const map = parseJson(await readText(join15(root, APPROVALS_REL), "{}")) ?? {};
36517
36837
  if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
36518
36838
  else delete map[req.taskId];
36519
- await writeFile(join14(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
36839
+ await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
36520
36840
  break;
36521
36841
  }
36522
36842
  case "userTodo": {
36523
- const cur = await readText(join14(root, BOOK_FILES.userTodo));
36843
+ const cur = await readText(join15(root, BOOK_FILES.userTodo));
36524
36844
  const trace = `posted by ${tableCell(by)} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
36525
36845
  const row = `| | ${tableCell(req.content)} | ${trace} |
36526
36846
  `;
36527
- await writeFile(join14(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
36847
+ await writeFile2(join15(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
36528
36848
  break;
36529
36849
  }
36530
36850
  case "cron": {
@@ -36540,7 +36860,7 @@ async function writeAutonomous(root, req, by) {
36540
36860
  }
36541
36861
 
36542
36862
  // src/core/network-probe.ts
36543
- import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
36863
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
36544
36864
  import { connect } from "net";
36545
36865
  var CANARY_TARGETS = [
36546
36866
  { host: "1.1.1.1", port: 443 },
@@ -36549,7 +36869,7 @@ var CANARY_TARGETS = [
36549
36869
  var PROBE_TIMEOUT_MS = 2500;
36550
36870
  function detectContainerized() {
36551
36871
  try {
36552
- if (existsSync10("/.dockerenv")) return true;
36872
+ if (existsSync11("/.dockerenv")) return true;
36553
36873
  const cgroup = readFileSync14("/proc/1/cgroup", "utf8");
36554
36874
  return /docker|containerd|kubepods|libpod/.test(cgroup);
36555
36875
  } catch {
@@ -36619,7 +36939,7 @@ async function probeNetwork() {
36619
36939
  // src/core/git-auth.ts
36620
36940
  import { writeFileSync as writeFileSync8 } from "fs";
36621
36941
  import { homedir as homedir4 } from "os";
36622
- import { join as join15 } from "path";
36942
+ import { join as join16 } from "path";
36623
36943
  function tokenTarget(method) {
36624
36944
  if (method === "gitlab-group-token") {
36625
36945
  return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
@@ -36637,7 +36957,7 @@ function applyGitAuth(method) {
36637
36957
  try {
36638
36958
  const line = `https://${target.user}:${token}@${target.host}
36639
36959
  `;
36640
- writeFileSync8(join15(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
36960
+ writeFileSync8(join16(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
36641
36961
  return true;
36642
36962
  } catch {
36643
36963
  return false;
@@ -36822,7 +37142,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
36822
37142
  const apiErr = parser.apiError();
36823
37143
  if (finalExit === 0 && !apiErr.isError) {
36824
37144
  const usage = parser.usage();
36825
- if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
37145
+ if (usage.tokens === 0) usage.tokens = estimateTokens2(captured);
36826
37146
  reportToolResult(attempt.cmd, true);
36827
37147
  return {
36828
37148
  exitCode: 0,
@@ -36874,8 +37194,8 @@ function formatTimestampInZone(date, timeZone) {
36874
37194
  }
36875
37195
 
36876
37196
  // src/ui/slash-commands.ts
36877
- import { existsSync as existsSync11 } from "fs";
36878
- import { join as join16 } from "path";
37197
+ import { existsSync as existsSync12 } from "fs";
37198
+ import { join as join17 } from "path";
36879
37199
  var CONFIG_KEYS = [
36880
37200
  "aiCli",
36881
37201
  "claudeHome",
@@ -36887,7 +37207,7 @@ var CONFIG_KEYS = [
36887
37207
  "aiEnv"
36888
37208
  ];
36889
37209
  function configPath(profileDir2) {
36890
- return join16(profileDir2, "config.json");
37210
+ return join17(profileDir2, "config.json");
36891
37211
  }
36892
37212
  function parseConfigAssignment(key, valueTokens) {
36893
37213
  const value = valueTokens.join(" ").trim();
@@ -36937,7 +37257,7 @@ function runConfig(ctx) {
36937
37257
  ctx.print(t("slash.config.wroteDefault", { verb, path }));
36938
37258
  ctx.print(t("slash.config.addClaudeHint"));
36939
37259
  };
36940
- if (existsSync11(path)) {
37260
+ if (existsSync12(path)) {
36941
37261
  ctx.confirm(
36942
37262
  t("slash.config.replaceConfirm", { path }),
36943
37263
  () => writeDefaults(t("slash.config.verbReplaced"))
@@ -37022,7 +37342,7 @@ function runOutput(ctx) {
37022
37342
  }
37023
37343
  function runLogs(ctx) {
37024
37344
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
37025
- const dir2 = join16(ctx.info.profileDir, "logs");
37345
+ const dir2 = join17(ctx.info.profileDir, "logs");
37026
37346
  const lines = readRecentLogLines(dir2, n);
37027
37347
  if (lines.length === 0) {
37028
37348
  ctx.print(t("slash.logs.none"));
@@ -37337,13 +37657,13 @@ async function runMemoryCompaction(ai, cwd2, input) {
37337
37657
  }
37338
37658
 
37339
37659
  // src/core/git-commit-identity.ts
37340
- import { execFile as execFile3 } from "child_process";
37341
- import { promisify as promisify3 } from "util";
37342
- var run3 = promisify3(execFile3);
37660
+ import { execFile as execFile5 } from "child_process";
37661
+ import { promisify as promisify5 } from "util";
37662
+ var run5 = promisify5(execFile5);
37343
37663
  async function setCommitAuthor(cwd2, machineUsername) {
37344
37664
  if (!machineUsername) return;
37345
37665
  try {
37346
- await run3("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
37666
+ await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
37347
37667
  } catch {
37348
37668
  }
37349
37669
  }
@@ -37442,7 +37762,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
37442
37762
  const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
37443
37763
  const perPromptLimit = config.perPromptTokenLimit ?? 0;
37444
37764
  if (perPromptLimit > 0) {
37445
- const estimated = estimateTokens(prompt);
37765
+ const estimated = estimateTokens2(prompt);
37446
37766
  if (estimated > perPromptLimit) {
37447
37767
  rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
37448
37768
  return;
@@ -37859,15 +38179,15 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
37859
38179
  }
37860
38180
 
37861
38181
  // src/core/fs-browse.ts
37862
- import { readdir as readdir2 } from "fs/promises";
37863
- import { resolve, sep as sep2 } from "path";
38182
+ import { readdir as readdir3 } from "fs/promises";
38183
+ import { resolve as resolve2, sep as sep2 } from "path";
37864
38184
  async function listDir(path, root) {
37865
38185
  if (!root) return { path: "", entries: [] };
37866
- const base = resolve(root);
37867
- const requested = resolve(base, path && path.trim() ? path : ".");
38186
+ const base = resolve2(root);
38187
+ const requested = resolve2(base, path && path.trim() ? path : ".");
37868
38188
  const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
37869
38189
  try {
37870
- const dirents = await readdir2(target, { withFileTypes: true });
38190
+ const dirents = await readdir3(target, { withFileTypes: true });
37871
38191
  const entries2 = dirents.map((d) => ({
37872
38192
  name: d.name,
37873
38193
  type: d.isDirectory() ? "dir" : "file"
@@ -37883,12 +38203,12 @@ async function listDir(path, root) {
37883
38203
 
37884
38204
  // src/core/fs-read.ts
37885
38205
  import { readFile as readFile3 } from "fs/promises";
37886
- import { resolve as resolve2, sep as sep3 } from "path";
38206
+ import { resolve as resolve3, sep as sep3 } from "path";
37887
38207
  var MAX_BYTES = 256 * 1024;
37888
38208
  function resolveInRoot(root, relPath) {
37889
38209
  if (!root) return null;
37890
- const base = resolve2(root);
37891
- const target = resolve2(base, relPath || ".");
38210
+ const base = resolve3(root);
38211
+ const target = resolve3(base, relPath || ".");
37892
38212
  return target === base || target.startsWith(base + sep3) ? target : null;
37893
38213
  }
37894
38214
  async function readWorkerFile(root, path) {
@@ -37907,13 +38227,13 @@ async function readWorkerFile(root, path) {
37907
38227
  }
37908
38228
 
37909
38229
  // src/core/fs-write.ts
37910
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
37911
- import { dirname as dirname3, resolve as resolve3, sep as sep4 } from "path";
38230
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
38231
+ import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
37912
38232
  var MAX_BYTES2 = 2 * 1024 * 1024;
37913
38233
  function resolveInRoot2(root, relPath) {
37914
38234
  if (!root) return null;
37915
- const base = resolve3(root);
37916
- const target = resolve3(base, relPath || ".");
38235
+ const base = resolve4(root);
38236
+ const target = resolve4(base, relPath || ".");
37917
38237
  return target === base || target.startsWith(base + sep4) ? target : null;
37918
38238
  }
37919
38239
  async function writeWorkerFile(root, relPath, content) {
@@ -37926,8 +38246,8 @@ async function writeWorkerFile(root, relPath, content) {
37926
38246
  return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
37927
38247
  }
37928
38248
  try {
37929
- await mkdir2(dirname3(target), { recursive: true });
37930
- await writeFile2(target, content, "utf8");
38249
+ await mkdir3(dirname4(target), { recursive: true });
38250
+ await writeFile3(target, content, "utf8");
37931
38251
  return { ok: true, path: target, bytes };
37932
38252
  } catch (err) {
37933
38253
  return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
@@ -37935,12 +38255,12 @@ async function writeWorkerFile(root, relPath, content) {
37935
38255
  }
37936
38256
 
37937
38257
  // src/core/fs-mutate.ts
37938
- import { mkdir as mkdir3, writeFile as writeFile3, rename, rm, stat } from "fs/promises";
37939
- import { basename as basename3, dirname as dirname4, resolve as resolve4, sep as sep5 } from "path";
38258
+ import { mkdir as mkdir4, writeFile as writeFile4, rename as rename2, rm as rm2, stat } from "fs/promises";
38259
+ import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
37940
38260
  var MAX_BYTES3 = 2 * 1024 * 1024;
37941
38261
  function resolveInRoot3(root, rel) {
37942
- const base = resolve4(root);
37943
- const target = resolve4(base, rel || ".");
38262
+ const base = resolve5(root);
38263
+ const target = resolve5(base, rel || ".");
37944
38264
  return target === base || target.startsWith(base + sep5) ? target : null;
37945
38265
  }
37946
38266
  async function isDir(p) {
@@ -37965,39 +38285,39 @@ async function mutateFs(root, req) {
37965
38285
  switch (req.op) {
37966
38286
  case "mkdir": {
37967
38287
  const target = resolveInRoot3(root, req.path ?? "");
37968
- if (!target || target === resolve4(root)) return escaped(req.path ?? "");
37969
- await mkdir3(target, { recursive: true });
38288
+ if (!target || target === resolve5(root)) return escaped(req.path ?? "");
38289
+ await mkdir4(target, { recursive: true });
37970
38290
  return { ok: true, path: target };
37971
38291
  }
37972
38292
  case "create": {
37973
38293
  const target = resolveInRoot3(root, req.path ?? "");
37974
- if (!target || target === resolve4(root)) return escaped(req.path ?? "");
38294
+ if (!target || target === resolve5(root)) return escaped(req.path ?? "");
37975
38295
  if (await exists(target)) return { ok: false, path: target, error: "already exists" };
37976
38296
  const content = req.content ?? "";
37977
38297
  if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
37978
38298
  return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
37979
38299
  }
37980
- await mkdir3(dirname4(target), { recursive: true });
37981
- await writeFile3(target, content, "utf8");
38300
+ await mkdir4(dirname5(target), { recursive: true });
38301
+ await writeFile4(target, content, "utf8");
37982
38302
  return { ok: true, path: target };
37983
38303
  }
37984
38304
  case "move": {
37985
38305
  const from = resolveInRoot3(root, req.from ?? "");
37986
38306
  let to = resolveInRoot3(root, req.to ?? "");
37987
38307
  if (!from || !to) return escaped(req.from ?? req.to ?? "");
37988
- if (from === resolve4(root)) return { ok: false, path: from, error: "cannot move the project root" };
37989
- if (await isDir(to)) to = resolve4(to, basename3(from));
37990
- if (to !== resolve4(root) && !to.startsWith(resolve4(root) + sep5)) return escaped(req.to ?? "");
38308
+ if (from === resolve5(root)) return { ok: false, path: from, error: "cannot move the project root" };
38309
+ if (await isDir(to)) to = resolve5(to, basename3(from));
38310
+ if (to !== resolve5(root) && !to.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
37991
38311
  if (await exists(to)) return { ok: false, path: to, error: "destination already exists" };
37992
- await mkdir3(dirname4(to), { recursive: true });
37993
- await rename(from, to);
38312
+ await mkdir4(dirname5(to), { recursive: true });
38313
+ await rename2(from, to);
37994
38314
  return { ok: true, path: to };
37995
38315
  }
37996
38316
  case "delete": {
37997
38317
  const target = resolveInRoot3(root, req.path ?? "");
37998
38318
  if (!target) return escaped(req.path ?? "");
37999
- if (target === resolve4(root)) return { ok: false, path: target, error: "cannot delete the project root" };
38000
- await rm(target, { recursive: true, force: true });
38319
+ if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
38320
+ await rm2(target, { recursive: true, force: true });
38001
38321
  return { ok: true, path: target };
38002
38322
  }
38003
38323
  default:
@@ -38009,12 +38329,12 @@ async function mutateFs(root, req) {
38009
38329
  }
38010
38330
 
38011
38331
  // src/core/fs-transfer.ts
38012
- import { mkdir as mkdir4, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
38013
- import { basename as basename4, dirname as dirname5, extname, resolve as resolve5, sep as sep6 } from "path";
38332
+ import { mkdir as mkdir5, readFile as readFile4, stat as stat2, writeFile as writeFile5 } from "fs/promises";
38333
+ import { basename as basename4, dirname as dirname6, extname, resolve as resolve6, sep as sep6 } from "path";
38014
38334
  function resolveInRoot4(root, relPath) {
38015
38335
  if (!root) return null;
38016
- const base = resolve5(root);
38017
- const target = resolve5(base, relPath || ".");
38336
+ const base = resolve6(root);
38337
+ const target = resolve6(base, relPath || ".");
38018
38338
  return target === base || target.startsWith(base + sep6) ? target : null;
38019
38339
  }
38020
38340
  var MIME_BY_EXT = {
@@ -38053,8 +38373,8 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
38053
38373
  return { ok: false, path: target, bytes: 0, error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
38054
38374
  }
38055
38375
  try {
38056
- await mkdir4(dirname5(target), { recursive: true });
38057
- await writeFile4(target, buf);
38376
+ await mkdir5(dirname6(target), { recursive: true });
38377
+ await writeFile5(target, buf);
38058
38378
  return { ok: true, path: target, bytes: buf.byteLength };
38059
38379
  } catch (err) {
38060
38380
  return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
@@ -38129,14 +38449,14 @@ function handleFsChannels(ctx, message, payload) {
38129
38449
 
38130
38450
  // src/core/git-diff.ts
38131
38451
  import { readFile as readFile5 } from "fs/promises";
38132
- import { execFile as execFile4 } from "child_process";
38133
- import { basename as basename5, dirname as dirname6, resolve as resolve6, sep as sep7 } from "path";
38134
- import { promisify as promisify4 } from "util";
38135
- var run4 = promisify4(execFile4);
38452
+ import { execFile as execFile6 } from "child_process";
38453
+ import { basename as basename5, dirname as dirname7, resolve as resolve7, sep as sep7 } from "path";
38454
+ import { promisify as promisify6 } from "util";
38455
+ var run6 = promisify6(execFile6);
38136
38456
  function resolveInRoot5(root, relPath) {
38137
38457
  if (!root) return null;
38138
- const base = resolve6(root);
38139
- const target = resolve6(base, relPath || ".");
38458
+ const base = resolve7(root);
38459
+ const target = resolve7(base, relPath || ".");
38140
38460
  return target === base || target.startsWith(base + sep7) ? target : null;
38141
38461
  }
38142
38462
  function looksBinary(buf) {
@@ -38151,7 +38471,7 @@ async function gitDiff(root, path) {
38151
38471
  if (!target) {
38152
38472
  return { path, oldContent: "", newContent: "", isBinary: false };
38153
38473
  }
38154
- const dir2 = dirname6(target);
38474
+ const dir2 = dirname7(target);
38155
38475
  const base = basename5(target);
38156
38476
  let newBuf = null;
38157
38477
  try {
@@ -38160,7 +38480,7 @@ async function gitDiff(root, path) {
38160
38480
  }
38161
38481
  let oldBuf = null;
38162
38482
  try {
38163
- const { stdout } = await run4("git", ["show", `HEAD:./${base}`], {
38483
+ const { stdout } = await run6("git", ["show", `HEAD:./${base}`], {
38164
38484
  cwd: dir2,
38165
38485
  timeout: 15e3,
38166
38486
  maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
@@ -38190,12 +38510,12 @@ async function gitDiff(root, path) {
38190
38510
  }
38191
38511
 
38192
38512
  // src/core/git-env.ts
38193
- import { execFile as execFile5 } from "child_process";
38194
- import { promisify as promisify5 } from "util";
38195
- var run5 = promisify5(execFile5);
38513
+ import { execFile as execFile7 } from "child_process";
38514
+ import { promisify as promisify7 } from "util";
38515
+ var run7 = promisify7(execFile7);
38196
38516
  async function hasBinary(bin, args = ["--version"]) {
38197
38517
  try {
38198
- await run5(bin, args, { timeout: 5e3 });
38518
+ await run7(bin, args, { timeout: 5e3 });
38199
38519
  return true;
38200
38520
  } catch {
38201
38521
  return false;
@@ -38208,7 +38528,7 @@ async function checkGitEnv(provider) {
38208
38528
  let account = null;
38209
38529
  if (installed) {
38210
38530
  try {
38211
- const { stdout, stderr } = await run5(bin, ["auth", "status"], { timeout: 8e3 });
38531
+ const { stdout, stderr } = await run7(bin, ["auth", "status"], { timeout: 8e3 });
38212
38532
  const out = `${stdout}
38213
38533
  ${stderr}`;
38214
38534
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -38221,14 +38541,14 @@ ${stderr}`;
38221
38541
  }
38222
38542
 
38223
38543
  // src/core/git-ssh-key.ts
38224
- import { execFile as execFile6 } from "child_process";
38225
- import { readFile as readFile6, rm as rm2, mkdir as mkdir5 } from "fs/promises";
38226
- import { join as join17 } from "path";
38227
- import { promisify as promisify6 } from "util";
38228
- var run6 = promisify6(execFile6);
38544
+ import { execFile as execFile8 } from "child_process";
38545
+ import { readFile as readFile6, rm as rm3, mkdir as mkdir6 } from "fs/promises";
38546
+ import { join as join18 } from "path";
38547
+ import { promisify as promisify8 } from "util";
38548
+ var run8 = promisify8(execFile8);
38229
38549
  function keyPaths(profileDir2) {
38230
- const dir2 = join17(profileDir2, ".ssh");
38231
- const key = join17(dir2, "id_4pm");
38550
+ const dir2 = join18(profileDir2, ".ssh");
38551
+ const key = join18(dir2, "id_4pm");
38232
38552
  return { dir: dir2, key, pub: `${key}.pub` };
38233
38553
  }
38234
38554
  async function readPublicKey(pub) {
@@ -38240,14 +38560,14 @@ async function readPublicKey(pub) {
38240
38560
  }
38241
38561
  async function fingerprintOf(pub) {
38242
38562
  try {
38243
- const { stdout } = await run6("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
38563
+ const { stdout } = await run8("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
38244
38564
  return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
38245
38565
  } catch {
38246
38566
  return null;
38247
38567
  }
38248
38568
  }
38249
38569
  async function configureGit(key) {
38250
- await run6(
38570
+ await run8(
38251
38571
  "git",
38252
38572
  ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
38253
38573
  { timeout: 5e3 }
@@ -38255,10 +38575,10 @@ async function configureGit(key) {
38255
38575
  }
38256
38576
  async function generate(profileDir2) {
38257
38577
  const { dir: dir2, key, pub } = keyPaths(profileDir2);
38258
- await mkdir5(dir2, { recursive: true });
38259
- await rm2(key, { force: true });
38260
- await rm2(pub, { force: true });
38261
- await run6("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
38578
+ await mkdir6(dir2, { recursive: true });
38579
+ await rm3(key, { force: true });
38580
+ await rm3(pub, { force: true });
38581
+ await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
38262
38582
  timeout: 15e3
38263
38583
  });
38264
38584
  await configureGit(key);
@@ -38266,9 +38586,9 @@ async function generate(profileDir2) {
38266
38586
  }
38267
38587
  async function remove(profileDir2) {
38268
38588
  const { key, pub } = keyPaths(profileDir2);
38269
- await rm2(key, { force: true });
38270
- await rm2(pub, { force: true });
38271
- await run6("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
38589
+ await rm3(key, { force: true });
38590
+ await rm3(pub, { force: true });
38591
+ await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
38272
38592
  () => void 0
38273
38593
  );
38274
38594
  return { publicKey: null, fingerprint: null };
@@ -38289,22 +38609,22 @@ async function manageSshKey(op, profileDir2) {
38289
38609
  }
38290
38610
 
38291
38611
  // src/core/git-history.ts
38292
- import { readdir as readdir3 } from "fs/promises";
38293
- import { execFile as execFile7 } from "child_process";
38294
- import { resolve as resolve7, sep as sep8 } from "path";
38295
- import { promisify as promisify7 } from "util";
38296
- var run7 = promisify7(execFile7);
38612
+ import { readdir as readdir4 } from "fs/promises";
38613
+ import { execFile as execFile9 } from "child_process";
38614
+ import { resolve as resolve8, sep as sep8 } from "path";
38615
+ import { promisify as promisify9 } from "util";
38616
+ var run9 = promisify9(execFile9);
38297
38617
  var FS = "";
38298
38618
  var RS = "";
38299
38619
  function resolveRepo(root, repo) {
38300
38620
  if (!root) return null;
38301
- const base = resolve7(root);
38302
- const requested = resolve7(base, repo && repo.trim() ? repo : ".");
38621
+ const base = resolve8(root);
38622
+ const requested = resolve8(base, repo && repo.trim() ? repo : ".");
38303
38623
  return requested === base || requested.startsWith(base + sep8) ? requested : base;
38304
38624
  }
38305
38625
  async function git(cwd2, args) {
38306
38626
  try {
38307
- const { stdout } = await run7("git", args, {
38627
+ const { stdout } = await run9("git", args, {
38308
38628
  cwd: cwd2,
38309
38629
  timeout: 15e3,
38310
38630
  maxBuffer: 8 * 1024 * 1024
@@ -38316,7 +38636,7 @@ async function git(cwd2, args) {
38316
38636
  }
38317
38637
  async function gitRepos(root) {
38318
38638
  if (!root) return { repos: [] };
38319
- const base = resolve7(root);
38639
+ const base = resolve8(root);
38320
38640
  const found = /* @__PURE__ */ new Set();
38321
38641
  const repos = [];
38322
38642
  const add = async (dir2) => {
@@ -38330,16 +38650,16 @@ async function gitRepos(root) {
38330
38650
  };
38331
38651
  await add(base);
38332
38652
  try {
38333
- const lvl1 = await readdir3(base, { withFileTypes: true });
38653
+ const lvl1 = await readdir4(base, { withFileTypes: true });
38334
38654
  for (const d of lvl1) {
38335
38655
  if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
38336
- const p1 = resolve7(base, d.name);
38656
+ const p1 = resolve8(base, d.name);
38337
38657
  await add(p1);
38338
38658
  try {
38339
- const lvl2 = await readdir3(p1, { withFileTypes: true });
38659
+ const lvl2 = await readdir4(p1, { withFileTypes: true });
38340
38660
  for (const d2 of lvl2) {
38341
38661
  if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
38342
- await add(resolve7(p1, d2.name));
38662
+ await add(resolve8(p1, d2.name));
38343
38663
  }
38344
38664
  } catch {
38345
38665
  }
@@ -38457,79 +38777,7 @@ function handleGitChannels(ctx, message, payload) {
38457
38777
  }
38458
38778
 
38459
38779
  // src/core/ws-client/handlers/misc.ts
38460
- import { join as join18 } from "path";
38461
-
38462
- // src/core/ai-assist.ts
38463
- import { execFile as execFile8 } from "child_process";
38464
- import { promisify as promisify8 } from "util";
38465
- var run8 = promisify8(execFile8);
38466
- function aiCli() {
38467
- return process.env.AI_CLI || "claude";
38468
- }
38469
- function aiArgs(cli, prompt) {
38470
- if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
38471
- if (cli === "codex") return ["exec", "--json", prompt];
38472
- return ["-p", prompt];
38473
- }
38474
- function estimateTokens2(prompt, output) {
38475
- return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
38476
- }
38477
- function parseAi(cli, stdout, prompt) {
38478
- if (cli === "claude") {
38479
- try {
38480
- const j = JSON.parse(stdout);
38481
- const text2 = (j.result ?? "").trim();
38482
- const tokens = (j.usage?.input_tokens ?? 0) + (j.usage?.output_tokens ?? 0);
38483
- if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens2(prompt, text2) };
38484
- } catch {
38485
- }
38486
- }
38487
- if (cli === "codex") {
38488
- try {
38489
- let text2 = "";
38490
- let tokens = 0;
38491
- for (const line of stdout.split("\n").filter(Boolean)) {
38492
- const obj = JSON.parse(line);
38493
- if (obj.text) text2 = obj.text;
38494
- else if (obj.message) text2 = obj.message;
38495
- if (obj.usage) {
38496
- tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
38497
- }
38498
- }
38499
- if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens2(prompt, text2) };
38500
- } catch {
38501
- }
38502
- }
38503
- const text = stdout.trim();
38504
- return { text, tokens: estimateTokens2(prompt, text) };
38505
- }
38506
- async function runAi(prompt) {
38507
- const cli = aiCli();
38508
- const { stdout } = await run8(cli, aiArgs(cli, prompt), {
38509
- timeout: 12e4,
38510
- maxBuffer: 8 * 1024 * 1024
38511
- });
38512
- return parseAi(cli, stdout, prompt);
38513
- }
38514
- async function aiGenerate(prompt) {
38515
- const { text } = await runAi(prompt);
38516
- return text;
38517
- }
38518
- async function aiOutboundReview(input, allowedRepos, projectPath) {
38519
- const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
38520
- ${input}`;
38521
- const { text, tokens } = await runAi(prompt);
38522
- const start = text.indexOf("{");
38523
- const end = text.lastIndexOf("}");
38524
- if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
38525
- try {
38526
- const j = JSON.parse(text.slice(start, end + 1));
38527
- const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
38528
- return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
38529
- } catch {
38530
- return { ok: false, reasons: ["ai_parse"], tokens };
38531
- }
38532
- }
38780
+ import { join as join19 } from "path";
38533
38781
 
38534
38782
  // src/core/outbound-review.ts
38535
38783
  function compilePatterns(sources, fallback) {
@@ -38605,9 +38853,13 @@ async function evaluateReview(payload) {
38605
38853
  // src/core/ws-client/handlers/misc.ts
38606
38854
  function handleMiscChannels(ctx, message, payload) {
38607
38855
  switch (message.channel) {
38856
+ case WsChannels.CLI_UPDATE: {
38857
+ ctx.updateCliNow();
38858
+ return true;
38859
+ }
38608
38860
  case WsChannels.LOG_READ: {
38609
38861
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
38610
- const lines = readRecentLogLines(join18(ctx.profileDir, "logs"), limit);
38862
+ const lines = readRecentLogLines(join19(ctx.profileDir, "logs"), limit);
38611
38863
  ctx.send(WsChannels.LOG_READ, { lines }, message.id);
38612
38864
  return true;
38613
38865
  }
@@ -38640,161 +38892,6 @@ function handleMiscChannels(ctx, message, payload) {
38640
38892
  // src/core/ws-client/handlers/project.ts
38641
38893
  import { existsSync as existsSync13, mkdirSync as mkdirSync6, renameSync, rmSync as rmSync6 } from "fs";
38642
38894
  import { join as join20 } from "path";
38643
-
38644
- // src/core/scaffold.ts
38645
- import { cp, mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
38646
- import { existsSync as existsSync12 } from "fs";
38647
- import { execFile as execFile9 } from "child_process";
38648
- import { dirname as dirname7, join as join19, resolve as resolve8 } from "path";
38649
- import { fileURLToPath as fileURLToPath3 } from "url";
38650
- import { promisify as promisify9 } from "util";
38651
- var run9 = promisify9(execFile9);
38652
- function reposOf(spec) {
38653
- const repos = spec?.repos;
38654
- return Array.isArray(repos) ? repos : [];
38655
- }
38656
- function subDirName(repo) {
38657
- const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
38658
- return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
38659
- }
38660
- async function provisionRepos(target, repos, emit, opts = {}) {
38661
- for (const repo of repos) {
38662
- if (repo.primary) {
38663
- if (opts.clonePrimary && repo.url) {
38664
- if (existsSync12(join19(target, ".git"))) {
38665
- emit("git", `Primary repo already present \u2014 skipping clone.`);
38666
- continue;
38667
- }
38668
- emit("git", `Cloning ${repo.url}\u2026`);
38669
- await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
38670
- continue;
38671
- }
38672
- emit("git", "Initializing the primary repo\u2026");
38673
- await run9("git", ["init"], { cwd: target, timeout: 2e4 });
38674
- if (repo.url) {
38675
- await run9("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
38676
- () => void 0
38677
- );
38678
- }
38679
- continue;
38680
- }
38681
- const dir2 = join19(target, subDirName(repo));
38682
- if (repo.url) {
38683
- if (existsSync12(join19(dir2, ".git"))) {
38684
- emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
38685
- continue;
38686
- }
38687
- emit("git", `Cloning sub-repo ${repo.url}\u2026`);
38688
- await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
38689
- } else {
38690
- emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
38691
- await mkdir6(dir2, { recursive: true });
38692
- await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
38693
- }
38694
- }
38695
- }
38696
- function sampleDir() {
38697
- if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
38698
- const here = dirname7(fileURLToPath3(import.meta.url));
38699
- const bundled = resolve8(here, "project-sample");
38700
- for (const candidate of [bundled, resolve8(here, "../project-sample"), resolve8(here, "../../project-sample")]) {
38701
- if (existsSync12(candidate)) return candidate;
38702
- }
38703
- return bundled;
38704
- }
38705
- async function scaffoldProject(payload, profileDir2, onProgress) {
38706
- let lastStep = "start";
38707
- const emit = (step, message) => {
38708
- lastStep = step;
38709
- onProgress?.({ projectId: payload.projectId, step, message });
38710
- };
38711
- try {
38712
- const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
38713
- const target = join19(root, payload.projectName);
38714
- await mkdir6(target, { recursive: true });
38715
- emit("copy", "Copying the sample template\u2026");
38716
- await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
38717
- if (payload.spec) {
38718
- emit("spec", "Writing project.spec.json\u2026");
38719
- await writeFile5(
38720
- join19(target, "project.spec.json"),
38721
- JSON.stringify(payload.spec, null, 2),
38722
- "utf8"
38723
- );
38724
- }
38725
- const repos = reposOf(payload.spec);
38726
- if (repos.length > 0) await provisionRepos(target, repos, emit);
38727
- if (payload.spec) await aiInit(target, payload.spec, emit);
38728
- emit("version", "Writing .4pm/.4pm.json\u2026");
38729
- await writeTemplateMarker(target);
38730
- onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
38731
- return { ok: true, path: target };
38732
- } catch (err) {
38733
- return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
38734
- }
38735
- }
38736
- async function writeTemplateMarker(target) {
38737
- const dir2 = join19(target, ".4pm");
38738
- await mkdir6(dir2, { recursive: true });
38739
- const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
38740
- await writeFile5(join19(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
38741
- }
38742
- async function aiInit(target, spec, emit) {
38743
- emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
38744
- const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
38745
- if (subagents.length > 0) {
38746
- await mkdir6(join19(target, ".claude", "agents"), { recursive: true });
38747
- for (const sa of subagents) {
38748
- const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
38749
- if (!name) continue;
38750
- const body = `---
38751
- name: ${name}
38752
- ---
38753
-
38754
- ${(sa.description || "").trim()}
38755
- `;
38756
- await writeFile5(join19(target, ".claude", "agents", `${name}.md`), body, "utf8");
38757
- }
38758
- }
38759
- const specJson = JSON.stringify(spec);
38760
- await generateFile(
38761
- join19(target, "README.md"),
38762
- `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
38763
- ${specJson}`
38764
- );
38765
- await generateFile(
38766
- join19(target, "CLAUDE.md"),
38767
- `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
38768
- ${specJson}`
38769
- );
38770
- }
38771
- async function generateFile(path, prompt) {
38772
- try {
38773
- const text = await aiGenerate(prompt);
38774
- if (text.trim()) await writeFile5(path, text.trim() + "\n", "utf8");
38775
- } catch {
38776
- }
38777
- }
38778
- async function addProject(payload, profileDir2, onProgress) {
38779
- let lastStep = "start";
38780
- const emit = (step, message) => {
38781
- lastStep = step;
38782
- onProgress?.({ projectId: payload.projectId, step, message });
38783
- };
38784
- try {
38785
- const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
38786
- const target = join19(root, payload.projectName);
38787
- await mkdir6(target, { recursive: true });
38788
- const repos = payload.repos ?? [];
38789
- if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
38790
- onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
38791
- return { ok: true, path: target };
38792
- } catch (err) {
38793
- return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
38794
- }
38795
- }
38796
-
38797
- // src/core/ws-client/handlers/project.ts
38798
38895
  function handleProjectChannels(ctx, message, payload) {
38799
38896
  switch (message.channel) {
38800
38897
  case WsChannels.PHYSIC_SYNC: {
@@ -38815,6 +38912,7 @@ function handleProjectChannels(ctx, message, payload) {
38815
38912
  ctx.physicRoot = ctx.physicFolderPath(sync.newName);
38816
38913
  ctx.bus.setProject(sync.newName);
38817
38914
  ctx.bus.log(t("project.folderSynced", { name: sync.newName }));
38915
+ void ctx.cloneServingRepos().catch(() => void 0);
38818
38916
  return true;
38819
38917
  }
38820
38918
  case WsChannels.PHYSIC_DELETE: {
@@ -39088,7 +39186,7 @@ function handleToolsChannels(ctx, message, payload) {
39088
39186
  }
39089
39187
 
39090
39188
  // src/core/agents.ts
39091
- import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir4, rm as rm3 } from "fs/promises";
39189
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir5, rm as rm4 } from "fs/promises";
39092
39190
  import { join as join21 } from "path";
39093
39191
  var AGENTS_REL = ".claude/agents";
39094
39192
  var SKILLS_REL = ".claude/skills";
@@ -39113,7 +39211,7 @@ async function listAgents(root) {
39113
39211
  const subagents = [];
39114
39212
  const skills = [];
39115
39213
  try {
39116
- for (const e of await readdir4(join21(root, AGENTS_REL), { withFileTypes: true })) {
39214
+ for (const e of await readdir5(join21(root, AGENTS_REL), { withFileTypes: true })) {
39117
39215
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
39118
39216
  const fm = frontmatter(await readText2(join21(root, AGENTS_REL, e.name)));
39119
39217
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
@@ -39121,7 +39219,7 @@ async function listAgents(root) {
39121
39219
  } catch {
39122
39220
  }
39123
39221
  try {
39124
- for (const e of await readdir4(join21(root, SKILLS_REL), { withFileTypes: true })) {
39222
+ for (const e of await readdir5(join21(root, SKILLS_REL), { withFileTypes: true })) {
39125
39223
  if (!e.isDirectory()) continue;
39126
39224
  const fm = frontmatter(await readText2(join21(root, SKILLS_REL, e.name, "SKILL.md")));
39127
39225
  skills.push({ name: e.name, description: fm.description });
@@ -39151,8 +39249,8 @@ async function writeAgent(root, req) {
39151
39249
  if (!n) return { ok: false, error: "invalid name" };
39152
39250
  try {
39153
39251
  if (req.action === "delete") {
39154
- if (req.kind === "subagent") await rm3(itemPath(root, "subagent", n), { force: true });
39155
- else await rm3(join21(root, SKILLS_REL, n), { recursive: true, force: true });
39252
+ if (req.kind === "subagent") await rm4(itemPath(root, "subagent", n), { force: true });
39253
+ else await rm4(join21(root, SKILLS_REL, n), { recursive: true, force: true });
39156
39254
  return { ok: true };
39157
39255
  }
39158
39256
  if (req.content === void 0) return { ok: false, error: "content is required" };
@@ -39273,7 +39371,7 @@ async function writeAgentTools(root, scope, permissions) {
39273
39371
 
39274
39372
  // src/core/packages.ts
39275
39373
  import { createHash as createHash4 } from "crypto";
39276
- import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir5, rm as rm4, stat as stat3 } from "fs/promises";
39374
+ import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir6, rm as rm5, stat as stat3 } from "fs/promises";
39277
39375
  import { dirname as dirname9, join as join24, relative } from "path";
39278
39376
  var AGENTS_REL2 = ".claude/agents";
39279
39377
  var SKILLS_REL2 = ".claude/skills";
@@ -39293,7 +39391,7 @@ async function listFilesRel(dir2) {
39293
39391
  async function walk2(d) {
39294
39392
  let entries2;
39295
39393
  try {
39296
- entries2 = await readdir5(d, { withFileTypes: true });
39394
+ entries2 = await readdir6(d, { withFileTypes: true });
39297
39395
  } catch {
39298
39396
  return;
39299
39397
  }
@@ -39446,11 +39544,11 @@ async function removePackage(root, slug) {
39446
39544
  const entry = manifest[n];
39447
39545
  if (!entry) return { ok: false, error: "not installed" };
39448
39546
  if (entry.kind === "subagent") {
39449
- await rm4(join24(root, AGENTS_REL2, `${n}.md`), { force: true });
39450
- await rm4(join24(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
39451
- await rm4(join24(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
39547
+ await rm5(join24(root, AGENTS_REL2, `${n}.md`), { force: true });
39548
+ await rm5(join24(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
39549
+ await rm5(join24(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
39452
39550
  } else {
39453
- await rm4(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
39551
+ await rm5(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
39454
39552
  }
39455
39553
  delete manifest[n];
39456
39554
  await writeManifest(root, manifest);
@@ -39461,7 +39559,7 @@ async function removePackage(root, slug) {
39461
39559
  }
39462
39560
 
39463
39561
  // src/core/graph.ts
39464
- import { readFile as readFile11, readdir as readdir6 } from "fs/promises";
39562
+ import { readFile as readFile11, readdir as readdir7 } from "fs/promises";
39465
39563
  import { join as join25, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
39466
39564
  var MAX_FILES = 2e3;
39467
39565
  var MAX_NODES = 3e3;
@@ -39474,7 +39572,7 @@ async function walk(root, accept) {
39474
39572
  const dir2 = stack.pop();
39475
39573
  let entries2;
39476
39574
  try {
39477
- entries2 = await readdir6(dir2, { withFileTypes: true });
39575
+ entries2 = await readdir7(dir2, { withFileTypes: true });
39478
39576
  } catch {
39479
39577
  continue;
39480
39578
  }
@@ -40170,6 +40268,9 @@ var WsClient = class {
40170
40268
  /** The physic project folder root this cli serves (null = no project) — fs.list is
40171
40269
  * scoped to it so the web FsPicker can only browse inward, never out. */
40172
40270
  physicRoot = null;
40271
+ /** Declared repos of the served project (ADR-0289), from each ws_token — cloned into the physic
40272
+ * root on connect (and after a PHYSIC_SYNC creates the folder) when any is missing its `.git`. */
40273
+ servingRepos = [];
40173
40274
  /** This machine-user's username — the git commit author for this project (ADR-0097).
40174
40275
  * Push uses the account already logged in with gh/glab on the worker. */
40175
40276
  machineUsername = "";
@@ -40287,9 +40388,24 @@ var WsClient = class {
40287
40388
  setConsoleWatching: (on) => self.setConsoleWatching(on),
40288
40389
  setMetricsWatching: (on) => self.setMetricsWatching(on),
40289
40390
  ensurePhysicFolderPath: (folder) => self.ensurePhysicFolderPath(folder),
40290
- physicFolderPath: (projectName) => self.physicFolderPath(projectName)
40391
+ physicFolderPath: (projectName) => self.physicFolderPath(projectName),
40392
+ cloneServingRepos: () => self.ensureServingReposCloned(),
40393
+ updateCliNow: () => self.updateScheduler.updateNow()
40291
40394
  };
40292
40395
  }
40396
+ /**
40397
+ * Clone any declared repo missing from the served physic root (ADR-0289) — idempotent (skips a
40398
+ * repo that already has `.git`). Best-effort: a clone failure never disrupts the session (the web
40399
+ * still surfaces "not provisioned"). No-op when idle (no physic root) or no repos declared.
40400
+ */
40401
+ async ensureServingReposCloned() {
40402
+ if (!this.physicRoot || this.servingRepos.length === 0) return;
40403
+ try {
40404
+ await ensureReposCloned(this.physicRoot, this.servingRepos, (_step, message) => this.bus.log(message));
40405
+ } catch (err) {
40406
+ logger.warn("repos.clone.failed", { error: err instanceof Error ? err.message : String(err) });
40407
+ }
40408
+ }
40293
40409
  /**
40294
40410
  * Detect the worker's tools and report the snapshot to the server (ADR-0254, `tools.report`) so the
40295
40411
  * DB-backed Tools panel + restore target stay current. One-way, best-effort — a detect/send failure
@@ -40473,6 +40589,8 @@ var WsClient = class {
40473
40589
  this.restrictToFolder = token.aiRestrictToFolder === true;
40474
40590
  applyGitAuth(token.gitAuth ?? null);
40475
40591
  this.machineUsername = token.machineUsername ?? "";
40592
+ this.servingRepos = token.repos ?? [];
40593
+ void this.ensureServingReposCloned();
40476
40594
  if (token.scope && token.scope !== this.credential.scope) {
40477
40595
  this.credential.scope = token.scope;
40478
40596
  writeCredential(this.context.profileDir, this.credential);