@4pm/cli 1.24.0 → 1.27.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 +561 -247
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28557,11 +28557,12 @@ function fetchWhoami(serverUrl, hashcode3) {
28557
28557
  function selfUnlink(serverUrl, hashcode3) {
28558
28558
  return post(serverUrl, "/machine-links/self-unlink", { hashcode3 });
28559
28559
  }
28560
- async function fetchCliVersion(serverUrl) {
28560
+ async function fetchCliVersion(serverUrl, version) {
28561
28561
  const ctrl = new AbortController();
28562
28562
  const timer = setTimeout(() => ctrl.abort(), 2e4);
28563
28563
  try {
28564
- const res = await fetch(`${serverUrl}/meta/cli-version`, { signal: ctrl.signal });
28564
+ const query = version ? `?version=${encodeURIComponent(version)}` : "";
28565
+ const res = await fetch(`${serverUrl}/meta/cli-version${query}`, { signal: ctrl.signal });
28565
28566
  return unwrap(res);
28566
28567
  } finally {
28567
28568
  clearTimeout(timer);
@@ -28798,6 +28799,8 @@ var en_default = {
28798
28799
  "update.updating": "Updating {from} \u2192 {to}\u2026",
28799
28800
  "update.checkVersionFailed": "Could not check cli version: {error}",
28800
28801
  "update.newVersionAutoUpdateOff": "A new version {latest} is available (running {current}) \u2014 autoUpdate is off.",
28802
+ "update.unsupportedSoon": "cli {current} stops being supported on {date} \u2014 update before then (4pm update).",
28803
+ "update.policyNote": "Note from 4PM: {note}",
28801
28804
  "update.applying": "{kind} cli update {from} \u2192 {to}\u2026",
28802
28805
  "update.kindMandatory": "Mandatory",
28803
28806
  "update.kindAutomatic": "Automatic",
@@ -28981,6 +28984,8 @@ var vi_default = {
28981
28984
  "update.updating": "\u0110ang c\u1EADp nh\u1EADt {from} \u2192 {to}\u2026",
28982
28985
  "update.checkVersionFailed": "Kh\xF4ng th\u1EC3 ki\u1EC3m tra phi\xEAn b\u1EA3n cli: {error}",
28983
28986
  "update.newVersionAutoUpdateOff": "C\xF3 phi\xEAn b\u1EA3n m\u1EDBi {latest} (\u0111ang ch\u1EA1y {current}) \u2014 autoUpdate \u0111ang t\u1EAFt.",
28987
+ "update.unsupportedSoon": "cli {current} s\u1EBD ng\u1EEBng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3 t\u1EEB {date} \u2014 h\xE3y c\u1EADp nh\u1EADt tr\u01B0\u1EDBc ng\xE0y \u0111\xF3 (4pm update).",
28988
+ "update.policyNote": "Ghi ch\xFA t\u1EEB 4PM: {note}",
28984
28989
  "update.applying": "C\u1EADp nh\u1EADt cli {kind} {from} \u2192 {to}\u2026",
28985
28990
  "update.kindMandatory": "b\u1EAFt bu\u1ED9c",
28986
28991
  "update.kindAutomatic": "t\u1EF1 \u0111\u1ED9ng",
@@ -29164,6 +29169,8 @@ var ja_default = {
29164
29169
  "update.updating": "\u66F4\u65B0\u4E2D {from} \u2192 {to}\u2026",
29165
29170
  "update.checkVersionFailed": "cli \u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F: {error}",
29166
29171
  "update.newVersionAutoUpdateOff": "\u65B0\u3057\u3044\u30D0\u30FC\u30B8\u30E7\u30F3 {latest} \u304C\u5229\u7528\u53EF\u80FD\u3067\u3059\uFF08\u5B9F\u884C\u4E2D {current}\uFF09\u2014 autoUpdate \u306F\u30AA\u30D5\u3067\u3059\u3002",
29172
+ "update.unsupportedSoon": "cli {current} \u306F {date} \u306B\u30B5\u30DD\u30FC\u30C8\u304C\u7D42\u4E86\u3057\u307E\u3059 \u2014 \u305D\u308C\u307E\u3067\u306B\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\uFF084pm update\uFF09\u3002",
29173
+ "update.policyNote": "4PM \u304B\u3089\u306E\u304A\u77E5\u3089\u305B: {note}",
29167
29174
  "update.applying": "{kind} \u306E cli \u66F4\u65B0 {from} \u2192 {to}\u2026",
29168
29175
  "update.kindMandatory": "\u5FC5\u9808",
29169
29176
  "update.kindAutomatic": "\u81EA\u52D5",
@@ -29347,6 +29354,8 @@ var zh_default = {
29347
29354
  "update.updating": "\u6B63\u5728\u66F4\u65B0 {from} \u2192 {to}\u2026",
29348
29355
  "update.checkVersionFailed": "\u65E0\u6CD5\u68C0\u67E5 cli \u7248\u672C\uFF1A{error}",
29349
29356
  "update.newVersionAutoUpdateOff": "\u6709\u65B0\u7248\u672C {latest} \u53EF\u7528\uFF08\u5F53\u524D\u8FD0\u884C {current}\uFF09\u2014 autoUpdate \u5DF2\u5173\u95ED\u3002",
29357
+ "update.unsupportedSoon": "cli {current} \u5C06\u4E8E {date} \u505C\u6B62\u652F\u6301 \u2014 \u8BF7\u5728\u6B64\u4E4B\u524D\u66F4\u65B0\uFF084pm update\uFF09\u3002",
29358
+ "update.policyNote": "\u6765\u81EA 4PM \u7684\u8BF4\u660E\uFF1A{note}",
29350
29359
  "update.applying": "{kind} cli \u66F4\u65B0 {from} \u2192 {to}\u2026",
29351
29360
  "update.kindMandatory": "\u5F3A\u5236",
29352
29361
  "update.kindAutomatic": "\u81EA\u52A8",
@@ -32701,12 +32710,18 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
32701
32710
  if (CLI_VERSION.startsWith("0.0.0")) return { action: "none" };
32702
32711
  let meta;
32703
32712
  try {
32704
- meta = await fetchCliVersion(serverUrl);
32713
+ meta = await fetchCliVersion(serverUrl, CLI_VERSION);
32705
32714
  } catch (err) {
32706
32715
  console.warn(t("update.checkVersionFailed", { error: String(err) }));
32707
32716
  return { action: "none" };
32708
32717
  }
32709
- const mandatory = compareSemver(CLI_VERSION, meta.minSupported) < 0;
32718
+ const mandatory = compareSemver(CLI_VERSION, meta.minSupported) < 0 || meta.status === "unsupported";
32719
+ if (meta.status === "warning" && meta.unsupportedFrom) {
32720
+ console.warn(t("update.unsupportedSoon", { current: CLI_VERSION, date: meta.unsupportedFrom.slice(0, 10) }));
32721
+ if (meta.note) console.warn(t("update.policyNote", { note: meta.note }));
32722
+ } else if (meta.status === "unsupported" && meta.note) {
32723
+ console.warn(t("update.policyNote", { note: meta.note }));
32724
+ }
32710
32725
  const outdated = shouldUpdateTo(CLI_VERSION, meta.latest);
32711
32726
  if (!outdated) return { action: "none" };
32712
32727
  if (!mandatory && !autoUpdate) {
@@ -32753,7 +32768,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
32753
32768
 
32754
32769
  // src/core/ws-client.ts
32755
32770
  init_wrapper();
32756
- import { randomUUID as randomUUID3 } from "crypto";
32771
+ import { randomUUID as randomUUID4 } from "crypto";
32757
32772
  import { basename as basename7, join as join32 } from "path";
32758
32773
  import { existsSync as existsSync18, mkdirSync as mkdirSync9 } from "fs";
32759
32774
 
@@ -37255,6 +37270,67 @@ var agentToolsWriteRequestSchema = external_exports.object({
37255
37270
  permissions: agentToolsPermissionsSchema
37256
37271
  });
37257
37272
 
37273
+ // vendor/@4pm/dto/meta.ts
37274
+ var cliVersionString = external_exports.string().trim().regex(/^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$/);
37275
+ var setCliMinVersionRequestSchema = external_exports.object({
37276
+ minSupported: cliVersionString.nullable(),
37277
+ disconnect: external_exports.boolean().optional()
37278
+ });
37279
+ var setCliVersionOverrideRequestSchema = external_exports.object({
37280
+ status: external_exports.enum(["supported", "warning", "unsupported"]),
37281
+ unsupportedFrom: external_exports.string().datetime({ offset: true }).nullable().optional(),
37282
+ note: external_exports.string().max(500).optional(),
37283
+ disconnect: external_exports.boolean().optional()
37284
+ });
37285
+
37286
+ // vendor/@4pm/dto/storage.ts
37287
+ var STORAGE_KINDS = [
37288
+ "messageText",
37289
+ "postText",
37290
+ "commentText",
37291
+ "memoText",
37292
+ "messageImages",
37293
+ "messageFiles",
37294
+ "postImages",
37295
+ "postFiles",
37296
+ "commentImages",
37297
+ "commentFiles",
37298
+ "memoImages",
37299
+ "memoFiles",
37300
+ "templates",
37301
+ "aiDocImages",
37302
+ "artifacts",
37303
+ "commandHistory"
37304
+ ];
37305
+ var COMMUNITY_STORAGE_KINDS = [
37306
+ "messageText",
37307
+ "postText",
37308
+ "commentText",
37309
+ "messageImages",
37310
+ "messageFiles",
37311
+ "postImages",
37312
+ "postFiles",
37313
+ "commentImages",
37314
+ "commentFiles"
37315
+ ];
37316
+ var SERVER_CLEARABLE_STORAGE_KINDS = [
37317
+ "memoText",
37318
+ "memoImages",
37319
+ "memoFiles",
37320
+ "templates",
37321
+ "commandHistory"
37322
+ ];
37323
+ var CLEARABLE_STORAGE_KINDS = [
37324
+ ...COMMUNITY_STORAGE_KINDS,
37325
+ ...SERVER_CLEARABLE_STORAGE_KINDS
37326
+ ];
37327
+ var storageClearRequestSchema = external_exports.object({
37328
+ kinds: external_exports.array(external_exports.enum(STORAGE_KINDS)).min(1).max(STORAGE_KINDS.length),
37329
+ before: external_exports.string().datetime({ offset: true }),
37330
+ /** org-0009 / community-0015 only — must equal the org name. */
37331
+ confirm: external_exports.string().max(200).optional()
37332
+ });
37333
+
37258
37334
  // vendor/@4pm/dto/org.ts
37259
37335
  var CLI_SETTINGS_BOUNDS = {
37260
37336
  reconnectMaxBackoffSec: { min: 5, max: 60 },
@@ -37274,6 +37350,8 @@ var TEMPLATE_KINDS = ["report", "est", "wbs", "proposal", "feasibility"];
37274
37350
  // vendor/@4pm/dto/project.ts
37275
37351
  var listProjectsQuerySchema = baseRequestSchema.extend({
37276
37352
  pinned: pinnedFilterSchema,
37353
+ /** Only projects this team is attached to (project-0001; the public v1 list — ADR-0364). */
37354
+ teamId: external_exports.string().uuid().optional(),
37277
37355
  /** ADMIN-only Trash: list soft-deleted projects (ADR-0109). */
37278
37356
  deleted: deletedFilterSchema
37279
37357
  });
@@ -37564,116 +37642,12 @@ var userCardsQuerySchema = external_exports.object({
37564
37642
  // src/core/scaffold.ts
37565
37643
  import { cp, mkdir, mkdtemp, rm, writeFile } from "fs/promises";
37566
37644
  import { existsSync as existsSync4 } from "fs";
37567
- import { execFile as execFile2 } from "child_process";
37645
+ import { execFile } from "child_process";
37568
37646
  import { tmpdir as tmpdir2 } from "os";
37569
- import { dirname as dirname3, join as join6, resolve as resolve2 } from "path";
37647
+ import { dirname as dirname3, join as join6, resolve } from "path";
37570
37648
  import { fileURLToPath as fileURLToPath3 } from "url";
37571
- import { promisify as promisify2 } from "util";
37572
-
37573
- // src/core/ai-assist.ts
37574
- import { execFile } from "child_process";
37575
37649
  import { promisify } from "util";
37576
-
37577
- // src/utils/agent-deny.ts
37578
- import { isAbsolute, resolve } from "path";
37579
- var HOME_SECRET_PATTERNS = [
37580
- "~/.4pm/**",
37581
- "~/.ssh/**",
37582
- "~/.config/gh/**",
37583
- "~/.git-credentials",
37584
- "~/.claude*",
37585
- "~/.claude*/**"
37586
- ];
37587
- function absDirPattern(dir2) {
37588
- const abs = (isAbsolute(dir2) ? dir2 : resolve(dir2)).replace(/\\/g, "/").replace(/\/+$/, "");
37589
- return `/${abs.startsWith("/") ? abs : `/${abs}`}/**`;
37590
- }
37591
- function agentDenyRules(profileDirs2 = []) {
37592
- const patterns = new Set(HOME_SECRET_PATTERNS);
37593
- for (const dir2 of profileDirs2) {
37594
- if (dir2 && dir2.trim()) patterns.add(absDirPattern(dir2.trim()));
37595
- }
37596
- const rules = [];
37597
- for (const p2 of patterns) rules.push(`Read(${p2})`, `Edit(${p2})`);
37598
- return rules;
37599
- }
37600
- function denySettingsArgs(cmd, profileDirs2 = []) {
37601
- if (!cmd.includes("claude")) return [];
37602
- return ["--settings", JSON.stringify({ permissions: { deny: agentDenyRules(profileDirs2) } })];
37603
- }
37604
-
37605
- // src/core/ai-assist.ts
37606
37650
  var run = promisify(execFile);
37607
- function aiCli() {
37608
- return process.env.AI_CLI || "claude";
37609
- }
37610
- function aiArgs(cli, prompt) {
37611
- if (cli === "claude") return ["-p", prompt, "--output-format", "json", ...denySettingsArgs(cli, [process.env.CLAUDE_CONFIG_DIR])];
37612
- if (cli === "codex") return ["exec", "--json", prompt];
37613
- return ["-p", prompt];
37614
- }
37615
- function estimateTokens(prompt, output) {
37616
- return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
37617
- }
37618
- function parseAi(cli, stdout, prompt) {
37619
- if (cli === "claude") {
37620
- try {
37621
- const j2 = JSON.parse(stdout);
37622
- const text2 = (j2.result ?? "").trim();
37623
- const tokens = (j2.usage?.input_tokens ?? 0) + (j2.usage?.output_tokens ?? 0);
37624
- if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens(prompt, text2) };
37625
- } catch {
37626
- }
37627
- }
37628
- if (cli === "codex") {
37629
- try {
37630
- let text2 = "";
37631
- let tokens = 0;
37632
- for (const line of stdout.split("\n").filter(Boolean)) {
37633
- const obj = JSON.parse(line);
37634
- if (obj.text) text2 = obj.text;
37635
- else if (obj.message) text2 = obj.message;
37636
- if (obj.usage) {
37637
- tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
37638
- }
37639
- }
37640
- if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens(prompt, text2) };
37641
- } catch {
37642
- }
37643
- }
37644
- const text = stdout.trim();
37645
- return { text, tokens: estimateTokens(prompt, text) };
37646
- }
37647
- async function runAi(prompt) {
37648
- const cli = aiCli();
37649
- const { stdout } = await run(cli, aiArgs(cli, prompt), {
37650
- timeout: 12e4,
37651
- maxBuffer: 8 * 1024 * 1024
37652
- });
37653
- return parseAi(cli, stdout, prompt);
37654
- }
37655
- async function aiGenerate(prompt) {
37656
- const { text } = await runAi(prompt);
37657
- return text;
37658
- }
37659
- async function aiOutboundReview(input, allowedRepos, projectPath) {
37660
- 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:
37661
- ${input}`;
37662
- const { text, tokens } = await runAi(prompt);
37663
- const start = text.indexOf("{");
37664
- const end = text.lastIndexOf("}");
37665
- if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
37666
- try {
37667
- const j2 = JSON.parse(text.slice(start, end + 1));
37668
- const reasons = Array.isArray(j2.reasons) ? j2.reasons.filter((r) => typeof r === "string") : [];
37669
- return { ok: j2.ok === true && reasons.length === 0, reasons, tokens };
37670
- } catch {
37671
- return { ok: false, reasons: ["ai_parse"], tokens };
37672
- }
37673
- }
37674
-
37675
- // src/core/scaffold.ts
37676
- var run2 = promisify2(execFile2);
37677
37651
  function reposOf(spec) {
37678
37652
  const repos = spec?.repos;
37679
37653
  return Array.isArray(repos) ? repos : [];
@@ -37695,7 +37669,7 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
37695
37669
  if (existsSync4(join6(root, dir2, ".git")) || existsSync4(join6(root, ".git", "modules", dir2))) {
37696
37670
  emit("submodule", `Submodule ${dir2} already present \u2014 updating\u2026`);
37697
37671
  try {
37698
- await run2("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37672
+ await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37699
37673
  } catch {
37700
37674
  }
37701
37675
  continue;
@@ -37709,11 +37683,11 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
37709
37683
  const b2 = (sub.branch ?? "").trim();
37710
37684
  if (b2 && createMissingBranch && !await ensureRemoteBranch(sub.url, b2, emit)) {
37711
37685
  try {
37712
- await run2("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 12e4 });
37713
- await run2("git", ["config", "-f", ".gitmodules", `submodule.${dir2}.branch`, b2], { cwd: root, timeout: 3e4 });
37686
+ await run("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 12e4 });
37687
+ await run("git", ["config", "-f", ".gitmodules", `submodule.${dir2}.branch`, b2], { cwd: root, timeout: 3e4 });
37714
37688
  try {
37715
- await run2("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
37716
- await run2("git", ["push", "-u", "origin", b2], { cwd: join6(root, dir2), timeout: 12e4 });
37689
+ await run("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
37690
+ await run("git", ["push", "-u", "origin", b2], { cwd: join6(root, dir2), timeout: 12e4 });
37717
37691
  } catch (pushErr) {
37718
37692
  const pmsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
37719
37693
  emit("git-branch-push-failed", `Submodule ${dir2}: branch "${b2}" created locally but the push failed: ${pmsg}`);
@@ -37728,13 +37702,13 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
37728
37702
  }
37729
37703
  }
37730
37704
  try {
37731
- await run2("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
37705
+ await run("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
37732
37706
  added++;
37733
37707
  } catch (err) {
37734
37708
  const msg = err instanceof Error ? err.message : String(err);
37735
37709
  if (/already exists|already registered|in the index/i.test(msg)) {
37736
37710
  try {
37737
- await run2("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37711
+ await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37738
37712
  added++;
37739
37713
  } catch {
37740
37714
  }
@@ -37753,18 +37727,18 @@ async function commitAndPushSubmodules(root, submodules, emit) {
37753
37727
  ];
37754
37728
  if (addPaths.length === 0) return;
37755
37729
  emit("submodule", "Committing .gitmodules\u2026");
37756
- await run2("git", ["add", ...addPaths], { cwd: root, timeout: 6e4 });
37757
- const staged = (await run2("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
37730
+ await run("git", ["add", ...addPaths], { cwd: root, timeout: 6e4 });
37731
+ const staged = (await run("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
37758
37732
  if (!staged) return;
37759
37733
  const commitArgs = ["commit", "-m", "chore: add git submodules (4PM)"];
37760
37734
  try {
37761
- await run2("git", commitArgs, { cwd: root, timeout: 6e4 });
37735
+ await run("git", commitArgs, { cwd: root, timeout: 6e4 });
37762
37736
  } catch {
37763
- await run2("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
37737
+ await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
37764
37738
  }
37765
37739
  emit("submodule", "Pushing .gitmodules to the primary remote\u2026");
37766
37740
  try {
37767
- await run2("git", ["push"], { cwd: root, timeout: 12e4 });
37741
+ await run("git", ["push"], { cwd: root, timeout: 12e4 });
37768
37742
  } catch (err) {
37769
37743
  const msg = err instanceof Error ? err.message : String(err);
37770
37744
  emit("submodule-push-failed", `Submodules committed locally but the push failed: ${msg}`);
@@ -37777,22 +37751,22 @@ function providerSlug(url) {
37777
37751
  }
37778
37752
  async function commitAndOpenPr(root, repo, emit) {
37779
37753
  emit("commit", "Committing the scaffolded project\u2026");
37780
- await run2("git", ["add", "-A"], { cwd: root, timeout: 6e4 });
37781
- const staged = (await run2("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
37754
+ await run("git", ["add", "-A"], { cwd: root, timeout: 6e4 });
37755
+ const staged = (await run("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
37782
37756
  if (!staged) {
37783
37757
  emit("commit", "Nothing to commit \u2014 skipping the pull request.");
37784
37758
  return;
37785
37759
  }
37786
37760
  const commitArgs = ["commit", "-m", "chore: scaffold project (4PM)"];
37787
37761
  try {
37788
- await run2("git", commitArgs, { cwd: root, timeout: 6e4 });
37762
+ await run("git", commitArgs, { cwd: root, timeout: 6e4 });
37789
37763
  } catch {
37790
- await run2("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
37764
+ await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
37791
37765
  }
37792
37766
  const branch = (repo.branch ?? "").trim();
37793
37767
  emit("push", "Pushing the scaffold branch\u2026");
37794
37768
  try {
37795
- await run2("git", ["push", "-u", "origin", ...branch ? [branch] : ["HEAD"]], { cwd: root, timeout: 12e4 });
37769
+ await run("git", ["push", "-u", "origin", ...branch ? [branch] : ["HEAD"]], { cwd: root, timeout: 12e4 });
37796
37770
  } catch (err) {
37797
37771
  const msg = err instanceof Error ? err.message : String(err);
37798
37772
  emit("push-failed", `Scaffold committed locally but the push failed: ${msg} \u2014 push it from the Git tab when credentials are available.`);
@@ -37806,7 +37780,7 @@ async function commitAndOpenPr(root, repo, emit) {
37806
37780
  emit("pr", `Opening a pull request via ${provider}\u2026`);
37807
37781
  try {
37808
37782
  const args = provider === "glab" ? ["mr", "create", "--fill", ...slug ? ["-R", slug] : []] : ["pr", "create", "--fill", ...slug ? ["-R", slug] : []];
37809
- const { stdout } = await run2(provider, args, { cwd: root, timeout: 12e4 });
37783
+ const { stdout } = await run(provider, args, { cwd: root, timeout: 12e4 });
37810
37784
  const link2 = stdout.trim().split(/\s+/).find((s3) => /^https?:\/\//.test(s3)) ?? stdout.trim().split("\n").pop() ?? "";
37811
37785
  emit("pr", `Pull request opened${link2 ? `: ${link2}` : "."}`);
37812
37786
  } catch (err) {
@@ -37820,7 +37794,7 @@ function cloneArgs(url, dest, branch) {
37820
37794
  }
37821
37795
  async function remoteHasBranch(url, branch) {
37822
37796
  try {
37823
- const { stdout } = await run2("git", ["ls-remote", "--heads", url, branch], { timeout: 3e4 });
37797
+ const { stdout } = await run("git", ["ls-remote", "--heads", url, branch], { timeout: 3e4 });
37824
37798
  return stdout.trim().length > 0;
37825
37799
  } catch {
37826
37800
  return false;
@@ -37831,9 +37805,9 @@ async function ensureRemoteBranch(url, branch, emit) {
37831
37805
  const tmp = await mkdtemp(join6(tmpdir2(), "4pm-branch-"));
37832
37806
  try {
37833
37807
  emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it from the default branch\u2026`);
37834
- await run2("git", ["clone", "--depth", "1", url, tmp], { timeout: 12e4 });
37835
- await run2("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
37836
- await run2("git", ["push", "-u", "origin", branch], { cwd: tmp, timeout: 12e4 });
37808
+ await run("git", ["clone", "--depth", "1", url, tmp], { timeout: 12e4 });
37809
+ await run("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
37810
+ await run("git", ["push", "-u", "origin", branch], { cwd: tmp, timeout: 12e4 });
37837
37811
  return true;
37838
37812
  } catch (err) {
37839
37813
  const msg = err instanceof Error ? err.message : String(err);
@@ -37846,9 +37820,9 @@ async function ensureRemoteBranch(url, branch, emit) {
37846
37820
  async function updateRepo(dir2, branch, emit) {
37847
37821
  const b2 = (branch ?? "").trim();
37848
37822
  emit("git", `Updating repository (fetch + ${b2 ? `checkout ${b2} + ` : ""}fast-forward pull)\u2026`);
37849
- await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
37850
- if (b2) await run2("git", ["checkout", b2], { cwd: dir2, timeout: 6e4 });
37851
- await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
37823
+ await run("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
37824
+ if (b2) await run("git", ["checkout", b2], { cwd: dir2, timeout: 6e4 });
37825
+ await run("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
37852
37826
  }
37853
37827
  async function provisionRepo(target, repo, emit, opts = {}) {
37854
37828
  const mode = opts.mode ?? "clone";
@@ -37856,7 +37830,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
37856
37830
  if (!existsSync4(join6(target, ".git"))) {
37857
37831
  emit("git", "Initializing repository\u2026");
37858
37832
  await mkdir(target, { recursive: true });
37859
- await run2("git", ["init"], { cwd: target, timeout: 2e4 });
37833
+ await run("git", ["init"], { cwd: target, timeout: 2e4 });
37860
37834
  }
37861
37835
  return;
37862
37836
  }
@@ -37866,7 +37840,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
37866
37840
  } else if (mode === "force") {
37867
37841
  emit("git", `Re-cloning ${repo.url} (force)\u2026`);
37868
37842
  await rm(target, { recursive: true, force: true });
37869
- await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
37843
+ await run("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
37870
37844
  } else {
37871
37845
  emit("git", "Repository already present \u2014 skipping clone.");
37872
37846
  }
@@ -37875,10 +37849,10 @@ async function provisionRepo(target, repo, emit, opts = {}) {
37875
37849
  const b2 = (repo.branch ?? "").trim();
37876
37850
  if (b2 && opts.createMissingBranch && !await remoteHasBranch(repo.url, b2)) {
37877
37851
  emit("git", `Cloning ${repo.url} (default branch) to create "${b2}"\u2026`);
37878
- await run2("git", ["clone", repo.url, target], { timeout: 12e4 });
37879
- await run2("git", ["checkout", "-b", b2], { cwd: target, timeout: 3e4 });
37852
+ await run("git", ["clone", repo.url, target], { timeout: 12e4 });
37853
+ await run("git", ["checkout", "-b", b2], { cwd: target, timeout: 3e4 });
37880
37854
  try {
37881
- await run2("git", ["push", "-u", "origin", b2], { cwd: target, timeout: 12e4 });
37855
+ await run("git", ["push", "-u", "origin", b2], { cwd: target, timeout: 12e4 });
37882
37856
  emit("git", `Created and pushed branch "${b2}".`);
37883
37857
  } catch (err) {
37884
37858
  const msg = err instanceof Error ? err.message : String(err);
@@ -37887,7 +37861,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
37887
37861
  return;
37888
37862
  }
37889
37863
  emit("git", `Cloning ${repo.url}\u2026`);
37890
- await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
37864
+ await run("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
37891
37865
  }
37892
37866
  async function ensureReposCloned(physicRoot, repos, emit) {
37893
37867
  const list = repos;
@@ -37900,27 +37874,27 @@ async function ensureReposCloned(physicRoot, repos, emit) {
37900
37874
  function sampleDir() {
37901
37875
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
37902
37876
  const here = dirname3(fileURLToPath3(import.meta.url));
37903
- const bundled = resolve2(here, "project-sample");
37904
- for (const candidate of [bundled, resolve2(here, "../project-sample"), resolve2(here, "../../project-sample")]) {
37877
+ const bundled = resolve(here, "project-sample");
37878
+ for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
37905
37879
  if (existsSync4(candidate)) return candidate;
37906
37880
  }
37907
37881
  return bundled;
37908
37882
  }
37909
- async function scaffoldProject(payload, profileDir2, onProgress) {
37883
+ async function scaffoldProject(payload, profileDir2, onProgress, ai2) {
37910
37884
  let lastStep = "start";
37911
37885
  const emit = (step, message) => {
37912
37886
  lastStep = step;
37913
37887
  onProgress?.({ projectId: payload.projectId, step, message });
37914
37888
  };
37915
37889
  try {
37916
- const root = process.env.SCAFFOLD_ROOT ? resolve2(process.env.SCAFFOLD_ROOT) : resolve2(profileDir2);
37890
+ const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
37917
37891
  const target = join6(root, payload.projectName);
37918
37892
  await mkdir(target, { recursive: true });
37919
37893
  const repo = singleRepo(reposOf(payload.spec));
37920
37894
  if (!repo) throw new Error("A project must declare one repo (ADR-0314).");
37921
37895
  emit("git", "Cloning repository\u2026");
37922
37896
  await provisionRepo(target, repo, emit, { createMissingBranch: true });
37923
- await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true });
37897
+ await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true, ai: ai2 });
37924
37898
  await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit, true);
37925
37899
  emit("version", "Writing .4pm/.4pm.json\u2026");
37926
37900
  await writeTemplateMarker(target);
@@ -37939,7 +37913,7 @@ async function scaffoldRepo(dir2, label, spec, emit, opts = {}) {
37939
37913
  if (spec) {
37940
37914
  emit("spec", `Writing project.spec.json into ${label}\u2026`);
37941
37915
  await writeFile(join6(dir2, "project.spec.json"), JSON.stringify(spec, null, 2), "utf8");
37942
- await aiInit(dir2, spec, emit);
37916
+ await aiInit(dir2, spec, emit, opts.ai);
37943
37917
  }
37944
37918
  }
37945
37919
  async function resetTemplateManagedClaude(dir2, label, emit) {
@@ -37965,7 +37939,7 @@ function readSpecField(spec, key) {
37965
37939
  }
37966
37940
  return typeof spec[key] === "string" ? spec[key] : "";
37967
37941
  }
37968
- async function aiInit(target, spec, emit) {
37942
+ async function aiInit(target, spec, emit, ai2) {
37969
37943
  const guideFile = readSpecField(spec, "ai_guide_file") === "AGENT.md" ? "AGENT.md" : "CLAUDE.md";
37970
37944
  const guideInstructions = readSpecField(spec, "ai_guide_instructions").trim();
37971
37945
  emit("ai-init", `AI init: subagents, README, ${guideFile}\u2026`);
@@ -37985,25 +37959,47 @@ ${(sa.description || "").trim()}
37985
37959
  }
37986
37960
  }
37987
37961
  const specJson = JSON.stringify(spec);
37988
- await generateFile(
37989
- join6(target, "README.md"),
37990
- `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
37962
+ let generate3 = null;
37963
+ let releaseSlot = () => void 0;
37964
+ if (ai2) {
37965
+ const slot = await ai2.acquireSlot(
37966
+ (q2) => emit("ai-queued", `Waiting for an AI run slot \u2014 ${q2.position} ahead (${q2.running}/${q2.limit} parallel runs in use)\u2026`)
37967
+ );
37968
+ if (slot.kind === "granted") {
37969
+ releaseSlot = () => slot.handle.release();
37970
+ generate3 = (prompt, label) => ai2.generate(prompt, target, label);
37971
+ if (slot.queued) emit("ai-init", `AI init: subagents, README, ${guideFile}\u2026`);
37972
+ } else {
37973
+ emit("ai-skipped", "No AI run slot freed up in time \u2014 AI init skipped; fallback content written.");
37974
+ }
37975
+ }
37976
+ try {
37977
+ await generateFile(
37978
+ generate3,
37979
+ join6(target, "README.md"),
37980
+ "README.md",
37981
+ `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
37991
37982
  ${specJson}`
37992
- );
37993
- await generateFile(
37994
- join6(target, guideFile),
37995
- `Write a ${guideFile} (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
37983
+ );
37984
+ await generateFile(
37985
+ generate3,
37986
+ join6(target, guideFile),
37987
+ guideFile,
37988
+ `Write a ${guideFile} (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
37996
37989
  ${specJson}` + (guideInstructions ? `
37997
37990
  Also incorporate these additional instructions/content:
37998
37991
  ${guideInstructions}
37999
37992
  ` : ""),
38000
- guideInstructions
38001
- );
37993
+ guideInstructions
37994
+ );
37995
+ } finally {
37996
+ releaseSlot();
37997
+ }
38002
37998
  }
38003
- async function generateFile(path, prompt, fallback) {
37999
+ async function generateFile(generate3, path, label, prompt, fallback) {
38004
38000
  try {
38005
- const text = await aiGenerate(prompt);
38006
- if (text.trim()) {
38001
+ const text = generate3 ? await generate3(prompt, label) : null;
38002
+ if (text && text.trim()) {
38007
38003
  await writeFile(path, text.trim() + "\n", "utf8");
38008
38004
  return;
38009
38005
  }
@@ -38011,21 +38007,21 @@ async function generateFile(path, prompt, fallback) {
38011
38007
  }
38012
38008
  if (fallback && fallback.trim()) await writeFile(path, fallback.trim() + "\n", "utf8");
38013
38009
  }
38014
- async function addProject(payload, profileDir2, onProgress) {
38010
+ async function addProject(payload, profileDir2, onProgress, ai2) {
38015
38011
  let lastStep = "start";
38016
38012
  const emit = (step, message) => {
38017
38013
  lastStep = step;
38018
38014
  onProgress?.({ projectId: payload.projectId, step, message });
38019
38015
  };
38020
38016
  try {
38021
- const root = process.env.SCAFFOLD_ROOT ? resolve2(process.env.SCAFFOLD_ROOT) : resolve2(profileDir2);
38017
+ const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
38022
38018
  const target = join6(root, payload.projectName);
38023
38019
  await mkdir(target, { recursive: true });
38024
38020
  const repo = singleRepo(payload.repos ?? []);
38025
38021
  const mode = payload.mode ?? "clone";
38026
38022
  if (repo) await provisionRepo(target, repo, emit, { mode });
38027
38023
  if (payload.scaffoldRepos && payload.scaffoldRepos.length > 0) {
38028
- await scaffoldRepo(target, payload.projectName, payload.spec, emit);
38024
+ await scaffoldRepo(target, payload.projectName, payload.spec, emit, { ai: ai2 });
38029
38025
  }
38030
38026
  await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit);
38031
38027
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
@@ -38432,6 +38428,9 @@ var WsChannels = {
38432
38428
  CLI_UPDATE_RESULT: "cli.update-result",
38433
38429
  // command lifecycle
38434
38430
  COMMAND_ANNOUNCE: "command.announce",
38431
+ // server → cli (reply): stop an AI run (ADR-0362) — queued ⇒ leave the run-slot queue, running ⇒ kill
38432
+ // the AI process with no failover; the cli then ends the stream with `command.output {done, cancelled}`.
38433
+ COMMAND_CANCEL: "command.cancel",
38435
38434
  // console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
38436
38435
  // transcript so the web Console renders it verbatim; gated by console.watch (server → cli:
38437
38436
  // whether a web viewer is attached) so an unwatched cli syncs nothing.
@@ -38449,6 +38448,9 @@ var WsChannels = {
38449
38448
  QUOTA_CHECK: "quota.check",
38450
38449
  // cli → server (reply): per-org concurrent AI-run slot — acquire / renew / release (ADR-0359)
38451
38450
  RUN_SLOT: "run.slot",
38451
+ // cli → server (reply): read / add the org's monthly autonomous-book counters — USER_TODO / USER_QA /
38452
+ // AI_TODO rows vs `autonomousRequestsPerMonth` (ADR-0365)
38453
+ AUTONOMOUS_BOOK_USAGE: "autonomous.bookUsage",
38452
38454
  USAGE_REPORT: "usage.report",
38453
38455
  // cli → server: Claude subscription usage snapshot (5h / weekly — ADR-0072)
38454
38456
  MACHINE_USAGE: "machine.usage",
@@ -38551,6 +38553,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
38551
38553
  var RUN_SLOT_RENEW_MS = 45e3;
38552
38554
  var RUN_SLOT_RETRY_MS = 3e3;
38553
38555
  var RUN_SLOT_MAX_WAIT_MS = 30 * 6e4;
38556
+ var COMMAND_CANCELLED_EXIT_CODE = 130;
38554
38557
 
38555
38558
  // src/core/update-scheduler.ts
38556
38559
  import { spawn as spawn2 } from "child_process";
@@ -38696,7 +38699,7 @@ function classifyToolFailure(code, out) {
38696
38699
  }
38697
38700
  return { reason: "other", retryable: false };
38698
38701
  }
38699
- function run3(cmd, args, onLine, timeoutMs = 18e4) {
38702
+ function run2(cmd, args, onLine, timeoutMs = 18e4) {
38700
38703
  return new Promise((resolve12) => {
38701
38704
  let out = "";
38702
38705
  let buf = "";
@@ -38736,13 +38739,13 @@ function run3(cmd, args, onLine, timeoutMs = 18e4) {
38736
38739
  });
38737
38740
  }
38738
38741
  async function detectOne(cmd, versionArg) {
38739
- const { code, out } = await run3(cmd, [versionArg], void 0, 15e3);
38742
+ const { code, out } = await run2(cmd, [versionArg], void 0, 15e3);
38740
38743
  if (code !== 0) return null;
38741
38744
  const first = out.split("\n").map((l) => l.trim()).find(Boolean);
38742
38745
  return first ?? "";
38743
38746
  }
38744
38747
  async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
38745
- const { out } = await run3("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
38748
+ const { out } = await run2("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
38746
38749
  if (!out.trim()) return [];
38747
38750
  let deps = {};
38748
38751
  try {
@@ -38818,7 +38821,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
38818
38821
  return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
38819
38822
  }
38820
38823
  onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
38821
- const { code, out } = await run3(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
38824
+ const { code, out } = await run2(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
38822
38825
  if (code === 0) return { ok: true, exitCode: code };
38823
38826
  const detail = extractToolError(out);
38824
38827
  return { ok: false, exitCode: code, error: detail ? `${manager} exited ${code}: ${detail}` : `${manager} exited ${code}` };
@@ -38851,7 +38854,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
38851
38854
  for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
38852
38855
  const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
38853
38856
  onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
38854
- const { code, out } = await run3(manager, args, onLine, timeoutMs);
38857
+ const { code, out } = await run2(manager, args, onLine, timeoutMs);
38855
38858
  if (code === 0) return { ok: true };
38856
38859
  const { reason, retryable } = classifyToolFailure(code, out);
38857
38860
  if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
@@ -39135,6 +39138,36 @@ function clearPinnedCredential(profileDir2) {
39135
39138
  import { readFileSync as readFileSync8 } from "fs";
39136
39139
  import { homedir as homedir3 } from "os";
39137
39140
  import { basename, isAbsolute as isAbsolute2, join as join9 } from "path";
39141
+
39142
+ // src/utils/agent-deny.ts
39143
+ import { isAbsolute, resolve as resolve2 } from "path";
39144
+ var HOME_SECRET_PATTERNS = [
39145
+ "~/.4pm/**",
39146
+ "~/.ssh/**",
39147
+ "~/.config/gh/**",
39148
+ "~/.git-credentials",
39149
+ "~/.claude*",
39150
+ "~/.claude*/**"
39151
+ ];
39152
+ function absDirPattern(dir2) {
39153
+ const abs = (isAbsolute(dir2) ? dir2 : resolve2(dir2)).replace(/\\/g, "/").replace(/\/+$/, "");
39154
+ return `/${abs.startsWith("/") ? abs : `/${abs}`}/**`;
39155
+ }
39156
+ function agentDenyRules(profileDirs2 = []) {
39157
+ const patterns = new Set(HOME_SECRET_PATTERNS);
39158
+ for (const dir2 of profileDirs2) {
39159
+ if (dir2 && dir2.trim()) patterns.add(absDirPattern(dir2.trim()));
39160
+ }
39161
+ const rules = [];
39162
+ for (const p2 of patterns) rules.push(`Read(${p2})`, `Edit(${p2})`);
39163
+ return rules;
39164
+ }
39165
+ function denySettingsArgs(cmd, profileDirs2 = []) {
39166
+ if (!cmd.includes("claude")) return [];
39167
+ return ["--settings", JSON.stringify({ permissions: { deny: agentDenyRules(profileDirs2) } })];
39168
+ }
39169
+
39170
+ // src/utils/ai-cli.ts
39138
39171
  var REQUIRED_AI_ARGS = {
39139
39172
  claude: ["-p", "--output-format", "stream-json", "--verbose"],
39140
39173
  codex: ["exec", "--json"],
@@ -39811,12 +39844,12 @@ function pruneCommandOutputByAge(days) {
39811
39844
 
39812
39845
  // src/core/support-answer.ts
39813
39846
  import { spawn as spawn4 } from "child_process";
39814
- import { execFile as execFile3 } from "child_process";
39847
+ import { execFile as execFile2 } from "child_process";
39815
39848
  import { createHash as createHash3 } from "crypto";
39816
39849
  import { existsSync as existsSync10, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
39817
39850
  import { join as join14 } from "path";
39818
39851
  import { tmpdir as tmpdir3 } from "os";
39819
- import { promisify as promisify3 } from "util";
39852
+ import { promisify as promisify2 } from "util";
39820
39853
 
39821
39854
  // src/core/ai-stream.ts
39822
39855
  function assistantText(ev) {
@@ -39921,7 +39954,7 @@ function aiProviderOf(cmd) {
39921
39954
  if (cmd.includes("antigravity")) return "antigravity";
39922
39955
  return "claude";
39923
39956
  }
39924
- function estimateTokens2(text) {
39957
+ function estimateTokens(text) {
39925
39958
  return Math.max(1, Math.ceil(text.length / 4));
39926
39959
  }
39927
39960
 
@@ -39977,7 +40010,7 @@ function gitAuthArgs(user, password) {
39977
40010
 
39978
40011
  // src/core/support-answer.ts
39979
40012
  var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
39980
- var execFileAsync = promisify3(execFile3);
40013
+ var execFileAsync = promisify2(execFile2);
39981
40014
  var MAX_CONTEXT_CHARS = 12e4;
39982
40015
  var ANSWER_TIMEOUT_MS = 12e4;
39983
40016
  var LEGAL_DRAFT_TIMEOUT_MS = 54e4;
@@ -40274,7 +40307,7 @@ async function runSupportAnswer(req, ai2, profileDir2) {
40274
40307
  if (!text) return { body: "", error: "empty answer" };
40275
40308
  const { body, moderation } = req.task ? { body: text.trim(), moderation: void 0 } : parseModeratedAnswer(text);
40276
40309
  if (!body) return { body: "", error: "empty answer" };
40277
- const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
40310
+ const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens(body) };
40278
40311
  return {
40279
40312
  body,
40280
40313
  ...moderation ? { moderation } : {},
@@ -40323,9 +40356,9 @@ function reportToolResult(tool, ok, message) {
40323
40356
  // src/core/autonomous.ts
40324
40357
  import { readFile as readFile4, writeFile as writeFile4, chmod } from "fs/promises";
40325
40358
  import { readFileSync as readFileSync14 } from "fs";
40326
- import { execFile as execFile4, spawn as spawn5 } from "child_process";
40359
+ import { execFile as execFile3, spawn as spawn5 } from "child_process";
40327
40360
  import { join as join17 } from "path";
40328
- import { promisify as promisify4 } from "util";
40361
+ import { promisify as promisify3 } from "util";
40329
40362
 
40330
40363
  // src/core/autonomous-config.ts
40331
40364
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
@@ -40436,7 +40469,7 @@ function pushRecord(h, rec) {
40436
40469
  }
40437
40470
 
40438
40471
  // src/core/autonomous.ts
40439
- var run4 = promisify4(execFile4);
40472
+ var run3 = promisify3(execFile3);
40440
40473
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
40441
40474
  var AUTHORS_REL = ".claude/.autonomous.authors.json";
40442
40475
  var TICK_REL = ".claude/hooks/autonomous-tick.sh";
@@ -40495,12 +40528,20 @@ function tableRowsById(md) {
40495
40528
  }
40496
40529
  return rows;
40497
40530
  }
40531
+ var CAPPED_BOOK_FILES = { USER_TODO: "USER_TODO.md", USER_QA: "USER_QA.md", AI_TODO: "AI_TODO.md" };
40532
+ async function cappedBookIds(root) {
40533
+ const out = {};
40534
+ for (const [book, file] of Object.entries(CAPPED_BOOK_FILES)) {
40535
+ out[book] = new Set(tableRowsById(await readText(join17(root, file))).keys());
40536
+ }
40537
+ return out;
40538
+ }
40498
40539
  function tickScript(root) {
40499
40540
  return join17(root, TICK_REL);
40500
40541
  }
40501
40542
  async function crontabList() {
40502
40543
  try {
40503
- const { stdout } = await run4("crontab", ["-l"], { timeout: 1e4 });
40544
+ const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
40504
40545
  return stdout;
40505
40546
  } catch {
40506
40547
  return "";
@@ -40586,6 +40627,7 @@ async function uninstallCron(root) {
40586
40627
  await crontabSet(kept.join("\n"));
40587
40628
  }
40588
40629
  async function writeAutonomous(root, profileDir2, req, by) {
40630
+ let added;
40589
40631
  try {
40590
40632
  switch (req.kind) {
40591
40633
  case "settings": {
@@ -40630,6 +40672,7 @@ async function writeAutonomous(root, profileDir2, req, by) {
40630
40672
  const row = `| ${id} | | | ${tableCell(req.content)} |
40631
40673
  `;
40632
40674
  await writeFile4(join17(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
40675
+ added = 1;
40633
40676
  const authors = await readJsonMap(join17(root, AUTHORS_REL));
40634
40677
  authors[id] = { by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
40635
40678
  await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
@@ -40642,8 +40685,10 @@ async function writeAutonomous(root, profileDir2, req, by) {
40642
40685
  const authors = await readJsonMap(join17(root, AUTHORS_REL));
40643
40686
  const at2 = (/* @__PURE__ */ new Date()).toISOString();
40644
40687
  const byLabel = req.byLabel ?? by;
40688
+ added = 0;
40645
40689
  for (const [id, cells] of next) {
40646
40690
  if (prev.get(id) !== cells) authors[id] = { by, byLabel, at: at2 };
40691
+ if (!prev.has(id)) added += 1;
40647
40692
  }
40648
40693
  await writeFile4(join17(root, file), req.content, "utf8");
40649
40694
  await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
@@ -40655,7 +40700,7 @@ async function writeAutonomous(root, profileDir2, req, by) {
40655
40700
  break;
40656
40701
  }
40657
40702
  }
40658
- return { ok: true, status: await getAutonomousStatus(root, profileDir2) };
40703
+ return { ok: true, status: await getAutonomousStatus(root, profileDir2), ...added !== void 0 ? { added } : {} };
40659
40704
  } catch (err) {
40660
40705
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
40661
40706
  }
@@ -40988,9 +41033,20 @@ async function runCommand(dispatch, emitRaw, opts) {
40988
41033
  hardKillTimer.unref?.();
40989
41034
  }, timeoutMs) : null;
40990
41035
  timeoutTimer?.unref?.();
41036
+ let cancelled = false;
41037
+ const onAbort = () => {
41038
+ if (cancelled) return;
41039
+ cancelled = true;
41040
+ child.kill("SIGTERM");
41041
+ hardKillTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
41042
+ hardKillTimer.unref?.();
41043
+ };
41044
+ if (opts?.signal?.aborted) onAbort();
41045
+ else opts?.signal?.addEventListener("abort", onAbort, { once: true });
40991
41046
  const clearTimers = () => {
40992
41047
  if (timeoutTimer) clearTimeout(timeoutTimer);
40993
41048
  if (hardKillTimer) clearTimeout(hardKillTimer);
41049
+ opts?.signal?.removeEventListener("abort", onAbort);
40994
41050
  };
40995
41051
  const batcher = new OutputBatcher({
40996
41052
  onFlush: (chunk, truncated) => {
@@ -41017,9 +41073,10 @@ async function runCommand(dispatch, emitRaw, opts) {
41017
41073
  seq: seq++,
41018
41074
  chunk: "",
41019
41075
  done: true,
41020
- // 124 (the conventional timeout code) when we killed it for exceeding the limit, so the
41021
- // server settles the command as failed and the web surfaces the note instead of spinning.
41022
- exitCode: timedOut ? 124 : exitCode ?? -1
41076
+ // 130 when the run was stopped (ADR-0362); 124 (the conventional timeout code) when we killed it
41077
+ // for exceeding the limit, so the server settles the command as failed and the web surfaces the
41078
+ // note instead of spinning.
41079
+ exitCode: cancelled ? COMMAND_CANCELLED_EXIT_CODE : timedOut ? 124 : exitCode ?? -1
41023
41080
  });
41024
41081
  resolve12();
41025
41082
  });
@@ -41055,7 +41112,7 @@ function isOutOfCredits(text) {
41055
41112
  text
41056
41113
  );
41057
41114
  }
41058
- function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
41115
+ function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs, signal) {
41059
41116
  return new Promise((resolve12) => {
41060
41117
  runCommand(
41061
41118
  { commandId, cmd, args, path: cwd2, env: env3 },
@@ -41063,16 +41120,17 @@ function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs)
41063
41120
  if (out.chunk) onChunk(out.chunk);
41064
41121
  if (out.done) resolve12(out.exitCode ?? -1);
41065
41122
  },
41066
- { stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
41123
+ { stdin, ...timeoutMs > 0 ? { timeoutMs } : {}, ...signal ? { signal } : {} }
41067
41124
  ).catch(() => resolve12(-1));
41068
41125
  });
41069
41126
  }
41070
- async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
41127
+ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0, signal) {
41071
41128
  const total = plan.attempts.length;
41072
41129
  let finalExit = -1;
41073
41130
  let lastCmd = plan.attempts[0]?.cmd ?? "claude";
41074
41131
  let lastCaptured = "";
41075
41132
  for (let i = 0; i < total; i++) {
41133
+ if (signal?.aborted) return cancelledResult();
41076
41134
  const attempt = plan.attempts[i];
41077
41135
  lastCmd = attempt.cmd;
41078
41136
  if (total > 1) handlers.onAttemptStart(attempt.label, i, total, attempt.cmd);
@@ -41085,17 +41143,18 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
41085
41143
  handlers.onChunk(text);
41086
41144
  }
41087
41145
  };
41088
- finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs);
41146
+ finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs, signal);
41089
41147
  const tail = parser.flush();
41090
41148
  if (tail) {
41091
41149
  captured += tail;
41092
41150
  handlers.onChunk(tail);
41093
41151
  }
41152
+ if (signal?.aborted) return cancelledResult();
41094
41153
  lastCaptured = captured;
41095
41154
  const apiErr = parser.apiError();
41096
41155
  if (finalExit === 0 && !apiErr.isError) {
41097
41156
  const usage = parser.usage();
41098
- if (usage.tokens === 0) usage.tokens = estimateTokens2(captured);
41157
+ if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
41099
41158
  reportToolResult(attempt.cmd, true);
41100
41159
  return {
41101
41160
  exitCode: 0,
@@ -41114,6 +41173,17 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
41114
41173
  reportToolResult(lastCmd, false, summarizeAiFailure(lastCaptured, finalExit));
41115
41174
  return { exitCode: finalExit, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
41116
41175
  }
41176
+ function cancelledResult() {
41177
+ return {
41178
+ exitCode: COMMAND_CANCELLED_EXIT_CODE,
41179
+ workedDir: null,
41180
+ workedKey: null,
41181
+ workedCmd: null,
41182
+ usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 },
41183
+ sessionId: "",
41184
+ cancelled: true
41185
+ };
41186
+ }
41117
41187
  function summarizeAiFailure(captured, exitCode) {
41118
41188
  if (isAuthFailure(captured)) return "Not logged in";
41119
41189
  if (isOutOfCredits(captured)) return "Out of usage credits";
@@ -41642,21 +41712,30 @@ async function runMemoryCompaction(ai2, cwd2, input) {
41642
41712
  }
41643
41713
 
41644
41714
  // src/core/git-commit-identity.ts
41645
- import { execFile as execFile5 } from "child_process";
41646
- import { promisify as promisify5 } from "util";
41647
- var run5 = promisify5(execFile5);
41715
+ import { execFile as execFile4 } from "child_process";
41716
+ import { promisify as promisify4 } from "util";
41717
+ var run4 = promisify4(execFile4);
41648
41718
  async function setCommitAuthor(cwd2, machineUsername) {
41649
41719
  if (!machineUsername) return;
41650
41720
  try {
41651
- await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
41721
+ await run4("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
41652
41722
  } catch {
41653
41723
  }
41654
41724
  }
41655
41725
 
41656
41726
  // src/core/ws-client/run-slot.ts
41657
41727
  var NO_SLOT = { release: () => void 0 };
41658
- function sleep2(ms2) {
41659
- return new Promise((resolve12) => setTimeout(resolve12, ms2));
41728
+ function sleep2(ms2, signal) {
41729
+ return new Promise((resolve12) => {
41730
+ if (signal?.aborted) return resolve12();
41731
+ const timer = setTimeout(done, ms2);
41732
+ function done() {
41733
+ clearTimeout(timer);
41734
+ signal?.removeEventListener("abort", done);
41735
+ resolve12();
41736
+ }
41737
+ signal?.addEventListener("abort", done, { once: true });
41738
+ });
41660
41739
  }
41661
41740
  function holdLease(ctx, lease) {
41662
41741
  const timer = setInterval(() => {
@@ -41673,23 +41752,30 @@ function holdLease(ctx, lease) {
41673
41752
  }
41674
41753
  };
41675
41754
  }
41676
- async function acquireRunSlot(ctx, onQueued) {
41755
+ async function acquireRunSlot(ctx, onQueued, signal) {
41677
41756
  const startedAt = Date.now();
41678
41757
  let ticket;
41679
41758
  let lastPosition = -1;
41680
41759
  let limit = 0;
41681
41760
  for (; ; ) {
41761
+ if (signal?.aborted) return leaveQueue(ctx, ticket);
41682
41762
  let reply;
41683
41763
  try {
41684
41764
  reply = await ctx.request(WsChannels.RUN_SLOT, { op: "acquire", ...ticket ? { ticket } : {} });
41685
41765
  } catch (err) {
41686
41766
  logger.warn("run.slot.unavailable", { error: err.message });
41687
- return { kind: "granted", handle: NO_SLOT };
41767
+ return { kind: "granted", handle: NO_SLOT, queued: ticket !== void 0 };
41688
41768
  }
41689
- if (!reply || !("granted" in reply)) return { kind: "granted", handle: NO_SLOT };
41769
+ if (!reply || !("granted" in reply)) return { kind: "granted", handle: NO_SLOT, queued: ticket !== void 0 };
41690
41770
  if (reply.granted) {
41691
- return { kind: "granted", handle: reply.lease ? holdLease(ctx, reply.lease) : NO_SLOT };
41771
+ const handle = reply.lease ? holdLease(ctx, reply.lease) : NO_SLOT;
41772
+ if (signal?.aborted) {
41773
+ handle.release();
41774
+ return { kind: "cancelled" };
41775
+ }
41776
+ return { kind: "granted", handle, queued: ticket !== void 0 };
41692
41777
  }
41778
+ if ("denied" in reply) return { kind: "denied", reason: reply.denied };
41693
41779
  ticket = reply.ticket;
41694
41780
  limit = reply.limit;
41695
41781
  if (reply.position !== lastPosition) {
@@ -41697,13 +41783,31 @@ async function acquireRunSlot(ctx, onQueued) {
41697
41783
  onQueued({ position: reply.position, limit: reply.limit, running: reply.running });
41698
41784
  }
41699
41785
  if (Date.now() - startedAt >= RUN_SLOT_MAX_WAIT_MS) return { kind: "timeout", limit };
41700
- await sleep2(reply.retryAfterMs > 0 ? reply.retryAfterMs : RUN_SLOT_RETRY_MS);
41786
+ await sleep2(reply.retryAfterMs > 0 ? reply.retryAfterMs : RUN_SLOT_RETRY_MS, signal);
41701
41787
  }
41702
41788
  }
41789
+ function leaveQueue(ctx, ticket) {
41790
+ if (ticket) ctx.request(WsChannels.RUN_SLOT, { op: "leave", ticket }).catch(() => void 0);
41791
+ return { kind: "cancelled" };
41792
+ }
41703
41793
 
41704
41794
  // src/core/ws-client/command-dispatch.ts
41705
41795
  var MEMORY_SEED_HEADER = "CONTEXT MEMORY from earlier in this conversation (may span prior sessions/accounts). Use it as background; do not repeat it back unless relevant:";
41796
+ var activeRuns = /* @__PURE__ */ new Map();
41797
+ function cancelActiveRun(commandId) {
41798
+ const run11 = activeRuns.get(commandId);
41799
+ if (!run11) return { ok: false, state: "unknown" };
41800
+ run11.controller.abort();
41801
+ return { ok: true, state: run11.state };
41802
+ }
41706
41803
  function handleCommandChannels(ctx, message, payload) {
41804
+ if (message.channel === WsChannels.COMMAND_CANCEL) {
41805
+ const commandId = typeof payload.commandId === "string" ? payload.commandId : "";
41806
+ const reply = cancelActiveRun(commandId);
41807
+ logger.info("command.cancel", { commandId, ...reply });
41808
+ ctx.send(WsChannels.COMMAND_CANCEL, reply, message.id);
41809
+ return true;
41810
+ }
41707
41811
  if (message.channel !== WsChannels.COMMAND_DISPATCH) return false;
41708
41812
  const dispatch = payload;
41709
41813
  if (ctx.hasHandledCommand(dispatch.commandId)) {
@@ -41790,6 +41894,15 @@ async function runLocalCommand(ctx, input) {
41790
41894
  await runAiPrompt(ctx, prompt, commandId, "local");
41791
41895
  }
41792
41896
  async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
41897
+ const run11 = { controller: new AbortController(), state: "queued" };
41898
+ activeRuns.set(commandId, run11);
41899
+ try {
41900
+ await runAiPromptInner(ctx, prompt, commandId, origin, run11, oneShot, images, aiConfig, readOnly, bypass);
41901
+ } finally {
41902
+ activeRuns.delete(commandId);
41903
+ }
41904
+ }
41905
+ async function runAiPromptInner(ctx, prompt, commandId, origin, run11, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
41793
41906
  const config = readProfileConfig(ctx.profileDir);
41794
41907
  const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
41795
41908
  const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
@@ -41797,7 +41910,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
41797
41910
  const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
41798
41911
  const perPromptLimit = config.perPromptTokenLimit ?? 0;
41799
41912
  if (perPromptLimit > 0) {
41800
- const estimated = estimateTokens2(prompt);
41913
+ const estimated = estimateTokens(prompt);
41801
41914
  if (estimated > perPromptLimit) {
41802
41915
  rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
41803
41916
  return;
@@ -41954,24 +42067,46 @@ ${guardedPrompt}`;
41954
42067
  }
41955
42068
  };
41956
42069
  let result;
41957
- let queueTimedOut = false;
42070
+ let neverStarted = false;
41958
42071
  try {
41959
- const slot = await acquireRunSlot(ctx, ({ position, limit, running: running2 }) => {
41960
- const text = `\u23F3 queued \u2014 ${position} ahead, ${running2}/${limit} parallel runs in use (plan limit)`;
41961
- ctx.bus.push({ source: origin, kind: "log", text });
41962
- ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
41963
- `, log: true });
41964
- });
42072
+ const slot = await acquireRunSlot(
42073
+ ctx,
42074
+ ({ position, limit, running: running2 }) => {
42075
+ const text = `\u23F3 queued \u2014 ${position} ahead, ${running2}/${limit} parallel runs in use (plan limit)`;
42076
+ ctx.bus.push({ source: origin, kind: "log", text });
42077
+ ctx.send(WsChannels.COMMAND_OUTPUT, {
42078
+ commandId,
42079
+ seq: seq++,
42080
+ chunk: `${text}
42081
+ `,
42082
+ log: true,
42083
+ queue: { position, limit, running: running2 }
42084
+ });
42085
+ },
42086
+ run11.controller.signal
42087
+ );
41965
42088
  if (slot.kind === "timeout") {
41966
- queueTimedOut = true;
42089
+ neverStarted = true;
41967
42090
  const text = `RUN_QUEUE_TIMEOUT: waited 30 min for a free run slot (plan limit ${slot.limit} parallel runs) \u2014 not run`;
41968
42091
  ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
41969
42092
  ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
41970
42093
  `, log: true });
41971
42094
  result = { exitCode: 1, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
42095
+ } else if (slot.kind === "denied") {
42096
+ neverStarted = true;
42097
+ const text = "STORAGE_QUOTA_EXCEEDED: the organization's hosted storage is full \u2014 clear storage in Settings \u203A Storage, then retry";
42098
+ ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
42099
+ ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
42100
+ `, log: true });
42101
+ result = { exitCode: 1, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
42102
+ } else if (slot.kind === "cancelled") {
42103
+ neverStarted = true;
42104
+ result = { exitCode: COMMAND_CANCELLED_EXIT_CODE, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "", cancelled: true };
41972
42105
  } else {
42106
+ if (slot.queued) ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", log: true, queue: null });
42107
+ run11.state = "running";
41973
42108
  try {
41974
- result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs);
42109
+ result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run11.controller.signal);
41975
42110
  } finally {
41976
42111
  slot.handle.release();
41977
42112
  }
@@ -41979,6 +42114,12 @@ ${guardedPrompt}`;
41979
42114
  } finally {
41980
42115
  ctx.bus.endBusy(plan.cmd);
41981
42116
  }
42117
+ if (result.cancelled) {
42118
+ const text = "\u25A0 stopped";
42119
+ ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
42120
+ ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
42121
+ `, log: true });
42122
+ }
41982
42123
  const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
41983
42124
  if (result.workedDir) {
41984
42125
  if (unified && result.workedKey) {
@@ -41993,14 +42134,16 @@ ${guardedPrompt}`;
41993
42134
  seq: seq++,
41994
42135
  chunk: "",
41995
42136
  done: true,
41996
- exitCode: result.exitCode
42137
+ exitCode: result.exitCode,
42138
+ // A stopped run (ADR-0362): the server settles it `cancelled` and the web applies nothing.
42139
+ ...result.cancelled ? { cancelled: true } : {}
41997
42140
  });
41998
42141
  finishCommand(commandId, result.exitCode, result.usage);
41999
42142
  ctx.send(WsChannels.COMMAND_HISTORY, {
42000
42143
  commandId,
42001
42144
  cmd: plan.cmd,
42002
42145
  args: markerArgs,
42003
- status: result.exitCode === 0 ? "done" : "failed",
42146
+ status: result.cancelled ? "cancelled" : result.exitCode === 0 ? "done" : "failed",
42004
42147
  exitCode: result.exitCode,
42005
42148
  tokens: result.usage.tokens,
42006
42149
  // The ai_tokens split for this run (ADR-0145); attached only when we metered real tokens.
@@ -42036,13 +42179,13 @@ ${guardedPrompt}`;
42036
42179
  });
42037
42180
  ctx.bus.addTokens?.(result.usage.tokens);
42038
42181
  }
42039
- if (!queueTimedOut) ctx.reportUsage(events, profileLabel);
42182
+ if (!neverStarted) ctx.reportUsage(events, profileLabel);
42040
42183
  void ctx.pollUsage();
42041
42184
  ctx.bus.push({
42042
42185
  source: origin,
42043
42186
  kind: "exit",
42044
- text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
42045
- level: result.exitCode === 0 ? "info" : "error",
42187
+ text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : result.cancelled ? "\u25A0 stopped \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
42188
+ level: result.exitCode === 0 ? "info" : result.cancelled ? "warn" : "error",
42046
42189
  durationMs: Date.now() - aiStartedMs
42047
42190
  });
42048
42191
  if (memCfg.enabled && result.exitCode === 0) {
@@ -42278,12 +42421,17 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
42278
42421
  import { randomUUID } from "crypto";
42279
42422
  import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile5, readdir as readdir2, rm as rm2, stat } from "fs/promises";
42280
42423
  import { join as join20 } from "path";
42281
- function buildAutonomousCyclePrompt() {
42424
+ function buildAutonomousCyclePrompt(opts = {}) {
42425
+ const capNote = opts.intakeBlocked ? `
42426
+
42427
+ **Step 2 is DISABLED this cycle** \u2014 the organization reached its monthly plan limit for new \`AI_TODO\` /
42428
+ \`USER_QA\` rows. Do NOT add any row to \`AI_TODO.md\` or \`USER_QA.md\`; skip Step 2 entirely and continue
42429
+ with the already-approved work.` : "";
42282
42430
  return `You are running UNATTENDED as the 4PM autonomous cycle (headless, one tick). Do EXACTLY ONE small
42283
42431
  work cycle with the steps below, then STOP. Keep each cycle to ONE small task so you never run out of
42284
42432
  tokens mid-way. If a step fails, append a row to the **Incidents** table of \`AI_DONE.md\` and STOP \u2014
42285
42433
  do not push on. Write all books/docs/code in the language the project's \`CLAUDE.md\` specifies (default
42286
- English).
42434
+ English).${capNote}
42287
42435
 
42288
42436
  ## Books & approval (MUST read the approvals file)
42289
42437
  The five "book" files at the project root \u2014 \`USER_TODO.md\`, \`USER_QA.md\`, \`AI_TODO.md\`,
@@ -42417,8 +42565,16 @@ async function runAutonomousCycle(ctx) {
42417
42565
  await log(`[run] cycle (tick ${hist.ticks.count}/${today})`);
42418
42566
  const commandId = randomUUID();
42419
42567
  const override = cfg.model ? { model: cfg.model } : void 0;
42568
+ const usage = await readBookUsage(ctx);
42569
+ const intakeBlocked = ["AI_TODO", "USER_QA"].some((b2) => {
42570
+ const c = usage?.books[b2];
42571
+ return !!c && c.limit !== null && c.used >= c.limit;
42572
+ });
42573
+ if (intakeBlocked) await log("[cap] monthly AI_TODO/USER_QA limit reached \u2014 intake skipped this cycle");
42574
+ const booksBefore = await cappedBookIds(root);
42420
42575
  try {
42421
- await runAiPrompt(ctx, buildAutonomousCyclePrompt(), commandId, "local", false, void 0, override, false, true);
42576
+ await runAiPrompt(ctx, buildAutonomousCyclePrompt({ intakeBlocked }), commandId, "local", false, void 0, override, false, true);
42577
+ await reportNewBookRows(ctx, root, booksBefore);
42422
42578
  hist = await readHistories(root);
42423
42579
  hist.consecutiveFails = 0;
42424
42580
  await recordRun(root, hist, "success", "cycle complete", today);
@@ -42426,6 +42582,7 @@ async function runAutonomousCycle(ctx) {
42426
42582
  ctx.bus.autonomousDone(true);
42427
42583
  } catch (err) {
42428
42584
  const msg = err instanceof Error ? err.message : String(err);
42585
+ await reportNewBookRows(ctx, root, booksBefore);
42429
42586
  hist = await readHistories(root);
42430
42587
  hist.consecutiveFails += 1;
42431
42588
  await recordRun(root, hist, "failure", msg, today);
@@ -42442,6 +42599,25 @@ async function runAutonomousCycle(ctx) {
42442
42599
  running.delete(root);
42443
42600
  }
42444
42601
  }
42602
+ async function readBookUsage(ctx) {
42603
+ try {
42604
+ return await ctx.request(WsChannels.AUTONOMOUS_BOOK_USAGE, {});
42605
+ } catch {
42606
+ return null;
42607
+ }
42608
+ }
42609
+ async function reportNewBookRows(ctx, root, before) {
42610
+ try {
42611
+ const after = await cappedBookIds(root);
42612
+ const added = {};
42613
+ for (const book of Object.keys(after)) {
42614
+ const n = [...after[book]].filter((id) => !before[book].has(id)).length;
42615
+ if (n > 0) added[book] = n;
42616
+ }
42617
+ if (Object.keys(added).length > 0) await ctx.request(WsChannels.AUTONOMOUS_BOOK_USAGE, { added });
42618
+ } catch {
42619
+ }
42620
+ }
42445
42621
  async function dayLog(root, line) {
42446
42622
  try {
42447
42623
  const dir2 = join20(root, ".claude", "logs");
@@ -42788,10 +42964,10 @@ function handleFsChannels(ctx, message, payload) {
42788
42964
 
42789
42965
  // src/core/git-diff.ts
42790
42966
  import { readFile as readFile8 } from "fs/promises";
42791
- import { execFile as execFile6 } from "child_process";
42967
+ import { execFile as execFile5 } from "child_process";
42792
42968
  import { basename as basename5, dirname as dirname8, resolve as resolve9, sep as sep7 } from "path";
42793
- import { promisify as promisify6 } from "util";
42794
- var run6 = promisify6(execFile6);
42969
+ import { promisify as promisify5 } from "util";
42970
+ var run5 = promisify5(execFile5);
42795
42971
  function resolveInRoot5(root, relPath) {
42796
42972
  if (!root) return null;
42797
42973
  const base = resolve9(root);
@@ -42819,7 +42995,7 @@ async function gitDiff(root, path) {
42819
42995
  }
42820
42996
  let oldBuf = null;
42821
42997
  try {
42822
- const { stdout } = await run6("git", ["show", `HEAD:./${base}`], {
42998
+ const { stdout } = await run5("git", ["show", `HEAD:./${base}`], {
42823
42999
  cwd: dir2,
42824
43000
  timeout: 15e3,
42825
43001
  maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
@@ -42849,12 +43025,12 @@ async function gitDiff(root, path) {
42849
43025
  }
42850
43026
 
42851
43027
  // src/core/git-env.ts
42852
- import { execFile as execFile7 } from "child_process";
42853
- import { promisify as promisify7 } from "util";
42854
- var run7 = promisify7(execFile7);
43028
+ import { execFile as execFile6 } from "child_process";
43029
+ import { promisify as promisify6 } from "util";
43030
+ var run6 = promisify6(execFile6);
42855
43031
  async function hasBinary(bin, args = ["--version"]) {
42856
43032
  try {
42857
- await run7(bin, args, { timeout: 5e3 });
43033
+ await run6(bin, args, { timeout: 5e3 });
42858
43034
  return true;
42859
43035
  } catch {
42860
43036
  return false;
@@ -42867,7 +43043,7 @@ async function checkGitEnv(provider) {
42867
43043
  let account = null;
42868
43044
  if (installed) {
42869
43045
  try {
42870
- const { stdout, stderr } = await run7(bin, ["auth", "status"], { timeout: 8e3 });
43046
+ const { stdout, stderr } = await run6(bin, ["auth", "status"], { timeout: 8e3 });
42871
43047
  const out = `${stdout}
42872
43048
  ${stderr}`;
42873
43049
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -42880,11 +43056,11 @@ ${stderr}`;
42880
43056
  }
42881
43057
 
42882
43058
  // src/core/git-ssh-key.ts
42883
- import { execFile as execFile8 } from "child_process";
43059
+ import { execFile as execFile7 } from "child_process";
42884
43060
  import { readFile as readFile9, rm as rm4, mkdir as mkdir7 } from "fs/promises";
42885
43061
  import { join as join21 } from "path";
42886
- import { promisify as promisify8 } from "util";
42887
- var run8 = promisify8(execFile8);
43062
+ import { promisify as promisify7 } from "util";
43063
+ var run7 = promisify7(execFile7);
42888
43064
  function keyPaths(profileDir2) {
42889
43065
  const dir2 = join21(profileDir2, ".ssh");
42890
43066
  const key = join21(dir2, "id_4pm");
@@ -42899,14 +43075,14 @@ async function readPublicKey(pub) {
42899
43075
  }
42900
43076
  async function fingerprintOf(pub) {
42901
43077
  try {
42902
- const { stdout } = await run8("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
43078
+ const { stdout } = await run7("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
42903
43079
  return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
42904
43080
  } catch {
42905
43081
  return null;
42906
43082
  }
42907
43083
  }
42908
43084
  async function configureGit(key) {
42909
- await run8(
43085
+ await run7(
42910
43086
  "git",
42911
43087
  ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
42912
43088
  { timeout: 5e3 }
@@ -42917,7 +43093,7 @@ async function generate(profileDir2) {
42917
43093
  await mkdir7(dir2, { recursive: true });
42918
43094
  await rm4(key, { force: true });
42919
43095
  await rm4(pub, { force: true });
42920
- await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
43096
+ await run7("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
42921
43097
  timeout: 15e3
42922
43098
  });
42923
43099
  await configureGit(key);
@@ -42927,7 +43103,7 @@ async function remove(profileDir2) {
42927
43103
  const { key, pub } = keyPaths(profileDir2);
42928
43104
  await rm4(key, { force: true });
42929
43105
  await rm4(pub, { force: true });
42930
- await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
43106
+ await run7("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
42931
43107
  () => void 0
42932
43108
  );
42933
43109
  return { publicKey: null, fingerprint: null };
@@ -42948,10 +43124,10 @@ async function manageSshKey(op, profileDir2) {
42948
43124
  }
42949
43125
 
42950
43126
  // src/core/git-history.ts
42951
- import { execFile as execFile9 } from "child_process";
43127
+ import { execFile as execFile8 } from "child_process";
42952
43128
  import { resolve as resolve10, sep as sep8 } from "path";
42953
- import { promisify as promisify9 } from "util";
42954
- var run9 = promisify9(execFile9);
43129
+ import { promisify as promisify8 } from "util";
43130
+ var run8 = promisify8(execFile8);
42955
43131
  var FS = "";
42956
43132
  var RS = "";
42957
43133
  function resolveRepo(root, repo) {
@@ -42962,7 +43138,7 @@ function resolveRepo(root, repo) {
42962
43138
  }
42963
43139
  async function git(cwd2, args) {
42964
43140
  try {
42965
- const { stdout } = await run9("git", args, {
43141
+ const { stdout } = await run8("git", args, {
42966
43142
  cwd: cwd2,
42967
43143
  timeout: 15e3,
42968
43144
  maxBuffer: 8 * 1024 * 1024
@@ -43104,6 +43280,74 @@ function handleGitChannels(ctx, message, payload) {
43104
43280
  // src/core/ws-client/handlers/misc.ts
43105
43281
  import { join as join22 } from "path";
43106
43282
 
43283
+ // src/core/ai-assist.ts
43284
+ import { execFile as execFile9 } from "child_process";
43285
+ import { promisify as promisify9 } from "util";
43286
+ var run9 = promisify9(execFile9);
43287
+ function aiCli() {
43288
+ return process.env.AI_CLI || "claude";
43289
+ }
43290
+ function aiArgs(cli, prompt) {
43291
+ if (cli === "claude") return ["-p", prompt, "--output-format", "json", ...denySettingsArgs(cli, [process.env.CLAUDE_CONFIG_DIR])];
43292
+ if (cli === "codex") return ["exec", "--json", prompt];
43293
+ return ["-p", prompt];
43294
+ }
43295
+ function estimateTokens2(prompt, output) {
43296
+ return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
43297
+ }
43298
+ function parseAi(cli, stdout, prompt) {
43299
+ if (cli === "claude") {
43300
+ try {
43301
+ const j2 = JSON.parse(stdout);
43302
+ const text2 = (j2.result ?? "").trim();
43303
+ const tokens = (j2.usage?.input_tokens ?? 0) + (j2.usage?.output_tokens ?? 0);
43304
+ if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens2(prompt, text2) };
43305
+ } catch {
43306
+ }
43307
+ }
43308
+ if (cli === "codex") {
43309
+ try {
43310
+ let text2 = "";
43311
+ let tokens = 0;
43312
+ for (const line of stdout.split("\n").filter(Boolean)) {
43313
+ const obj = JSON.parse(line);
43314
+ if (obj.text) text2 = obj.text;
43315
+ else if (obj.message) text2 = obj.message;
43316
+ if (obj.usage) {
43317
+ tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
43318
+ }
43319
+ }
43320
+ if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens2(prompt, text2) };
43321
+ } catch {
43322
+ }
43323
+ }
43324
+ const text = stdout.trim();
43325
+ return { text, tokens: estimateTokens2(prompt, text) };
43326
+ }
43327
+ async function runAi(prompt) {
43328
+ const cli = aiCli();
43329
+ const { stdout } = await run9(cli, aiArgs(cli, prompt), {
43330
+ timeout: 12e4,
43331
+ maxBuffer: 8 * 1024 * 1024
43332
+ });
43333
+ return parseAi(cli, stdout, prompt);
43334
+ }
43335
+ async function aiOutboundReview(input, allowedRepos, projectPath) {
43336
+ 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:
43337
+ ${input}`;
43338
+ const { text, tokens } = await runAi(prompt);
43339
+ const start = text.indexOf("{");
43340
+ const end = text.lastIndexOf("}");
43341
+ if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
43342
+ try {
43343
+ const j2 = JSON.parse(text.slice(start, end + 1));
43344
+ const reasons = Array.isArray(j2.reasons) ? j2.reasons.filter((r) => typeof r === "string") : [];
43345
+ return { ok: j2.ok === true && reasons.length === 0, reasons, tokens };
43346
+ } catch {
43347
+ return { ok: false, reasons: ["ai_parse"], tokens };
43348
+ }
43349
+ }
43350
+
43107
43351
  // src/core/outbound-review.ts
43108
43352
  function compilePatterns(sources, fallback) {
43109
43353
  const list = sources ?? fallback;
@@ -43217,6 +43461,72 @@ function handleMiscChannels(ctx, message, payload) {
43217
43461
  // src/core/ws-client/handlers/project.ts
43218
43462
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, renameSync, rmSync as rmSync7 } from "fs";
43219
43463
  import { join as join23 } from "path";
43464
+
43465
+ // src/core/ai-task.ts
43466
+ import { randomUUID as randomUUID2 } from "crypto";
43467
+ function createAiTaskRunner(ctx) {
43468
+ return {
43469
+ acquireSlot: (onQueued) => acquireRunSlot(ctx, onQueued),
43470
+ generate: (prompt, cwd2, label) => generate2(ctx, prompt, cwd2, label)
43471
+ };
43472
+ }
43473
+ async function generate2(ctx, prompt, cwd2, label) {
43474
+ const config = readProfileConfig(ctx.profileDir);
43475
+ const cmd = config.aiCli || "claude";
43476
+ const unified = isUnifiedConfig(config);
43477
+ const hint = unified ? {
43478
+ credential: getPinnedCredential(ctx.profileDir) ?? ((config.aiFailoverMode ?? "remember") === "remember" ? getWorkingCredential(ctx.profileDir) : null)
43479
+ } : { dir: getWorkingProfile(ctx.profileDir, cmd) };
43480
+ const plan = planAiRun(prompt, config, hint, /* @__PURE__ */ new Map(), true);
43481
+ const timeoutSec = config.projectAiRunTimeoutSec || config.aiRunTimeoutSec || 0;
43482
+ ctx.bus.push({ source: "system", kind: "aireq", text: `${plan.cmd} \u2039 ${label}` });
43483
+ let text = "";
43484
+ const handlers = {
43485
+ onChunk: (chunk) => {
43486
+ text += chunk;
43487
+ },
43488
+ onAttemptStart: (profile, index, total, attemptCmd) => {
43489
+ ctx.bus.push({ source: "system", kind: "log", text: `\u2192 trying ${attemptCmd} profile "${profile}" (${index + 1}/${total})\u2026` });
43490
+ if (index > 0) text = "";
43491
+ },
43492
+ onAttemptFail: (profile) => {
43493
+ ctx.bus.push({ source: "system", kind: "log", text: `profile "${profile}" failed \u2014 trying next`, level: "warn" });
43494
+ }
43495
+ };
43496
+ ctx.bus.startBusy(plan.cmd);
43497
+ let result;
43498
+ try {
43499
+ result = await runAiFailover(plan, `ai-task-${randomUUID2()}`, cwd2, handlers, timeoutSec * 1e3);
43500
+ } finally {
43501
+ ctx.bus.endBusy(plan.cmd);
43502
+ }
43503
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
43504
+ const events = [{ metric: UsageMetric.COMMANDS, amount: 1, occurredAt }];
43505
+ if (result.exitCode === 0 && result.usage.tokens > 0) {
43506
+ events.push({
43507
+ metric: UsageMetric.AI_TOKENS,
43508
+ amount: result.usage.tokens,
43509
+ occurredAt,
43510
+ inputTokens: result.usage.input,
43511
+ outputTokens: result.usage.output,
43512
+ cacheReadTokens: result.usage.cacheRead,
43513
+ cacheCreationTokens: result.usage.cacheCreation,
43514
+ provider: aiProviderOf(result.workedCmd ?? plan.cmd),
43515
+ authMode: resolveClaudeAuthMode(config)
43516
+ });
43517
+ }
43518
+ ctx.reportUsage(events, result.workedDir ? profileDisplayLabel(result.workedDir) : null);
43519
+ const ok = result.exitCode === 0 && text.trim().length > 0;
43520
+ ctx.bus.push({
43521
+ source: "system",
43522
+ kind: ok ? "aires" : "log",
43523
+ text: ok ? `${plan.cmd} \u203A ${label} \u2713` : `${label}: AI run failed (exit ${result.exitCode}) \u2014 using the fallback`,
43524
+ ...ok ? {} : { level: "warn" }
43525
+ });
43526
+ return ok ? text.trim() : null;
43527
+ }
43528
+
43529
+ // src/core/ws-client/handlers/project.ts
43220
43530
  function handleProjectChannels(ctx, message, payload) {
43221
43531
  switch (message.channel) {
43222
43532
  case WsChannels.PHYSIC_SYNC: {
@@ -43274,14 +43584,18 @@ function handleProjectChannels(ctx, message, payload) {
43274
43584
  void scaffoldProject(
43275
43585
  payload,
43276
43586
  ctx.profileDir,
43277
- (p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2)
43587
+ (p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2),
43588
+ // AI init on the standard AI path + one org run slot (ADR-0362).
43589
+ createAiTaskRunner(ctx)
43278
43590
  ).then((reply) => ctx.send(WsChannels.PROJECT_CREATE, reply, message.id));
43279
43591
  return true;
43280
43592
  case WsChannels.PROJECT_ADD:
43281
43593
  void addProject(
43282
43594
  payload,
43283
43595
  ctx.profileDir,
43284
- (p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2)
43596
+ (p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2),
43597
+ // An add-with-scaffold runs AI init too (ADR-0362).
43598
+ createAiTaskRunner(ctx)
43285
43599
  ).then((reply) => ctx.send(WsChannels.PROJECT_ADD, reply, message.id));
43286
43600
  return true;
43287
43601
  default:
@@ -43364,7 +43678,7 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
43364
43678
  // src/core/faq-compose.ts
43365
43679
  import { spawn as spawn9, execFile as execFile10 } from "child_process";
43366
43680
  import { existsSync as existsSync15, mkdirSync as mkdirSync8, rmSync as rmSync8 } from "fs";
43367
- import { randomUUID as randomUUID2 } from "crypto";
43681
+ import { randomUUID as randomUUID3 } from "crypto";
43368
43682
  import { join as join24 } from "path";
43369
43683
  import { tmpdir as tmpdir4 } from "os";
43370
43684
  import { promisify as promisify10 } from "util";
@@ -43486,7 +43800,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
43486
43800
  var MAX_OUTPUT_CHARS = 8e3;
43487
43801
  async function runFaqCompose(req, ai2) {
43488
43802
  if (!req.repo.token) return { prUrl: "", error: "no write token" };
43489
- const workDir = join24(tmpdir4(), "4pm-faq-sync", randomUUID2());
43803
+ const workDir = join24(tmpdir4(), "4pm-faq-sync", randomUUID3());
43490
43804
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
43491
43805
  const auth = gitAuthArgs("x-access-token", req.repo.token);
43492
43806
  try {
@@ -43501,7 +43815,7 @@ async function runFaqCompose(req, ai2) {
43501
43815
  await execFileAsync2("git", ["-C", workDir, "add", "-A"], { env: gitEnv });
43502
43816
  const staged = await execFileAsync2("git", ["-C", workDir, "diff", "--cached", "--name-only"], { env: gitEnv });
43503
43817
  if (!staged.stdout.trim()) {
43504
- return { prUrl: "", output, perTicket, tokens: usage.tokens || estimateTokens2(text), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
43818
+ return { prUrl: "", output, perTicket, tokens: usage.tokens || estimateTokens(text), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
43505
43819
  }
43506
43820
  await execFileAsync2("git", ["-C", workDir, "commit", "-m", "docs(faq): synthesize from support tickets (4PM)"], { env: gitEnv });
43507
43821
  await execFileAsync2("git", [...auth, "-C", workDir, "push", "-u", "origin", req.headBranch], { env: gitEnv });
@@ -45290,7 +45604,7 @@ var WsClient = class {
45290
45604
  }
45291
45605
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
45292
45606
  const envelope = {
45293
- id: randomUUID3(),
45607
+ id: randomUUID4(),
45294
45608
  replyTo,
45295
45609
  channel,
45296
45610
  wsToken: this.wsToken,
@@ -45311,7 +45625,7 @@ var WsClient = class {
45311
45625
  }
45312
45626
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
45313
45627
  const envelope = {
45314
- id: randomUUID3(),
45628
+ id: randomUUID4(),
45315
45629
  replyTo: null,
45316
45630
  channel,
45317
45631
  wsToken: this.wsToken,
@@ -45703,7 +46017,7 @@ function releaseInstanceLock(profileDir2) {
45703
46017
 
45704
46018
  // src/core/session-bus.ts
45705
46019
  import { EventEmitter } from "events";
45706
- import { randomUUID as randomUUID4 } from "crypto";
46020
+ import { randomUUID as randomUUID5 } from "crypto";
45707
46021
  var REPLAY_CAP = 500;
45708
46022
  var SessionBus = class {
45709
46023
  emitter = new EventEmitter();
@@ -45734,7 +46048,7 @@ var SessionBus = class {
45734
46048
  /** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
45735
46049
  push(input) {
45736
46050
  const entry = {
45737
- id: randomUUID4(),
46051
+ id: randomUUID5(),
45738
46052
  level: "info",
45739
46053
  ...input,
45740
46054
  ts: Date.now()
@@ -48127,7 +48441,7 @@ var src_default = Yoga;
48127
48441
 
48128
48442
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
48129
48443
  var import_react_reconciler = __toESM(require_react_reconciler(), 1);
48130
- var import_constants11 = __toESM(require_constants2(), 1);
48444
+ var import_constants12 = __toESM(require_constants2(), 1);
48131
48445
  import process3 from "process";
48132
48446
 
48133
48447
  // ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
@@ -49369,7 +49683,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
49369
49683
  scheduleTimeout: setTimeout,
49370
49684
  cancelTimeout: clearTimeout,
49371
49685
  noTimeout: -1,
49372
- getCurrentEventPriority: () => import_constants11.DefaultEventPriority,
49686
+ getCurrentEventPriority: () => import_constants12.DefaultEventPriority,
49373
49687
  beforeActiveInstanceBlur() {
49374
49688
  },
49375
49689
  afterActiveInstanceBlur() {