@4pm/cli 1.20.0 → 1.22.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 +471 -183
  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
  }
@@ -39265,7 +39335,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot, over
39265
39335
  return {
39266
39336
  cmd: active,
39267
39337
  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) } }
39338
+ { 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
39339
  ]
39270
39340
  };
39271
39341
  }
@@ -39283,7 +39353,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot, over
39283
39353
  label: cred.label?.trim() || profileDisplayLabel(dir2),
39284
39354
  dir: dir2,
39285
39355
  key,
39286
- args: buildRunArgs(cred, cmd, resume.get(key), oneShot, override, readOnly, bypass),
39356
+ args: buildRunArgs(cred, cmd, resume.get(key), oneShot, override, readOnly, bypass, [...allAiProfileDirs(config), dir2]),
39287
39357
  stdin: prompt,
39288
39358
  env: envVar ? { ...baseEnv, [envVar]: dir2, ...overrideEnv(cmd, override) } : { ...baseEnv, ...overrideEnv(cmd, override) }
39289
39359
  }));
@@ -39296,7 +39366,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
39296
39366
  const defaultPlan = {
39297
39367
  cmd,
39298
39368
  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) } }
39369
+ { 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
39370
  ]
39301
39371
  };
39302
39372
  if (!envVar) return defaultPlan;
@@ -39313,7 +39383,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
39313
39383
  label: profileDisplayLabel(dir2),
39314
39384
  dir: dir2,
39315
39385
  key,
39316
- args: buildRunArgs(profile, cmd, resume.get(key), oneShot, override, readOnly, bypass),
39386
+ args: buildRunArgs(profile, cmd, resume.get(key), oneShot, override, readOnly, bypass, [...allAiProfileDirs(config), dir2]),
39317
39387
  stdin: prompt,
39318
39388
  env: { ...baseEnv, [envVar]: dir2, ...overrideEnv(cmd, override) }
39319
39389
  };
@@ -39356,16 +39426,16 @@ async function fetchUsage(token) {
39356
39426
  }
39357
39427
  }
