@4pm/cli 1.21.0 → 1.23.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 +443 -182
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5713,14 +5713,14 @@ var require_react_development = __commonJS({
5713
5713
  var thenableResult = result;
5714
5714
  var wasAwaited = false;
5715
5715
  var thenable = {
5716
- then: function(resolve10, reject) {
5716
+ then: function(resolve11, reject) {
5717
5717
  wasAwaited = true;
5718
5718
  thenableResult.then(function(returnValue2) {
5719
5719
  popActScope(prevActScopeDepth);
5720
5720
  if (actScopeDepth === 0) {
5721
- recursivelyFlushAsyncActWork(returnValue2, resolve10, reject);
5721
+ recursivelyFlushAsyncActWork(returnValue2, resolve11, reject);
5722
5722
  } else {
5723
- resolve10(returnValue2);
5723
+ resolve11(returnValue2);
5724
5724
  }
5725
5725
  }, function(error2) {
5726
5726
  popActScope(prevActScopeDepth);
@@ -5750,20 +5750,20 @@ var require_react_development = __commonJS({
5750
5750
  ReactCurrentActQueue.current = null;
5751
5751
  }
5752
5752
  var _thenable = {
5753
- then: function(resolve10, reject) {
5753
+ then: function(resolve11, reject) {
5754
5754
  if (ReactCurrentActQueue.current === null) {
5755
5755
  ReactCurrentActQueue.current = [];
5756
- recursivelyFlushAsyncActWork(returnValue, resolve10, reject);
5756
+ recursivelyFlushAsyncActWork(returnValue, resolve11, reject);
5757
5757
  } else {
5758
- resolve10(returnValue);
5758
+ resolve11(returnValue);
5759
5759
  }
5760
5760
  }
5761
5761
  };
5762
5762
  return _thenable;
5763
5763
  } else {
5764
5764
  var _thenable2 = {
5765
- then: function(resolve10, reject) {
5766
- resolve10(returnValue);
5765
+ then: function(resolve11, reject) {
5766
+ resolve11(returnValue);
5767
5767
  }
5768
5768
  };
5769
5769
  return _thenable2;
@@ -5779,7 +5779,7 @@ var require_react_development = __commonJS({
5779
5779
  actScopeDepth = prevActScopeDepth;
5780
5780
  }
5781
5781
  }
5782
- function recursivelyFlushAsyncActWork(returnValue, resolve10, reject) {
5782
+ function recursivelyFlushAsyncActWork(returnValue, resolve11, reject) {
5783
5783
  {
5784
5784
  var queue = ReactCurrentActQueue.current;
5785
5785
  if (queue !== null) {
@@ -5788,16 +5788,16 @@ var require_react_development = __commonJS({
5788
5788
  enqueueTask(function() {
5789
5789
  if (queue.length === 0) {
5790
5790
  ReactCurrentActQueue.current = null;
5791
- resolve10(returnValue);
5791
+ resolve11(returnValue);
5792
5792
  } else {
5793
- recursivelyFlushAsyncActWork(returnValue, resolve10, reject);
5793
+ recursivelyFlushAsyncActWork(returnValue, resolve11, reject);
5794
5794
  }
5795
5795
  });
5796
5796
  } catch (error2) {
5797
5797
  reject(error2);
5798
5798
  }
5799
5799
  } else {
5800
- resolve10(returnValue);
5800
+ resolve11(returnValue);
5801
5801
  }
5802
5802
  }
5803
5803
  }
@@ -28329,7 +28329,10 @@ function defaultProfileConfig() {
28329
28329
  webBlockedCommands: ["quit", "config"],
28330
28330
  // Shared AI memory (ADR-0245) — off by default (opt-in; costs a compaction call per turn).
28331
28331
  aiMemoryEnabled: false,
28332
- aiMemoryBudgetChars: 1e3
28332
+ aiMemoryBudgetChars: 1e3,
28333
+ // Bounded native resume (ADR-0339) — resume only a warm (≤5 min), not-too-large session.
28334
+ aiResumeMaxIdleMinutes: 5,
28335
+ aiResumeMaxContextTokens: 1e5
28333
28336
  };
28334
28337
  }
28335
28338
  function resolveMemoryConfig(config) {
@@ -28339,6 +28342,14 @@ function resolveMemoryConfig(config) {
28339
28342
  const budgetChars = projectBudget > 0 ? projectBudget : config.aiMemoryBudgetChars ?? 1e3;
28340
28343
  return { enabled, budgetChars };
28341
28344
  }
28345
+ function resolveResumePolicy(config) {
28346
+ const idleMin = config.aiResumeMaxIdleMinutes ?? 5;
28347
+ const maxCtx = config.aiResumeMaxContextTokens ?? 1e5;
28348
+ return {
28349
+ maxIdleMs: idleMin > 0 ? idleMin * 6e4 : Number.POSITIVE_INFINITY,
28350
+ maxContextTokens: maxCtx > 0 ? maxCtx : Number.POSITIVE_INFINITY
28351
+ };
28352
+ }
28342
28353
  function resolveIdleAutoClearMinutes(config) {
28343
28354
  const project = config.projectAutoClearIdleMinutes ?? 0;
28344
28355
  return project > 0 ? project : config.autoClearIdleMinutes ?? 10;
@@ -28628,6 +28639,16 @@ var UsageMetric = {
28628
28639
  COMMANDS: "commands",
28629
28640
  AUTONOMOUS_MINUTES: "autonomous_minutes"
28630
28641
  };
28642
+ var AiTokenProvider = {
28643
+ CLAUDE: "claude",
28644
+ CODEX: "codex",
28645
+ ANTIGRAVITY: "antigravity"
28646
+ };
28647
+ var AI_TOKEN_PROVIDERS = [
28648
+ AiTokenProvider.CLAUDE,
28649
+ AiTokenProvider.CODEX,
28650
+ AiTokenProvider.ANTIGRAVITY
28651
+ ];
28631
28652
 
28632
28653
  // vendor/@4pm/constants/roles.ts
28633
28654
  var Role = {
@@ -32732,7 +32753,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
32732
32753
 
32733
32754
  // src/core/ws-client.ts
32734
32755
  init_wrapper();
32735
- import { randomUUID as randomUUID4 } from "crypto";
32756
+ import { randomUUID as randomUUID3 } from "crypto";
32736
32757
  import { basename as basename7, join as join32 } from "path";
32737
32758
  import { existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
32738
32759
 
@@ -37498,7 +37519,13 @@ var contactChangeConfirmSchema = external_exports.object({
37498
37519
  token: external_exports.string().min(1)
37499
37520
  });
37500
37521
  var activityHeartbeatSchema = external_exports.object({
37501
- projectId: external_exports.string().uuid().optional()
37522
+ projectId: external_exports.string().uuid().optional(),
37523
+ /** ADR-0342 — the client is idle (5 min no input · tab hidden) ⇒ presence `idle`. */
37524
+ idle: external_exports.boolean().optional()
37525
+ });
37526
+ var USER_CARDS_MAX_IDS = 100;
37527
+ var userCardsQuerySchema = external_exports.object({
37528
+ ids: external_exports.string().transform((v2) => [...new Set(v2.split(",").map((s3) => s3.trim()).filter(Boolean))]).pipe(external_exports.array(external_exports.string().uuid()).min(1).max(USER_CARDS_MAX_IDS))
37502
37529
  });
37503
37530
 
37504
37531
  // src/core/scaffold.ts
@@ -37506,19 +37533,49 @@ import { cp, mkdir, mkdtemp, rm, writeFile } from "fs/promises";
37506
37533
  import { existsSync as existsSync4 } from "fs";
37507
37534
  import { execFile as execFile2 } from "child_process";
37508
37535
  import { tmpdir as tmpdir2 } from "os";
37509
- import { dirname as dirname3, join as join6, resolve } from "path";
37536
+ import { dirname as dirname3, join as join6, resolve as resolve2 } from "path";
37510
37537
  import { fileURLToPath as fileURLToPath3 } from "url";
37511
37538
  import { promisify as promisify2 } from "util";
37512
37539
 
37513
37540
  // src/core/ai-assist.ts
37514
37541
  import { execFile } from "child_process";
37515
37542
  import { promisify } from "util";
37543
+
37544
+ // src/utils/agent-deny.ts
37545
+ import { isAbsolute, resolve } from "path";
37546
+ var HOME_SECRET_PATTERNS = [
37547
+ "~/.4pm/**",
37548
+ "~/.ssh/**",
37549
+ "~/.config/gh/**",
37550
+ "~/.git-credentials",
37551
+ "~/.claude*",
37552
+ "~/.claude*/**"
37553
+ ];
37554
+ function absDirPattern(dir2) {
37555
+ const abs = (isAbsolute(dir2) ? dir2 : resolve(dir2)).replace(/\\/g, "/").replace(/\/+$/, "");
37556
+ return `/${abs.startsWith("/") ? abs : `/${abs}`}/**`;
37557
+ }
37558
+ function agentDenyRules(profileDirs2 = []) {
37559
+ const patterns = new Set(HOME_SECRET_PATTERNS);
37560
+ for (const dir2 of profileDirs2) {
37561
+ if (dir2 && dir2.trim()) patterns.add(absDirPattern(dir2.trim()));
37562
+ }
37563
+ const rules = [];
37564
+ for (const p2 of patterns) rules.push(`Read(${p2})`, `Edit(${p2})`);
37565
+ return rules;
37566
+ }
37567
+ function denySettingsArgs(cmd, profileDirs2 = []) {
37568
+ if (!cmd.includes("claude")) return [];
37569
+ return ["--settings", JSON.stringify({ permissions: { deny: agentDenyRules(profileDirs2) } })];
37570
+ }
37571
+
37572
+ // src/core/ai-assist.ts
37516
37573
  var run = promisify(execFile);
37517
37574
  function aiCli() {
37518
37575
  return process.env.AI_CLI || "claude";
37519
37576
  }
37520
37577
  function aiArgs(cli, prompt) {
37521
- if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
37578
+ if (cli === "claude") return ["-p", prompt, "--output-format", "json", ...denySettingsArgs(cli, [process.env.CLAUDE_CONFIG_DIR])];
37522
37579
  if (cli === "codex") return ["exec", "--json", prompt];
37523
37580
  return ["-p", prompt];
37524
37581
  }
@@ -37810,8 +37867,8 @@ async function ensureReposCloned(physicRoot, repos, emit) {
37810
37867
  function sampleDir() {
37811
37868
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
37812
37869
  const here = dirname3(fileURLToPath3(import.meta.url));
37813
- const bundled = resolve(here, "project-sample");
37814
- for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
37870
+ const bundled = resolve2(here, "project-sample");
37871
+ for (const candidate of [bundled, resolve2(here, "../project-sample"), resolve2(here, "../../project-sample")]) {
37815
37872
  if (existsSync4(candidate)) return candidate;
37816
37873
  }
37817
37874
  return bundled;
@@ -37823,7 +37880,7 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
37823
37880
  onProgress?.({ projectId: payload.projectId, step, message });
37824
37881
  };
37825
37882
  try {
37826
- const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
37883
+ const root = process.env.SCAFFOLD_ROOT ? resolve2(process.env.SCAFFOLD_ROOT) : resolve2(profileDir2);
37827
37884
  const target = join6(root, payload.projectName);
37828
37885
  await mkdir(target, { recursive: true });
37829
37886
  const repo = singleRepo(reposOf(payload.spec));
@@ -37928,7 +37985,7 @@ async function addProject(payload, profileDir2, onProgress) {
37928
37985
  onProgress?.({ projectId: payload.projectId, step, message });
37929
37986
  };
37930
37987
  try {
37931
- const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
37988
+ const root = process.env.SCAFFOLD_ROOT ? resolve2(process.env.SCAFFOLD_ROOT) : resolve2(profileDir2);
37932
37989
  const target = join6(root, payload.projectName);
37933
37990
  await mkdir(target, { recursive: true });
37934
37991
  const repo = singleRepo(payload.repos ?? []);
@@ -38568,8 +38625,8 @@ function resolveInstallTimeoutMs(sec) {
38568
38625
  return sec && sec > 0 ? sec * 1e3 : TOOL_INSTALL_TIMEOUT_DEFAULT_MS;
38569
38626
  }
38570
38627
  function sleep(ms2) {
38571
- return new Promise((resolve10) => {
38572
- const t2 = setTimeout(resolve10, ms2);
38628
+ return new Promise((resolve11) => {
38629
+ const t2 = setTimeout(resolve11, ms2);
38573
38630
  t2.unref?.();
38574
38631
  });
38575
38632
  }
@@ -38597,14 +38654,14 @@ function classifyToolFailure(code, out) {
38597
38654
  return { reason: "other", retryable: false };
38598
38655
  }
38599
38656
  function run3(cmd, args, onLine, timeoutMs = 18e4) {
38600
- return new Promise((resolve10) => {
38657
+ return new Promise((resolve11) => {
38601
38658
  let out = "";
38602
38659
  let buf = "";
38603
38660
  let child = null;
38604
38661
  const done = (code) => {
38605
38662
  clearTimeout(timer);
38606
38663
  if (onLine && buf.trim()) onLine(buf.trim());
38607
- resolve10({ code, out });
38664
+ resolve11({ code, out });
38608
38665
  };
38609
38666
  const timer = setTimeout(() => {
38610
38667
  child?.kill();
@@ -39034,7 +39091,7 @@ function clearPinnedCredential(profileDir2) {
39034
39091
  // src/utils/ai-cli.ts
39035
39092
  import { readFileSync as readFileSync8 } from "fs";
39036
39093
  import { homedir as homedir3 } from "os";
39037
- import { basename, isAbsolute, join as join9 } from "path";
39094
+ import { basename, isAbsolute as isAbsolute2, join as join9 } from "path";
39038
39095
  var REQUIRED_AI_ARGS = {
39039
39096
  claude: ["-p", "--output-format", "stream-json", "--verbose"],
39040
39097
  codex: ["exec", "--json"],
@@ -39171,13 +39228,22 @@ function claudeCredentialKeys(config) {
39171
39228
  }
39172
39229
  function resolveHomePath(value) {
39173
39230
  if (value.startsWith("~")) return join9(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
39174
- if (isAbsolute(value)) return value;
39231
+ if (isAbsolute2(value)) return value;
39175
39232
  return join9(homedir3(), value);
39176
39233
  }
39177
39234
  function profileDirs(profiles) {
39178
39235
  if (!profiles) return [];
39179
39236
  return profiles.filter(isUsableProfile).map((p2) => resolveHomePath(p2.profile));
39180
39237
  }
39238
+ function allAiProfileDirs(config) {
39239
+ const dirs = [
39240
+ ...profileDirs(config.claudeHome),
39241
+ ...profileDirs(config.codexHome),
39242
+ ...profileDirs(config.antigravityHome),
39243
+ ...profileDirs(config.aiProfiles)
39244
+ ];
39245
+ return [...new Set(dirs)];
39246
+ }
39181
39247
  function folderScopeGuard(folder, prompt) {
39182
39248
  return [
39183
39249
  `IMPORTANT: You are working inside the project folder \`${folder}\`. Only read, use, and`,
@@ -39201,7 +39267,7 @@ function overrideEnv(cmd, o) {
39201
39267
  const budget = THINKING_TOKENS[o.thinking];
39202
39268
  return budget ? { MAX_THINKING_TOKENS: String(budget) } : {};
39203
39269
  }
39204
- function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnly = false, bypass = false) {
39270
+ function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnly = false, bypass = false, denyDirs = []) {
39205
39271
  const extras = profile.args ?? [];
39206
39272
  const model = override?.model?.trim() || profile.model?.trim();
39207
39273
  const modelArgs = model ? ["--model", model] : [];
@@ -39213,6 +39279,10 @@ function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnl
39213
39279
  ...extras,
39214
39280
  ...modelArgs,
39215
39281
  ...overrideArgs(cmd, override),
39282
+ // Secret-path deny rules (ADR-0347) — every configured credential dir + ~/.4pm, ~/.ssh, gh, git
39283
+ // credentials, ~/.claude*. A single-value flag, placed before the mode caps so their non-variadic
39284
+ // terminator still bounds `--disallowedTools`.
39285
+ ...denySettingsArgs(cmd, denyDirs),
39216
39286
  ...modeExtra
39217
39287
  ];
39218
39288
  }
@@ -39221,13 +39291,25 @@ function claudeHomeDirs(config) {
39221
39291
  return dirs.length > 0 ? dirs : [join9(homedir3(), ".claude")];
39222
39292
  }
39223
39293
  function profileDisplayLabel(dir2) {
39294
+ return profileAccountEmail(dir2) ?? basename(dir2);
39295
+ }
39296
+ function profileAccountEmail(dir2) {
39224
39297
  try {
39225
39298
  const raw = JSON.parse(readFileSync8(join9(dir2, ".claude.json"), "utf8"));
39226
- const email = raw.oauthAccount?.emailAddress;
39227
- if (email && email.trim()) return email;
39299
+ const email = raw.oauthAccount?.emailAddress?.trim();
39300
+ return email ? email : null;
39228
39301
  } catch {
39302
+ return null;
39229
39303
  }
39230
- return basename(dir2);
39304
+ }
39305
+ function aiAccountLabels(config) {
39306
+ const labels = isUnifiedConfig(config) ? config.aiProfiles.filter(isUsableCredential).map((c) => {
39307
+ const dir2 = resolveHomePath(c.profile);
39308
+ return profileAccountEmail(dir2) ?? (c.label?.trim() || basename(dir2));
39309
+ }) : [...profileDirs(config.claudeHome), ...profileDirs(config.codexHome), ...profileDirs(config.antigravityHome)].map(
39310
+ (dir2) => profileDisplayLabel(dir2)
39311
+ );
39312
+ return [...new Set(labels)];
39231
39313
  }
39232
39314
  function profileEnvVar(cmd) {
39233
39315
  if (cmd.includes("claude")) return "CLAUDE_CONFIG_DIR";
@@ -39265,7 +39347,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot, over
39265
39347
  return {
39266
39348
  cmd: active,
39267
39349
  attempts: [
39268
- { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot, override, readOnly, bypass), stdin: prompt, env: { ...baseEnv, ...overrideEnv(active, override) } }
39350
+ { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot, override, readOnly, bypass, allAiProfileDirs(config)), stdin: prompt, env: { ...baseEnv, ...overrideEnv(active, override) } }
39269
39351
  ]
39270
39352
  };
39271
39353
  }
@@ -39283,7 +39365,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot, over
39283
39365
  label: cred.label?.trim() || profileDisplayLabel(dir2),
39284
39366
  dir: dir2,
39285
39367
  key,
39286
- args: buildRunArgs(cred, cmd, resume.get(key), oneShot, override, readOnly, bypass),
39368
+ args: buildRunArgs(cred, cmd, resume.get(key), oneShot, override, readOnly, bypass, [...allAiProfileDirs(config), dir2]),
39287
39369
  stdin: prompt,
39288
39370
  env: envVar ? { ...baseEnv, [envVar]: dir2, ...overrideEnv(cmd, override) } : { ...baseEnv, ...overrideEnv(cmd, override) }
39289
39371
  }));
@@ -39296,7 +39378,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
39296
39378
  const defaultPlan = {
39297
39379
  cmd,
39298
39380
  attempts: [
39299
- { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot, override, readOnly, bypass), stdin: prompt, env: { ...baseEnv, ...overrideEnv(cmd, override) } }
39381
+ { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot, override, readOnly, bypass, allAiProfileDirs(config)), stdin: prompt, env: { ...baseEnv, ...overrideEnv(cmd, override) } }
39300
39382
  ]
39301
39383
  };
39302
39384
  if (!envVar) return defaultPlan;
@@ -39313,7 +39395,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
39313
39395
  label: profileDisplayLabel(dir2),
39314
39396
  dir: dir2,
39315
39397
  key,
39316
- args: buildRunArgs(profile, cmd, resume.get(key), oneShot, override, readOnly, bypass),
39398
+ args: buildRunArgs(profile, cmd, resume.get(key), oneShot, override, readOnly, bypass, [...allAiProfileDirs(config), dir2]),
39317
39399
  stdin: prompt,
39318
39400
  env: { ...baseEnv, [envVar]: dir2, ...overrideEnv(cmd, override) }
39319
39401
  };
@@ -39356,16 +39438,16 @@ async function fetchUsage(token) {
39356
39438
  }
39357
39439
  }
39358
39440
  function runUsageCli(dir2, aiCli2) {
39359
- return new Promise((resolve10) => {
39441
+ return new Promise((resolve11) => {
39360
39442
  let out = "";
39361
39443
  let child = null;
39362
39444
  const finish = () => {
39363
39445
  clearTimeout(timer);
39364
- resolve10(out);
39446
+ resolve11(out);
39365
39447
  };
39366
39448
  const timer = setTimeout(() => {
39367
39449
  child?.kill();
39368
- resolve10(out);
39450
+ resolve11(out);
39369
39451
  }, 3e4);
39370
39452
  timer.unref?.();
39371
39453
  try {
@@ -39688,9 +39770,9 @@ function pruneCommandOutputByAge(days) {
39688
39770
  import { spawn as spawn4 } from "child_process";
39689
39771
  import { execFile as execFile3 } from "child_process";
39690
39772
  import { createHash as createHash3 } from "crypto";
39691
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
39692
- import { randomUUID } from "crypto";
39773
+ 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";
39693
39774
  import { join as join14 } from "path";
39775
+ import { tmpdir as tmpdir3 } from "os";
39694
39776
  import { promisify as promisify3 } from "util";
39695
39777
 
39696
39778
  // src/core/ai-stream.ts
@@ -39720,12 +39802,16 @@ function createAiStreamParser(cli) {
39720
39802
  acc.output = ev.usage.output_tokens ?? 0;
39721
39803
  acc.cacheRead = ev.usage.cache_read_input_tokens ?? 0;
39722
39804
  acc.cacheCreation = ev.usage.cache_creation_input_tokens ?? 0;
39723
- acc.tokens = acc.input + acc.output;
39805
+ acc.tokens = acc.input + acc.output + acc.cacheRead + acc.cacheCreation;
39724
39806
  if (typeof ev.total_cost_usd === "number") acc.costUsd = ev.total_cost_usd;
39725
39807
  }
39726
39808
  return "";
39727
39809
  }
39728
39810
  if (ev.type === "assistant") {
39811
+ const u2 = ev.message?.usage;
39812
+ if (u2) {
39813
+ acc.contextTokens = (u2.input_tokens ?? 0) + (u2.cache_read_input_tokens ?? 0) + (u2.cache_creation_input_tokens ?? 0) + (u2.output_tokens ?? 0);
39814
+ }
39729
39815
  const text = assistantText(ev);
39730
39816
  return text ? `${text}
39731
39817
  ` : "";
@@ -39734,11 +39820,12 @@ function createAiStreamParser(cli) {
39734
39820
  }
39735
39821
  function handleCodex(ev) {
39736
39822
  if (ev.type === "turn.completed" && ev.usage) {
39737
- acc.input = ev.usage.input_tokens ?? 0;
39823
+ const cached = ev.usage.cached_input_tokens ?? 0;
39824
+ acc.input = Math.max(0, (ev.usage.input_tokens ?? 0) - cached);
39738
39825
  acc.output = (ev.usage.output_tokens ?? 0) + (ev.usage.reasoning_output_tokens ?? 0);
39739
- acc.cacheRead = ev.usage.cached_input_tokens ?? 0;
39826
+ acc.cacheRead = cached;
39740
39827
  acc.cacheCreation = 0;
39741
- acc.tokens = acc.input + acc.output;
39828
+ acc.tokens = acc.input + acc.output + acc.cacheRead;
39742
39829
  return "";
39743
39830
  }
39744
39831
  if (ev.type === "item.completed" && ev.item?.type === "agent_message") {
@@ -39786,10 +39873,65 @@ function createAiStreamParser(cli) {
39786
39873
  apiError: () => ({ ...apiError })
39787
39874
  };
39788
39875
  }
39876
+ function aiProviderOf(cmd) {
39877
+ if (cmd.includes("codex")) return "codex";
39878
+ if (cmd.includes("antigravity")) return "antigravity";
39879
+ return "claude";
39880
+ }
39789
39881
  function estimateTokens2(text) {
39790
39882
  return Math.max(1, Math.ceil(text.length / 4));
39791
39883
  }
39792
39884
 
39885
+ // src/core/agent-sandbox.ts
39886
+ var ENV_ALLOW = /* @__PURE__ */ new Set([
39887
+ "PATH",
39888
+ "HOME",
39889
+ "USER",
39890
+ "LOGNAME",
39891
+ "SHELL",
39892
+ "LANG",
39893
+ "TERM",
39894
+ "TZ",
39895
+ "TMPDIR",
39896
+ "TMP",
39897
+ "TEMP",
39898
+ "HTTP_PROXY",
39899
+ "HTTPS_PROXY",
39900
+ "NO_PROXY",
39901
+ "ALL_PROXY",
39902
+ "http_proxy",
39903
+ "https_proxy",
39904
+ "no_proxy",
39905
+ "all_proxy",
39906
+ "SSL_CERT_FILE",
39907
+ "SSL_CERT_DIR",
39908
+ "NODE_EXTRA_CA_CERTS",
39909
+ "SYSTEMROOT",
39910
+ "SYSTEMDRIVE",
39911
+ "WINDIR",
39912
+ "COMSPEC",
39913
+ "PATHEXT",
39914
+ "USERPROFILE",
39915
+ "APPDATA",
39916
+ "LOCALAPPDATA",
39917
+ "PROGRAMDATA",
39918
+ "PROGRAMFILES"
39919
+ ]);
39920
+ var ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "ANTHROPIC_", "CLAUDE_", "CODEX_", "OPENAI_", "AWS_", "GOOGLE_", "CLOUD_ML_", "VERTEX_"];
39921
+ function agentEnv(extraEnv, overrides) {
39922
+ const env3 = {};
39923
+ for (const [key, value] of Object.entries(process.env)) {
39924
+ if (value === void 0) continue;
39925
+ if (ENV_ALLOW.has(key) || ENV_ALLOW_PREFIXES.some((p2) => key.startsWith(p2))) env3[key] = value;
39926
+ }
39927
+ return { ...env3, ...extraEnv, ...overrides };
39928
+ }
39929
+ function gitAuthArgs(user, password) {
39930
+ if (!user && !password) return [];
39931
+ const basic = Buffer.from(`${user}:${password}`).toString("base64");
39932
+ return ["-c", `http.extraHeader=Authorization: Basic ${basic}`];
39933
+ }
39934
+
39793
39935
  // src/core/support-answer.ts
39794
39936
  var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
39795
39937
  var execFileAsync = promisify3(execFile3);
@@ -39803,18 +39945,34 @@ function repoDir(profileDir2, url, branch) {
39803
39945
  const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
39804
39946
  return join14(kbRoot(profileDir2), key);
39805
39947
  }
39806
- function authedUrl(url, token) {
39807
- if (!token) return url;
39808
- if (url.startsWith("https://")) return url.replace("https://", `https://${token}@`);
39809
- return url;
39948
+ var DENIED_TOOLS = ["Bash", "Edit", "Write", "NotebookEdit", "WebFetch", "WebSearch", "Task"];
39949
+ var KB_STALE_MS = 24 * 60 * 60 * 1e3;
39950
+ var kbTokens = /* @__PURE__ */ new Map();
39951
+ function kbAuth(token) {
39952
+ return token ? gitAuthArgs(token, "") : [];
39953
+ }
39954
+ async function pullKb(dir2) {
39955
+ const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
39956
+ await execFileAsync("git", [...kbAuth(kbTokens.get(dir2)), "-C", dir2, "pull", "--ff-only"], { env: gitEnv });
39810
39957
  }
39811
39958
  async function ensureRepo(profileDir2, repo) {
39812
39959
  const dir2 = repoDir(profileDir2, repo.url, repo.branch);
39813
- if (existsSync10(join14(dir2, ".git"))) return dir2;
39814
- const url = authedUrl(repo.url, repo.token);
39815
39960
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
39961
+ if (repo.token) kbTokens.set(dir2, repo.token);
39962
+ else kbTokens.delete(dir2);
39963
+ if (existsSync10(join14(dir2, ".git"))) {
39964
+ const current2 = await execFileAsync("git", ["-C", dir2, "remote", "get-url", "origin"], { env: gitEnv }).then((r) => r.stdout.trim()).catch(() => repo.url);
39965
+ if (current2 !== repo.url) {
39966
+ await execFileAsync("git", ["-C", dir2, "remote", "set-url", "origin", repo.url], { env: gitEnv }).catch(() => void 0);
39967
+ }
39968
+ const marker = existsSync10(join14(dir2, ".git", "FETCH_HEAD")) ? join14(dir2, ".git", "FETCH_HEAD") : join14(dir2, ".git", "HEAD");
39969
+ if (Date.now() - statSync4(marker).mtimeMs > KB_STALE_MS) {
39970
+ void pullKb(dir2).catch((err) => logger.warn("support.kb.refresh.failed", { dir: dir2, error: String(err) }));
39971
+ }
39972
+ return dir2;
39973
+ }
39816
39974
  mkdirSync5(kbRoot(profileDir2), { recursive: true });
39817
- await execFileAsync("git", ["clone", "--depth", "1", "--branch", repo.branch, url, dir2], {
39975
+ await execFileAsync("git", [...kbAuth(repo.token), "clone", "--depth", "1", "--branch", repo.branch, repo.url, dir2], {
39818
39976
  env: gitEnv
39819
39977
  });
39820
39978
  return dir2;
@@ -39822,12 +39980,11 @@ async function ensureRepo(profileDir2, repo) {
39822
39980
  async function refreshSupportKb(profileDir2) {
39823
39981
  const root = kbRoot(profileDir2);
39824
39982
  if (!existsSync10(root)) return;
39825
- const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
39826
39983
  for (const entry of readdirSync5(root)) {
39827
39984
  const dir2 = join14(root, entry);
39828
39985
  if (!existsSync10(join14(dir2, ".git"))) continue;
39829
39986
  try {
39830
- await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
39987
+ await pullKb(dir2);
39831
39988
  } catch (err) {
39832
39989
  logger.warn("support.kb.refresh.failed", { dir: dir2, error: String(err) });
39833
39990
  }
@@ -39890,6 +40047,33 @@ function buildPrompt(docs, question, askerRole) {
39890
40047
  question
39891
40048
  ].join("\n");
39892
40049
  }
40050
+ function buildDraftPrompt(docs, context, task) {
40051
+ const goal = task === "reply_draft" ? [
40052
+ "You are drafting a reply FROM the 4PM support team TO the customer in the support ticket below.",
40053
+ "Address the customer's latest message, using the whole conversation for context. Be accurate,",
40054
+ "friendly and concise. Write in the language the customer uses in the ticket."
40055
+ ] : [
40056
+ "You are drafting an outreach message FROM the 4PM platform team TO the customer organizations.",
40057
+ "Use the subject and any current draft below as the starting point. Be clear, friendly and concise.",
40058
+ "Write in the language of the subject/draft (or of the admin's instructions when those are empty)."
40059
+ ];
40060
+ return [
40061
+ ...goal,
40062
+ "Use the documentation and FAQ below for any product facts; never invent features, prices or",
40063
+ "commitments that are not in the docs \u2014 leave a clear [placeholder] for the admin to fill instead.",
40064
+ "Follow the admin's instructions when given. A human admin reviews and edits your draft before",
40065
+ "sending it.",
40066
+ "",
40067
+ "Output ONLY the message body in markdown \u2014 no subject line, no preamble, no explanation, no code",
40068
+ "fences around the whole message.",
40069
+ "",
40070
+ "===== DOCUMENTATION =====",
40071
+ docs,
40072
+ "",
40073
+ "===== CONTEXT =====",
40074
+ context
40075
+ ].join("\n");
40076
+ }
39893
40077
  function imageExt(mime) {
39894
40078
  switch (mime) {
39895
40079
  case "image/png":
@@ -39904,9 +40088,8 @@ function imageExt(mime) {
39904
40088
  return "png";
39905
40089
  }
39906
40090
  }
39907
- function materializeImages2(profileDir2, images, question) {
39908
- const dir2 = join14(profileDir2, "help-images", randomUUID());
39909
- mkdirSync5(dir2, { recursive: true });
40091
+ function materializeImages2(images, question) {
40092
+ const dir2 = mkdtempSync2(join14(tmpdir3(), "4pm-help-images-"));
39910
40093
  let rewritten = question;
39911
40094
  images.forEach((img, i) => {
39912
40095
  const file = join14(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
@@ -39932,23 +40115,32 @@ function parseModeratedAnswer(text) {
39932
40115
  }
39933
40116
  return { body: text.trim() };
39934
40117
  }
39935
- function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir) {
39936
- return new Promise((resolve10, reject) => {
40118
+ function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir, denyDirs = []) {
40119
+ return new Promise((resolve11, reject) => {
39937
40120
  const isClaude = cmd.includes("claude");
39938
40121
  const args = [
39939
40122
  "-p",
39940
40123
  ...profile?.model ? ["--model", profile.model] : [],
39941
40124
  ...isClaude ? ["--output-format", "stream-json", "--verbose"] : [],
39942
- // When the question references pasted images (ADR-0273), allow the agent to Read them from the
39943
- // materialized folder (the question already carries their absolute paths).
39944
- ...isClaude && imageDir ? ["--add-dir", imageDir, "--allowedTools", "Read"] : []
40125
+ // No shell / edits / web for an untrusted question (ADR-0346). Reads need no grant inside the
40126
+ // (empty) working dir and any added dir; anywhere else they would need an approval a headless
40127
+ // run never gets.
40128
+ // Secret-path deny rules (ADR-0347) — reads are not confined to the working dir.
40129
+ ...denySettingsArgs(cmd, [...denyDirs, profile?.dir]),
40130
+ ...isClaude ? ["--disallowedTools", ...DENIED_TOOLS] : [],
40131
+ // When the question references pasted images (ADR-0273), add the materialized folder so the
40132
+ // agent can Read them (the question already carries their absolute paths) — scoped to it only.
40133
+ ...isClaude && imageDir ? ["--add-dir", imageDir] : []
39945
40134
  ];
39946
- const env3 = {
39947
- ...process.env,
39948
- ...extraEnv,
39949
- ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
40135
+ const env3 = agentEnv(extraEnv, profile ? { CLAUDE_CONFIG_DIR: profile.dir } : void 0);
40136
+ const cwd2 = mkdtempSync2(join14(tmpdir3(), "4pm-support-"));
40137
+ const cleanup = () => {
40138
+ try {
40139
+ rmSync5(cwd2, { recursive: true, force: true });
40140
+ } catch {
40141
+ }
39950
40142
  };
39951
- const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
40143
+ const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3, cwd: cwd2 });
39952
40144
  const parser = createAiStreamParser(cmd);
39953
40145
  let out = "";
39954
40146
  let err = "";
@@ -39961,12 +40153,14 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir) {
39961
40153
  child.stderr.on("data", (d) => err += d.toString());
39962
40154
  child.on("error", (e) => {
39963
40155
  clearTimeout(timer);
40156
+ cleanup();
39964
40157
  reject(e);
39965
40158
  });
39966
40159
  child.on("close", (code) => {
39967
40160
  clearTimeout(timer);
40161
+ cleanup();
39968
40162
  out += parser.flush();
39969
- resolve10({ code: code ?? -1, out, err, usage: parser.usage() });
40163
+ resolve11({ code: code ?? -1, out, err, usage: parser.usage() });
39970
40164
  });
39971
40165
  child.stdin.write(prompt);
39972
40166
  child.stdin.end();
@@ -39976,7 +40170,7 @@ async function runClaudeWithFailover(prompt, ai2, imageDir) {
39976
40170
  const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
39977
40171
  let lastReason = "no attempt";
39978
40172
  for (let i = 0; i < attempts.length; i++) {
39979
- const { code, out, err, usage } = await runClaudeOnce(ai2.cmd, attempts[i], prompt, ai2.env, imageDir);
40173
+ const { code, out, err, usage } = await runClaudeOnce(ai2.cmd, attempts[i], prompt, ai2.env, imageDir, ai2.profiles.map((p2) => p2.dir));
39980
40174
  if (code === 0 && out.trim()) return { text: out.trim(), usage };
39981
40175
  const combined = err || out;
39982
40176
  lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
@@ -39988,7 +40182,7 @@ async function runSupportAnswer(req, ai2, profileDir2) {
39988
40182
  let imageDir;
39989
40183
  let question = req.question;
39990
40184
  if (req.images?.length) {
39991
- const m2 = materializeImages2(profileDir2, req.images, req.question);
40185
+ const m2 = materializeImages2(req.images, req.question);
39992
40186
  imageDir = m2.dir;
39993
40187
  question = m2.question;
39994
40188
  }
@@ -39996,13 +40190,10 @@ async function runSupportAnswer(req, ai2, profileDir2) {
39996
40190
  const dir2 = await ensureRepo(profileDir2, req.repo);
39997
40191
  const docs = collectDocs(dir2);
39998
40192
  if (!docs.trim()) return { body: "", error: "KB repo has no documentation" };
39999
- const { text, usage } = await runClaudeWithFailover(
40000
- buildPrompt(docs, question, req.askerRole),
40001
- ai2,
40002
- imageDir
40003
- );
40193
+ const prompt = req.task ? buildDraftPrompt(docs, question, req.task) : buildPrompt(docs, question, req.askerRole);
40194
+ const { text, usage } = await runClaudeWithFailover(prompt, ai2, imageDir);
40004
40195
  if (!text) return { body: "", error: "empty answer" };
40005
- const { body, moderation } = parseModeratedAnswer(text);
40196
+ const { body, moderation } = req.task ? { body: text.trim(), moderation: void 0 } : parseModeratedAnswer(text);
40006
40197
  if (!body) return { body: "", error: "empty answer" };
40007
40198
  const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
40008
40199
  return {
@@ -40237,10 +40428,10 @@ async function crontabList() {
40237
40428
  }
40238
40429
  }
40239
40430
  async function crontabSet(content) {
40240
- return new Promise((resolve10) => {
40431
+ return new Promise((resolve11) => {
40241
40432
  const child = spawn5("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
40242
- child.on("error", () => resolve10(false));
40243
- child.on("close", (code) => resolve10(code === 0));
40433
+ child.on("error", () => resolve11(false));
40434
+ child.on("close", (code) => resolve11(code === 0));
40244
40435
  child.stdin.end(content.endsWith("\n") ? content : content + "\n");
40245
40436
  });
40246
40437
  }
@@ -40409,14 +40600,14 @@ function detectContainerized() {
40409
40600
  }
40410
40601
  }
40411
40602
  function canReach(host, port) {
40412
- return new Promise((resolve10) => {
40603
+ return new Promise((resolve11) => {
40413
40604
  const socket = connect({ host, port });
40414
40605
  let done = false;
40415
40606
  const finish = (ok) => {
40416
40607
  if (done) return;
40417
40608
  done = true;
40418
40609
  socket.destroy();
40419
- resolve10(ok);
40610
+ resolve11(ok);
40420
40611
  };
40421
40612
  socket.setTimeout(PROBE_TIMEOUT_MS);
40422
40613
  socket.once("connect", () => finish(true));
@@ -40588,7 +40779,7 @@ async function runCommand(dispatch, emit, opts) {
40588
40779
  });
40589
40780
  child.stdout?.on("data", (data) => batcher.push(data.toString("utf8")));
40590
40781
  child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
40591
- await new Promise((resolve10) => {
40782
+ await new Promise((resolve11) => {
40592
40783
  child.on("close", (exitCode) => {
40593
40784
  clearTimers();
40594
40785
  batcher.flush();
@@ -40601,7 +40792,7 @@ async function runCommand(dispatch, emit, opts) {
40601
40792
  // server settles the command as failed and the web surfaces the note instead of spinning.
40602
40793
  exitCode: timedOut ? 124 : exitCode ?? -1
40603
40794
  });
40604
- resolve10();
40795
+ resolve11();
40605
40796
  });
40606
40797
  child.on("error", (err) => {
40607
40798
  clearTimers();
@@ -40613,7 +40804,7 @@ async function runCommand(dispatch, emit, opts) {
40613
40804
  done: true,
40614
40805
  exitCode: -1
40615
40806
  });
40616
- resolve10();
40807
+ resolve11();
40617
40808
  });
40618
40809
  });
40619
40810
  }
@@ -40636,15 +40827,15 @@ function isOutOfCredits(text) {
40636
40827
  );
40637
40828
  }
40638
40829
  function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
40639
- return new Promise((resolve10) => {
40830
+ return new Promise((resolve11) => {
40640
40831
  runCommand(
40641
40832
  { commandId, cmd, args, path: cwd2, env: env3 },
40642
40833
  (out) => {
40643
40834
  if (out.chunk) onChunk(out.chunk);
40644
- if (out.done) resolve10(out.exitCode ?? -1);
40835
+ if (out.done) resolve11(out.exitCode ?? -1);
40645
40836
  },
40646
40837
  { stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
40647
- ).catch(() => resolve10(-1));
40838
+ ).catch(() => resolve11(-1));
40648
40839
  });
40649
40840
  }
40650
40841
  async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
@@ -41122,6 +41313,24 @@ function runSlashCommand(line, base) {
41122
41313
 
41123
41314
  // src/core/memory-compact.ts
41124
41315
  import { spawn as spawn7 } from "child_process";
41316
+ function parseJsonResult(out) {
41317
+ let parsed;
41318
+ try {
41319
+ parsed = JSON.parse(out.trim());
41320
+ } catch {
41321
+ return null;
41322
+ }
41323
+ if (parsed.is_error || typeof parsed.result !== "string") return null;
41324
+ const u2 = parsed.usage;
41325
+ const usage = u2 ? {
41326
+ input: u2.input_tokens ?? 0,
41327
+ output: u2.output_tokens ?? 0,
41328
+ cacheRead: u2.cache_read_input_tokens ?? 0,
41329
+ cacheCreation: u2.cache_creation_input_tokens ?? 0,
41330
+ tokens: (u2.input_tokens ?? 0) + (u2.output_tokens ?? 0) + (u2.cache_read_input_tokens ?? 0) + (u2.cache_creation_input_tokens ?? 0)
41331
+ } : null;
41332
+ return { text: parsed.result, usage };
41333
+ }
41125
41334
  var COMPACT_TIMEOUT_MS = 6e4;
41126
41335
  function buildPrompt2(input) {
41127
41336
  return [
@@ -41141,9 +41350,21 @@ function buildPrompt2(input) {
41141
41350
  input.answer
41142
41351
  ].join("\n");
41143
41352
  }
41144
- function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
41145
- return new Promise((resolve10, reject) => {
41146
- const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
41353
+ function runOnce(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
41354
+ return new Promise((resolve11, reject) => {
41355
+ const args = [
41356
+ "-p",
41357
+ "--output-format",
41358
+ "json",
41359
+ ...profile?.model ? ["--model", profile.model] : [],
41360
+ // Secret-path deny rules (ADR-0347) — before the variadic `--disallowedTools`.
41361
+ ...denySettingsArgs(cmd, [...denyDirs, profile?.dir]),
41362
+ "--max-turns=1",
41363
+ "--disallowedTools",
41364
+ ONE_SHOT_DISALLOWED_CLAUDE_TOOLS,
41365
+ "--permission-mode",
41366
+ "default"
41367
+ ];
41147
41368
  const env3 = {
41148
41369
  ...process.env,
41149
41370
  ...extraEnv,
@@ -41165,7 +41386,7 @@ function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
41165
41386
  });
41166
41387
  child.on("close", (code) => {
41167
41388
  clearTimeout(timer);
41168
- resolve10({ code: code ?? -1, out, err });
41389
+ resolve11({ code: code ?? -1, out, err });
41169
41390
  });
41170
41391
  child.stdin.write(prompt);
41171
41392
  child.stdin.end();
@@ -41176,17 +41397,19 @@ async function runMemoryCompaction(ai2, cwd2, input) {
41176
41397
  const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
41177
41398
  for (let i = 0; i < attempts.length; i++) {
41178
41399
  try {
41179
- const { code, out } = await runOnce(ai2.cmd, attempts[i], prompt, cwd2, ai2.env);
41180
- if (code === 0 && out.trim()) {
41400
+ const profile = attempts[i];
41401
+ const { code, out } = await runOnce(ai2.cmd, profile, prompt, cwd2, ai2.env, ai2.profiles.map((p2) => p2.dir));
41402
+ const parsed = code === 0 ? parseJsonResult(out) : null;
41403
+ if (parsed && parsed.text.trim()) {
41181
41404
  reportToolResult(ai2.cmd, true);
41182
- return out.trim().slice(0, input.budgetChars);
41405
+ return { memory: parsed.text.trim().slice(0, input.budgetChars), usage: parsed.usage, dir: profile?.dir ?? null };
41183
41406
  }
41184
41407
  if (i === attempts.length - 1) break;
41185
41408
  } catch {
41186
41409
  break;
41187
41410
  }
41188
41411
  }
41189
- return "";
41412
+ return { memory: "", usage: null, dir: null };
41190
41413
  }
41191
41414
 
41192
41415
  // src/core/git-commit-identity.ts
@@ -41361,6 +41584,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
41361
41584
  const guardedPrompt = ctx.restrictToFolder && ctx.physicRoot ? folderScopeGuard(ctx.physicRoot, aiBodyPrompt) : aiBodyPrompt;
41362
41585
  const hint = unified ? { credential: startCred ?? workingCred } : { dir: startDir ?? workingDir };
41363
41586
  const memCfg = resolveMemoryConfig(config);
41587
+ pruneResumableSessions(ctx, resolveResumePolicy(config), Date.now());
41364
41588
  const firstAttempt = planAiRun(guardedPrompt, config, hint, /* @__PURE__ */ new Map(), oneShot, aiConfig, readOnly, bypass).attempts[0];
41365
41589
  const resumeId = memCfg.enabled && firstAttempt?.key && firstAttempt.cmd === "claude" ? ctx.sessionIdByKey.get(firstAttempt.key) : void 0;
41366
41590
  let effectivePrompt = guardedPrompt;
@@ -41508,6 +41732,8 @@ ${guardedPrompt}`;
41508
41732
  outputTokens: result.usage.output,
41509
41733
  cacheReadTokens: result.usage.cacheRead,
41510
41734
  cacheCreationTokens: result.usage.cacheCreation,
41735
+ // Provider → the server's per-provider quota-token weights (ADR-0340).
41736
+ provider: aiProviderOf(result.workedCmd ?? plan.cmd),
41511
41737
  // Auth mode for billing split (ADR-0192 §5): subscription (OAuth) vs api-key (API-billed).
41512
41738
  authMode: resolveClaudeAuthMode(config)
41513
41739
  });
@@ -41525,9 +41751,23 @@ ${guardedPrompt}`;
41525
41751
  if (memCfg.enabled && result.exitCode === 0) {
41526
41752
  if (result.workedKey && result.sessionId) {
41527
41753
  ctx.sessionIdByKey.set(result.workedKey, result.sessionId);
41754
+ ctx.sessionMetaByKey.set(result.workedKey, { at: Date.now(), contextTokens: result.usage.contextTokens ?? 0 });
41528
41755
  }
41529
41756
  void updateSharedMemory(ctx, config, prompt, answerText, memCfg.budgetChars).catch(() => {
41530
41757
  });
41758
+ } else if (result.exitCode !== 0) {
41759
+ ctx.sessionIdByKey.clear();
41760
+ ctx.sessionMetaByKey.clear();
41761
+ }
41762
+ }
41763
+ function pruneResumableSessions(ctx, policy, now) {
41764
+ for (const key of [...ctx.sessionIdByKey.keys()]) {
41765
+ const meta = ctx.sessionMetaByKey.get(key);
41766
+ const stale = !meta || now - meta.at > policy.maxIdleMs || meta.contextTokens > policy.maxContextTokens;
41767
+ if (stale) {
41768
+ ctx.sessionIdByKey.delete(key);
41769
+ ctx.sessionMetaByKey.delete(key);
41770
+ }
41531
41771
  }
41532
41772
  }
41533
41773
  async function runWebSlashCommand(ctx, line, commandId) {
@@ -41635,11 +41875,29 @@ async function updateSharedMemory(ctx, config, userPrompt, answer, budgetChars)
41635
41875
  if (!answer.trim()) return;
41636
41876
  const profiles = resolveClaudeProfiles(config, ctx.physicRoot);
41637
41877
  const cwd2 = ctx.physicRoot ?? process.cwd();
41638
- const newMemory = await runMemoryCompaction(
41878
+ const { memory: newMemory, usage, dir: dir2 } = await runMemoryCompaction(
41639
41879
  { cmd: "claude", profiles, env: config.aiEnv },
41640
41880
  cwd2,
41641
41881
  { oldMemory: ctx.aiMemory, prompt: userPrompt, answer, budgetChars }
41642
41882
  );
41883
+ if (usage && usage.tokens > 0) {
41884
+ ctx.reportUsage(
41885
+ [
41886
+ {
41887
+ metric: UsageMetric.AI_TOKENS,
41888
+ amount: usage.tokens,
41889
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
41890
+ inputTokens: usage.input,
41891
+ outputTokens: usage.output,
41892
+ cacheReadTokens: usage.cacheRead,
41893
+ cacheCreationTokens: usage.cacheCreation,
41894
+ provider: "claude",
41895
+ authMode: resolveClaudeAuthMode(config)
41896
+ }
41897
+ ],
41898
+ dir2 ? profileDisplayLabel(dir2) : null
41899
+ );
41900
+ }
41643
41901
  if (!newMemory) return;
41644
41902
  ctx.aiMemory = newMemory;
41645
41903
  ctx.send(WsChannels.MEMORY_UPDATE, { text: newMemory });
@@ -41647,6 +41905,7 @@ async function updateSharedMemory(ctx, config, userPrompt, answer, budgetChars)
41647
41905
  function resetMemorySession(ctx) {
41648
41906
  ctx.aiMemory = "";
41649
41907
  ctx.sessionIdByKey.clear();
41908
+ ctx.sessionMetaByKey.clear();
41650
41909
  ctx.send(WsChannels.MEMORY_UPDATE, { text: "" });
41651
41910
  }
41652
41911
  function profileUnderSessionPressure(ctx, config, cmd, workingDir) {
@@ -41719,7 +41978,7 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
41719
41978
  }
41720
41979
 
41721
41980
  // src/core/autonomous-cycle.ts
41722
- import { randomUUID as randomUUID2 } from "crypto";
41981
+ import { randomUUID } from "crypto";
41723
41982
  import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile5, readdir as readdir2, rm as rm2, stat } from "fs/promises";
41724
41983
  import { join as join20 } from "path";
41725
41984
  function buildAutonomousCyclePrompt() {
@@ -41859,7 +42118,7 @@ async function runAutonomousCycle(ctx) {
41859
42118
  hist.ticks = { day: today, count: todayTickCount(hist, today) + 1 };
41860
42119
  await writeHistories(root, hist);
41861
42120
  await log(`[run] cycle (tick ${hist.ticks.count}/${today})`);
41862
- const commandId = randomUUID2();
42121
+ const commandId = randomUUID();
41863
42122
  const override = cfg.model ? { model: cfg.model } : void 0;
41864
42123
  try {
41865
42124
  await runAiPrompt(ctx, buildAutonomousCyclePrompt(), commandId, "local", false, void 0, override, false, true);
@@ -41963,11 +42222,11 @@ async function allAiProfilesExhausted(ctx, cfg) {
41963
42222
 
41964
42223
  // src/core/fs-browse.ts
41965
42224
  import { readdir as readdir3 } from "fs/promises";
41966
- import { resolve as resolve2, sep as sep2 } from "path";
42225
+ import { resolve as resolve3, sep as sep2 } from "path";
41967
42226
  async function listDir(path, root) {
41968
42227
  if (!root) return { path: "", entries: [] };
41969
- const base = resolve2(root);
41970
- const requested = resolve2(base, path && path.trim() ? path : ".");
42228
+ const base = resolve3(root);
42229
+ const requested = resolve3(base, path && path.trim() ? path : ".");
41971
42230
  const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
41972
42231
  try {
41973
42232
  const dirents = await readdir3(target, { withFileTypes: true });
@@ -41986,12 +42245,12 @@ async function listDir(path, root) {
41986
42245
 
41987
42246
  // src/core/fs-read.ts
41988
42247
  import { readFile as readFile6 } from "fs/promises";
41989
- import { resolve as resolve3, sep as sep3 } from "path";
42248
+ import { resolve as resolve4, sep as sep3 } from "path";
41990
42249
  var MAX_BYTES = 256 * 1024;
41991
42250
  function resolveInRoot(root, relPath) {
41992
42251
  if (!root) return null;
41993
- const base = resolve3(root);
41994
- const target = resolve3(base, relPath || ".");
42252
+ const base = resolve4(root);
42253
+ const target = resolve4(base, relPath || ".");
41995
42254
  return target === base || target.startsWith(base + sep3) ? target : null;
41996
42255
  }
41997
42256
  async function readWorkerFile(root, path) {
@@ -42011,12 +42270,12 @@ async function readWorkerFile(root, path) {
42011
42270
 
42012
42271
  // src/core/fs-write.ts
42013
42272
  import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
42014
- import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
42273
+ import { dirname as dirname4, resolve as resolve5, sep as sep4 } from "path";
42015
42274
  var MAX_BYTES2 = 2 * 1024 * 1024;
42016
42275
  function resolveInRoot2(root, relPath) {
42017
42276
  if (!root) return null;
42018
- const base = resolve4(root);
42019
- const target = resolve4(base, relPath || ".");
42277
+ const base = resolve5(root);
42278
+ const target = resolve5(base, relPath || ".");
42020
42279
  return target === base || target.startsWith(base + sep4) ? target : null;
42021
42280
  }
42022
42281
  async function writeWorkerFile(root, relPath, content) {
@@ -42039,11 +42298,11 @@ async function writeWorkerFile(root, relPath, content) {
42039
42298
 
42040
42299
  // src/core/fs-mutate.ts
42041
42300
  import { mkdir as mkdir5, writeFile as writeFile6, rename, rm as rm3, stat as stat2 } from "fs/promises";
42042
- import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
42301
+ import { basename as basename3, dirname as dirname5, resolve as resolve6, sep as sep5 } from "path";
42043
42302
  var MAX_BYTES3 = 2 * 1024 * 1024;
42044
42303
  function resolveInRoot3(root, rel) {
42045
- const base = resolve5(root);
42046
- const target = resolve5(base, rel || ".");
42304
+ const base = resolve6(root);
42305
+ const target = resolve6(base, rel || ".");
42047
42306
  return target === base || target.startsWith(base + sep5) ? target : null;
42048
42307
  }
42049
42308
  async function isDir(p2) {
@@ -42068,13 +42327,13 @@ async function mutateFs(root, req) {
42068
42327
  switch (req.op) {
42069
42328
  case "mkdir": {
42070
42329
  const target = resolveInRoot3(root, req.path ?? "");
42071
- if (!target || target === resolve5(root)) return escaped(req.path ?? "");
42330
+ if (!target || target === resolve6(root)) return escaped(req.path ?? "");
42072
42331
  await mkdir5(target, { recursive: true });
42073
42332
  return { ok: true, path: target };
42074
42333
  }
42075
42334
  case "create": {
42076
42335
  const target = resolveInRoot3(root, req.path ?? "");
42077
- if (!target || target === resolve5(root)) return escaped(req.path ?? "");
42336
+ if (!target || target === resolve6(root)) return escaped(req.path ?? "");
42078
42337
  if (await exists(target)) return { ok: false, path: target, error: "already exists" };
42079
42338
  const content = req.content ?? "";
42080
42339
  if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
@@ -42088,9 +42347,9 @@ async function mutateFs(root, req) {
42088
42347
  const from = resolveInRoot3(root, req.from ?? "");
42089
42348
  let to2 = resolveInRoot3(root, req.to ?? "");
42090
42349
  if (!from || !to2) return escaped(req.from ?? req.to ?? "");
42091
- if (from === resolve5(root)) return { ok: false, path: from, error: "cannot move the project root" };
42092
- if (await isDir(to2)) to2 = resolve5(to2, basename3(from));
42093
- if (to2 !== resolve5(root) && !to2.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
42350
+ if (from === resolve6(root)) return { ok: false, path: from, error: "cannot move the project root" };
42351
+ if (await isDir(to2)) to2 = resolve6(to2, basename3(from));
42352
+ if (to2 !== resolve6(root) && !to2.startsWith(resolve6(root) + sep5)) return escaped(req.to ?? "");
42094
42353
  if (await exists(to2)) return { ok: false, path: to2, error: "destination already exists" };
42095
42354
  await mkdir5(dirname5(to2), { recursive: true });
42096
42355
  await rename(from, to2);
@@ -42099,7 +42358,7 @@ async function mutateFs(root, req) {
42099
42358
  case "delete": {
42100
42359
  const target = resolveInRoot3(root, req.path ?? "");
42101
42360
  if (!target) return escaped(req.path ?? "");
42102
- if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
42361
+ if (target === resolve6(root)) return { ok: false, path: target, error: "cannot delete the project root" };
42103
42362
  await rm3(target, { recursive: true, force: true });
42104
42363
  return { ok: true, path: target };
42105
42364
  }
@@ -42113,11 +42372,11 @@ async function mutateFs(root, req) {
42113
42372
 
42114
42373
  // src/core/fs-transfer.ts
42115
42374
  import { mkdir as mkdir6, readFile as readFile7, stat as stat3, writeFile as writeFile7 } from "fs/promises";
42116
- import { basename as basename4, dirname as dirname6, extname, resolve as resolve6, sep as sep6 } from "path";
42375
+ import { basename as basename4, dirname as dirname6, extname, resolve as resolve7, sep as sep6 } from "path";
42117
42376
  function resolveInRoot4(root, relPath) {
42118
42377
  if (!root) return null;
42119
- const base = resolve6(root);
42120
- const target = resolve6(base, relPath || ".");
42378
+ const base = resolve7(root);
42379
+ const target = resolve7(base, relPath || ".");
42121
42380
  return target === base || target.startsWith(base + sep6) ? target : null;
42122
42381
  }
42123
42382
  var MIME_BY_EXT = {
@@ -42233,13 +42492,13 @@ function handleFsChannels(ctx, message, payload) {
42233
42492
  // src/core/git-diff.ts
42234
42493
  import { readFile as readFile8 } from "fs/promises";
42235
42494
  import { execFile as execFile6 } from "child_process";
42236
- import { basename as basename5, dirname as dirname7, resolve as resolve7, sep as sep7 } from "path";
42495
+ import { basename as basename5, dirname as dirname7, resolve as resolve8, sep as sep7 } from "path";
42237
42496
  import { promisify as promisify6 } from "util";
42238
42497
  var run6 = promisify6(execFile6);
42239
42498
  function resolveInRoot5(root, relPath) {
42240
42499
  if (!root) return null;
42241
- const base = resolve7(root);
42242
- const target = resolve7(base, relPath || ".");
42500
+ const base = resolve8(root);
42501
+ const target = resolve8(base, relPath || ".");
42243
42502
  return target === base || target.startsWith(base + sep7) ? target : null;
42244
42503
  }
42245
42504
  function looksBinary(buf) {
@@ -42393,15 +42652,15 @@ async function manageSshKey(op, profileDir2) {
42393
42652
 
42394
42653
  // src/core/git-history.ts
42395
42654
  import { execFile as execFile9 } from "child_process";
42396
- import { resolve as resolve8, sep as sep8 } from "path";
42655
+ import { resolve as resolve9, sep as sep8 } from "path";
42397
42656
  import { promisify as promisify9 } from "util";
42398
42657
  var run9 = promisify9(execFile9);
42399
42658
  var FS = "";
42400
42659
  var RS = "";
42401
42660
  function resolveRepo(root, repo) {
42402
42661
  if (!root) return null;
42403
- const base = resolve8(root);
42404
- const requested = resolve8(base, repo && repo.trim() ? repo : ".");
42662
+ const base = resolve9(root);
42663
+ const requested = resolve9(base, repo && repo.trim() ? repo : ".");
42405
42664
  return requested === base || requested.startsWith(base + sep8) ? requested : base;
42406
42665
  }
42407
42666
  async function git(cwd2, args) {
@@ -42418,7 +42677,7 @@ async function git(cwd2, args) {
42418
42677
  }
42419
42678
  async function gitRepos(root) {
42420
42679
  if (!root) return { repos: [] };
42421
- const base = resolve8(root);
42680
+ const base = resolve9(root);
42422
42681
  const top = (await git(base, ["rev-parse", "--show-toplevel"])).trim();
42423
42682
  if (!top || !(top === base || top.startsWith(base + sep8))) return { repos: [] };
42424
42683
  const repos = [];
@@ -42430,7 +42689,7 @@ async function gitRepos(root) {
42430
42689
  const m2 = line.trim().match(/^[-+U ]?[0-9a-f]{7,40}\s+(\S+)/);
42431
42690
  const subPath = m2?.[1];
42432
42691
  if (!subPath) continue;
42433
- const abs = resolve8(base, subPath);
42692
+ const abs = resolve9(base, subPath);
42434
42693
  if (!(abs === base || abs.startsWith(base + sep8))) continue;
42435
42694
  const remote = (await git(abs, ["remote", "get-url", "origin"])).trim();
42436
42695
  const branch = (await git(abs, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
@@ -42752,9 +43011,9 @@ Extra focus from the author: ${req.prompt}` : "";
42752
43011
  "Output ONLY the Markdown article, no preamble or code fences around the whole thing." + focus
42753
43012
  ].join("\n");
42754
43013
  }
42755
- function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
42756
- return new Promise((resolve10, reject) => {
42757
- const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
43014
+ function runOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
43015
+ return new Promise((resolve11, reject) => {
43016
+ const args = ["-p", ...profile?.model ? ["--model", profile.model] : [], ...denySettingsArgs(cmd, [...denyDirs, profile?.dir])];
42758
43017
  const env3 = {
42759
43018
  ...process.env,
42760
43019
  ...extraEnv,
@@ -42776,7 +43035,7 @@ function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
42776
43035
  });
42777
43036
  child.on("close", (code) => {
42778
43037
  clearTimeout(timer);
42779
- resolve10({ code: code ?? -1, out, err });
43038
+ resolve11({ code: code ?? -1, out, err });
42780
43039
  });
42781
43040
  child.stdin.write(prompt);
42782
43041
  child.stdin.end();
@@ -42788,7 +43047,7 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
42788
43047
  let lastReason = "no attempt";
42789
43048
  for (let i = 0; i < attempts.length; i++) {
42790
43049
  try {
42791
- const { code, out, err } = await runOnce2(ai2.cmd, attempts[i], prompt, physicRoot, ai2.env);
43050
+ const { code, out, err } = await runOnce2(ai2.cmd, attempts[i], prompt, physicRoot, ai2.env, ai2.profiles.map((p2) => p2.dir));
42792
43051
  if (code === 0 && out.trim()) {
42793
43052
  reportToolResult(ai2.cmd, true);
42794
43053
  return { bodyMarkdown: out.trim() };
@@ -42808,17 +43067,14 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
42808
43067
  // src/core/faq-compose.ts
42809
43068
  import { spawn as spawn9, execFile as execFile10 } from "child_process";
42810
43069
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, rmSync as rmSync7 } from "fs";
42811
- import { randomUUID as randomUUID3 } from "crypto";
43070
+ import { randomUUID as randomUUID2 } from "crypto";
42812
43071
  import { join as join24 } from "path";
42813
- import { tmpdir as tmpdir3 } from "os";
43072
+ import { tmpdir as tmpdir4 } from "os";
42814
43073
  import { promisify as promisify10 } from "util";
42815
43074
  var execFileAsync2 = promisify10(execFile10);
42816
43075
  var NO_USAGE2 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
42817
43076
  var COMPOSE_TIMEOUT_MS2 = 10 * 60 * 1e3;
42818
- function authedUrl2(url, token) {
42819
- if (url.startsWith("https://")) return url.replace("https://", `https://x-access-token:${token}@`);
42820
- return url;
42821
- }
43077
+ var AGENT_DENIED_TOOLS = ["Bash", "WebFetch", "WebSearch", "NotebookEdit", "Task"];
42822
43078
  function buildPrompt4(tickets, customPrompt) {
42823
43079
  const transcript = tickets.map((t2) => {
42824
43080
  const thread = t2.messages.map((m2) => ` [${m2.author}] ${m2.body}`).join("\n");
@@ -42879,22 +43135,22 @@ function parsePerTicket(text) {
42879
43135
  }
42880
43136
  return [];
42881
43137
  }
42882
- function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv) {
42883
- return new Promise((resolve10, reject) => {
43138
+ function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
43139
+ return new Promise((resolve11, reject) => {
42884
43140
  const isClaude = cmd.includes("claude");
42885
43141
  const args = [
42886
43142
  "-p",
42887
43143
  ...profile?.model ? ["--model", profile.model] : [],
42888
43144
  ...isClaude ? ["--output-format", "stream-json", "--verbose"] : [],
42889
- // Write-capable headless agent (ADR-0271): auto-approve tools so it never stalls on a prompt.
42890
- ...isClaude ? ["--permission-mode", "bypassPermissions"] : [],
42891
- ...cmd.includes("codex") ? ["--dangerously-bypass-approvals-and-sandbox"] : []
43145
+ // Secret-path deny rules (ADR-0347): reads are not confined to the clone, so the credential
43146
+ // dirs / cli profile / ssh / gh / git credentials are explicitly denied.
43147
+ ...denySettingsArgs(cmd, [...denyDirs, profile?.dir]),
43148
+ // Edit-only headless agent (ADR-0346): file edits are auto-accepted, everything else (Bash, web)
43149
+ // is denied, since the prompt carries untrusted ticket text. git/PR are done by this module.
43150
+ ...isClaude ? ["--permission-mode", "acceptEdits", "--disallowedTools", ...AGENT_DENIED_TOOLS] : [],
43151
+ ...cmd.includes("codex") ? ["--sandbox", "workspace-write"] : []
42892
43152
  ];
42893
- const env3 = {
42894
- ...process.env,
42895
- ...extraEnv,
42896
- ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
42897
- };
43153
+ const env3 = agentEnv(extraEnv, profile ? { CLAUDE_CONFIG_DIR: profile.dir } : void 0);
42898
43154
  const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
42899
43155
  const parser = createAiStreamParser(cmd);
42900
43156
  let out = "";
@@ -42913,7 +43169,7 @@ function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv) {
42913
43169
  child.on("close", (code) => {
42914
43170
  clearTimeout(timer);
42915
43171
  out += parser.flush();
42916
- resolve10({ code: code ?? -1, out, err, usage: parser.usage() });
43172
+ resolve11({ code: code ?? -1, out, err, usage: parser.usage() });
42917
43173
  });
42918
43174
  child.stdin.write(prompt);
42919
43175
  child.stdin.end();
@@ -42923,7 +43179,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
42923
43179
  const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
42924
43180
  let lastReason = "no attempt";
42925
43181
  for (let i = 0; i < attempts.length; i++) {
42926
- const { code, out, err, usage } = await runClaudeOnce2(ai2.cmd, attempts[i], prompt, cwd2, ai2.env);
43182
+ const { code, out, err, usage } = await runClaudeOnce2(ai2.cmd, attempts[i], prompt, cwd2, ai2.env, ai2.profiles.map((p2) => p2.dir));
42927
43183
  if (code === 0) return { usage, text: out.trim() };
42928
43184
  lastReason = `exited ${code}: ${(err || out).slice(0, 500)}`;
42929
43185
  if (i === attempts.length - 1) break;
@@ -42933,12 +43189,12 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
42933
43189
  var MAX_OUTPUT_CHARS = 8e3;
42934
43190
  async function runFaqCompose(req, ai2) {
42935
43191
  if (!req.repo.token) return { prUrl: "", error: "no write token" };
42936
- const workDir = join24(tmpdir3(), "4pm-faq-sync", randomUUID3());
43192
+ const workDir = join24(tmpdir4(), "4pm-faq-sync", randomUUID2());
42937
43193
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
42938
- const authed = authedUrl2(req.repo.url, req.repo.token);
43194
+ const auth = gitAuthArgs("x-access-token", req.repo.token);
42939
43195
  try {
42940
43196
  mkdirSync7(workDir, { recursive: true });
42941
- await execFileAsync2("git", ["clone", "--depth", "1", "--branch", req.repo.branch, authed, workDir], { env: gitEnv });
43197
+ await execFileAsync2("git", [...auth, "clone", "--depth", "1", "--branch", req.repo.branch, req.repo.url, workDir], { env: gitEnv });
42942
43198
  await execFileAsync2("git", ["-C", workDir, "config", "user.name", "4PM FAQ Bot"], { env: gitEnv });
42943
43199
  await execFileAsync2("git", ["-C", workDir, "config", "user.email", "faq-bot@4pm.app"], { env: gitEnv });
42944
43200
  await execFileAsync2("git", ["-C", workDir, "checkout", "-b", req.headBranch], { env: gitEnv });
@@ -42951,7 +43207,7 @@ async function runFaqCompose(req, ai2) {
42951
43207
  return { prUrl: "", output, perTicket, tokens: usage.tokens || estimateTokens2(text), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
42952
43208
  }
42953
43209
  await execFileAsync2("git", ["-C", workDir, "commit", "-m", "docs(faq): synthesize from support tickets (4PM)"], { env: gitEnv });
42954
- await execFileAsync2("git", ["-C", workDir, "push", "-u", "origin", req.headBranch], { env: gitEnv });
43210
+ await execFileAsync2("git", [...auth, "-C", workDir, "push", "-u", "origin", req.headBranch], { env: gitEnv });
42955
43211
  let prUrl = "";
42956
43212
  try {
42957
43213
  const pr2 = await execFileAsync2(
@@ -43533,7 +43789,7 @@ async function removePackage(root, slug) {
43533
43789
 
43534
43790
  // src/core/graph.ts
43535
43791
  import { readFile as readFile14, readdir as readdir6 } from "fs/promises";
43536
- import { join as join29, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
43792
+ import { join as join29, relative as relative2, dirname as dirname10, resolve as resolve10, extname as extname2, basename as basename6 } from "path";
43537
43793
  var MAX_FILES = 2e3;
43538
43794
  var MAX_NODES = 3e3;
43539
43795
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -43588,7 +43844,7 @@ async function buildDocs(root) {
43588
43844
  for (const m2 of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
43589
43845
  const href = (m2[1] ?? "").split("#")[0].trim();
43590
43846
  if (!href || !href.endsWith(".md") || href.startsWith("http")) continue;
43591
- const target = relative2(root, resolve9(dirname10(f2), href)).split("\\").join("/");
43847
+ const target = relative2(root, resolve10(dirname10(f2), href)).split("\\").join("/");
43592
43848
  if (ids.has(target) && target !== rel) {
43593
43849
  const key = `${rel}->${target}`;
43594
43850
  if (!seen.has(key)) {
@@ -44220,6 +44476,7 @@ var WsClient = class {
44220
44476
  */
44221
44477
  aiMemory = "";
44222
44478
  sessionIdByKey = /* @__PURE__ */ new Map();
44479
+ sessionMetaByKey = /* @__PURE__ */ new Map();
44223
44480
  stopped = false;
44224
44481
  /** Resolver to wake the backoff sleep early (set while waiting) — for /reconnect. */
44225
44482
  wakeReconnect = null;
@@ -44313,6 +44570,7 @@ var WsClient = class {
44313
44570
  serverUrl: this.context.credential.serverUrl,
44314
44571
  startedAtMs: this.startedAtMs,
44315
44572
  sessionIdByKey: this.sessionIdByKey,
44573
+ sessionMetaByKey: this.sessionMetaByKey,
44316
44574
  get machineUsername() {
44317
44575
  return self.machineUsername;
44318
44576
  },
@@ -44425,14 +44683,14 @@ var WsClient = class {
44425
44683
  */
44426
44684
  awaitConnected(timeoutMs) {
44427
44685
  if (this.sessionKey && this.bus.status === "connected") return Promise.resolve(true);
44428
- return new Promise((resolve10) => {
44686
+ return new Promise((resolve11) => {
44429
44687
  const waiter = () => {
44430
44688
  clearTimeout(timer);
44431
- resolve10(true);
44689
+ resolve11(true);
44432
44690
  };
44433
44691
  const timer = setTimeout(() => {
44434
44692
  this.connectWaiters = this.connectWaiters.filter((w2) => w2 !== waiter);
44435
- resolve10(false);
44693
+ resolve11(false);
44436
44694
  }, timeoutMs);
44437
44695
  this.connectWaiters.push(waiter);
44438
44696
  });
@@ -44459,16 +44717,16 @@ var WsClient = class {
44459
44717
  this.forceReconnect = false;
44460
44718
  return Promise.resolve();
44461
44719
  }
44462
- return new Promise((resolve10) => {
44720
+ return new Promise((resolve11) => {
44463
44721
  const timer = setTimeout(() => {
44464
44722
  this.wakeReconnect = null;
44465
- resolve10();
44723
+ resolve11();
44466
44724
  }, ms2);
44467
44725
  this.wakeReconnect = () => {
44468
44726
  clearTimeout(timer);
44469
44727
  this.wakeReconnect = null;
44470
44728
  this.forceReconnect = false;
44471
- resolve10();
44729
+ resolve11();
44472
44730
  };
44473
44731
  });
44474
44732
  }
@@ -44522,7 +44780,7 @@ var WsClient = class {
44522
44780
  * One WS session: token → connect → handshake → handle messages until close.
44523
44781
  */
44524
44782
  connectOnce() {
44525
- return new Promise((resolve10, reject) => {
44783
+ return new Promise((resolve11, reject) => {
44526
44784
  void (async () => {
44527
44785
  this.sessionEstablished = false;
44528
44786
  this.bus.setStatus("connecting");
@@ -44618,7 +44876,7 @@ var WsClient = class {
44618
44876
  if (code === CLOSE_DRAINING) {
44619
44877
  this.bus.log(t("error.serverDraining"));
44620
44878
  }
44621
- resolve10();
44879
+ resolve11();
44622
44880
  });
44623
44881
  socket.on("error", (err) => {
44624
44882
  this.clearHeartbeat();
@@ -44730,7 +44988,7 @@ var WsClient = class {
44730
44988
  }
44731
44989
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
44732
44990
  const envelope = {
44733
- id: randomUUID4(),
44991
+ id: randomUUID3(),
44734
44992
  replyTo,
44735
44993
  channel,
44736
44994
  wsToken: this.wsToken,
@@ -44744,14 +45002,14 @@ var WsClient = class {
44744
45002
  * Used for quota.check before spawning an AI cli (ADR-0020).
44745
45003
  */
44746
45004
  request(channel, data) {
44747
- return new Promise((resolve10, reject) => {
45005
+ return new Promise((resolve11, reject) => {
44748
45006
  if (!this.socket || !this.sessionKey) {
44749
45007
  reject(new Error("Not connected to the server."));
44750
45008
  return;
44751
45009
  }
44752
45010
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
44753
45011
  const envelope = {
44754
- id: randomUUID4(),
45012
+ id: randomUUID3(),
44755
45013
  replyTo: null,
44756
45014
  channel,
44757
45015
  wsToken: this.wsToken,
@@ -44764,7 +45022,7 @@ var WsClient = class {
44764
45022
  }, 15e3);
44765
45023
  timer.unref?.();
44766
45024
  this.pending.set(envelope.id, {
44767
- resolve: (value) => resolve10(value),
45025
+ resolve: (value) => resolve11(value),
44768
45026
  timer
44769
45027
  });
44770
45028
  this.socket.send(JSON.stringify(envelope));
@@ -44854,7 +45112,10 @@ var WsClient = class {
44854
45112
  // The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
44855
45113
  // effective timeout and derive the SSE reply windows server-side (ADR-0256).
44856
45114
  aiRunTimeoutSec: config.aiRunTimeoutSec ?? 0,
44857
- aiProfileCount
45115
+ aiProfileCount,
45116
+ // The configured AI accounts (emails when readable) — admin-only visibility of which AI
45117
+ // account a (rented) machine runs on + the shared-account check (ADR-0354).
45118
+ aiAccounts: aiAccountLabels(config)
44858
45119
  });
44859
45120
  }
44860
45121
  /** Heartbeat every 30s: ping + periodically re-report machine.status. */
@@ -45140,7 +45401,7 @@ function releaseInstanceLock(profileDir2) {
45140
45401
 
45141
45402
  // src/core/session-bus.ts
45142
45403
  import { EventEmitter } from "events";
45143
- import { randomUUID as randomUUID5 } from "crypto";
45404
+ import { randomUUID as randomUUID4 } from "crypto";
45144
45405
  var REPLAY_CAP = 500;
45145
45406
  var SessionBus = class {
45146
45407
  emitter = new EventEmitter();
@@ -45171,7 +45432,7 @@ var SessionBus = class {
45171
45432
  /** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
45172
45433
  push(input) {
45173
45434
  const entry = {
45174
- id: randomUUID5(),
45435
+ id: randomUUID4(),
45175
45436
  level: "info",
45176
45437
  ...input,
45177
45438
  ts: Date.now()
@@ -50875,8 +51136,8 @@ var Ink = class {
50875
51136
  }
50876
51137
  }
50877
51138
  async waitUntilExit() {
50878
- this.exitPromise ||= new Promise((resolve10, reject) => {
50879
- this.resolveExitPromise = resolve10;
51139
+ this.exitPromise ||= new Promise((resolve11, reject) => {
51140
+ this.resolveExitPromise = resolve11;
50880
51141
  this.rejectExitPromise = reject;
50881
51142
  });
50882
51143
  return this.exitPromise;
@@ -51929,7 +52190,7 @@ async function runStart(profileDir2, profileName) {
51929
52190
  logger.error("ws.run.crash", { message: e.message, stack: e.stack });
51930
52191
  if (client.isStopped) return;
51931
52192
  bus.log(t("start.recovered", { message: e.message }), "error");
51932
- await new Promise((resolve10) => setTimeout(resolve10, 1e3));
52193
+ await new Promise((resolve11) => setTimeout(resolve11, 1e3));
51933
52194
  }
51934
52195
  }
51935
52196
  };
@@ -51975,7 +52236,7 @@ import { join as join38 } from "path";
51975
52236
  // src/core/control-client.ts
51976
52237
  import { connect as connect2 } from "net";
51977
52238
  function connectControl(socketPath, bus, token) {
51978
- return new Promise((resolve10, reject) => {
52239
+ return new Promise((resolve11, reject) => {
51979
52240
  const socket = connect2(socketPath);
51980
52241
  socket.setEncoding("utf8");
51981
52242
  let ready = false;
@@ -52028,7 +52289,7 @@ function connectControl(socketPath, bus, token) {
52028
52289
  bus.setTokens(frame.tokens);
52029
52290
  for (const entry of frame.transcript) bus.pushEntry(entry);
52030
52291
  ready = true;
52031
- resolve10({ info: frame.info, close });
52292
+ resolve11({ info: frame.info, close });
52032
52293
  break;
52033
52294
  case "transcript":
52034
52295
  bus.pushEntry(frame.entry);
@@ -52116,7 +52377,7 @@ async function runAutoRun(profileDir2, profileName) {
52116
52377
  initI18n(readProfileConfig(profileDir2).locale);
52117
52378
  const socketPath = join39(profileDir2, CONTROL_SOCKET_FILE);
52118
52379
  const token = readControlToken(profileDir2);
52119
- const code = await new Promise((resolve10) => {
52380
+ const code = await new Promise((resolve11) => {
52120
52381
  let settled = false;
52121
52382
  const done = (c) => {
52122
52383
  if (settled) return;
@@ -52126,7 +52387,7 @@ async function runAutoRun(profileDir2, profileName) {
52126
52387
  socket.destroy();
52127
52388
  } catch {
52128
52389
  }
52129
- resolve10(c);
52390
+ resolve11(c);
52130
52391
  };
52131
52392
  const socket = connect3(socketPath);
52132
52393
  socket.setEncoding("utf8");
@@ -52174,16 +52435,16 @@ import { spawn as spawn12 } from "child_process";
52174
52435
  import { mkdirSync as mkdirSync9 } from "fs";
52175
52436
  import { basename as basename8 } from "path";
52176
52437
  function runProviderLogin(cmd, envVar, dir2) {
52177
- return new Promise((resolve10) => {
52438
+ return new Promise((resolve11) => {
52178
52439
  const child = spawn12(cmd, [], {
52179
52440
  stdio: "inherit",
52180
52441
  env: { ...process.env, [envVar]: dir2 }
52181
52442
  });
52182
52443
  child.on("error", (err) => {
52183
52444
  console.error(t("aiLogin.launchFailed", { cmd, message: err.message }));
52184
- resolve10(1);
52445
+ resolve11(1);
52185
52446
  });
52186
- child.on("exit", (code) => resolve10(code ?? 0));
52447
+ child.on("exit", (code) => resolve11(code ?? 0));
52187
52448
  });
52188
52449
  }
52189
52450
  async function runAiLogin(profileDir2, profileName, aiFilter) {
@@ -52323,10 +52584,10 @@ function Picker({
52323
52584
  ] });
52324
52585
  }
52325
52586
  function selectFromList(items, title) {
52326
- return new Promise((resolve10) => {
52587
+ return new Promise((resolve11) => {
52327
52588
  let chosen = null;
52328
52589
  const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v2) => chosen = v2 }));
52329
- void app.waitUntilExit().then(() => resolve10(chosen));
52590
+ void app.waitUntilExit().then(() => resolve11(chosen));
52330
52591
  });
52331
52592
  }
52332
52593