39358
39428
  function runUsageCli(dir2, aiCli2) {
39359
- return new Promise((resolve10) => {
39429
+ return new Promise((resolve11) => {
39360
39430
  let out = "";
39361
39431
  let child = null;
39362
39432
  const finish = () => {
39363
39433
  clearTimeout(timer);
39364
- resolve10(out);
39434
+ resolve11(out);
39365
39435
  };
39366
39436
  const timer = setTimeout(() => {
39367
39437
  child?.kill();
39368
- resolve10(out);
39438
+ resolve11(out);
39369
39439
  }, 3e4);
39370
39440
  timer.unref?.();
39371
39441
  try {
@@ -39688,9 +39758,9 @@ function pruneCommandOutputByAge(days) {
39688
39758
  import { spawn as spawn4 } from "child_process";
39689
39759
  import { execFile as execFile3 } from "child_process";
39690
39760
  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";
39761
+ 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
39762
  import { join as join14 } from "path";
39763
+ import { tmpdir as tmpdir3 } from "os";
39694
39764
  import { promisify as promisify3 } from "util";
39695
39765
 
39696
39766
  // src/core/ai-stream.ts
@@ -39720,12 +39790,16 @@ function createAiStreamParser(cli) {
39720
39790
  acc.output = ev.usage.output_tokens ?? 0;
39721
39791
  acc.cacheRead = ev.usage.cache_read_input_tokens ?? 0;
39722
39792
  acc.cacheCreation = ev.usage.cache_creation_input_tokens ?? 0;
39723
- acc.tokens = acc.input + acc.output;
39793
+ acc.tokens = acc.input + acc.output + acc.cacheRead + acc.cacheCreation;
39724
39794
  if (typeof ev.total_cost_usd === "number") acc.costUsd = ev.total_cost_usd;
39725
39795
  }
39726
39796
  return "";
39727
39797
  }
39728
39798
  if (ev.type === "assistant") {
39799
+ const u2 = ev.message?.usage;
39800
+ if (u2) {
39801
+ acc.contextTokens = (u2.input_tokens ?? 0) + (u2.cache_read_input_tokens ?? 0) + (u2.cache_creation_input_tokens ?? 0) + (u2.output_tokens ?? 0);
39802
+ }
39729
39803
  const text = assistantText(ev);
39730
39804
  return text ? `${text}
39731
39805
  ` : "";
@@ -39734,11 +39808,12 @@ function createAiStreamParser(cli) {
39734
39808
  }
39735
39809
  function handleCodex(ev) {
39736
39810
  if (ev.type === "turn.completed" && ev.usage) {
39737
- acc.input = ev.usage.input_tokens ?? 0;
39811
+ const cached = ev.usage.cached_input_tokens ?? 0;
39812
+ acc.input = Math.max(0, (ev.usage.input_tokens ?? 0) - cached);
39738
39813
  acc.output = (ev.usage.output_tokens ?? 0) + (ev.usage.reasoning_output_tokens ?? 0);
39739
- acc.cacheRead = ev.usage.cached_input_tokens ?? 0;
39814
+ acc.cacheRead = cached;
39740
39815
  acc.cacheCreation = 0;
39741
- acc.tokens = acc.input + acc.output;
39816
+ acc.tokens = acc.input + acc.output + acc.cacheRead;
39742
39817
  return "";
39743
39818
  }
39744
39819
  if (ev.type === "item.completed" && ev.item?.type === "agent_message") {
@@ -39786,10 +39861,65 @@ function createAiStreamParser(cli) {
39786
39861
  apiError: () => ({ ...apiError })
39787
39862
  };
39788
39863
  }
39864
+ function aiProviderOf(cmd) {
39865
+ if (cmd.includes("codex")) return "codex";
39866
+ if (cmd.includes("antigravity")) return "antigravity";
39867
+ return "claude";
39868
+ }
39789
39869
  function estimateTokens2(text) {
39790
39870
  return Math.max(1, Math.ceil(text.length / 4));
39791
39871
  }
39792
39872
 
39873
+ // src/core/agent-sandbox.ts
39874
+ var ENV_ALLOW = /* @__PURE__ */ new Set([
39875
+ "PATH",
39876
+ "HOME",
39877
+ "USER",
39878
+ "LOGNAME",
39879
+ "SHELL",
39880
+ "LANG",
39881
+ "TERM",
39882
+ "TZ",
39883
+ "TMPDIR",
39884
+ "TMP",
39885
+ "TEMP",
39886
+ "HTTP_PROXY",
39887
+ "HTTPS_PROXY",
39888
+ "NO_PROXY",
39889
+ "ALL_PROXY",
39890
+ "http_proxy",
39891
+ "https_proxy",
39892
+ "no_proxy",
39893
+ "all_proxy",
39894
+ "SSL_CERT_FILE",
39895
+ "SSL_CERT_DIR",
39896
+ "NODE_EXTRA_CA_CERTS",
39897
+ "SYSTEMROOT",
39898
+ "SYSTEMDRIVE",
39899
+ "WINDIR",
39900
+ "COMSPEC",
39901
+ "PATHEXT",
39902
+ "USERPROFILE",
39903
+ "APPDATA",
39904
+ "LOCALAPPDATA",
39905
+ "PROGRAMDATA",
39906
+ "PROGRAMFILES"
39907
+ ]);
39908
+ var ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "ANTHROPIC_", "CLAUDE_", "CODEX_", "OPENAI_", "AWS_", "GOOGLE_", "CLOUD_ML_", "VERTEX_"];
39909
+ function agentEnv(extraEnv, overrides) {
39910
+ const env3 = {};
39911
+ for (const [key, value] of Object.entries(process.env)) {
39912
+ if (value === void 0) continue;
39913
+ if (ENV_ALLOW.has(key) || ENV_ALLOW_PREFIXES.some((p2) => key.startsWith(p2))) env3[key] = value;
39914
+ }
39915
+ return { ...env3, ...extraEnv, ...overrides };
39916
+ }
39917
+ function gitAuthArgs(user, password) {
39918
+ if (!user && !password) return [];
39919
+ const basic = Buffer.from(`${user}:${password}`).toString("base64");
39920
+ return ["-c", `http.extraHeader=Authorization: Basic ${basic}`];
39921
+ }
39922
+
39793
39923
  // src/core/support-answer.ts
39794
39924
  var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
39795
39925
  var execFileAsync = promisify3(execFile3);
@@ -39803,18 +39933,34 @@ function repoDir(profileDir2, url, branch) {
39803
39933
  const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
39804
39934
  return join14(kbRoot(profileDir2), key);
39805
39935
  }
39806
- function authedUrl(url, token) {
39807
- if (!token) return url;
39808
- if (url.startsWith("https://")) return url.replace("https://", `https://${token}@`);
39809
- return url;
39936
+ var DENIED_TOOLS = ["Bash", "Edit", "Write", "NotebookEdit", "WebFetch", "WebSearch", "Task"];
39937
+ var KB_STALE_MS = 24 * 60 * 60 * 1e3;
39938
+ var kbTokens = /* @__PURE__ */ new Map();
39939
+ function kbAuth(token) {
39940
+ return token ? gitAuthArgs(token, "") : [];
39941
+ }
39942
+ async function pullKb(dir2) {
39943
+ const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
39944
+ await execFileAsync("git", [...kbAuth(kbTokens.get(dir2)), "-C", dir2, "pull", "--ff-only"], { env: gitEnv });
39810
39945
  }
39811
39946
  async function ensureRepo(profileDir2, repo) {
39812
39947
  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
39948
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
39949
+ if (repo.token) kbTokens.set(dir2, repo.token);
39950
+ else kbTokens.delete(dir2);
39951
+ if (existsSync10(join14(dir2, ".git"))) {
39952
+ const current2 = await execFileAsync("git", ["-C", dir2, "remote", "get-url", "origin"], { env: gitEnv }).then((r) => r.stdout.trim()).catch(() => repo.url);
39953
+ if (current2 !== repo.url) {
39954
+ await execFileAsync("git", ["-C", dir2, "remote", "set-url", "origin", repo.url], { env: gitEnv }).catch(() => void 0);
39955
+ }
39956
+ const marker = existsSync10(join14(dir2, ".git", "FETCH_HEAD")) ? join14(dir2, ".git", "FETCH_HEAD") : join14(dir2, ".git", "HEAD");
39957
+ if (Date.now() - statSync4(marker).mtimeMs > KB_STALE_MS) {
39958
+ void pullKb(dir2).catch((err) => logger.warn("support.kb.refresh.failed", { dir: dir2, error: String(err) }));
39959
+ }
39960
+ return dir2;
39961
+ }
39816
39962
  mkdirSync5(kbRoot(profileDir2), { recursive: true });
39817
- await execFileAsync("git", ["clone", "--depth", "1", "--branch", repo.branch, url, dir2], {
39963
+ await execFileAsync("git", [...kbAuth(repo.token), "clone", "--depth", "1", "--branch", repo.branch, repo.url, dir2], {
39818
39964
  env: gitEnv
39819
39965
  });
39820
39966
  return dir2;
@@ -39822,12 +39968,11 @@ async function ensureRepo(profileDir2, repo) {
39822
39968
  async function refreshSupportKb(profileDir2) {
39823
39969
  const root = kbRoot(profileDir2);
39824
39970
  if (!existsSync10(root)) return;
39825
- const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
39826
39971
  for (const entry of readdirSync5(root)) {
39827
39972
  const dir2 = join14(root, entry);
39828
39973
  if (!existsSync10(join14(dir2, ".git"))) continue;
39829
39974
  try {
39830
- await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
39975
+ await pullKb(dir2);
39831
39976
  } catch (err) {
39832
39977
  logger.warn("support.kb.refresh.failed", { dir: dir2, error: String(err) });
39833
39978
  }
@@ -39890,6 +40035,33 @@ function buildPrompt(docs, question, askerRole) {
39890
40035
  question
39891
40036
  ].join("\n");
39892
40037
  }
40038
+ function buildDraftPrompt(docs, context, task) {
40039
+ const goal = task === "reply_draft" ? [
40040
+ "You are drafting a reply FROM the 4PM support team TO the customer in the support ticket below.",
40041
+ "Address the customer's latest message, using the whole conversation for context. Be accurate,",
40042
+ "friendly and concise. Write in the language the customer uses in the ticket."
40043
+ ] : [
40044
+ "You are drafting an outreach message FROM the 4PM platform team TO the customer organizations.",
40045
+ "Use the subject and any current draft below as the starting point. Be clear, friendly and concise.",
40046
+ "Write in the language of the subject/draft (or of the admin's instructions when those are empty)."
40047
+ ];
40048
+ return [
40049
+ ...goal,
40050
+ "Use the documentation and FAQ below for any product facts; never invent features, prices or",
40051
+ "commitments that are not in the docs \u2014 leave a clear [placeholder] for the admin to fill instead.",
40052
+ "Follow the admin's instructions when given. A human admin reviews and edits your draft before",
40053
+ "sending it.",
40054
+ "",
40055
+ "Output ONLY the message body in markdown \u2014 no subject line, no preamble, no explanation, no code",
40056
+ "fences around the whole message.",
40057
+ "",
40058
+ "===== DOCUMENTATION =====",
40059
+ docs,
40060
+ "",
40061
+ "===== CONTEXT =====",
40062
+ context
40063
+ ].join("\n");
40064
+ }
39893
40065
  function imageExt(mime) {
39894
40066
  switch (mime) {
39895
40067
  case "image/png":
@@ -39904,9 +40076,8 @@ function imageExt(mime) {
39904
40076
  return "png";
39905
40077
  }
39906
40078
  }
39907
- function materializeImages2(profileDir2, images, question) {
39908
- const dir2 = join14(profileDir2, "help-images", randomUUID());
39909
- mkdirSync5(dir2, { recursive: true });
40079
+ function materializeImages2(images, question) {
40080
+ const dir2 = mkdtempSync2(join14(tmpdir3(), "4pm-help-images-"));
39910
40081
  let rewritten = question;
39911
40082
  images.forEach((img, i) => {
39912
40083
  const file = join14(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
@@ -39932,23 +40103,32 @@ function parseModeratedAnswer(text) {
39932
40103
  }
39933
40104
  return { body: text.trim() };
39934
40105
  }
39935
- function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir) {
39936
- return new Promise((resolve10, reject) => {
40106
+ function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir, denyDirs = []) {
40107
+ return new Promise((resolve11, reject) => {
39937
40108
  const isClaude = cmd.includes("claude");
39938
40109
  const args = [
39939
40110
  "-p",
39940
40111
  ...profile?.model ? ["--model", profile.model] : [],
39941
40112
  ...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"] : []
40113
+ // No shell / edits / web for an untrusted question (ADR-0346). Reads need no grant inside the
40114
+ // (empty) working dir and any added dir; anywhere else they would need an approval a headless
40115
+ // run never gets.
40116
+ // Secret-path deny rules (ADR-0347) — reads are not confined to the working dir.
40117
+ ...denySettingsArgs(cmd, [...denyDirs, profile?.dir]),
40118
+ ...isClaude ? ["--disallowedTools", ...DENIED_TOOLS] : [],
40119
+ // When the question references pasted images (ADR-0273), add the materialized folder so the
40120
+ // agent can Read them (the question already carries their absolute paths) — scoped to it only.
40121
+ ...isClaude && imageDir ? ["--add-dir", imageDir] : []
39945
40122
  ];
39946
- const env3 = {
39947
- ...process.env,
39948
- ...extraEnv,
39949
- ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
40123
+ const env3 = agentEnv(extraEnv, profile ? { CLAUDE_CONFIG_DIR: profile.dir } : void 0);
40124
+ const cwd2 = mkdtempSync2(join14(tmpdir3(), "4pm-support-"));
40125
+ const cleanup = () => {
40126
+ try {
40127
+ rmSync5(cwd2, { recursive: true, force: true });
40128
+ } catch {
40129
+ }
39950
40130
  };
39951
- const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
40131
+ const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3, cwd: cwd2 });
39952
40132
  const parser = createAiStreamParser(cmd);
39953
40133
  let out = "";
39954
40134
  let err = "";
@@ -39961,12 +40141,14 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir) {
39961
40141
  child.stderr.on("data", (d) => err += d.toString());
39962
40142
  child.on("error", (e) => {
39963
40143
  clearTimeout(timer);
40144
+ cleanup();
39964
40145
  reject(e);
39965
40146
  });
39966
40147
  child.on("close", (code) => {
39967
40148
  clearTimeout(timer);
40149
+ cleanup();
39968
40150
  out += parser.flush();
39969
- resolve10({ code: code ?? -1, out, err, usage: parser.usage() });
40151
+ resolve11({ code: code ?? -1, out, err, usage: parser.usage() });
39970
40152
  });
39971
40153
  child.stdin.write(prompt);
39972
40154
  child.stdin.end();
@@ -39976,7 +40158,7 @@ async function runClaudeWithFailover(prompt, ai2, imageDir) {
39976
40158
  const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
39977
40159
  let lastReason = "no attempt";
39978
40160
  for (let i = 0; i < attempts.length; i++) {
39979
- const { code, out, err, usage } = await runClaudeOnce(ai2.cmd, attempts[i], prompt, ai2.env, imageDir);
40161
+ const { code, out, err, usage } = await runClaudeOnce(ai2.cmd, attempts[i], prompt, ai2.env, imageDir, ai2.profiles.map((p2) => p2.dir));
39980
40162
  if (code === 0 && out.trim()) return { text: out.trim(), usage };
39981
40163
  const combined = err || out;
39982
40164
  lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
@@ -39988,7 +40170,7 @@ async function runSupportAnswer(req, ai2, profileDir2) {
39988
40170
  let imageDir;
39989
40171
  let question = req.question;
39990
40172
  if (req.images?.length) {
39991
- const m2 = materializeImages2(profileDir2, req.images, req.question);
40173
+ const m2 = materializeImages2(req.images, req.question);
39992
40174
  imageDir = m2.dir;
39993
40175
  question = m2.question;
39994
40176
  }
@@ -39996,13 +40178,10 @@ async function runSupportAnswer(req, ai2, profileDir2) {
39996
40178
  const dir2 = await ensureRepo(profileDir2, req.repo);
39997
40179
  const docs = collectDocs(dir2);
39998
40180
  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
- );
40181
+ const prompt = req.task ? buildDraftPrompt(docs, question, req.task) : buildPrompt(docs, question, req.askerRole);
40182
+ const { text, usage } = await runClaudeWithFailover(prompt, ai2, imageDir);
40004
40183
  if (!text) return { body: "", error: "empty answer" };
40005
- const { body, moderation } = parseModeratedAnswer(text);
40184
+ const { body, moderation } = req.task ? { body: text.trim(), moderation: void 0 } : parseModeratedAnswer(text);
40006
40185
  if (!body) return { body: "", error: "empty answer" };
40007
40186
  const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
40008
40187
  return {
@@ -40237,10 +40416,10 @@ async function crontabList() {
40237
40416
  }
40238
40417
  }
40239
40418
  async function crontabSet(content) {
40240
- return new Promise((resolve10) => {
40419
+ return new Promise((resolve11) => {
40241
40420
  const child = spawn5("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
40242
- child.on("error", () => resolve10(false));
40243
- child.on("close", (code) => resolve10(code === 0));
40421
+ child.on("error", () => resolve11(false));
40422
+ child.on("close", (code) => resolve11(code === 0));
40244
40423
  child.stdin.end(content.endsWith("\n") ? content : content + "\n");
40245
40424
  });
40246
40425
  }
@@ -40409,14 +40588,14 @@ function detectContainerized() {
40409
40588
  }
40410
40589
  }
40411
40590
  function canReach(host, port) {
40412
- return new Promise((resolve10) => {
40591
+ return new Promise((resolve11) => {
40413
40592
  const socket = connect({ host, port });
40414
40593
  let done = false;
40415
40594
  const finish = (ok) => {
40416
40595
  if (done) return;
40417
40596
  done = true;
40418
40597
  socket.destroy();
40419
- resolve10(ok);
40598
+ resolve11(ok);
40420
40599
  };
40421
40600
  socket.setTimeout(PROBE_TIMEOUT_MS);
40422
40601
  socket.once("connect", () => finish(true));
@@ -40588,7 +40767,7 @@ async function runCommand(dispatch, emit, opts) {
40588
40767
  });
40589
40768
  child.stdout?.on("data", (data) => batcher.push(data.toString("utf8")));
40590
40769
  child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
40591
- await new Promise((resolve10) => {
40770
+ await new Promise((resolve11) => {
40592
40771
  child.on("close", (exitCode) => {
40593
40772
  clearTimers();
40594
40773
  batcher.flush();
@@ -40601,7 +40780,7 @@ async function runCommand(dispatch, emit, opts) {
40601
40780
  // server settles the command as failed and the web surfaces the note instead of spinning.
40602
40781
  exitCode: timedOut ? 124 : exitCode ?? -1
40603
40782
  });
40604
- resolve10();
40783
+ resolve11();
40605
40784
  });
40606
40785
  child.on("error", (err) => {
40607
40786
  clearTimers();
@@ -40613,7 +40792,7 @@ async function runCommand(dispatch, emit, opts) {
40613
40792
  done: true,
40614
40793
  exitCode: -1
40615
40794
  });
40616
- resolve10();
40795
+ resolve11();
40617
40796
  });
40618
40797
  });
40619
40798
  }
@@ -40636,15 +40815,15 @@ function isOutOfCredits(text) {
40636
40815
  );
40637
40816
  }
40638
40817
  function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
40639
- return new Promise((resolve10) => {
40818
+ return new Promise((resolve11) => {
40640
40819
  runCommand(
40641
40820
  { commandId, cmd, args, path: cwd2, env: env3 },
40642
40821
  (out) => {
40643
40822
  if (out.chunk) onChunk(out.chunk);
40644
- if (out.done) resolve10(out.exitCode ?? -1);
40823
+ if (out.done) resolve11(out.exitCode ?? -1);
40645
40824
  },
40646
40825
  { stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
40647
- ).catch(() => resolve10(-1));
40826
+ ).catch(() => resolve11(-1));
40648
40827
  });
40649
40828
  }
40650
40829
  async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
@@ -41122,6 +41301,24 @@ function runSlashCommand(line, base) {
41122
41301
 
41123
41302
  // src/core/memory-compact.ts
41124
41303
  import { spawn as spawn7 } from "child_process";
41304
+ function parseJsonResult(out) {
41305
+ let parsed;
41306
+ try {
41307
+ parsed = JSON.parse(out.trim());
41308
+ } catch {
41309
+ return null;
41310
+ }
41311
+ if (parsed.is_error || typeof parsed.result !== "string") return null;
41312
+ const u2 = parsed.usage;
41313
+ const usage = u2 ? {
41314
+ input: u2.input_tokens ?? 0,
41315
+ output: u2.output_tokens ?? 0,
41316
+ cacheRead: u2.cache_read_input_tokens ?? 0,
41317
+ cacheCreation: u2.cache_creation_input_tokens ?? 0,
41318
+ tokens: (u2.input_tokens ?? 0) + (u2.output_tokens ?? 0) + (u2.cache_read_input_tokens ?? 0) + (u2.cache_creation_input_tokens ?? 0)
41319
+ } : null;
41320
+ return { text: parsed.result, usage };
41321
+ }
41125
41322
  var COMPACT_TIMEOUT_MS = 6e4;
41126
41323
  function buildPrompt2(input) {
41127
41324
  return [
@@ -41141,9 +41338,21 @@ function buildPrompt2(input) {
41141
41338
  input.answer
41142
41339
  ].join("\n");
41143
41340
  }
41144
- function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
41145
- return new Promise((resolve10, reject) => {
41146
- const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
41341
+ function runOnce(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
41342
+ return new Promise((resolve11, reject) => {
41343
+ const args = [
41344
+ "-p",
41345
+ "--output-format",
41346
+ "json",
41347
+ ...profile?.model ? ["--model", profile.model] : [],
41348
+ // Secret-path deny rules (ADR-0347) — before the variadic `--disallowedTools`.
41349
+ ...denySettingsArgs(cmd, [...denyDirs, profile?.dir]),
41350
+ "--max-turns=1",
41351
+ "--disallowedTools",
41352
+ ONE_SHOT_DISALLOWED_CLAUDE_TOOLS,
41353
+ "--permission-mode",
41354
+ "default"
41355
+ ];
41147
41356
  const env3 = {
41148
41357
  ...process.env,
41149
41358
  ...extraEnv,
@@ -41165,7 +41374,7 @@ function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
41165
41374
  });
41166
41375
  child.on("close", (code) => {
41167
41376
  clearTimeout(timer);
41168
- resolve10({ code: code ?? -1, out, err });
41377
+ resolve11({ code: code ?? -1, out, err });
41169
41378
  });
41170
41379
  child.stdin.write(prompt);
41171
41380
  child.stdin.end();
@@ -41176,17 +41385,19 @@ async function runMemoryCompaction(ai2, cwd2, input) {
41176
41385
  const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
41177
41386
  for (let i = 0; i < attempts.length; i++) {
41178
41387
  try {
41179
- const { code, out } = await runOnce(ai2.cmd, attempts[i], prompt, cwd2, ai2.env);
41180
- if (code === 0 && out.trim()) {
41388
+ const profile = attempts[i];
41389
+ const { code, out } = await runOnce(ai2.cmd, profile, prompt, cwd2, ai2.env, ai2.profiles.map((p2) => p2.dir));
41390
+ const parsed = code === 0 ? parseJsonResult(out) : null;
41391
+ if (parsed && parsed.text.trim()) {
41181
41392
  reportToolResult(ai2.cmd, true);
41182
- return out.trim().slice(0, input.budgetChars);
41393
+ return { memory: parsed.text.trim().slice(0, input.budgetChars), usage: parsed.usage, dir: profile?.dir ?? null };
41183
41394
  }
41184
41395
  if (i === attempts.length - 1) break;
41185
41396
  } catch {
41186
41397
  break;
41187
41398
  }
41188
41399
  }
41189
- return "";
41400
+ return { memory: "", usage: null, dir: null };
41190
41401
  }
41191
41402
 
41192
41403
  // src/core/git-commit-identity.ts
@@ -41361,6 +41572,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
41361
41572
  const guardedPrompt = ctx.restrictToFolder && ctx.physicRoot ? folderScopeGuard(ctx.physicRoot, aiBodyPrompt) : aiBodyPrompt;
41362
41573
  const hint = unified ? { credential: startCred ?? workingCred } : { dir: startDir ?? workingDir };
41363
41574
  const memCfg = resolveMemoryConfig(config);
41575
+ pruneResumableSessions(ctx, resolveResumePolicy(config), Date.now());
41364
41576
  const firstAttempt = planAiRun(guardedPrompt, config, hint, /* @__PURE__ */ new Map(), oneShot, aiConfig, readOnly, bypass).attempts[0];
41365
41577
  const resumeId = memCfg.enabled && firstAttempt?.key && firstAttempt.cmd === "claude" ? ctx.sessionIdByKey.get(firstAttempt.key) : void 0;
41366
41578
  let effectivePrompt = guardedPrompt;
@@ -41508,6 +41720,8 @@ ${guardedPrompt}`;
41508
41720
  outputTokens: result.usage.output,
41509
41721
  cacheReadTokens: result.usage.cacheRead,
41510
41722
  cacheCreationTokens: result.usage.cacheCreation,
41723
+ // Provider → the server's per-provider quota-token weights (ADR-0340).
41724
+ provider: aiProviderOf(result.workedCmd ?? plan.cmd),
41511
41725
  // Auth mode for billing split (ADR-0192 §5): subscription (OAuth) vs api-key (API-billed).
41512
41726
  authMode: resolveClaudeAuthMode(config)
41513
41727
  });
@@ -41525,9 +41739,23 @@ ${guardedPrompt}`;
41525
41739
  if (memCfg.enabled && result.exitCode === 0) {
41526
41740
  if (result.workedKey && result.sessionId) {
41527
41741
  ctx.sessionIdByKey.set(result.workedKey, result.sessionId);
41742
+ ctx.sessionMetaByKey.set(result.workedKey, { at: Date.now(), contextTokens: result.usage.contextTokens ?? 0 });
41528
41743
  }
41529
41744
  void updateSharedMemory(ctx, config, prompt, answerText, memCfg.budgetChars).catch(() => {
41530
41745
  });
41746
+ } else if (result.exitCode !== 0) {
41747
+ ctx.sessionIdByKey.clear();
41748
+ ctx.sessionMetaByKey.clear();
41749
+ }
41750
+ }
41751
+ function pruneResumableSessions(ctx, policy, now) {
41752
+ for (const key of [...ctx.sessionIdByKey.keys()]) {
41753
+ const meta = ctx.sessionMetaByKey.get(key);
41754
+ const stale = !meta || now - meta.at > policy.maxIdleMs || meta.contextTokens > policy.maxContextTokens;
41755
+ if (stale) {
41756
+ ctx.sessionIdByKey.delete(key);
41757
+ ctx.sessionMetaByKey.delete(key);
41758
+ }
41531
41759
  }
41532
41760
  }
41533
41761
  async function runWebSlashCommand(ctx, line, commandId) {
@@ -41635,11 +41863,29 @@ async function updateSharedMemory(ctx, config, userPrompt, answer, budgetChars)
41635
41863
  if (!answer.trim()) return;
41636
41864
  const profiles = resolveClaudeProfiles(config, ctx.physicRoot);
41637
41865
  const cwd2 = ctx.physicRoot ?? process.cwd();
41638
- const newMemory = await runMemoryCompaction(
41866
+ const { memory: newMemory, usage, dir: dir2 } = await runMemoryCompaction(
41639
41867
  { cmd: "claude", profiles, env: config.aiEnv },
41640
41868
  cwd2,
41641
41869
  { oldMemory: ctx.aiMemory, prompt: userPrompt, answer, budgetChars }
41642
41870
  );
41871
+ if (usage && usage.tokens > 0) {
41872
+ ctx.reportUsage(
41873
+ [
41874
+ {
41875
+ metric: UsageMetric.AI_TOKENS,
41876
+ amount: usage.tokens,
41877
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
41878
+ inputTokens: usage.input,
41879
+ outputTokens: usage.output,
41880
+ cacheReadTokens: usage.cacheRead,
41881
+ cacheCreationTokens: usage.cacheCreation,
41882
+ provider: "claude",
41883
+ authMode: resolveClaudeAuthMode(config)
41884
+ }
41885
+ ],
41886
+ dir2 ? profileDisplayLabel(dir2) : null
41887
+ );
41888
+ }
41643
41889
  if (!newMemory) return;
41644
41890
  ctx.aiMemory = newMemory;
41645
41891
  ctx.send(WsChannels.MEMORY_UPDATE, { text: newMemory });
@@ -41647,6 +41893,7 @@ async function updateSharedMemory(ctx, config, userPrompt, answer, budgetChars)
41647
41893
  function resetMemorySession(ctx) {
41648
41894
  ctx.aiMemory = "";
41649
41895
  ctx.sessionIdByKey.clear();
41896
+ ctx.sessionMetaByKey.clear();
41650
41897
  ctx.send(WsChannels.MEMORY_UPDATE, { text: "" });
41651
41898
  }
41652
41899
  function profileUnderSessionPressure(ctx, config, cmd, workingDir) {
@@ -41719,7 +41966,7 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
41719
41966
  }
41720
41967
 
41721
41968
  // src/core/autonomous-cycle.ts
41722
- import { randomUUID as randomUUID2 } from "crypto";
41969
+ import { randomUUID } from "crypto";
41723
41970
  import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile5, readdir as readdir2, rm as rm2, stat } from "fs/promises";
41724
41971
  import { join as join20 } from "path";
41725
41972
  function buildAutonomousCyclePrompt() {
@@ -41859,7 +42106,7 @@ async function runAutonomousCycle(ctx) {
41859
42106
  hist.ticks = { day: today, count: todayTickCount(hist, today) + 1 };
41860
42107
  await writeHistories(root, hist);
41861
42108
  await log(`[run] cycle (tick ${hist.ticks.count}/${today})`);
41862
- const commandId = randomUUID2();
42109
+ const commandId = randomUUID();
41863
42110
  const override = cfg.model ? { model: cfg.model } : void 0;
41864
42111
  try {
41865
42112
  await runAiPrompt(ctx, buildAutonomousCyclePrompt(), commandId, "local", false, void 0, override, false, true);
@@ -41963,11 +42210,11 @@ async function allAiProfilesExhausted(ctx, cfg) {
41963
42210
 
41964
42211
  // src/core/fs-browse.ts
41965
42212
  import { readdir as readdir3 } from "fs/promises";
41966
- import { resolve as resolve2, sep as sep2 } from "path";
42213
+ import { resolve as resolve3, sep as sep2 } from "path";
41967
42214
  async function listDir(path, root) {
41968
42215
  if (!root) return { path: "", entries: [] };
41969
- const base = resolve2(root);
41970
- const requested = resolve2(base, path && path.trim() ? path : ".");
42216
+ const base = resolve3(root);
42217
+ const requested = resolve3(base, path && path.trim() ? path : ".");
41971
42218
  const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
41972
42219
  try {
41973
42220
  const dirents = await readdir3(target, { withFileTypes: true });
@@ -41986,12 +42233,12 @@ async function listDir(path, root) {
41986
42233
 
41987
42234
  // src/core/fs-read.ts
41988
42235
  import { readFile as readFile6 } from "fs/promises";
41989
- import { resolve as resolve3, sep as sep3 } from "path";
42236
+ import { resolve as resolve4, sep as sep3 } from "path";
41990
42237
  var MAX_BYTES = 256 * 1024;
41991
42238
  function resolveInRoot(root, relPath) {
41992
42239
  if (!root) return null;
41993
- const base = resolve3(root);
41994
- const target = resolve3(base, relPath || ".");
42240
+ const base = resolve4(root);
42241
+ const target = resolve4(base, relPath || ".");
41995
42242
  return target === base || target.startsWith(base + sep3) ? target : null;
41996
42243
  }
41997
42244
  async function readWorkerFile(root, path) {
@@ -42011,12 +42258,12 @@ async function readWorkerFile(root, path) {
42011
42258
 
42012
42259
  // src/core/fs-write.ts
42013
42260
  import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
42014
- import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
42261
+ import { dirname as dirname4, resolve as resolve5, sep as sep4 } from "path";
42015
42262
  var MAX_BYTES2 = 2 * 1024 * 1024;
42016
42263
  function resolveInRoot2(root, relPath) {
42017
42264
  if (!root) return null;
42018
- const base = resolve4(root);
42019
- const target = resolve4(base, relPath || ".");
42265
+ const base = resolve5(root);
42266
+ const target = resolve5(base, relPath || ".");
42020
42267
  return target === base || target.startsWith(base + sep4) ? target : null;
42021
42268
  }
42022
42269
  async function writeWorkerFile(root, relPath, content) {
@@ -42039,11 +42286,11 @@ async function writeWorkerFile(root, relPath, content) {
42039
42286
 
42040
42287
  // src/core/fs-mutate.ts
42041
42288
  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";
42289
+ import { basename as basename3, dirname as dirname5, resolve as resolve6, sep as sep5 } from "path";
42043
42290
  var MAX_BYTES3 = 2 * 1024 * 1024;
42044
42291
  function resolveInRoot3(root, rel) {
42045
- const base = resolve5(root);
42046
- const target = resolve5(base, rel || ".");
42292
+ const base = resolve6(root);
42293
+ const target = resolve6(base, rel || ".");
42047
42294
  return target === base || target.startsWith(base + sep5) ? target : null;
42048
42295
  }
42049
42296
  async function isDir(p2) {
@@ -42068,13 +42315,13 @@ async function mutateFs(root, req) {
42068
42315
  switch (req.op) {
42069
42316
  case "mkdir": {
42070
42317
  const target = resolveInRoot3(root, req.path ?? "");
42071
- if (!target || target === resolve5(root)) return escaped(req.path ?? "");
42318
+ if (!target || target === resolve6(root)) return escaped(req.path ?? "");
42072
42319
  await mkdir5(target, { recursive: true });
42073
42320
  return { ok: true, path: target };
42074
42321
  }
42075
42322
  case "create": {
42076
42323
  const target = resolveInRoot3(root, req.path ?? "");
42077
- if (!target || target === resolve5(root)) return escaped(req.path ?? "");
42324
+ if (!target || target === resolve6(root)) return escaped(req.path ?? "");
42078
42325
  if (await exists(target)) return { ok: false, path: target, error: "already exists" };
42079
42326
  const content = req.content ?? "";
42080
42327
  if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
@@ -42088,9 +42335,9 @@ async function mutateFs(root, req) {
42088
42335
  const from = resolveInRoot3(root, req.from ?? "");
42089
42336
  let to2 = resolveInRoot3(root, req.to ?? "");
42090
42337
  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 ?? "");
42338
+ if (from === resolve6(root)) return { ok: false, path: from, error: "cannot move the project root" };
42339
+ if (await isDir(to2)) to2 = resolve6(to2, basename3(from));
42340
+ if (to2 !== resolve6(root) && !to2.startsWith(resolve6(root) + sep5)) return escaped(req.to ?? "");
42094
42341
  if (await exists(to2)) return { ok: false, path: to2, error: "destination already exists" };
42095
42342
  await mkdir5(dirname5(to2), { recursive: true });
42096
42343
  await rename(from, to2);
@@ -42099,7 +42346,7 @@ async function mutateFs(root, req) {
42099
42346
  case "delete": {
42100
42347
  const target = resolveInRoot3(root, req.path ?? "");
42101
42348
  if (!target) return escaped(req.path ?? "");
42102
- if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
42349
+ if (target === resolve6(root)) return { ok: false, path: target, error: "cannot delete the project root" };
42103
42350
  await rm3(target, { recursive: true, force: true });
42104
42351
  return { ok: true, path: target };
42105
42352
  }
@@ -42113,11 +42360,11 @@ async function mutateFs(root, req) {
42113
42360
 
42114
42361
  // src/core/fs-transfer.ts
42115
42362
  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";
42363
+ import { basename as basename4, dirname as dirname6, extname, resolve as resolve7, sep as sep6 } from "path";
42117
42364
  function resolveInRoot4(root, relPath) {
42118
42365
  if (!root) return null;
42119
- const base = resolve6(root);
42120
- const target = resolve6(base, relPath || ".");
42366
+ const base = resolve7(root);
42367
+ const target = resolve7(base, relPath || ".");
42121
42368
  return target === base || target.startsWith(base + sep6) ? target : null;
42122
42369
  }
42123
42370
  var MIME_BY_EXT = {
@@ -42233,13 +42480,13 @@ function handleFsChannels(ctx, message, payload) {
42233
42480
  // src/core/git-diff.ts
42234
42481
  import { readFile as readFile8 } from "fs/promises";
42235
42482
  import { execFile as execFile6 } from "child_process";
42236
- import { basename as basename5, dirname as dirname7, resolve as resolve7, sep as sep7 } from "path";
42483
+ import { basename as basename5, dirname as dirname7, resolve as resolve8, sep as sep7 } from "path";
42237
42484
  import { promisify as promisify6 } from "util";
42238
42485
  var run6 = promisify6(execFile6);
42239
42486
  function resolveInRoot5(root, relPath) {
42240
42487
  if (!root) return null;
42241
- const base = resolve7(root);
42242
- const target = resolve7(base, relPath || ".");
42488
+ const base = resolve8(root);
42489
+ const target = resolve8(base, relPath || ".");
42243
42490
  return target === base || target.startsWith(base + sep7) ? target : null;
42244
42491
  }
42245
42492
  function looksBinary(buf) {
@@ -42393,15 +42640,15 @@ async function manageSshKey(op, profileDir2) {
42393
42640
 
42394
42641
  // src/core/git-history.ts
42395
42642
  import { execFile as execFile9 } from "child_process";
42396
- import { resolve as resolve8, sep as sep8 } from "path";
42643
+ import { resolve as resolve9, sep as sep8 } from "path";
42397
42644
  import { promisify as promisify9 } from "util";
42398
42645
  var run9 = promisify9(execFile9);
42399
42646
  var FS = "";
42400
42647
  var RS = "";
42401
42648
  function resolveRepo(root, repo) {
42402
42649
  if (!root) return null;
42403
- const base = resolve8(root);
42404
- const requested = resolve8(base, repo && repo.trim() ? repo : ".");
42650
+ const base = resolve9(root);
42651
+ const requested = resolve9(base, repo && repo.trim() ? repo : ".");
42405
42652
  return requested === base || requested.startsWith(base + sep8) ? requested : base;
42406
42653
  }
42407
42654
  async function git(cwd2, args) {
@@ -42418,7 +42665,7 @@ async function git(cwd2, args) {
42418
42665
  }
42419
42666
  async function gitRepos(root) {
42420
42667
  if (!root) return { repos: [] };
42421
- const base = resolve8(root);
42668
+ const base = resolve9(root);
42422
42669
  const top = (await git(base, ["rev-parse", "--show-toplevel"])).trim();
42423
42670
  if (!top || !(top === base || top.startsWith(base + sep8))) return { repos: [] };
42424
42671
  const repos = [];
@@ -42430,7 +42677,7 @@ async function gitRepos(root) {
42430
42677
  const m2 = line.trim().match(/^[-+U ]?[0-9a-f]{7,40}\s+(\S+)/);
42431
42678
  const subPath = m2?.[1];
42432
42679
  if (!subPath) continue;
42433
- const abs = resolve8(base, subPath);
42680
+ const abs = resolve9(base, subPath);
42434
42681
  if (!(abs === base || abs.startsWith(base + sep8))) continue;
42435
42682
  const remote = (await git(abs, ["remote", "get-url", "origin"])).trim();
42436
42683
  const branch = (await git(abs, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
@@ -42752,9 +42999,9 @@ Extra focus from the author: ${req.prompt}` : "";
42752
42999
  "Output ONLY the Markdown article, no preamble or code fences around the whole thing." + focus
42753
43000
  ].join("\n");
42754
43001
  }
42755
- function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
42756
- return new Promise((resolve10, reject) => {
42757
- const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
43002
+ function runOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
43003
+ return new Promise((resolve11, reject) => {
43004
+ const args = ["-p", ...profile?.model ? ["--model", profile.model] : [], ...denySettingsArgs(cmd, [...denyDirs, profile?.dir])];
42758
43005
  const env3 = {
42759
43006
  ...process.env,
42760
43007
  ...extraEnv,
@@ -42776,7 +43023,7 @@ function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
42776
43023
  });
42777
43024
  child.on("close", (code) => {
42778
43025
  clearTimeout(timer);
42779
- resolve10({ code: code ?? -1, out, err });
43026
+ resolve11({ code: code ?? -1, out, err });
42780
43027
  });
42781
43028
  child.stdin.write(prompt);
42782
43029
  child.stdin.end();
@@ -42788,7 +43035,7 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
42788
43035
  let lastReason = "no attempt";
42789
43036
  for (let i = 0; i < attempts.length; i++) {
42790
43037
  try {
42791
- const { code, out, err } = await runOnce2(ai2.cmd, attempts[i], prompt, physicRoot, ai2.env);
43038
+ const { code, out, err } = await runOnce2(ai2.cmd, attempts[i], prompt, physicRoot, ai2.env, ai2.profiles.map((p2) => p2.dir));
42792
43039
  if (code === 0 && out.trim()) {
42793
43040
  reportToolResult(ai2.cmd, true);
42794
43041
  return { bodyMarkdown: out.trim() };
@@ -42808,23 +43055,21 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
42808
43055
  // src/core/faq-compose.ts
42809
43056
  import { spawn as spawn9, execFile as execFile10 } from "child_process";
42810
43057
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, rmSync as rmSync7 } from "fs";
42811
- import { randomUUID as randomUUID3 } from "crypto";
43058
+ import { randomUUID as randomUUID2 } from "crypto";
42812
43059
  import { join as join24 } from "path";
42813
- import { tmpdir as tmpdir3 } from "os";
43060
+ import { tmpdir as tmpdir4 } from "os";
42814
43061
  import { promisify as promisify10 } from "util";
42815
43062
  var execFileAsync2 = promisify10(execFile10);
42816
43063
  var NO_USAGE2 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
42817
43064
  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
- }
42822
- function buildPrompt4(tickets) {
43065
+ var AGENT_DENIED_TOOLS = ["Bash", "WebFetch", "WebSearch", "NotebookEdit", "Task"];
43066
+ function buildPrompt4(tickets, customPrompt) {
42823
43067
  const transcript = tickets.map((t2) => {
42824
43068
  const thread = t2.messages.map((m2) => ` [${m2.author}] ${m2.body}`).join("\n");
42825
43069
  return `### Ticket ${t2.id} \u2014 ${t2.subject} (${t2.category})
42826
43070
  ${thread}`;
42827
43071
  }).join("\n\n");
43072
+ const ids = tickets.map((t2) => t2.id);
42828
43073
  return [
42829
43074
  "You are maintaining the 4PM product FAQ, stored as markdown in THIS git repository (your CWD).",
42830
43075
  "From the resolved support tickets below, distil reusable, product-general FAQ entries and add or",
@@ -42839,27 +43084,61 @@ ${thread}`;
42839
43084
  "- Only edit markdown documentation files; do not touch code, CI, or unrelated files.",
42840
43085
  "- Do NOT run git commit/push or open a PR yourself \u2014 just leave the edited files in the working",
42841
43086
  " tree; the surrounding tooling handles commit + PR.",
43087
+ ...customPrompt?.trim() ? ["", "Additional instruction from the operator (follow it too):", customPrompt.trim()] : [],
42842
43088
  "",
42843
43089
  "===== RESOLVED TICKETS =====",
42844
- transcript
43090
+ transcript,
43091
+ "",
43092
+ "===== REQUIRED FINAL OUTPUT =====",
43093
+ "After finishing all file edits, output \u2014 as the LAST thing, on its own \u2014 a single fenced JSON",
43094
+ "block summarizing what you did PER TICKET (created/updated which FAQ entry, or skipped + why):",
43095
+ "```json",
43096
+ `{"perTicket":[${ids.map((id) => `{"ticketId":"${id}","summary":"..."}`).join(",")}]}`,
43097
+ "```",
43098
+ "Include exactly one entry per ticket id above; keep each summary to one or two sentences."
42845
43099
  ].join("\n");
42846
43100
  }
42847
- function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv) {
42848
- return new Promise((resolve10, reject) => {
43101
+ function parsePerTicket(text) {
43102
+ const idx = text.lastIndexOf('"perTicket"');
43103
+ if (idx === -1) return [];
43104
+ const open = text.lastIndexOf("{", idx);
43105
+ if (open === -1) return [];
43106
+ let depth = 0;
43107
+ for (let i = open; i < text.length; i++) {
43108
+ if (text[i] === "{") depth++;
43109
+ else if (text[i] === "}") {
43110
+ depth--;
43111
+ if (depth === 0) {
43112
+ try {
43113
+ const obj = JSON.parse(text.slice(open, i + 1));
43114
+ if (Array.isArray(obj.perTicket)) {
43115
+ return obj.perTicket.filter((e) => !!e && typeof e.ticketId === "string").map((e) => ({ ticketId: e.ticketId, summary: String(e.summary ?? "") }));
43116
+ }
43117
+ } catch {
43118
+ return [];
43119
+ }
43120
+ return [];
43121
+ }
43122
+ }
43123
+ }
43124
+ return [];
43125
+ }
43126
+ function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
43127
+ return new Promise((resolve11, reject) => {
42849
43128
  const isClaude = cmd.includes("claude");
42850
43129
  const args = [
42851
43130
  "-p",
42852
43131
  ...profile?.model ? ["--model", profile.model] : [],
42853
43132
  ...isClaude ? ["--output-format", "stream-json", "--verbose"] : [],
42854
- // Write-capable headless agent (ADR-0271): auto-approve tools so it never stalls on a prompt.
42855
- ...isClaude ? ["--permission-mode", "bypassPermissions"] : [],
42856
- ...cmd.includes("codex") ? ["--dangerously-bypass-approvals-and-sandbox"] : []
43133
+ // Secret-path deny rules (ADR-0347): reads are not confined to the clone, so the credential
43134
+ // dirs / cli profile / ssh / gh / git credentials are explicitly denied.
43135
+ ...denySettingsArgs(cmd, [...denyDirs, profile?.dir]),
43136
+ // Edit-only headless agent (ADR-0346): file edits are auto-accepted, everything else (Bash, web)
43137
+ // is denied, since the prompt carries untrusted ticket text. git/PR are done by this module.
43138
+ ...isClaude ? ["--permission-mode", "acceptEdits", "--disallowedTools", ...AGENT_DENIED_TOOLS] : [],
43139
+ ...cmd.includes("codex") ? ["--sandbox", "workspace-write"] : []
42857
43140
  ];
42858
- const env3 = {
42859
- ...process.env,
42860
- ...extraEnv,
42861
- ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
42862
- };
43141
+ const env3 = agentEnv(extraEnv, profile ? { CLAUDE_CONFIG_DIR: profile.dir } : void 0);
42863
43142
  const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
42864
43143
  const parser = createAiStreamParser(cmd);
42865
43144
  let out = "";
@@ -42878,7 +43157,7 @@ function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv) {
42878
43157
  child.on("close", (code) => {
42879
43158
  clearTimeout(timer);
42880
43159
  out += parser.flush();
42881
- resolve10({ code: code ?? -1, out, err, usage: parser.usage() });
43160
+ resolve11({ code: code ?? -1, out, err, usage: parser.usage() });
42882
43161
  });
42883
43162
  child.stdin.write(prompt);
42884
43163
  child.stdin.end();
@@ -42888,30 +43167,35 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
42888
43167
  const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
42889
43168
  let lastReason = "no attempt";
42890
43169
  for (let i = 0; i < attempts.length; i++) {
42891
- const { code, out, err, usage } = await runClaudeOnce2(ai2.cmd, attempts[i], prompt, cwd2, ai2.env);
42892
- if (code === 0) return usage;
43170
+ const { code, out, err, usage } = await runClaudeOnce2(ai2.cmd, attempts[i], prompt, cwd2, ai2.env, ai2.profiles.map((p2) => p2.dir));
43171
+ if (code === 0) return { usage, text: out.trim() };
42893
43172
  lastReason = `exited ${code}: ${(err || out).slice(0, 500)}`;
42894
43173
  if (i === attempts.length - 1) break;
42895
43174
  }
42896
43175
  throw new Error(`claude ${lastReason}`);
42897
43176
  }
43177
+ var MAX_OUTPUT_CHARS = 8e3;
42898
43178
  async function runFaqCompose(req, ai2) {
42899
43179
  if (!req.repo.token) return { prUrl: "", error: "no write token" };
42900
- const workDir = join24(tmpdir3(), "4pm-faq-sync", randomUUID3());
43180
+ const workDir = join24(tmpdir4(), "4pm-faq-sync", randomUUID2());
42901
43181
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
42902
- const authed = authedUrl2(req.repo.url, req.repo.token);
43182
+ const auth = gitAuthArgs("x-access-token", req.repo.token);
42903
43183
  try {
42904
43184
  mkdirSync7(workDir, { recursive: true });
42905
- await execFileAsync2("git", ["clone", "--depth", "1", "--branch", req.repo.branch, authed, workDir], { env: gitEnv });
43185
+ await execFileAsync2("git", [...auth, "clone", "--depth", "1", "--branch", req.repo.branch, req.repo.url, workDir], { env: gitEnv });
42906
43186
  await execFileAsync2("git", ["-C", workDir, "config", "user.name", "4PM FAQ Bot"], { env: gitEnv });
42907
43187
  await execFileAsync2("git", ["-C", workDir, "config", "user.email", "faq-bot@4pm.app"], { env: gitEnv });
42908
43188
  await execFileAsync2("git", ["-C", workDir, "checkout", "-b", req.headBranch], { env: gitEnv });
42909
- const usage = await runClaudeWithFailover2(buildPrompt4(req.tickets), workDir, ai2);
43189
+ const { usage, text } = await runClaudeWithFailover2(buildPrompt4(req.tickets, req.customPrompt), workDir, ai2);
43190
+ const output = text.slice(0, MAX_OUTPUT_CHARS);
43191
+ const perTicket = parsePerTicket(text);
42910
43192
  await execFileAsync2("git", ["-C", workDir, "add", "-A"], { env: gitEnv });
42911
43193
  const staged = await execFileAsync2("git", ["-C", workDir, "diff", "--cached", "--name-only"], { env: gitEnv });
42912
- if (!staged.stdout.trim()) return { prUrl: "", tokens: usage.tokens || estimateTokens2(""), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
43194
+ if (!staged.stdout.trim()) {
43195
+ return { prUrl: "", output, perTicket, tokens: usage.tokens || estimateTokens2(text), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
43196
+ }
42913
43197
  await execFileAsync2("git", ["-C", workDir, "commit", "-m", "docs(faq): synthesize from support tickets (4PM)"], { env: gitEnv });
42914
- await execFileAsync2("git", ["-C", workDir, "push", "-u", "origin", req.headBranch], { env: gitEnv });
43198
+ await execFileAsync2("git", [...auth, "-C", workDir, "push", "-u", "origin", req.headBranch], { env: gitEnv });
42915
43199
  let prUrl = "";
42916
43200
  try {
42917
43201
  const pr2 = await execFileAsync2(
@@ -42927,6 +43211,8 @@ async function runFaqCompose(req, ai2) {
42927
43211
  return {
42928
43212
  prUrl,
42929
43213
  branch: req.headBranch,
43214
+ output,
43215
+ perTicket,
42930
43216
  tokens: u2.tokens,
42931
43217
  ...usage.tokens > 0 ? { tokensBreakdown: { input: usage.input, output: usage.output, cacheRead: usage.cacheRead, cacheCreation: usage.cacheCreation } } : {},
42932
43218
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -43491,7 +43777,7 @@ async function removePackage(root, slug) {
43491
43777
 
43492
43778
  // src/core/graph.ts
43493
43779
  import { readFile as readFile14, readdir as readdir6 } from "fs/promises";
43494
- import { join as join29, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
43780
+ import { join as join29, relative as relative2, dirname as dirname10, resolve as resolve10, extname as extname2, basename as basename6 } from "path";
43495
43781
  var MAX_FILES = 2e3;
43496
43782
  var MAX_NODES = 3e3;
43497
43783
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -43546,7 +43832,7 @@ async function buildDocs(root) {
43546
43832
  for (const m2 of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
43547
43833
  const href = (m2[1] ?? "").split("#")[0].trim();
43548
43834
  if (!href || !href.endsWith(".md") || href.startsWith("http")) continue;
43549
- const target = relative2(root, resolve9(dirname10(f2), href)).split("\\").join("/");
43835
+ const target = relative2(root, resolve10(dirname10(f2), href)).split("\\").join("/");
43550
43836
  if (ids.has(target) && target !== rel) {
43551
43837
  const key = `${rel}->${target}`;
43552
43838
  if (!seen.has(key)) {
@@ -44178,6 +44464,7 @@ var WsClient = class {
44178
44464
  */
44179
44465
  aiMemory = "";
44180
44466
  sessionIdByKey = /* @__PURE__ */ new Map();
44467
+ sessionMetaByKey = /* @__PURE__ */ new Map();
44181
44468
  stopped = false;
44182
44469
  /** Resolver to wake the backoff sleep early (set while waiting) — for /reconnect. */
44183
44470
  wakeReconnect = null;
@@ -44271,6 +44558,7 @@ var WsClient = class {
44271
44558
  serverUrl: this.context.credential.serverUrl,
44272
44559
  startedAtMs: this.startedAtMs,
44273
44560
  sessionIdByKey: this.sessionIdByKey,
44561
+ sessionMetaByKey: this.sessionMetaByKey,
44274
44562
  get machineUsername() {
44275
44563
  return self.machineUsername;
44276
44564
  },
@@ -44383,14 +44671,14 @@ var WsClient = class {
44383
44671
  */
44384
44672
  awaitConnected(timeoutMs) {
44385
44673
  if (this.sessionKey && this.bus.status === "connected") return Promise.resolve(true);
44386
- return new Promise((resolve10) => {
44674
+ return new Promise((resolve11) => {
44387
44675
  const waiter = () => {
44388
44676
  clearTimeout(timer);
44389
- resolve10(true);
44677
+ resolve11(true);
44390
44678
  };
44391
44679
  const timer = setTimeout(() => {
44392
44680
  this.connectWaiters = this.connectWaiters.filter((w2) => w2 !== waiter);
44393
- resolve10(false);
44681
+ resolve11(false);
44394
44682
  }, timeoutMs);
44395
44683
  this.connectWaiters.push(waiter);
44396
44684
  });
@@ -44417,16 +44705,16 @@ var WsClient = class {
44417
44705
  this.forceReconnect = false;
44418
44706
  return Promise.resolve();
44419
44707
  }
44420
- return new Promise((resolve10) => {
44708
+ return new Promise((resolve11) => {
44421
44709
  const timer = setTimeout(() => {
44422
44710
  this.wakeReconnect = null;
44423
- resolve10();
44711
+ resolve11();
44424
44712
  }, ms2);
44425
44713
  this.wakeReconnect = () => {
44426
44714
  clearTimeout(timer);
44427
44715
  this.wakeReconnect = null;
44428
44716
  this.forceReconnect = false;
44429
- resolve10();
44717
+ resolve11();
44430
44718
  };
44431
44719
  });
44432
44720
  }
@@ -44480,7 +44768,7 @@ var WsClient = class {
44480
44768
  * One WS session: token → connect → handshake → handle messages until close.
44481
44769
  */
44482
44770
  connectOnce() {
44483
- return new Promise((resolve10, reject) => {
44771
+ return new Promise((resolve11, reject) => {
44484
44772
  void (async () => {
44485
44773
  this.sessionEstablished = false;
44486
44774
  this.bus.setStatus("connecting");
@@ -44576,7 +44864,7 @@ var WsClient = class {
44576
44864
  if (code === CLOSE_DRAINING) {
44577
44865
  this.bus.log(t("error.serverDraining"));
44578
44866
  }
44579
- resolve10();
44867
+ resolve11();
44580
44868
  });
44581
44869
  socket.on("error", (err) => {
44582
44870
  this.clearHeartbeat();
@@ -44688,7 +44976,7 @@ var WsClient = class {
44688
44976
  }
44689
44977
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
44690
44978
  const envelope = {
44691
- id: randomUUID4(),
44979
+ id: randomUUID3(),
44692
44980
  replyTo,
44693
44981
  channel,
44694
44982
  wsToken: this.wsToken,
@@ -44702,14 +44990,14 @@ var WsClient = class {
44702
44990
  * Used for quota.check before spawning an AI cli (ADR-0020).
44703
44991
  */
44704
44992
  request(channel, data) {
44705
- return new Promise((resolve10, reject) => {
44993
+ return new Promise((resolve11, reject) => {
44706
44994
  if (!this.socket || !this.sessionKey) {
44707
44995
  reject(new Error("Not connected to the server."));
44708
44996
  return;
44709
44997
  }
44710
44998
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
44711
44999
  const envelope = {
44712
- id: randomUUID4(),
45000
+ id: randomUUID3(),
44713
45001
  replyTo: null,
44714
45002
  channel,
44715
45003
  wsToken: this.wsToken,
@@ -44722,7 +45010,7 @@ var WsClient = class {
44722
45010
  }, 15e3);
44723
45011
  timer.unref?.();
44724
45012
  this.pending.set(envelope.id, {
44725
- resolve: (value) => resolve10(value),
45013
+ resolve: (value) => resolve11(value),
44726
45014
  timer
44727
45015
  });
44728
45016
  this.socket.send(JSON.stringify(envelope));
@@ -45098,7 +45386,7 @@ function releaseInstanceLock(profileDir2) {
45098
45386
 
45099
45387
  // src/core/session-bus.ts
45100
45388
  import { EventEmitter } from "events";
45101
- import { randomUUID as randomUUID5 } from "crypto";
45389
+ import { randomUUID as randomUUID4 } from "crypto";
45102
45390
  var REPLAY_CAP = 500;
45103
45391
  var SessionBus = class {
45104
45392
  emitter = new EventEmitter();
@@ -45129,7 +45417,7 @@ var SessionBus = class {
45129
45417
  /** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
45130
45418
  push(input) {
45131
45419
  const entry = {
45132
- id: randomUUID5(),
45420
+ id: randomUUID4(),
45133
45421
  level: "info",
45134
45422
  ...input,
45135
45423
  ts: Date.now()
@@ -50833,8 +51121,8 @@ var Ink = class {
50833
51121
  }
50834
51122
  }
50835
51123
  async waitUntilExit() {
50836
- this.exitPromise ||= new Promise((resolve10, reject) => {
50837
- this.resolveExitPromise = resolve10;
51124
+ this.exitPromise ||= new Promise((resolve11, reject) => {
51125
+ this.resolveExitPromise = resolve11;
50838
51126
  this.rejectExitPromise = reject;
50839
51127
  });
50840
51128
  return this.exitPromise;
@@ -51887,7 +52175,7 @@ async function runStart(profileDir2, profileName) {
51887
52175
  logger.error("ws.run.crash", { message: e.message, stack: e.stack });
51888
52176
  if (client.isStopped) return;
51889
52177
  bus.log(t("start.recovered", { message: e.message }), "error");
51890
- await new Promise((resolve10) => setTimeout(resolve10, 1e3));
52178
+ await new Promise((resolve11) => setTimeout(resolve11, 1e3));
51891
52179
  }
51892
52180
  }
51893
52181
  };
@@ -51933,7 +52221,7 @@ import { join as join38 } from "path";
51933
52221
  // src/core/control-client.ts
51934
52222
  import { connect as connect2 } from "net";
51935
52223
  function connectControl(socketPath, bus, token) {
51936
- return new Promise((resolve10, reject) => {
52224
+ return new Promise((resolve11, reject) => {
51937
52225
  const socket = connect2(socketPath);
51938
52226
  socket.setEncoding("utf8");
51939
52227
  let ready = false;
@@ -51986,7 +52274,7 @@ function connectControl(socketPath, bus, token) {
51986
52274
  bus.setTokens(frame.tokens);
51987
52275
  for (const entry of frame.transcript) bus.pushEntry(entry);
51988
52276
  ready = true;
51989
- resolve10({ info: frame.info, close });
52277
+ resolve11({ info: frame.info, close });
51990
52278
  break;
51991
52279
  case "transcript":
51992
52280
  bus.pushEntry(frame.entry);
@@ -52074,7 +52362,7 @@ async function runAutoRun(profileDir2, profileName) {
52074
52362
  initI18n(readProfileConfig(profileDir2).locale);
52075
52363
  const socketPath = join39(profileDir2, CONTROL_SOCKET_FILE);
52076
52364
  const token = readControlToken(profileDir2);
52077
- const code = await new Promise((resolve10) => {
52365
+ const code = await new Promise((resolve11) => {
52078
52366
  let settled = false;
52079
52367
  const done = (c) => {
52080
52368
  if (settled) return;
@@ -52084,7 +52372,7 @@ async function runAutoRun(profileDir2, profileName) {
52084
52372
  socket.destroy();
52085
52373
  } catch {
52086
52374
  }
52087
- resolve10(c);
52375
+ resolve11(c);
52088
52376
  };
52089
52377
  const socket = connect3(socketPath);
52090
52378
  socket.setEncoding("utf8");
@@ -52132,16 +52420,16 @@ import { spawn as spawn12 } from "child_process";
52132
52420
  import { mkdirSync as mkdirSync9 } from "fs";
52133
52421
  import { basename as basename8 } from "path";
52134
52422
  function runProviderLogin(cmd, envVar, dir2) {
52135
- return new Promise((resolve10) => {
52423
+ return new Promise((resolve11) => {
52136
52424
  const child = spawn12(cmd, [], {
52137
52425
  stdio: "inherit",
52138
52426
  env: { ...process.env, [envVar]: dir2 }
52139
52427
  });
52140
52428
  child.on("error", (err) => {
52141
52429
  console.error(t("aiLogin.launchFailed", { cmd, message: err.message }));
52142
- resolve10(1);
52430
+ resolve11(1);
52143
52431
  });
52144
- child.on("exit", (code) => resolve10(code ?? 0));
52432
+ child.on("exit", (code) => resolve11(code ?? 0));
52145
52433
  });
52146
52434
  }
52147
52435
  async function runAiLogin(profileDir2, profileName, aiFilter) {
@@ -52281,10 +52569,10 @@ function Picker({
52281
52569
  ] });
52282
52570
  }
52283
52571
  function selectFromList(items, title) {
52284
- return new Promise((resolve10) => {
52572
+ return new Promise((resolve11) => {
52285
52573
  let chosen = null;
52286
52574
  const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v2) => chosen = v2 }));
52287
- void app.waitUntilExit().then(() => resolve10(chosen));
52575
+ void app.waitUntilExit().then(() => resolve11(chosen));
52288
52576
  });
52289
52577
  }
52290
52578