@4pm/cli 1.23.0 → 1.25.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 +1001 -435
  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(resolve11, reject) {
5716
+ then: function(resolve12, reject) {
5717
5717
  wasAwaited = true;
5718
5718
  thenableResult.then(function(returnValue2) {
5719
5719
  popActScope(prevActScopeDepth);
5720
5720
  if (actScopeDepth === 0) {
5721
- recursivelyFlushAsyncActWork(returnValue2, resolve11, reject);
5721
+ recursivelyFlushAsyncActWork(returnValue2, resolve12, reject);
5722
5722
  } else {
5723
- resolve11(returnValue2);
5723
+ resolve12(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(resolve11, reject) {
5753
+ then: function(resolve12, reject) {
5754
5754
  if (ReactCurrentActQueue.current === null) {
5755
5755
  ReactCurrentActQueue.current = [];
5756
- recursivelyFlushAsyncActWork(returnValue, resolve11, reject);
5756
+ recursivelyFlushAsyncActWork(returnValue, resolve12, reject);
5757
5757
  } else {
5758
- resolve11(returnValue);
5758
+ resolve12(returnValue);
5759
5759
  }
5760
5760
  }
5761
5761
  };
5762
5762
  return _thenable;
5763
5763
  } else {
5764
5764
  var _thenable2 = {
5765
- then: function(resolve11, reject) {
5766
- resolve11(returnValue);
5765
+ then: function(resolve12, reject) {
5766
+ resolve12(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, resolve11, reject) {
5782
+ function recursivelyFlushAsyncActWork(returnValue, resolve12, 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
- resolve11(returnValue);
5791
+ resolve12(returnValue);
5792
5792
  } else {
5793
- recursivelyFlushAsyncActWork(returnValue, resolve11, reject);
5793
+ recursivelyFlushAsyncActWork(returnValue, resolve12, reject);
5794
5794
  }
5795
5795
  });
5796
5796
  } catch (error2) {
5797
5797
  reject(error2);
5798
5798
  }
5799
5799
  } else {
5800
- resolve11(returnValue);
5800
+ resolve12(returnValue);
5801
5801
  }
5802
5802
  }
5803
5803
  }
@@ -16203,9 +16203,9 @@ var require_react_reconciler_development = __commonJS({
16203
16203
  var node = row;
16204
16204
  while (node !== null) {
16205
16205
  if (node.tag === SuspenseComponent) {
16206
- var state = node.memoizedState;
16207
- if (state !== null) {
16208
- var dehydrated = state.dehydrated;
16206
+ var state2 = node.memoizedState;
16207
+ if (state2 !== null) {
16208
+ var dehydrated = state2.dehydrated;
16209
16209
  if (dehydrated === null || isSuspenseInstancePending(dehydrated) || isSuspenseInstanceFallback(dehydrated)) {
16210
16210
  return node;
16211
16211
  }
@@ -16549,8 +16549,8 @@ var require_react_reconciler_development = __commonJS({
16549
16549
  stores: null
16550
16550
  };
16551
16551
  }
16552
- function basicStateReducer(state, action) {
16553
- return typeof action === "function" ? action(state) : action;
16552
+ function basicStateReducer(state2, action) {
16553
+ return typeof action === "function" ? action(state2) : action;
16554
16554
  }
16555
16555
  function mountReducer(reducer, initialArg, init) {
16556
16556
  var hook = mountWorkInProgressHook();
@@ -18537,10 +18537,10 @@ var require_react_reconciler_development = __commonJS({
18537
18537
  }
18538
18538
  }
18539
18539
  }
18540
- var state = workInProgress2.memoizedState = instance.state !== null && instance.state !== void 0 ? instance.state : null;
18540
+ var state2 = workInProgress2.memoizedState = instance.state !== null && instance.state !== void 0 ? instance.state : null;
18541
18541
  adoptClassInstance(workInProgress2, instance);
18542
18542
  {
18543
- if (typeof ctor.getDerivedStateFromProps === "function" && state === null) {
18543
+ if (typeof ctor.getDerivedStateFromProps === "function" && state2 === null) {
18544
18544
  var componentName = getComponentNameFromType(ctor) || "Component";
18545
18545
  if (!didWarnAboutUninitializedState.has(componentName)) {
18546
18546
  didWarnAboutUninitializedState.add(componentName);
@@ -19463,8 +19463,8 @@ var require_react_reconciler_development = __commonJS({
19463
19463
  var _instance = workInProgress2.stateNode;
19464
19464
  var ctor = workInProgress2.type;
19465
19465
  var tempInstance = new ctor(workInProgress2.memoizedProps, _instance.context);
19466
- var state = tempInstance.state;
19467
- _instance.updater.enqueueSetState(_instance, state, null);
19466
+ var state2 = tempInstance.state;
19467
+ _instance.updater.enqueueSetState(_instance, state2, null);
19468
19468
  break;
19469
19469
  }
19470
19470
  case true: {
@@ -20239,8 +20239,8 @@ var require_react_reconciler_development = __commonJS({
20239
20239
  var node = firstChild;
20240
20240
  while (node !== null) {
20241
20241
  if (node.tag === SuspenseComponent) {
20242
- var state = node.memoizedState;
20243
- if (state !== null) {
20242
+ var state2 = node.memoizedState;
20243
+ if (state2 !== null) {
20244
20244
  scheduleSuspenseWorkOnFiber(node, renderLanes2, workInProgress2);
20245
20245
  }
20246
20246
  } else if (node.tag === SuspenseListComponent) {
@@ -20669,9 +20669,9 @@ var require_react_reconciler_development = __commonJS({
20669
20669
  }
20670
20670
  break;
20671
20671
  case SuspenseComponent: {
20672
- var state = workInProgress2.memoizedState;
20673
- if (state !== null) {
20674
- if (state.dehydrated !== null) {
20672
+ var state2 = workInProgress2.memoizedState;
20673
+ if (state2 !== null) {
20674
+ if (state2.dehydrated !== null) {
20675
20675
  pushSuspenseContext(workInProgress2, setDefaultShallowSuspenseContext(suspenseStackCursor.current));
20676
20676
  workInProgress2.flags |= DidCapture;
20677
20677
  return null;
@@ -32753,9 +32753,9 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
32753
32753
 
32754
32754
  // src/core/ws-client.ts
32755
32755
  init_wrapper();
32756
- import { randomUUID as randomUUID3 } from "crypto";
32756
+ import { randomUUID as randomUUID4 } from "crypto";
32757
32757
  import { basename as basename7, join as join32 } from "path";
32758
- import { existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
32758
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9 } from "fs";
32759
32759
 
32760
32760
  // vendor/@4pm/utils/helpers.ts
32761
32761
  function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
@@ -37017,6 +37017,39 @@ var CLI_SLASH_COMMANDS = [
37017
37017
  ];
37018
37018
  var CLI_SLASH_COMMAND_NAMES = CLI_SLASH_COMMANDS.map((c) => c.name);
37019
37019
 
37020
+ // vendor/@4pm/dto/git-credential.ts
37021
+ var hostSchema = external_exports.string().trim().toLowerCase().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+(:\d{1,5})?$/, "invalid host");
37022
+ var gitCredentialRepoInputSchema = external_exports.object({
37023
+ url: external_exports.string().trim().max(500),
37024
+ subdir: external_exports.string().trim().max(120).optional().default(""),
37025
+ primary: external_exports.boolean().optional().default(false)
37026
+ });
37027
+ var testGitCredentialRequestSchema = external_exports.object({
37028
+ repos: external_exports.array(gitCredentialRepoInputSchema).max(50).optional()
37029
+ });
37030
+ var putGitCredentialRequestSchema = external_exports.object({
37031
+ appId: external_exports.string().trim().regex(/^\d{1,20}$/, "App ID must be numeric"),
37032
+ /** PEM; omit to keep the stored key (required on first save — enforced server-side). */
37033
+ privateKey: external_exports.string().trim().max(16384).optional(),
37034
+ enterprise: external_exports.boolean().default(false),
37035
+ host: hostSchema.optional(),
37036
+ apiBaseUrl: external_exports.string().trim().url().startsWith("https://").max(500).nullable().optional(),
37037
+ caCert: external_exports.string().trim().max(32768).nullable().optional(),
37038
+ installationOverrides: external_exports.array(external_exports.object({ url: external_exports.string().trim().max(500), installationId: external_exports.string().trim().regex(/^\d{1,20}$/).nullable() })).max(50).optional(),
37039
+ /** The wizard's in-progress repos (else the project's spec / draft repos are used). */
37040
+ repos: external_exports.array(gitCredentialRepoInputSchema).max(50).optional()
37041
+ }).superRefine((v2, ctx) => {
37042
+ if (v2.enterprise && !v2.host) {
37043
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["host"], message: "host is required for GitHub Enterprise" });
37044
+ }
37045
+ });
37046
+ function githubHostKind(host) {
37047
+ const h = host.toLowerCase();
37048
+ if (h === "github.com") return "github";
37049
+ if (h.endsWith(".ghe.com")) return "ghe-cloud";
37050
+ return "ghes";
37051
+ }
37052
+
37020
37053
  // vendor/@4pm/validation/schemas.ts
37021
37054
  var passwordSchema = external_exports.string().min(8, "password: at least 8 characters").max(32, "password: at most 32 characters").regex(/[a-zA-Z]/, "password: needs at least 1 letter").regex(/[0-9]/, "password: needs at least 1 digit");
37022
37055
  var usernameSchema = external_exports.string().min(3).max(32).regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/, "username: invalid characters");
@@ -37531,116 +37564,12 @@ var userCardsQuerySchema = external_exports.object({
37531
37564
  // src/core/scaffold.ts
37532
37565
  import { cp, mkdir, mkdtemp, rm, writeFile } from "fs/promises";
37533
37566
  import { existsSync as existsSync4 } from "fs";
37534
- import { execFile as execFile2 } from "child_process";
37567
+ import { execFile } from "child_process";
37535
37568
  import { tmpdir as tmpdir2 } from "os";
37536
- import { dirname as dirname3, join as join6, resolve as resolve2 } from "path";
37569
+ import { dirname as dirname3, join as join6, resolve } from "path";
37537
37570
  import { fileURLToPath as fileURLToPath3 } from "url";
37538
- import { promisify as promisify2 } from "util";
37539
-
37540
- // src/core/ai-assist.ts
37541
- import { execFile } from "child_process";
37542
37571
  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
37573
37572
  var run = promisify(execFile);
37574
- function aiCli() {
37575
- return process.env.AI_CLI || "claude";
37576
- }
37577
- function aiArgs(cli, prompt) {
37578
- if (cli === "claude") return ["-p", prompt, "--output-format", "json", ...denySettingsArgs(cli, [process.env.CLAUDE_CONFIG_DIR])];
37579
- if (cli === "codex") return ["exec", "--json", prompt];
37580
- return ["-p", prompt];
37581
- }
37582
- function estimateTokens(prompt, output) {
37583
- return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
37584
- }
37585
- function parseAi(cli, stdout, prompt) {
37586
- if (cli === "claude") {
37587
- try {
37588
- const j2 = JSON.parse(stdout);
37589
- const text2 = (j2.result ?? "").trim();
37590
- const tokens = (j2.usage?.input_tokens ?? 0) + (j2.usage?.output_tokens ?? 0);
37591
- if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens(prompt, text2) };
37592
- } catch {
37593
- }
37594
- }
37595
- if (cli === "codex") {
37596
- try {
37597
- let text2 = "";
37598
- let tokens = 0;
37599
- for (const line of stdout.split("\n").filter(Boolean)) {
37600
- const obj = JSON.parse(line);
37601
- if (obj.text) text2 = obj.text;
37602
- else if (obj.message) text2 = obj.message;
37603
- if (obj.usage) {
37604
- tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
37605
- }
37606
- }
37607
- if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens(prompt, text2) };
37608
- } catch {
37609
- }
37610
- }
37611
- const text = stdout.trim();
37612
- return { text, tokens: estimateTokens(prompt, text) };
37613
- }
37614
- async function runAi(prompt) {
37615
- const cli = aiCli();
37616
- const { stdout } = await run(cli, aiArgs(cli, prompt), {
37617
- timeout: 12e4,
37618
- maxBuffer: 8 * 1024 * 1024
37619
- });
37620
- return parseAi(cli, stdout, prompt);
37621
- }
37622
- async function aiGenerate(prompt) {
37623
- const { text } = await runAi(prompt);
37624
- return text;
37625
- }
37626
- async function aiOutboundReview(input, allowedRepos, projectPath) {
37627
- const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
37628
- ${input}`;
37629
- const { text, tokens } = await runAi(prompt);
37630
- const start = text.indexOf("{");
37631
- const end = text.lastIndexOf("}");
37632
- if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
37633
- try {
37634
- const j2 = JSON.parse(text.slice(start, end + 1));
37635
- const reasons = Array.isArray(j2.reasons) ? j2.reasons.filter((r) => typeof r === "string") : [];
37636
- return { ok: j2.ok === true && reasons.length === 0, reasons, tokens };
37637
- } catch {
37638
- return { ok: false, reasons: ["ai_parse"], tokens };
37639
- }
37640
- }
37641
-
37642
- // src/core/scaffold.ts
37643
- var run2 = promisify2(execFile2);
37644
37573
  function reposOf(spec) {
37645
37574
  const repos = spec?.repos;
37646
37575
  return Array.isArray(repos) ? repos : [];
@@ -37662,7 +37591,7 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
37662
37591
  if (existsSync4(join6(root, dir2, ".git")) || existsSync4(join6(root, ".git", "modules", dir2))) {
37663
37592
  emit("submodule", `Submodule ${dir2} already present \u2014 updating\u2026`);
37664
37593
  try {
37665
- await run2("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37594
+ await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37666
37595
  } catch {
37667
37596
  }
37668
37597
  continue;
@@ -37676,11 +37605,11 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
37676
37605
  const b2 = (sub.branch ?? "").trim();
37677
37606
  if (b2 && createMissingBranch && !await ensureRemoteBranch(sub.url, b2, emit)) {
37678
37607
  try {
37679
- await run2("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 12e4 });
37680
- await run2("git", ["config", "-f", ".gitmodules", `submodule.${dir2}.branch`, b2], { cwd: root, timeout: 3e4 });
37608
+ await run("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 12e4 });
37609
+ await run("git", ["config", "-f", ".gitmodules", `submodule.${dir2}.branch`, b2], { cwd: root, timeout: 3e4 });
37681
37610
  try {
37682
- await run2("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
37683
- await run2("git", ["push", "-u", "origin", b2], { cwd: join6(root, dir2), timeout: 12e4 });
37611
+ await run("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
37612
+ await run("git", ["push", "-u", "origin", b2], { cwd: join6(root, dir2), timeout: 12e4 });
37684
37613
  } catch (pushErr) {
37685
37614
  const pmsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
37686
37615
  emit("git-branch-push-failed", `Submodule ${dir2}: branch "${b2}" created locally but the push failed: ${pmsg}`);
@@ -37695,13 +37624,13 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
37695
37624
  }
37696
37625
  }
37697
37626
  try {
37698
- await run2("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
37627
+ await run("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
37699
37628
  added++;
37700
37629
  } catch (err) {
37701
37630
  const msg = err instanceof Error ? err.message : String(err);
37702
37631
  if (/already exists|already registered|in the index/i.test(msg)) {
37703
37632
  try {
37704
- await run2("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37633
+ await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37705
37634
  added++;
37706
37635
  } catch {
37707
37636
  }
@@ -37720,18 +37649,18 @@ async function commitAndPushSubmodules(root, submodules, emit) {
37720
37649
  ];
37721
37650
  if (addPaths.length === 0) return;
37722
37651
  emit("submodule", "Committing .gitmodules\u2026");
37723
- await run2("git", ["add", ...addPaths], { cwd: root, timeout: 6e4 });
37724
- const staged = (await run2("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
37652
+ await run("git", ["add", ...addPaths], { cwd: root, timeout: 6e4 });
37653
+ const staged = (await run("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
37725
37654
  if (!staged) return;
37726
37655
  const commitArgs = ["commit", "-m", "chore: add git submodules (4PM)"];
37727
37656
  try {
37728
- await run2("git", commitArgs, { cwd: root, timeout: 6e4 });
37657
+ await run("git", commitArgs, { cwd: root, timeout: 6e4 });
37729
37658
  } catch {
37730
- await run2("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
37659
+ await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
37731
37660
  }
37732
37661
  emit("submodule", "Pushing .gitmodules to the primary remote\u2026");
37733
37662
  try {
37734
- await run2("git", ["push"], { cwd: root, timeout: 12e4 });
37663
+ await run("git", ["push"], { cwd: root, timeout: 12e4 });
37735
37664
  } catch (err) {
37736
37665
  const msg = err instanceof Error ? err.message : String(err);
37737
37666
  emit("submodule-push-failed", `Submodules committed locally but the push failed: ${msg}`);
@@ -37744,22 +37673,22 @@ function providerSlug(url) {
37744
37673
  }
37745
37674
  async function commitAndOpenPr(root, repo, emit) {
37746
37675
  emit("commit", "Committing the scaffolded project\u2026");
37747
- await run2("git", ["add", "-A"], { cwd: root, timeout: 6e4 });
37748
- const staged = (await run2("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
37676
+ await run("git", ["add", "-A"], { cwd: root, timeout: 6e4 });
37677
+ const staged = (await run("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
37749
37678
  if (!staged) {
37750
37679
  emit("commit", "Nothing to commit \u2014 skipping the pull request.");
37751
37680
  return;
37752
37681
  }
37753
37682
  const commitArgs = ["commit", "-m", "chore: scaffold project (4PM)"];
37754
37683
  try {
37755
- await run2("git", commitArgs, { cwd: root, timeout: 6e4 });
37684
+ await run("git", commitArgs, { cwd: root, timeout: 6e4 });
37756
37685
  } catch {
37757
- await run2("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
37686
+ await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
37758
37687
  }
37759
37688
  const branch = (repo.branch ?? "").trim();
37760
37689
  emit("push", "Pushing the scaffold branch\u2026");
37761
37690
  try {
37762
- await run2("git", ["push", "-u", "origin", ...branch ? [branch] : ["HEAD"]], { cwd: root, timeout: 12e4 });
37691
+ await run("git", ["push", "-u", "origin", ...branch ? [branch] : ["HEAD"]], { cwd: root, timeout: 12e4 });
37763
37692
  } catch (err) {
37764
37693
  const msg = err instanceof Error ? err.message : String(err);
37765
37694
  emit("push-failed", `Scaffold committed locally but the push failed: ${msg} \u2014 push it from the Git tab when credentials are available.`);
@@ -37773,7 +37702,7 @@ async function commitAndOpenPr(root, repo, emit) {
37773
37702
  emit("pr", `Opening a pull request via ${provider}\u2026`);
37774
37703
  try {
37775
37704
  const args = provider === "glab" ? ["mr", "create", "--fill", ...slug ? ["-R", slug] : []] : ["pr", "create", "--fill", ...slug ? ["-R", slug] : []];
37776
- const { stdout } = await run2(provider, args, { cwd: root, timeout: 12e4 });
37705
+ const { stdout } = await run(provider, args, { cwd: root, timeout: 12e4 });
37777
37706
  const link2 = stdout.trim().split(/\s+/).find((s3) => /^https?:\/\//.test(s3)) ?? stdout.trim().split("\n").pop() ?? "";
37778
37707
  emit("pr", `Pull request opened${link2 ? `: ${link2}` : "."}`);
37779
37708
  } catch (err) {
@@ -37787,7 +37716,7 @@ function cloneArgs(url, dest, branch) {
37787
37716
  }
37788
37717
  async function remoteHasBranch(url, branch) {
37789
37718
  try {
37790
- const { stdout } = await run2("git", ["ls-remote", "--heads", url, branch], { timeout: 3e4 });
37719
+ const { stdout } = await run("git", ["ls-remote", "--heads", url, branch], { timeout: 3e4 });
37791
37720
  return stdout.trim().length > 0;
37792
37721
  } catch {
37793
37722
  return false;
@@ -37798,9 +37727,9 @@ async function ensureRemoteBranch(url, branch, emit) {
37798
37727
  const tmp = await mkdtemp(join6(tmpdir2(), "4pm-branch-"));
37799
37728
  try {
37800
37729
  emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it from the default branch\u2026`);
37801
- await run2("git", ["clone", "--depth", "1", url, tmp], { timeout: 12e4 });
37802
- await run2("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
37803
- await run2("git", ["push", "-u", "origin", branch], { cwd: tmp, timeout: 12e4 });
37730
+ await run("git", ["clone", "--depth", "1", url, tmp], { timeout: 12e4 });
37731
+ await run("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
37732
+ await run("git", ["push", "-u", "origin", branch], { cwd: tmp, timeout: 12e4 });
37804
37733
  return true;
37805
37734
  } catch (err) {
37806
37735
  const msg = err instanceof Error ? err.message : String(err);
@@ -37813,9 +37742,9 @@ async function ensureRemoteBranch(url, branch, emit) {
37813
37742
  async function updateRepo(dir2, branch, emit) {
37814
37743
  const b2 = (branch ?? "").trim();
37815
37744
  emit("git", `Updating repository (fetch + ${b2 ? `checkout ${b2} + ` : ""}fast-forward pull)\u2026`);
37816
- await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
37817
- if (b2) await run2("git", ["checkout", b2], { cwd: dir2, timeout: 6e4 });
37818
- await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
37745
+ await run("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
37746
+ if (b2) await run("git", ["checkout", b2], { cwd: dir2, timeout: 6e4 });
37747
+ await run("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
37819
37748
  }
37820
37749
  async function provisionRepo(target, repo, emit, opts = {}) {
37821
37750
  const mode = opts.mode ?? "clone";
@@ -37823,7 +37752,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
37823
37752
  if (!existsSync4(join6(target, ".git"))) {
37824
37753
  emit("git", "Initializing repository\u2026");
37825
37754
  await mkdir(target, { recursive: true });
37826
- await run2("git", ["init"], { cwd: target, timeout: 2e4 });
37755
+ await run("git", ["init"], { cwd: target, timeout: 2e4 });
37827
37756
  }
37828
37757
  return;
37829
37758
  }
@@ -37833,7 +37762,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
37833
37762
  } else if (mode === "force") {
37834
37763
  emit("git", `Re-cloning ${repo.url} (force)\u2026`);
37835
37764
  await rm(target, { recursive: true, force: true });
37836
- await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
37765
+ await run("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
37837
37766
  } else {
37838
37767
  emit("git", "Repository already present \u2014 skipping clone.");
37839
37768
  }
@@ -37842,10 +37771,10 @@ async function provisionRepo(target, repo, emit, opts = {}) {
37842
37771
  const b2 = (repo.branch ?? "").trim();
37843
37772
  if (b2 && opts.createMissingBranch && !await remoteHasBranch(repo.url, b2)) {
37844
37773
  emit("git", `Cloning ${repo.url} (default branch) to create "${b2}"\u2026`);
37845
- await run2("git", ["clone", repo.url, target], { timeout: 12e4 });
37846
- await run2("git", ["checkout", "-b", b2], { cwd: target, timeout: 3e4 });
37774
+ await run("git", ["clone", repo.url, target], { timeout: 12e4 });
37775
+ await run("git", ["checkout", "-b", b2], { cwd: target, timeout: 3e4 });
37847
37776
  try {
37848
- await run2("git", ["push", "-u", "origin", b2], { cwd: target, timeout: 12e4 });
37777
+ await run("git", ["push", "-u", "origin", b2], { cwd: target, timeout: 12e4 });
37849
37778
  emit("git", `Created and pushed branch "${b2}".`);
37850
37779
  } catch (err) {
37851
37780
  const msg = err instanceof Error ? err.message : String(err);
@@ -37854,7 +37783,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
37854
37783
  return;
37855
37784
  }
37856
37785
  emit("git", `Cloning ${repo.url}\u2026`);
37857
- await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
37786
+ await run("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
37858
37787
  }
37859
37788
  async function ensureReposCloned(physicRoot, repos, emit) {
37860
37789
  const list = repos;
@@ -37867,27 +37796,27 @@ async function ensureReposCloned(physicRoot, repos, emit) {
37867
37796
  function sampleDir() {
37868
37797
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
37869
37798
  const here = dirname3(fileURLToPath3(import.meta.url));
37870
- const bundled = resolve2(here, "project-sample");
37871
- for (const candidate of [bundled, resolve2(here, "../project-sample"), resolve2(here, "../../project-sample")]) {
37799
+ const bundled = resolve(here, "project-sample");
37800
+ for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
37872
37801
  if (existsSync4(candidate)) return candidate;
37873
37802
  }
37874
37803
  return bundled;
37875
37804
  }
37876
- async function scaffoldProject(payload, profileDir2, onProgress) {
37805
+ async function scaffoldProject(payload, profileDir2, onProgress, ai2) {
37877
37806
  let lastStep = "start";
37878
37807
  const emit = (step, message) => {
37879
37808
  lastStep = step;
37880
37809
  onProgress?.({ projectId: payload.projectId, step, message });
37881
37810
  };
37882
37811
  try {
37883
- const root = process.env.SCAFFOLD_ROOT ? resolve2(process.env.SCAFFOLD_ROOT) : resolve2(profileDir2);
37812
+ const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
37884
37813
  const target = join6(root, payload.projectName);
37885
37814
  await mkdir(target, { recursive: true });
37886
37815
  const repo = singleRepo(reposOf(payload.spec));
37887
37816
  if (!repo) throw new Error("A project must declare one repo (ADR-0314).");
37888
37817
  emit("git", "Cloning repository\u2026");
37889
37818
  await provisionRepo(target, repo, emit, { createMissingBranch: true });
37890
- await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true });
37819
+ await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true, ai: ai2 });
37891
37820
  await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit, true);
37892
37821
  emit("version", "Writing .4pm/.4pm.json\u2026");
37893
37822
  await writeTemplateMarker(target);
@@ -37906,7 +37835,7 @@ async function scaffoldRepo(dir2, label, spec, emit, opts = {}) {
37906
37835
  if (spec) {
37907
37836
  emit("spec", `Writing project.spec.json into ${label}\u2026`);
37908
37837
  await writeFile(join6(dir2, "project.spec.json"), JSON.stringify(spec, null, 2), "utf8");
37909
- await aiInit(dir2, spec, emit);
37838
+ await aiInit(dir2, spec, emit, opts.ai);
37910
37839
  }
37911
37840
  }
37912
37841
  async function resetTemplateManagedClaude(dir2, label, emit) {
@@ -37932,7 +37861,7 @@ function readSpecField(spec, key) {
37932
37861
  }
37933
37862
  return typeof spec[key] === "string" ? spec[key] : "";
37934
37863
  }
37935
- async function aiInit(target, spec, emit) {
37864
+ async function aiInit(target, spec, emit, ai2) {
37936
37865
  const guideFile = readSpecField(spec, "ai_guide_file") === "AGENT.md" ? "AGENT.md" : "CLAUDE.md";
37937
37866
  const guideInstructions = readSpecField(spec, "ai_guide_instructions").trim();
37938
37867
  emit("ai-init", `AI init: subagents, README, ${guideFile}\u2026`);
@@ -37952,25 +37881,47 @@ ${(sa.description || "").trim()}
37952
37881
  }
37953
37882
  }
37954
37883
  const specJson = JSON.stringify(spec);
37955
- await generateFile(
37956
- join6(target, "README.md"),
37957
- `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
37884
+ let generate3 = null;
37885
+ let releaseSlot = () => void 0;
37886
+ if (ai2) {
37887
+ const slot = await ai2.acquireSlot(
37888
+ (q2) => emit("ai-queued", `Waiting for an AI run slot \u2014 ${q2.position} ahead (${q2.running}/${q2.limit} parallel runs in use)\u2026`)
37889
+ );
37890
+ if (slot.kind === "granted") {
37891
+ releaseSlot = () => slot.handle.release();
37892
+ generate3 = (prompt, label) => ai2.generate(prompt, target, label);
37893
+ if (slot.queued) emit("ai-init", `AI init: subagents, README, ${guideFile}\u2026`);
37894
+ } else {
37895
+ emit("ai-skipped", "No AI run slot freed up in time \u2014 AI init skipped; fallback content written.");
37896
+ }
37897
+ }
37898
+ try {
37899
+ await generateFile(
37900
+ generate3,
37901
+ join6(target, "README.md"),
37902
+ "README.md",
37903
+ `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
37958
37904
  ${specJson}`
37959
- );
37960
- await generateFile(
37961
- join6(target, guideFile),
37962
- `Write a ${guideFile} (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
37905
+ );
37906
+ await generateFile(
37907
+ generate3,
37908
+ join6(target, guideFile),
37909
+ guideFile,
37910
+ `Write a ${guideFile} (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
37963
37911
  ${specJson}` + (guideInstructions ? `
37964
37912
  Also incorporate these additional instructions/content:
37965
37913
  ${guideInstructions}
37966
37914
  ` : ""),
37967
- guideInstructions
37968
- );
37915
+ guideInstructions
37916
+ );
37917
+ } finally {
37918
+ releaseSlot();
37919
+ }
37969
37920
  }
37970
- async function generateFile(path, prompt, fallback) {
37921
+ async function generateFile(generate3, path, label, prompt, fallback) {
37971
37922
  try {
37972
- const text = await aiGenerate(prompt);
37973
- if (text.trim()) {
37923
+ const text = generate3 ? await generate3(prompt, label) : null;
37924
+ if (text && text.trim()) {
37974
37925
  await writeFile(path, text.trim() + "\n", "utf8");
37975
37926
  return;
37976
37927
  }
@@ -37978,21 +37929,21 @@ async function generateFile(path, prompt, fallback) {
37978
37929
  }
37979
37930
  if (fallback && fallback.trim()) await writeFile(path, fallback.trim() + "\n", "utf8");
37980
37931
  }
37981
- async function addProject(payload, profileDir2, onProgress) {
37932
+ async function addProject(payload, profileDir2, onProgress, ai2) {
37982
37933
  let lastStep = "start";
37983
37934
  const emit = (step, message) => {
37984
37935
  lastStep = step;
37985
37936
  onProgress?.({ projectId: payload.projectId, step, message });
37986
37937
  };
37987
37938
  try {
37988
- const root = process.env.SCAFFOLD_ROOT ? resolve2(process.env.SCAFFOLD_ROOT) : resolve2(profileDir2);
37939
+ const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
37989
37940
  const target = join6(root, payload.projectName);
37990
37941
  await mkdir(target, { recursive: true });
37991
37942
  const repo = singleRepo(payload.repos ?? []);
37992
37943
  const mode = payload.mode ?? "clone";
37993
37944
  if (repo) await provisionRepo(target, repo, emit, { mode });
37994
37945
  if (payload.scaffoldRepos && payload.scaffoldRepos.length > 0) {
37995
- await scaffoldRepo(target, payload.projectName, payload.spec, emit);
37946
+ await scaffoldRepo(target, payload.projectName, payload.spec, emit, { ai: ai2 });
37996
37947
  }
37997
37948
  await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit);
37998
37949
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
@@ -38372,6 +38323,9 @@ var WsChannels = {
38372
38323
  // server → cli (reply): manage the rented worker's ssh deploy key (ADR-0173) — generate
38373
38324
  // (ed25519, on the worker) / get / delete. The private key never leaves the worker.
38374
38325
  GIT_SSH_KEY: "git.ssh-key",
38326
+ // cli → server (reply): a short-lived GitHub-App installation token for the served project's repo,
38327
+ // per job scope; also the cli's "scope revoked" report (ADR-0356). The App key stays on the server.
38328
+ GIT_TOKEN: "git.token",
38375
38329
  PROJECT_CREATE: "project.create",
38376
38330
  PROJECT_ADD: "project.add",
38377
38331
  PROJECT_PROGRESS: "project.progress",
@@ -38396,6 +38350,9 @@ var WsChannels = {
38396
38350
  CLI_UPDATE_RESULT: "cli.update-result",
38397
38351
  // command lifecycle
38398
38352
  COMMAND_ANNOUNCE: "command.announce",
38353
+ // server → cli (reply): stop an AI run (ADR-0362) — queued ⇒ leave the run-slot queue, running ⇒ kill
38354
+ // the AI process with no failover; the cli then ends the stream with `command.output {done, cancelled}`.
38355
+ COMMAND_CANCEL: "command.cancel",
38399
38356
  // console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
38400
38357
  // transcript so the web Console renders it verbatim; gated by console.watch (server → cli:
38401
38358
  // whether a web viewer is attached) so an unwatched cli syncs nothing.
@@ -38411,6 +38368,8 @@ var WsChannels = {
38411
38368
  // command.dispatch({ ai:true }) → the profile-failover AI path (ADR-0100).
38412
38369
  // quota & usage (ADR-0020)
38413
38370
  QUOTA_CHECK: "quota.check",
38371
+ // cli → server (reply): per-org concurrent AI-run slot — acquire / renew / release (ADR-0359)
38372
+ RUN_SLOT: "run.slot",
38414
38373
  USAGE_REPORT: "usage.report",
38415
38374
  // cli → server: Claude subscription usage snapshot (5h / weekly — ADR-0072)
38416
38375
  MACHINE_USAGE: "machine.usage",
@@ -38509,6 +38468,12 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
38509
38468
  return JSON.parse(plaintext.toString("utf8"));
38510
38469
  }
38511
38470
 
38471
+ // vendor/@4pm/ws/messages/run-slot.ts
38472
+ var RUN_SLOT_RENEW_MS = 45e3;
38473
+ var RUN_SLOT_RETRY_MS = 3e3;
38474
+ var RUN_SLOT_MAX_WAIT_MS = 30 * 6e4;
38475
+ var COMMAND_CANCELLED_EXIT_CODE = 130;
38476
+
38512
38477
  // src/core/update-scheduler.ts
38513
38478
  import { spawn as spawn2 } from "child_process";
38514
38479
 
@@ -38625,8 +38590,8 @@ function resolveInstallTimeoutMs(sec) {
38625
38590
  return sec && sec > 0 ? sec * 1e3 : TOOL_INSTALL_TIMEOUT_DEFAULT_MS;
38626
38591
  }
38627
38592
  function sleep(ms2) {
38628
- return new Promise((resolve11) => {
38629
- const t2 = setTimeout(resolve11, ms2);
38593
+ return new Promise((resolve12) => {
38594
+ const t2 = setTimeout(resolve12, ms2);
38630
38595
  t2.unref?.();
38631
38596
  });
38632
38597
  }
@@ -38653,15 +38618,15 @@ function classifyToolFailure(code, out) {
38653
38618
  }
38654
38619
  return { reason: "other", retryable: false };
38655
38620
  }
38656
- function run3(cmd, args, onLine, timeoutMs = 18e4) {
38657
- return new Promise((resolve11) => {
38621
+ function run2(cmd, args, onLine, timeoutMs = 18e4) {
38622
+ return new Promise((resolve12) => {
38658
38623
  let out = "";
38659
38624
  let buf = "";
38660
38625
  let child = null;
38661
38626
  const done = (code) => {
38662
38627
  clearTimeout(timer);
38663
38628
  if (onLine && buf.trim()) onLine(buf.trim());
38664
- resolve11({ code, out });
38629
+ resolve12({ code, out });
38665
38630
  };
38666
38631
  const timer = setTimeout(() => {
38667
38632
  child?.kill();
@@ -38693,13 +38658,13 @@ function run3(cmd, args, onLine, timeoutMs = 18e4) {
38693
38658
  });
38694
38659
  }
38695
38660
  async function detectOne(cmd, versionArg) {
38696
- const { code, out } = await run3(cmd, [versionArg], void 0, 15e3);
38661
+ const { code, out } = await run2(cmd, [versionArg], void 0, 15e3);
38697
38662
  if (code !== 0) return null;
38698
38663
  const first = out.split("\n").map((l) => l.trim()).find(Boolean);
38699
38664
  return first ?? "";
38700
38665
  }
38701
38666
  async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
38702
- const { out } = await run3("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
38667
+ const { out } = await run2("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
38703
38668
  if (!out.trim()) return [];
38704
38669
  let deps = {};
38705
38670
  try {
@@ -38775,7 +38740,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
38775
38740
  return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
38776
38741
  }
38777
38742
  onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
38778
- const { code, out } = await run3(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
38743
+ const { code, out } = await run2(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
38779
38744
  if (code === 0) return { ok: true, exitCode: code };
38780
38745
  const detail = extractToolError(out);
38781
38746
  return { ok: false, exitCode: code, error: detail ? `${manager} exited ${code}: ${detail}` : `${manager} exited ${code}` };
@@ -38808,7 +38773,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
38808
38773
  for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
38809
38774
  const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
38810
38775
  onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
38811
- const { code, out } = await run3(manager, args, onLine, timeoutMs);
38776
+ const { code, out } = await run2(manager, args, onLine, timeoutMs);
38812
38777
  if (code === 0) return { ok: true };
38813
38778
  const { reason, retryable } = classifyToolFailure(code, out);
38814
38779
  if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
@@ -39044,11 +39009,11 @@ function getWorkingProfile(profileDir2, cmd) {
39044
39009
  return readState(profileDir2)[cmd] ?? null;
39045
39010
  }
39046
39011
  function setWorkingProfile(profileDir2, cmd, dir2) {
39047
- const state = readState(profileDir2);
39048
- if (state[cmd] === dir2) return;
39049
- state[cmd] = dir2;
39012
+ const state2 = readState(profileDir2);
39013
+ if (state2[cmd] === dir2) return;
39014
+ state2[cmd] = dir2;
39050
39015
  try {
39051
- writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
39016
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state2, null, 2), "utf8");
39052
39017
  } catch {
39053
39018
  }
39054
39019
  }
@@ -39057,11 +39022,11 @@ function getWorkingCredential(profileDir2) {
39057
39022
  return readState(profileDir2)[WORKING_CREDENTIAL_KEY] ?? null;
39058
39023
  }
39059
39024
  function setWorkingCredential(profileDir2, key) {
39060
- const state = readState(profileDir2);
39061
- if (state[WORKING_CREDENTIAL_KEY] === key) return;
39062
- state[WORKING_CREDENTIAL_KEY] = key;
39025
+ const state2 = readState(profileDir2);
39026
+ if (state2[WORKING_CREDENTIAL_KEY] === key) return;
39027
+ state2[WORKING_CREDENTIAL_KEY] = key;
39063
39028
  try {
39064
- writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
39029
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state2, null, 2), "utf8");
39065
39030
  } catch {
39066
39031
  }
39067
39032
  }
@@ -39070,20 +39035,20 @@ function getPinnedCredential(profileDir2) {
39070
39035
  return readState(profileDir2)[PINNED_CREDENTIAL_KEY] ?? null;
39071
39036
  }
39072
39037
  function setPinnedCredential(profileDir2, key) {
39073
- const state = readState(profileDir2);
39074
- if (state[PINNED_CREDENTIAL_KEY] === key) return;
39075
- state[PINNED_CREDENTIAL_KEY] = key;
39038
+ const state2 = readState(profileDir2);
39039
+ if (state2[PINNED_CREDENTIAL_KEY] === key) return;
39040
+ state2[PINNED_CREDENTIAL_KEY] = key;
39076
39041
  try {
39077
- writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
39042
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state2, null, 2), "utf8");
39078
39043
  } catch {
39079
39044
  }
39080
39045
  }
39081
39046
  function clearPinnedCredential(profileDir2) {
39082
- const state = readState(profileDir2);
39083
- if (!(PINNED_CREDENTIAL_KEY in state)) return;
39084
- delete state[PINNED_CREDENTIAL_KEY];
39047
+ const state2 = readState(profileDir2);
39048
+ if (!(PINNED_CREDENTIAL_KEY in state2)) return;
39049
+ delete state2[PINNED_CREDENTIAL_KEY];
39085
39050
  try {
39086
- writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
39051
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state2, null, 2), "utf8");
39087
39052
  } catch {
39088
39053
  }
39089
39054
  }
@@ -39092,6 +39057,36 @@ function clearPinnedCredential(profileDir2) {
39092
39057
  import { readFileSync as readFileSync8 } from "fs";
39093
39058
  import { homedir as homedir3 } from "os";
39094
39059
  import { basename, isAbsolute as isAbsolute2, join as join9 } from "path";
39060
+
39061
+ // src/utils/agent-deny.ts
39062
+ import { isAbsolute, resolve as resolve2 } from "path";
39063
+ var HOME_SECRET_PATTERNS = [
39064
+ "~/.4pm/**",
39065
+ "~/.ssh/**",
39066
+ "~/.config/gh/**",
39067
+ "~/.git-credentials",
39068
+ "~/.claude*",
39069
+ "~/.claude*/**"
39070
+ ];
39071
+ function absDirPattern(dir2) {
39072
+ const abs = (isAbsolute(dir2) ? dir2 : resolve2(dir2)).replace(/\\/g, "/").replace(/\/+$/, "");
39073
+ return `/${abs.startsWith("/") ? abs : `/${abs}`}/**`;
39074
+ }
39075
+ function agentDenyRules(profileDirs2 = []) {
39076
+ const patterns = new Set(HOME_SECRET_PATTERNS);
39077
+ for (const dir2 of profileDirs2) {
39078
+ if (dir2 && dir2.trim()) patterns.add(absDirPattern(dir2.trim()));
39079
+ }
39080
+ const rules = [];
39081
+ for (const p2 of patterns) rules.push(`Read(${p2})`, `Edit(${p2})`);
39082
+ return rules;
39083
+ }
39084
+ function denySettingsArgs(cmd, profileDirs2 = []) {
39085
+ if (!cmd.includes("claude")) return [];
39086
+ return ["--settings", JSON.stringify({ permissions: { deny: agentDenyRules(profileDirs2) } })];
39087
+ }
39088
+
39089
+ // src/utils/ai-cli.ts
39095
39090
  var REQUIRED_AI_ARGS = {
39096
39091
  claude: ["-p", "--output-format", "stream-json", "--verbose"],
39097
39092
  codex: ["exec", "--json"],
@@ -39438,16 +39433,16 @@ async function fetchUsage(token) {
39438
39433
  }
39439
39434
  }
39440
39435
  function runUsageCli(dir2, aiCli2) {
39441
- return new Promise((resolve11) => {
39436
+ return new Promise((resolve12) => {
39442
39437
  let out = "";
39443
39438
  let child = null;
39444
39439
  const finish = () => {
39445
39440
  clearTimeout(timer);
39446
- resolve11(out);
39441
+ resolve12(out);
39447
39442
  };
39448
39443
  const timer = setTimeout(() => {
39449
39444
  child?.kill();
39450
- resolve11(out);
39445
+ resolve12(out);
39451
39446
  }, 3e4);
39452
39447
  timer.unref?.();
39453
39448
  try {
@@ -39768,12 +39763,12 @@ function pruneCommandOutputByAge(days) {
39768
39763
 
39769
39764
  // src/core/support-answer.ts
39770
39765
  import { spawn as spawn4 } from "child_process";
39771
- import { execFile as execFile3 } from "child_process";
39766
+ import { execFile as execFile2 } from "child_process";
39772
39767
  import { createHash as createHash3 } from "crypto";
39773
39768
  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";
39774
39769
  import { join as join14 } from "path";
39775
39770
  import { tmpdir as tmpdir3 } from "os";
39776
- import { promisify as promisify3 } from "util";
39771
+ import { promisify as promisify2 } from "util";
39777
39772
 
39778
39773
  // src/core/ai-stream.ts
39779
39774
  function assistantText(ev) {
@@ -39878,7 +39873,7 @@ function aiProviderOf(cmd) {
39878
39873
  if (cmd.includes("antigravity")) return "antigravity";
39879
39874
  return "claude";
39880
39875
  }
39881
- function estimateTokens2(text) {
39876
+ function estimateTokens(text) {
39882
39877
  return Math.max(1, Math.ceil(text.length / 4));
39883
39878
  }
39884
39879
 
@@ -39934,9 +39929,10 @@ function gitAuthArgs(user, password) {
39934
39929
 
39935
39930
  // src/core/support-answer.ts
39936
39931
  var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
39937
- var execFileAsync = promisify3(execFile3);
39932
+ var execFileAsync = promisify2(execFile2);
39938
39933
  var MAX_CONTEXT_CHARS = 12e4;
39939
39934
  var ANSWER_TIMEOUT_MS = 12e4;
39935
+ var LEGAL_DRAFT_TIMEOUT_MS = 54e4;
39940
39936
  var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
39941
39937
  function kbRoot(profileDir2) {
39942
39938
  return join14(profileDir2, "support-kb");
@@ -40048,6 +40044,7 @@ function buildPrompt(docs, question, askerRole) {
40048
40044
  ].join("\n");
40049
40045
  }
40050
40046
  function buildDraftPrompt(docs, context, task) {
40047
+ if (task === "legal_draft") return buildLegalDraftPrompt(docs, context);
40051
40048
  const goal = task === "reply_draft" ? [
40052
40049
  "You are drafting a reply FROM the 4PM support team TO the customer in the support ticket below.",
40053
40050
  "Address the customer's latest message, using the whole conversation for context. Be accurate,",
@@ -40074,6 +40071,31 @@ function buildDraftPrompt(docs, context, task) {
40074
40071
  context
40075
40072
  ].join("\n");
40076
40073
  }
40074
+ function buildLegalDraftPrompt(docs, context) {
40075
+ return [
40076
+ "You are drafting the body of one of 4PM's legal documents (Terms of Service, Privacy Policy,",
40077
+ "Add-on Terms, Rented Machine Addendum, \u2026) for the platform admin to review and publish.",
40078
+ "Write or revise the WHOLE document body in the target locale given in the context. Keep the",
40079
+ "existing heading structure and numbering unless the admin's instructions say otherwise.",
40080
+ "When the target locale is not English and an English reference is given, follow its meaning closely.",
40081
+ "Rules:",
40082
+ "- Every {{placeholder}} (e.g. {{companyName}}) must be kept verbatim \u2014 never fill or rename it.",
40083
+ "- Describe plans, prices, upgrades, downgrades, cancellations, renewals, refunds and add-ons ONLY",
40084
+ " as stated in the PLAN CATALOG and BILLING FACTS below or the documentation; never invent",
40085
+ " commitments, prices or legal guarantees. Where something is unknown write `[TODO: \u2026]`.",
40086
+ "- Plain, precise legal English (or the target language); short numbered sections and bullet lists.",
40087
+ "- This is a draft, not legal advice; do not add a disclaimer about that inside the document.",
40088
+ "",
40089
+ "Output ONLY the document body in markdown \u2014 no title line, no preamble, no explanation, no code",
40090
+ "fences around the whole document.",
40091
+ "",
40092
+ "===== DOCUMENTATION =====",
40093
+ docs,
40094
+ "",
40095
+ "===== CONTEXT =====",
40096
+ context
40097
+ ].join("\n");
40098
+ }
40077
40099
  function imageExt(mime) {
40078
40100
  switch (mime) {
40079
40101
  case "image/png":
@@ -40115,8 +40137,8 @@ function parseModeratedAnswer(text) {
40115
40137
  }
40116
40138
  return { body: text.trim() };
40117
40139
  }
40118
- function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir, denyDirs = []) {
40119
- return new Promise((resolve11, reject) => {
40140
+ function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir, denyDirs = [], timeoutMs = ANSWER_TIMEOUT_MS) {
40141
+ return new Promise((resolve12, reject) => {
40120
40142
  const isClaude = cmd.includes("claude");
40121
40143
  const args = [
40122
40144
  "-p",
@@ -40147,7 +40169,7 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir, denyDirs = [])
40147
40169
  const timer = setTimeout(() => {
40148
40170
  child.kill("SIGKILL");
40149
40171
  reject(new Error("claude answer timed out"));
40150
- }, ANSWER_TIMEOUT_MS);
40172
+ }, timeoutMs);
40151
40173
  timer.unref();
40152
40174
  child.stdout.on("data", (d) => out += parser.push(d.toString()));
40153
40175
  child.stderr.on("data", (d) => err += d.toString());
@@ -40160,17 +40182,25 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv, imageDir, denyDirs = [])
40160
40182
  clearTimeout(timer);
40161
40183
  cleanup();
40162
40184
  out += parser.flush();
40163
- resolve11({ code: code ?? -1, out, err, usage: parser.usage() });
40185
+ resolve12({ code: code ?? -1, out, err, usage: parser.usage() });
40164
40186
  });
40165
40187
  child.stdin.write(prompt);
40166
40188
  child.stdin.end();
40167
40189
  });
40168
40190
  }
40169
- async function runClaudeWithFailover(prompt, ai2, imageDir) {
40191
+ async function runClaudeWithFailover(prompt, ai2, imageDir, timeoutMs = ANSWER_TIMEOUT_MS) {
40170
40192
  const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
40171
40193
  let lastReason = "no attempt";
40172
40194
  for (let i = 0; i < attempts.length; i++) {
40173
- const { code, out, err, usage } = await runClaudeOnce(ai2.cmd, attempts[i], prompt, ai2.env, imageDir, ai2.profiles.map((p2) => p2.dir));
40195
+ const { code, out, err, usage } = await runClaudeOnce(
40196
+ ai2.cmd,
40197
+ attempts[i],
40198
+ prompt,
40199
+ ai2.env,
40200
+ imageDir,
40201
+ ai2.profiles.map((p2) => p2.dir),
40202
+ timeoutMs
40203
+ );
40174
40204
  if (code === 0 && out.trim()) return { text: out.trim(), usage };
40175
40205
  const combined = err || out;
40176
40206
  lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
@@ -40191,11 +40221,12 @@ async function runSupportAnswer(req, ai2, profileDir2) {
40191
40221
  const docs = collectDocs(dir2);
40192
40222
  if (!docs.trim()) return { body: "", error: "KB repo has no documentation" };
40193
40223
  const prompt = req.task ? buildDraftPrompt(docs, question, req.task) : buildPrompt(docs, question, req.askerRole);
40194
- const { text, usage } = await runClaudeWithFailover(prompt, ai2, imageDir);
40224
+ const timeoutMs = req.task === "legal_draft" ? LEGAL_DRAFT_TIMEOUT_MS : ANSWER_TIMEOUT_MS;
40225
+ const { text, usage } = await runClaudeWithFailover(prompt, ai2, imageDir, timeoutMs);
40195
40226
  if (!text) return { body: "", error: "empty answer" };
40196
40227
  const { body, moderation } = req.task ? { body: text.trim(), moderation: void 0 } : parseModeratedAnswer(text);
40197
40228
  if (!body) return { body: "", error: "empty answer" };
40198
- const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
40229
+ const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens(body) };
40199
40230
  return {
40200
40231
  body,
40201
40232
  ...moderation ? { moderation } : {},
@@ -40244,9 +40275,9 @@ function reportToolResult(tool, ok, message) {
40244
40275
  // src/core/autonomous.ts
40245
40276
  import { readFile as readFile4, writeFile as writeFile4, chmod } from "fs/promises";
40246
40277
  import { readFileSync as readFileSync14 } from "fs";
40247
- import { execFile as execFile4, spawn as spawn5 } from "child_process";
40278
+ import { execFile as execFile3, spawn as spawn5 } from "child_process";
40248
40279
  import { join as join17 } from "path";
40249
- import { promisify as promisify4 } from "util";
40280
+ import { promisify as promisify3 } from "util";
40250
40281
 
40251
40282
  // src/core/autonomous-config.ts
40252
40283
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
@@ -40357,7 +40388,7 @@ function pushRecord(h, rec) {
40357
40388
  }
40358
40389
 
40359
40390
  // src/core/autonomous.ts
40360
- var run4 = promisify4(execFile4);
40391
+ var run3 = promisify3(execFile3);
40361
40392
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
40362
40393
  var AUTHORS_REL = ".claude/.autonomous.authors.json";
40363
40394
  var TICK_REL = ".claude/hooks/autonomous-tick.sh";
@@ -40421,17 +40452,17 @@ function tickScript(root) {
40421
40452
  }
40422
40453
  async function crontabList() {
40423
40454
  try {
40424
- const { stdout } = await run4("crontab", ["-l"], { timeout: 1e4 });
40455
+ const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
40425
40456
  return stdout;
40426
40457
  } catch {
40427
40458
  return "";
40428
40459
  }
40429
40460
  }
40430
40461
  async function crontabSet(content) {
40431
- return new Promise((resolve11) => {
40462
+ return new Promise((resolve12) => {
40432
40463
  const child = spawn5("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
40433
- child.on("error", () => resolve11(false));
40434
- child.on("close", (code) => resolve11(code === 0));
40464
+ child.on("error", () => resolve12(false));
40465
+ child.on("close", (code) => resolve12(code === 0));
40435
40466
  child.stdin.end(content.endsWith("\n") ? content : content + "\n");
40436
40467
  });
40437
40468
  }
@@ -40600,14 +40631,14 @@ function detectContainerized() {
40600
40631
  }
40601
40632
  }
40602
40633
  function canReach(host, port) {
40603
- return new Promise((resolve11) => {
40634
+ return new Promise((resolve12) => {
40604
40635
  const socket = connect({ host, port });
40605
40636
  let done = false;
40606
40637
  const finish = (ok) => {
40607
40638
  if (done) return;
40608
40639
  done = true;
40609
40640
  socket.destroy();
40610
- resolve11(ok);
40641
+ resolve12(ok);
40611
40642
  };
40612
40643
  socket.setTimeout(PROBE_TIMEOUT_MS);
40613
40644
  socket.once("connect", () => finish(true));
@@ -40660,32 +40691,177 @@ async function probeNetwork() {
40660
40691
  }
40661
40692
 
40662
40693
  // src/core/git-auth.ts
40663
- import { writeFileSync as writeFileSync8 } from "fs";
40664
- import { homedir as homedir4 } from "os";
40665
- import { join as join18 } from "path";
40666
- function tokenTarget(method) {
40667
- if (method === "gitlab-group-token") {
40668
- return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
40669
- }
40670
- if (method === "github-app") {
40671
- return { host: process.env.FOURPM_GIT_HOST || "github.com", user: "x-access-token" };
40694
+ import { chmodSync as chmodSync3, existsSync as existsSync12, mkdirSync as mkdirSync6, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
40695
+ import { request as httpsRequest } from "https";
40696
+ import { delimiter, dirname as dirname4, join as join18, resolve as resolve3 } from "path";
40697
+ var JOB_ID_ENV = "FOURPM_JOB_ID";
40698
+ var PROFILE_DIR_ENV = "FOURPM_PROFILE_DIR";
40699
+ var GIT_HOST_ENV = "FOURPM_GIT_HOST";
40700
+ var GIT_HOST_KIND_ENV = "FOURPM_GIT_HOST_KIND";
40701
+ var DEFAULT_SCOPE = "link";
40702
+ var HELPER_ENV_ENTRIES = 3;
40703
+ var DEFAULT_SCOPE_IDLE_MS = 10 * 60 * 1e3;
40704
+ var state = { host: null, transport: null, baseEnv: null, scopes: /* @__PURE__ */ new Map(), defaultTimer: null };
40705
+ function cliInvocation() {
40706
+ const quote = (s3) => `"${s3.replace(/(["\\$`])/g, "\\$1")}"`;
40707
+ const script = process.argv[1];
40708
+ return [process.execPath, ...process.execArgv, ...script ? [script] : []].map(quote).join(" ");
40709
+ }
40710
+ function helperCommand() {
40711
+ return `!${cliInvocation()} git-credential`;
40712
+ }
40713
+ function findRealGh(pathValue, shimDir) {
40714
+ for (const dir2 of pathValue.split(delimiter)) {
40715
+ if (!dir2 || dir2 === shimDir) continue;
40716
+ const candidate = join18(dir2, process.platform === "win32" ? "gh.exe" : "gh");
40717
+ if (existsSync12(candidate)) return candidate;
40672
40718
  }
40673
40719
  return null;
40674
40720
  }
40675
- function applyGitAuth(method) {
40676
- if (!method) return false;
40677
- const target = tokenTarget(method);
40678
- const token = process.env.FOURPM_GIT_TOKEN;
40679
- if (!target || !token) return false;
40680
- try {
40681
- const line = `https://${target.user}:${token}@${target.host}
40721
+ function shimScript(realGh, cli) {
40722
+ return `#!/bin/sh
40723
+ # 4PM gh shim (ADR-0356) \u2014 injects a short-lived GitHub-App token for the served project.
40724
+ if [ -z "$GH_TOKEN$GH_ENTERPRISE_TOKEN$GITHUB_TOKEN" ]; then
40725
+ T="$(${cli} git-token 2>/dev/null)"
40726
+ if [ -n "$T" ]; then
40727
+ case "$${GIT_HOST_KIND_ENV}" in
40728
+ ghes) export GH_HOST="$${GIT_HOST_ENV}" GH_ENTERPRISE_TOKEN="$T"; unset GH_TOKEN ;;
40729
+ ghe-cloud) export GH_HOST="$${GIT_HOST_ENV}" GH_TOKEN="$T"; unset GH_ENTERPRISE_TOKEN ;;
40730
+ *) export GH_TOKEN="$T"; unset GH_ENTERPRISE_TOKEN ;;
40731
+ esac
40732
+ fi
40733
+ fi
40734
+ exec "${realGh}" "$@"
40682
40735
  `;
40683
- writeFileSync8(join18(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
40736
+ }
40737
+ function clearEnv() {
40738
+ if (!state.baseEnv) return;
40739
+ const { count, path } = state.baseEnv;
40740
+ for (let i = count; i < count + HELPER_ENV_ENTRIES; i++) {
40741
+ delete process.env[`GIT_CONFIG_KEY_${i}`];
40742
+ delete process.env[`GIT_CONFIG_VALUE_${i}`];
40743
+ }
40744
+ if (count > 0) process.env.GIT_CONFIG_COUNT = String(count);
40745
+ else delete process.env.GIT_CONFIG_COUNT;
40746
+ process.env.PATH = path;
40747
+ delete process.env[GIT_HOST_ENV];
40748
+ delete process.env[GIT_HOST_KIND_ENV];
40749
+ state.baseEnv = null;
40750
+ }
40751
+ function configureGitAuth(method, host, profileDir2, transport) {
40752
+ try {
40753
+ const active = method === "github-app" && !!host;
40754
+ if (!active || state.host && state.host !== host) {
40755
+ if (state.host) void revokeAll();
40756
+ clearEnv();
40757
+ state.host = null;
40758
+ }
40759
+ if (!active) return false;
40760
+ state.transport = transport;
40761
+ if (state.host === host && state.baseEnv) return true;
40762
+ const baseCount = Number(process.env.GIT_CONFIG_COUNT ?? "0") || 0;
40763
+ state.baseEnv = { count: baseCount, path: process.env.PATH ?? "" };
40764
+ const prefix = `credential.https://${host}`;
40765
+ const entries2 = [
40766
+ [`${prefix}.helper`, ""],
40767
+ [`${prefix}.helper`, helperCommand()],
40768
+ [`${prefix}.useHttpPath`, "true"]
40769
+ ];
40770
+ entries2.forEach(([key, value], i) => {
40771
+ process.env[`GIT_CONFIG_KEY_${baseCount + i}`] = key;
40772
+ process.env[`GIT_CONFIG_VALUE_${baseCount + i}`] = value;
40773
+ });
40774
+ process.env.GIT_CONFIG_COUNT = String(baseCount + HELPER_ENV_ENTRIES);
40775
+ process.env[PROFILE_DIR_ENV] = resolve3(profileDir2);
40776
+ const script = process.argv[1];
40777
+ const devTsconfig = script ? resolve3(dirname4(script), "..", "tsconfig.json") : "";
40778
+ if (process.execArgv.some((a) => a.includes("tsx")) && devTsconfig && existsSync12(devTsconfig) && !process.env.TSX_TSCONFIG_PATH) {
40779
+ process.env.TSX_TSCONFIG_PATH = devTsconfig;
40780
+ }
40781
+ process.env[GIT_HOST_ENV] = host;
40782
+ process.env[GIT_HOST_KIND_ENV] = githubHostKind(host);
40783
+ if (process.platform !== "win32") {
40784
+ const shimDir = join18(profileDir2, "bin");
40785
+ const realGh = findRealGh(state.baseEnv.path, shimDir);
40786
+ if (realGh) {
40787
+ mkdirSync6(shimDir, { recursive: true });
40788
+ const shim = join18(shimDir, "gh");
40789
+ writeFileSync8(shim, shimScript(realGh, cliInvocation()), { encoding: "utf8", mode: 493 });
40790
+ chmodSync3(shim, 493);
40791
+ process.env.PATH = `${shimDir}${delimiter}${state.baseEnv.path}`;
40792
+ } else {
40793
+ rmSync6(join18(shimDir, "gh"), { force: true });
40794
+ }
40795
+ }
40796
+ state.host = host;
40797
+ logger.info("git.auth.github-app", { host });
40684
40798
  return true;
40685
- } catch {
40799
+ } catch (err) {
40800
+ logger.warn("git.auth.configure.failed", { error: String(err) });
40686
40801
  return false;
40687
40802
  }
40688
40803
  }
40804
+ function touchDefaultScope() {
40805
+ if (state.defaultTimer) clearTimeout(state.defaultTimer);
40806
+ state.defaultTimer = setTimeout(() => void endGitScope(DEFAULT_SCOPE), DEFAULT_SCOPE_IDLE_MS);
40807
+ state.defaultTimer.unref?.();
40808
+ }
40809
+ async function issueGitToken(scope, host, path) {
40810
+ try {
40811
+ if (!state.host || !state.transport || host.toLowerCase() !== state.host) return null;
40812
+ const s3 = scope || DEFAULT_SCOPE;
40813
+ const reply = await state.transport({ op: "issue", scope: s3, repo: { host, path } });
40814
+ if (!reply.token) return null;
40815
+ const list = state.scopes.get(s3) ?? [];
40816
+ if (!list.some((t2) => t2.token === reply.token)) list.push({ token: reply.token, apiBase: reply.apiBase ?? "" });
40817
+ state.scopes.set(s3, list);
40818
+ if (s3 === DEFAULT_SCOPE) touchDefaultScope();
40819
+ return reply.token;
40820
+ } catch (err) {
40821
+ logger.warn("git.token.issue.failed", { error: String(err) });
40822
+ return null;
40823
+ }
40824
+ }
40825
+ function revokeAtGithub(t2) {
40826
+ return new Promise((resolve12) => {
40827
+ try {
40828
+ const req = httpsRequest(
40829
+ new URL(`${t2.apiBase.replace(/\/+$/, "")}/installation/token`),
40830
+ {
40831
+ method: "DELETE",
40832
+ timeout: 1e4,
40833
+ headers: { Authorization: `token ${t2.token}`, Accept: "application/vnd.github+json", "User-Agent": "4pm-cli" }
40834
+ },
40835
+ (res) => {
40836
+ res.resume();
40837
+ res.on("end", () => resolve12());
40838
+ }
40839
+ );
40840
+ req.on("timeout", () => req.destroy());
40841
+ req.on("error", () => resolve12());
40842
+ req.end();
40843
+ } catch {
40844
+ resolve12();
40845
+ }
40846
+ });
40847
+ }
40848
+ async function endGitScope(scope) {
40849
+ const tokens = state.scopes.get(scope);
40850
+ if (!tokens) return;
40851
+ state.scopes.delete(scope);
40852
+ if (scope === DEFAULT_SCOPE && state.defaultTimer) {
40853
+ clearTimeout(state.defaultTimer);
40854
+ state.defaultTimer = null;
40855
+ }
40856
+ await Promise.all(tokens.filter((t2) => t2.apiBase).map((t2) => revokeAtGithub(t2)));
40857
+ try {
40858
+ await state.transport?.({ op: "revoked", scope });
40859
+ } catch {
40860
+ }
40861
+ }
40862
+ async function revokeAll() {
40863
+ await Promise.all([...state.scopes.keys()].map((s3) => endGitScope(s3)));
40864
+ }
40689
40865
 
40690
40866
  // src/core/ws-client/transcript.ts
40691
40867
  function toDtoEntry(e) {
@@ -40716,13 +40892,18 @@ import { randomUUID as randomCommandId } from "crypto";
40716
40892
  // src/core/executor.ts
40717
40893
  import { spawn as spawn6 } from "child_process";
40718
40894
  var KILL_GRACE_MS = 5e3;
40719
- async function runCommand(dispatch, emit, opts) {
40895
+ async function runCommand(dispatch, emitRaw, opts) {
40720
40896
  let seq = 0;
40897
+ const emit = (output) => {
40898
+ emitRaw(output);
40899
+ if (output.done) void endGitScope(dispatch.commandId);
40900
+ };
40721
40901
  let child;
40722
40902
  try {
40723
40903
  child = spawn6(dispatch.cmd, dispatch.args ?? [], {
40724
40904
  cwd: dispatch.path,
40725
- env: { ...process.env, ...dispatch.env },
40905
+ // FOURPM_JOB_ID scopes the git helper / gh shim tokens to this run (ADR-0356).
40906
+ env: { ...process.env, ...dispatch.env, [JOB_ID_ENV]: dispatch.commandId },
40726
40907
  shell: false
40727
40908
  });
40728
40909
  } catch (err) {
@@ -40759,9 +40940,20 @@ async function runCommand(dispatch, emit, opts) {
40759
40940
  hardKillTimer.unref?.();
40760
40941
  }, timeoutMs) : null;
40761
40942
  timeoutTimer?.unref?.();
40943
+ let cancelled = false;
40944
+ const onAbort = () => {
40945
+ if (cancelled) return;
40946
+ cancelled = true;
40947
+ child.kill("SIGTERM");
40948
+ hardKillTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
40949
+ hardKillTimer.unref?.();
40950
+ };
40951
+ if (opts?.signal?.aborted) onAbort();
40952
+ else opts?.signal?.addEventListener("abort", onAbort, { once: true });
40762
40953
  const clearTimers = () => {
40763
40954
  if (timeoutTimer) clearTimeout(timeoutTimer);
40764
40955
  if (hardKillTimer) clearTimeout(hardKillTimer);
40956
+ opts?.signal?.removeEventListener("abort", onAbort);
40765
40957
  };
40766
40958
  const batcher = new OutputBatcher({
40767
40959
  onFlush: (chunk, truncated) => {
@@ -40779,7 +40971,7 @@ async function runCommand(dispatch, emit, opts) {
40779
40971
  });
40780
40972
  child.stdout?.on("data", (data) => batcher.push(data.toString("utf8")));
40781
40973
  child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
40782
- await new Promise((resolve11) => {
40974
+ await new Promise((resolve12) => {
40783
40975
  child.on("close", (exitCode) => {
40784
40976
  clearTimers();
40785
40977
  batcher.flush();
@@ -40788,11 +40980,12 @@ async function runCommand(dispatch, emit, opts) {
40788
40980
  seq: seq++,
40789
40981
  chunk: "",
40790
40982
  done: true,
40791
- // 124 (the conventional timeout code) when we killed it for exceeding the limit, so the
40792
- // server settles the command as failed and the web surfaces the note instead of spinning.
40793
- exitCode: timedOut ? 124 : exitCode ?? -1
40983
+ // 130 when the run was stopped (ADR-0362); 124 (the conventional timeout code) when we killed it
40984
+ // for exceeding the limit, so the server settles the command as failed and the web surfaces the
40985
+ // note instead of spinning.
40986
+ exitCode: cancelled ? COMMAND_CANCELLED_EXIT_CODE : timedOut ? 124 : exitCode ?? -1
40794
40987
  });
40795
- resolve11();
40988
+ resolve12();
40796
40989
  });
40797
40990
  child.on("error", (err) => {
40798
40991
  clearTimers();
@@ -40804,7 +40997,7 @@ async function runCommand(dispatch, emit, opts) {
40804
40997
  done: true,
40805
40998
  exitCode: -1
40806
40999
  });
40807
- resolve11();
41000
+ resolve12();
40808
41001
  });
40809
41002
  });
40810
41003
  }
@@ -40826,24 +41019,25 @@ function isOutOfCredits(text) {
40826
41019
  text
40827
41020
  );
40828
41021
  }
40829
- function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
40830
- return new Promise((resolve11) => {
41022
+ function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs, signal) {
41023
+ return new Promise((resolve12) => {
40831
41024
  runCommand(
40832
41025
  { commandId, cmd, args, path: cwd2, env: env3 },
40833
41026
  (out) => {
40834
41027
  if (out.chunk) onChunk(out.chunk);
40835
- if (out.done) resolve11(out.exitCode ?? -1);
41028
+ if (out.done) resolve12(out.exitCode ?? -1);
40836
41029
  },
40837
- { stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
40838
- ).catch(() => resolve11(-1));
41030
+ { stdin, ...timeoutMs > 0 ? { timeoutMs } : {}, ...signal ? { signal } : {} }
41031
+ ).catch(() => resolve12(-1));
40839
41032
  });
40840
41033
  }
40841
- async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
41034
+ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0, signal) {
40842
41035
  const total = plan.attempts.length;
40843
41036
  let finalExit = -1;
40844
41037
  let lastCmd = plan.attempts[0]?.cmd ?? "claude";
40845
41038
  let lastCaptured = "";
40846
41039
  for (let i = 0; i < total; i++) {
41040
+ if (signal?.aborted) return cancelledResult();
40847
41041
  const attempt = plan.attempts[i];
40848
41042
  lastCmd = attempt.cmd;
40849
41043
  if (total > 1) handlers.onAttemptStart(attempt.label, i, total, attempt.cmd);
@@ -40856,17 +41050,18 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
40856
41050
  handlers.onChunk(text);
40857
41051
  }
40858
41052
  };
40859
- finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs);
41053
+ finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs, signal);
40860
41054
  const tail = parser.flush();
40861
41055
  if (tail) {
40862
41056
  captured += tail;
40863
41057
  handlers.onChunk(tail);
40864
41058
  }
41059
+ if (signal?.aborted) return cancelledResult();
40865
41060
  lastCaptured = captured;
40866
41061
  const apiErr = parser.apiError();
40867
41062
  if (finalExit === 0 && !apiErr.isError) {
40868
41063
  const usage = parser.usage();
40869
- if (usage.tokens === 0) usage.tokens = estimateTokens2(captured);
41064
+ if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
40870
41065
  reportToolResult(attempt.cmd, true);
40871
41066
  return {
40872
41067
  exitCode: 0,
@@ -40885,6 +41080,17 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
40885
41080
  reportToolResult(lastCmd, false, summarizeAiFailure(lastCaptured, finalExit));
40886
41081
  return { exitCode: finalExit, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
40887
41082
  }
41083
+ function cancelledResult() {
41084
+ return {
41085
+ exitCode: COMMAND_CANCELLED_EXIT_CODE,
41086
+ workedDir: null,
41087
+ workedKey: null,
41088
+ workedCmd: null,
41089
+ usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 },
41090
+ sessionId: "",
41091
+ cancelled: true
41092
+ };
41093
+ }
40888
41094
  function summarizeAiFailure(captured, exitCode) {
40889
41095
  if (isAuthFailure(captured)) return "Not logged in";
40890
41096
  if (isOutOfCredits(captured)) return "Out of usage credits";
@@ -40918,7 +41124,7 @@ function formatTimestampInZone(date, timeZone) {
40918
41124
  }
40919
41125
 
40920
41126
  // src/ui/slash-commands.ts
40921
- import { existsSync as existsSync12 } from "fs";
41127
+ import { existsSync as existsSync13 } from "fs";
40922
41128
  import { join as join19 } from "path";
40923
41129
  var CONFIG_KEYS = [
40924
41130
  "aiCli",
@@ -40981,7 +41187,7 @@ function runConfig(ctx) {
40981
41187
  ctx.print(t("slash.config.wroteDefault", { verb, path }));
40982
41188
  ctx.print(t("slash.config.addClaudeHint"));
40983
41189
  };
40984
- if (existsSync12(path)) {
41190
+ if (existsSync13(path)) {
40985
41191
  ctx.confirm(
40986
41192
  t("slash.config.replaceConfirm", { path }),
40987
41193
  () => writeDefaults(t("slash.config.verbReplaced"))
@@ -41043,8 +41249,8 @@ function runHistory(ctx) {
41043
41249
  ctx.print(t("slash.history.header", { n: recent.length, total: all.length }));
41044
41250
  recent.forEach((e, i) => {
41045
41251
  const time = new Date(e.startedAt).toLocaleTimeString();
41046
- const state = e.status === "done" ? "\u2713" : e.status === "failed" ? `\u2717 exit ${e.exitCode}` : t("slash.history.running");
41047
- ctx.print(` [${i + 1}] ${time} ${commandLine(e.cmd, e.args)} \xB7 ${state}`);
41252
+ const state2 = e.status === "done" ? "\u2713" : e.status === "failed" ? `\u2717 exit ${e.exitCode}` : t("slash.history.running");
41253
+ ctx.print(` [${i + 1}] ${time} ${commandLine(e.cmd, e.args)} \xB7 ${state2}`);
41048
41254
  });
41049
41255
  }
41050
41256
  function runOutput(ctx) {
@@ -41055,8 +41261,8 @@ function runOutput(ctx) {
41055
41261
  return;
41056
41262
  }
41057
41263
  const entry = all.slice().reverse()[n - 1];
41058
- const state = entry.exitCode != null ? ` (exit ${entry.exitCode})` : "";
41059
- ctx.print(t("slash.output.header", { n, cmd: commandLine(entry.cmd, entry.args), status: entry.status, state }));
41264
+ const state2 = entry.exitCode != null ? ` (exit ${entry.exitCode})` : "";
41265
+ ctx.print(t("slash.output.header", { n, cmd: commandLine(entry.cmd, entry.args), status: entry.status, state: state2 }));
41060
41266
  const out = readCommandOutput(entry.commandId);
41061
41267
  if (out == null) {
41062
41268
  ctx.print(t("slash.output.none"), "warn");
@@ -41351,7 +41557,7 @@ function buildPrompt2(input) {
41351
41557
  ].join("\n");
41352
41558
  }
41353
41559
  function runOnce(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
41354
- return new Promise((resolve11, reject) => {
41560
+ return new Promise((resolve12, reject) => {
41355
41561
  const args = [
41356
41562
  "-p",
41357
41563
  "--output-format",
@@ -41386,7 +41592,7 @@ function runOnce(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
41386
41592
  });
41387
41593
  child.on("close", (code) => {
41388
41594
  clearTimeout(timer);
41389
- resolve11({ code: code ?? -1, out, err });
41595
+ resolve12({ code: code ?? -1, out, err });
41390
41596
  });
41391
41597
  child.stdin.write(prompt);
41392
41598
  child.stdin.end();
@@ -41413,20 +41619,101 @@ async function runMemoryCompaction(ai2, cwd2, input) {
41413
41619
  }
41414
41620
 
41415
41621
  // src/core/git-commit-identity.ts
41416
- import { execFile as execFile5 } from "child_process";
41417
- import { promisify as promisify5 } from "util";
41418
- var run5 = promisify5(execFile5);
41622
+ import { execFile as execFile4 } from "child_process";
41623
+ import { promisify as promisify4 } from "util";
41624
+ var run4 = promisify4(execFile4);
41419
41625
  async function setCommitAuthor(cwd2, machineUsername) {
41420
41626
  if (!machineUsername) return;
41421
41627
  try {
41422
- await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
41628
+ await run4("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
41423
41629
  } catch {
41424
41630
  }
41425
41631
  }
41426
41632
 
41633
+ // src/core/ws-client/run-slot.ts
41634
+ var NO_SLOT = { release: () => void 0 };
41635
+ function sleep2(ms2, signal) {
41636
+ return new Promise((resolve12) => {
41637
+ if (signal?.aborted) return resolve12();
41638
+ const timer = setTimeout(done, ms2);
41639
+ function done() {
41640
+ clearTimeout(timer);
41641
+ signal?.removeEventListener("abort", done);
41642
+ resolve12();
41643
+ }
41644
+ signal?.addEventListener("abort", done, { once: true });
41645
+ });
41646
+ }
41647
+ function holdLease(ctx, lease) {
41648
+ const timer = setInterval(() => {
41649
+ ctx.request(WsChannels.RUN_SLOT, { op: "renew", lease }).catch(() => void 0);
41650
+ }, RUN_SLOT_RENEW_MS);
41651
+ timer.unref?.();
41652
+ let released = false;
41653
+ return {
41654
+ release: () => {
41655
+ if (released) return;
41656
+ released = true;
41657
+ clearInterval(timer);
41658
+ ctx.request(WsChannels.RUN_SLOT, { op: "release", lease }).catch(() => void 0);
41659
+ }
41660
+ };
41661
+ }
41662
+ async function acquireRunSlot(ctx, onQueued, signal) {
41663
+ const startedAt = Date.now();
41664
+ let ticket;
41665
+ let lastPosition = -1;
41666
+ let limit = 0;
41667
+ for (; ; ) {
41668
+ if (signal?.aborted) return leaveQueue(ctx, ticket);
41669
+ let reply;
41670
+ try {
41671
+ reply = await ctx.request(WsChannels.RUN_SLOT, { op: "acquire", ...ticket ? { ticket } : {} });
41672
+ } catch (err) {
41673
+ logger.warn("run.slot.unavailable", { error: err.message });
41674
+ return { kind: "granted", handle: NO_SLOT, queued: ticket !== void 0 };
41675
+ }
41676
+ if (!reply || !("granted" in reply)) return { kind: "granted", handle: NO_SLOT, queued: ticket !== void 0 };
41677
+ if (reply.granted) {
41678
+ const handle = reply.lease ? holdLease(ctx, reply.lease) : NO_SLOT;
41679
+ if (signal?.aborted) {
41680
+ handle.release();
41681
+ return { kind: "cancelled" };
41682
+ }
41683
+ return { kind: "granted", handle, queued: ticket !== void 0 };
41684
+ }
41685
+ ticket = reply.ticket;
41686
+ limit = reply.limit;
41687
+ if (reply.position !== lastPosition) {
41688
+ lastPosition = reply.position;
41689
+ onQueued({ position: reply.position, limit: reply.limit, running: reply.running });
41690
+ }
41691
+ if (Date.now() - startedAt >= RUN_SLOT_MAX_WAIT_MS) return { kind: "timeout", limit };
41692
+ await sleep2(reply.retryAfterMs > 0 ? reply.retryAfterMs : RUN_SLOT_RETRY_MS, signal);
41693
+ }
41694
+ }
41695
+ function leaveQueue(ctx, ticket) {
41696
+ if (ticket) ctx.request(WsChannels.RUN_SLOT, { op: "leave", ticket }).catch(() => void 0);
41697
+ return { kind: "cancelled" };
41698
+ }
41699
+
41427
41700
  // src/core/ws-client/command-dispatch.ts
41428
41701
  var MEMORY_SEED_HEADER = "CONTEXT MEMORY from earlier in this conversation (may span prior sessions/accounts). Use it as background; do not repeat it back unless relevant:";
41702
+ var activeRuns = /* @__PURE__ */ new Map();
41703
+ function cancelActiveRun(commandId) {
41704
+ const run11 = activeRuns.get(commandId);
41705
+ if (!run11) return { ok: false, state: "unknown" };
41706
+ run11.controller.abort();
41707
+ return { ok: true, state: run11.state };
41708
+ }
41429
41709
  function handleCommandChannels(ctx, message, payload) {
41710
+ if (message.channel === WsChannels.COMMAND_CANCEL) {
41711
+ const commandId = typeof payload.commandId === "string" ? payload.commandId : "";
41712
+ const reply = cancelActiveRun(commandId);
41713
+ logger.info("command.cancel", { commandId, ...reply });
41714
+ ctx.send(WsChannels.COMMAND_CANCEL, reply, message.id);
41715
+ return true;
41716
+ }
41430
41717
  if (message.channel !== WsChannels.COMMAND_DISPATCH) return false;
41431
41718
  const dispatch = payload;
41432
41719
  if (ctx.hasHandledCommand(dispatch.commandId)) {
@@ -41513,6 +41800,15 @@ async function runLocalCommand(ctx, input) {
41513
41800
  await runAiPrompt(ctx, prompt, commandId, "local");
41514
41801
  }
41515
41802
  async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
41803
+ const run11 = { controller: new AbortController(), state: "queued" };
41804
+ activeRuns.set(commandId, run11);
41805
+ try {
41806
+ await runAiPromptInner(ctx, prompt, commandId, origin, run11, oneShot, images, aiConfig, readOnly, bypass);
41807
+ } finally {
41808
+ activeRuns.delete(commandId);
41809
+ }
41810
+ }
41811
+ async function runAiPromptInner(ctx, prompt, commandId, origin, run11, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
41516
41812
  const config = readProfileConfig(ctx.profileDir);
41517
41813
  const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
41518
41814
  const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
@@ -41520,7 +41816,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
41520
41816
  const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
41521
41817
  const perPromptLimit = config.perPromptTokenLimit ?? 0;
41522
41818
  if (perPromptLimit > 0) {
41523
- const estimated = estimateTokens2(prompt);
41819
+ const estimated = estimateTokens(prompt);
41524
41820
  if (estimated > perPromptLimit) {
41525
41821
  rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
41526
41822
  return;
@@ -41677,11 +41973,52 @@ ${guardedPrompt}`;
41677
41973
  }
41678
41974
  };
41679
41975
  let result;
41976
+ let neverStarted = false;
41680
41977
  try {
41681
- result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs);
41978
+ const slot = await acquireRunSlot(
41979
+ ctx,
41980
+ ({ position, limit, running: running2 }) => {
41981
+ const text = `\u23F3 queued \u2014 ${position} ahead, ${running2}/${limit} parallel runs in use (plan limit)`;
41982
+ ctx.bus.push({ source: origin, kind: "log", text });
41983
+ ctx.send(WsChannels.COMMAND_OUTPUT, {
41984
+ commandId,
41985
+ seq: seq++,
41986
+ chunk: `${text}
41987
+ `,
41988
+ log: true,
41989
+ queue: { position, limit, running: running2 }
41990
+ });
41991
+ },
41992
+ run11.controller.signal
41993
+ );
41994
+ if (slot.kind === "timeout") {
41995
+ neverStarted = true;
41996
+ const text = `RUN_QUEUE_TIMEOUT: waited 30 min for a free run slot (plan limit ${slot.limit} parallel runs) \u2014 not run`;
41997
+ ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
41998
+ ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
41999
+ `, log: true });
42000
+ result = { exitCode: 1, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
42001
+ } else if (slot.kind === "cancelled") {
42002
+ neverStarted = true;
42003
+ result = { exitCode: COMMAND_CANCELLED_EXIT_CODE, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "", cancelled: true };
42004
+ } else {
42005
+ if (slot.queued) ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", log: true, queue: null });
42006
+ run11.state = "running";
42007
+ try {
42008
+ result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run11.controller.signal);
42009
+ } finally {
42010
+ slot.handle.release();
42011
+ }
42012
+ }
41682
42013
  } finally {
41683
42014
  ctx.bus.endBusy(plan.cmd);
41684
42015
  }
42016
+ if (result.cancelled) {
42017
+ const text = "\u25A0 stopped";
42018
+ ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
42019
+ ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
42020
+ `, log: true });
42021
+ }
41685
42022
  const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
41686
42023
  if (result.workedDir) {
41687
42024
  if (unified && result.workedKey) {
@@ -41696,14 +42033,16 @@ ${guardedPrompt}`;
41696
42033
  seq: seq++,
41697
42034
  chunk: "",
41698
42035
  done: true,
41699
- exitCode: result.exitCode
42036
+ exitCode: result.exitCode,
42037
+ // A stopped run (ADR-0362): the server settles it `cancelled` and the web applies nothing.
42038
+ ...result.cancelled ? { cancelled: true } : {}
41700
42039
  });
41701
42040
  finishCommand(commandId, result.exitCode, result.usage);
41702
42041
  ctx.send(WsChannels.COMMAND_HISTORY, {
41703
42042
  commandId,
41704
42043
  cmd: plan.cmd,
41705
42044
  args: markerArgs,
41706
- status: result.exitCode === 0 ? "done" : "failed",
42045
+ status: result.cancelled ? "cancelled" : result.exitCode === 0 ? "done" : "failed",
41707
42046
  exitCode: result.exitCode,
41708
42047
  tokens: result.usage.tokens,
41709
42048
  // The ai_tokens split for this run (ADR-0145); attached only when we metered real tokens.
@@ -41739,13 +42078,13 @@ ${guardedPrompt}`;
41739
42078
  });
41740
42079
  ctx.bus.addTokens?.(result.usage.tokens);
41741
42080
  }
41742
- ctx.reportUsage(events, profileLabel);
42081
+ if (!neverStarted) ctx.reportUsage(events, profileLabel);
41743
42082
  void ctx.pollUsage();
41744
42083
  ctx.bus.push({
41745
42084
  source: origin,
41746
42085
  kind: "exit",
41747
- text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
41748
- level: result.exitCode === 0 ? "info" : "error",
42086
+ text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : result.cancelled ? "\u25A0 stopped \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
42087
+ level: result.exitCode === 0 ? "info" : result.cancelled ? "warn" : "error",
41749
42088
  durationMs: Date.now() - aiStartedMs
41750
42089
  });
41751
42090
  if (memCfg.enabled && result.exitCode === 0) {
@@ -42222,11 +42561,11 @@ async function allAiProfilesExhausted(ctx, cfg) {
42222
42561
 
42223
42562
  // src/core/fs-browse.ts
42224
42563
  import { readdir as readdir3 } from "fs/promises";
42225
- import { resolve as resolve3, sep as sep2 } from "path";
42564
+ import { resolve as resolve4, sep as sep2 } from "path";
42226
42565
  async function listDir(path, root) {
42227
42566
  if (!root) return { path: "", entries: [] };
42228
- const base = resolve3(root);
42229
- const requested = resolve3(base, path && path.trim() ? path : ".");
42567
+ const base = resolve4(root);
42568
+ const requested = resolve4(base, path && path.trim() ? path : ".");
42230
42569
  const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
42231
42570
  try {
42232
42571
  const dirents = await readdir3(target, { withFileTypes: true });
@@ -42245,12 +42584,12 @@ async function listDir(path, root) {
42245
42584
 
42246
42585
  // src/core/fs-read.ts
42247
42586
  import { readFile as readFile6 } from "fs/promises";
42248
- import { resolve as resolve4, sep as sep3 } from "path";
42587
+ import { resolve as resolve5, sep as sep3 } from "path";
42249
42588
  var MAX_BYTES = 256 * 1024;
42250
42589
  function resolveInRoot(root, relPath) {
42251
42590
  if (!root) return null;
42252
- const base = resolve4(root);
42253
- const target = resolve4(base, relPath || ".");
42591
+ const base = resolve5(root);
42592
+ const target = resolve5(base, relPath || ".");
42254
42593
  return target === base || target.startsWith(base + sep3) ? target : null;
42255
42594
  }
42256
42595
  async function readWorkerFile(root, path) {
@@ -42270,12 +42609,12 @@ async function readWorkerFile(root, path) {
42270
42609
 
42271
42610
  // src/core/fs-write.ts
42272
42611
  import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
42273
- import { dirname as dirname4, resolve as resolve5, sep as sep4 } from "path";
42612
+ import { dirname as dirname5, resolve as resolve6, sep as sep4 } from "path";
42274
42613
  var MAX_BYTES2 = 2 * 1024 * 1024;
42275
42614
  function resolveInRoot2(root, relPath) {
42276
42615
  if (!root) return null;
42277
- const base = resolve5(root);
42278
- const target = resolve5(base, relPath || ".");
42616
+ const base = resolve6(root);
42617
+ const target = resolve6(base, relPath || ".");
42279
42618
  return target === base || target.startsWith(base + sep4) ? target : null;
42280
42619
  }
42281
42620
  async function writeWorkerFile(root, relPath, content) {
@@ -42288,7 +42627,7 @@ async function writeWorkerFile(root, relPath, content) {
42288
42627
  return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
42289
42628
  }
42290
42629
  try {
42291
- await mkdir4(dirname4(target), { recursive: true });
42630
+ await mkdir4(dirname5(target), { recursive: true });
42292
42631
  await writeFile5(target, content, "utf8");
42293
42632
  return { ok: true, path: target, bytes };
42294
42633
  } catch (err) {
@@ -42298,11 +42637,11 @@ async function writeWorkerFile(root, relPath, content) {
42298
42637
 
42299
42638
  // src/core/fs-mutate.ts
42300
42639
  import { mkdir as mkdir5, writeFile as writeFile6, rename, rm as rm3, stat as stat2 } from "fs/promises";
42301
- import { basename as basename3, dirname as dirname5, resolve as resolve6, sep as sep5 } from "path";
42640
+ import { basename as basename3, dirname as dirname6, resolve as resolve7, sep as sep5 } from "path";
42302
42641
  var MAX_BYTES3 = 2 * 1024 * 1024;
42303
42642
  function resolveInRoot3(root, rel) {
42304
- const base = resolve6(root);
42305
- const target = resolve6(base, rel || ".");
42643
+ const base = resolve7(root);
42644
+ const target = resolve7(base, rel || ".");
42306
42645
  return target === base || target.startsWith(base + sep5) ? target : null;
42307
42646
  }
42308
42647
  async function isDir(p2) {
@@ -42327,19 +42666,19 @@ async function mutateFs(root, req) {
42327
42666
  switch (req.op) {
42328
42667
  case "mkdir": {
42329
42668
  const target = resolveInRoot3(root, req.path ?? "");
42330
- if (!target || target === resolve6(root)) return escaped(req.path ?? "");
42669
+ if (!target || target === resolve7(root)) return escaped(req.path ?? "");
42331
42670
  await mkdir5(target, { recursive: true });
42332
42671
  return { ok: true, path: target };
42333
42672
  }
42334
42673
  case "create": {
42335
42674
  const target = resolveInRoot3(root, req.path ?? "");
42336
- if (!target || target === resolve6(root)) return escaped(req.path ?? "");
42675
+ if (!target || target === resolve7(root)) return escaped(req.path ?? "");
42337
42676
  if (await exists(target)) return { ok: false, path: target, error: "already exists" };
42338
42677
  const content = req.content ?? "";
42339
42678
  if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
42340
42679
  return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
42341
42680
  }
42342
- await mkdir5(dirname5(target), { recursive: true });
42681
+ await mkdir5(dirname6(target), { recursive: true });
42343
42682
  await writeFile6(target, content, "utf8");
42344
42683
  return { ok: true, path: target };
42345
42684
  }
@@ -42347,18 +42686,18 @@ async function mutateFs(root, req) {
42347
42686
  const from = resolveInRoot3(root, req.from ?? "");
42348
42687
  let to2 = resolveInRoot3(root, req.to ?? "");
42349
42688
  if (!from || !to2) return escaped(req.from ?? 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 ?? "");
42689
+ if (from === resolve7(root)) return { ok: false, path: from, error: "cannot move the project root" };
42690
+ if (await isDir(to2)) to2 = resolve7(to2, basename3(from));
42691
+ if (to2 !== resolve7(root) && !to2.startsWith(resolve7(root) + sep5)) return escaped(req.to ?? "");
42353
42692
  if (await exists(to2)) return { ok: false, path: to2, error: "destination already exists" };
42354
- await mkdir5(dirname5(to2), { recursive: true });
42693
+ await mkdir5(dirname6(to2), { recursive: true });
42355
42694
  await rename(from, to2);
42356
42695
  return { ok: true, path: to2 };
42357
42696
  }
42358
42697
  case "delete": {
42359
42698
  const target = resolveInRoot3(root, req.path ?? "");
42360
42699
  if (!target) return escaped(req.path ?? "");
42361
- if (target === resolve6(root)) return { ok: false, path: target, error: "cannot delete the project root" };
42700
+ if (target === resolve7(root)) return { ok: false, path: target, error: "cannot delete the project root" };
42362
42701
  await rm3(target, { recursive: true, force: true });
42363
42702
  return { ok: true, path: target };
42364
42703
  }
@@ -42372,11 +42711,11 @@ async function mutateFs(root, req) {
42372
42711
 
42373
42712
  // src/core/fs-transfer.ts
42374
42713
  import { mkdir as mkdir6, readFile as readFile7, stat as stat3, writeFile as writeFile7 } from "fs/promises";
42375
- import { basename as basename4, dirname as dirname6, extname, resolve as resolve7, sep as sep6 } from "path";
42714
+ import { basename as basename4, dirname as dirname7, extname, resolve as resolve8, sep as sep6 } from "path";
42376
42715
  function resolveInRoot4(root, relPath) {
42377
42716
  if (!root) return null;
42378
- const base = resolve7(root);
42379
- const target = resolve7(base, relPath || ".");
42717
+ const base = resolve8(root);
42718
+ const target = resolve8(base, relPath || ".");
42380
42719
  return target === base || target.startsWith(base + sep6) ? target : null;
42381
42720
  }
42382
42721
  var MIME_BY_EXT = {
@@ -42415,7 +42754,7 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
42415
42754
  return { ok: false, path: target, bytes: 0, error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
42416
42755
  }
42417
42756
  try {
42418
- await mkdir6(dirname6(target), { recursive: true });
42757
+ await mkdir6(dirname7(target), { recursive: true });
42419
42758
  await writeFile7(target, buf);
42420
42759
  return { ok: true, path: target, bytes: buf.byteLength };
42421
42760
  } catch (err) {
@@ -42491,14 +42830,14 @@ function handleFsChannels(ctx, message, payload) {
42491
42830
 
42492
42831
  // src/core/git-diff.ts
42493
42832
  import { readFile as readFile8 } from "fs/promises";
42494
- import { execFile as execFile6 } from "child_process";
42495
- import { basename as basename5, dirname as dirname7, resolve as resolve8, sep as sep7 } from "path";
42496
- import { promisify as promisify6 } from "util";
42497
- var run6 = promisify6(execFile6);
42833
+ import { execFile as execFile5 } from "child_process";
42834
+ import { basename as basename5, dirname as dirname8, resolve as resolve9, sep as sep7 } from "path";
42835
+ import { promisify as promisify5 } from "util";
42836
+ var run5 = promisify5(execFile5);
42498
42837
  function resolveInRoot5(root, relPath) {
42499
42838
  if (!root) return null;
42500
- const base = resolve8(root);
42501
- const target = resolve8(base, relPath || ".");
42839
+ const base = resolve9(root);
42840
+ const target = resolve9(base, relPath || ".");
42502
42841
  return target === base || target.startsWith(base + sep7) ? target : null;
42503
42842
  }
42504
42843
  function looksBinary(buf) {
@@ -42513,7 +42852,7 @@ async function gitDiff(root, path) {
42513
42852
  if (!target) {
42514
42853
  return { path, oldContent: "", newContent: "", isBinary: false };
42515
42854
  }
42516
- const dir2 = dirname7(target);
42855
+ const dir2 = dirname8(target);
42517
42856
  const base = basename5(target);
42518
42857
  let newBuf = null;
42519
42858
  try {
@@ -42522,7 +42861,7 @@ async function gitDiff(root, path) {
42522
42861
  }
42523
42862
  let oldBuf = null;
42524
42863
  try {
42525
- const { stdout } = await run6("git", ["show", `HEAD:./${base}`], {
42864
+ const { stdout } = await run5("git", ["show", `HEAD:./${base}`], {
42526
42865
  cwd: dir2,
42527
42866
  timeout: 15e3,
42528
42867
  maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
@@ -42552,12 +42891,12 @@ async function gitDiff(root, path) {
42552
42891
  }
42553
42892
 
42554
42893
  // src/core/git-env.ts
42555
- import { execFile as execFile7 } from "child_process";
42556
- import { promisify as promisify7 } from "util";
42557
- var run7 = promisify7(execFile7);
42894
+ import { execFile as execFile6 } from "child_process";
42895
+ import { promisify as promisify6 } from "util";
42896
+ var run6 = promisify6(execFile6);
42558
42897
  async function hasBinary(bin, args = ["--version"]) {
42559
42898
  try {
42560
- await run7(bin, args, { timeout: 5e3 });
42899
+ await run6(bin, args, { timeout: 5e3 });
42561
42900
  return true;
42562
42901
  } catch {
42563
42902
  return false;
@@ -42570,7 +42909,7 @@ async function checkGitEnv(provider) {
42570
42909
  let account = null;
42571
42910
  if (installed) {
42572
42911
  try {
42573
- const { stdout, stderr } = await run7(bin, ["auth", "status"], { timeout: 8e3 });
42912
+ const { stdout, stderr } = await run6(bin, ["auth", "status"], { timeout: 8e3 });
42574
42913
  const out = `${stdout}
42575
42914
  ${stderr}`;
42576
42915
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -42583,11 +42922,11 @@ ${stderr}`;
42583
42922
  }
42584
42923
 
42585
42924
  // src/core/git-ssh-key.ts
42586
- import { execFile as execFile8 } from "child_process";
42925
+ import { execFile as execFile7 } from "child_process";
42587
42926
  import { readFile as readFile9, rm as rm4, mkdir as mkdir7 } from "fs/promises";
42588
42927
  import { join as join21 } from "path";
42589
- import { promisify as promisify8 } from "util";
42590
- var run8 = promisify8(execFile8);
42928
+ import { promisify as promisify7 } from "util";
42929
+ var run7 = promisify7(execFile7);
42591
42930
  function keyPaths(profileDir2) {
42592
42931
  const dir2 = join21(profileDir2, ".ssh");
42593
42932
  const key = join21(dir2, "id_4pm");
@@ -42602,14 +42941,14 @@ async function readPublicKey(pub) {
42602
42941
  }
42603
42942
  async function fingerprintOf(pub) {
42604
42943
  try {
42605
- const { stdout } = await run8("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
42944
+ const { stdout } = await run7("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
42606
42945
  return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
42607
42946
  } catch {
42608
42947
  return null;
42609
42948
  }
42610
42949
  }
42611
42950
  async function configureGit(key) {
42612
- await run8(
42951
+ await run7(
42613
42952
  "git",
42614
42953
  ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
42615
42954
  { timeout: 5e3 }
@@ -42620,7 +42959,7 @@ async function generate(profileDir2) {
42620
42959
  await mkdir7(dir2, { recursive: true });
42621
42960
  await rm4(key, { force: true });
42622
42961
  await rm4(pub, { force: true });
42623
- await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
42962
+ await run7("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
42624
42963
  timeout: 15e3
42625
42964
  });
42626
42965
  await configureGit(key);
@@ -42630,7 +42969,7 @@ async function remove(profileDir2) {
42630
42969
  const { key, pub } = keyPaths(profileDir2);
42631
42970
  await rm4(key, { force: true });
42632
42971
  await rm4(pub, { force: true });
42633
- await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
42972
+ await run7("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
42634
42973
  () => void 0
42635
42974
  );
42636
42975
  return { publicKey: null, fingerprint: null };
@@ -42651,21 +42990,21 @@ async function manageSshKey(op, profileDir2) {
42651
42990
  }
42652
42991
 
42653
42992
  // src/core/git-history.ts
42654
- import { execFile as execFile9 } from "child_process";
42655
- import { resolve as resolve9, sep as sep8 } from "path";
42656
- import { promisify as promisify9 } from "util";
42657
- var run9 = promisify9(execFile9);
42993
+ import { execFile as execFile8 } from "child_process";
42994
+ import { resolve as resolve10, sep as sep8 } from "path";
42995
+ import { promisify as promisify8 } from "util";
42996
+ var run8 = promisify8(execFile8);
42658
42997
  var FS = "";
42659
42998
  var RS = "";
42660
42999
  function resolveRepo(root, repo) {
42661
43000
  if (!root) return null;
42662
- const base = resolve9(root);
42663
- const requested = resolve9(base, repo && repo.trim() ? repo : ".");
43001
+ const base = resolve10(root);
43002
+ const requested = resolve10(base, repo && repo.trim() ? repo : ".");
42664
43003
  return requested === base || requested.startsWith(base + sep8) ? requested : base;
42665
43004
  }
42666
43005
  async function git(cwd2, args) {
42667
43006
  try {
42668
- const { stdout } = await run9("git", args, {
43007
+ const { stdout } = await run8("git", args, {
42669
43008
  cwd: cwd2,
42670
43009
  timeout: 15e3,
42671
43010
  maxBuffer: 8 * 1024 * 1024
@@ -42677,7 +43016,7 @@ async function git(cwd2, args) {
42677
43016
  }
42678
43017
  async function gitRepos(root) {
42679
43018
  if (!root) return { repos: [] };
42680
- const base = resolve9(root);
43019
+ const base = resolve10(root);
42681
43020
  const top = (await git(base, ["rev-parse", "--show-toplevel"])).trim();
42682
43021
  if (!top || !(top === base || top.startsWith(base + sep8))) return { repos: [] };
42683
43022
  const repos = [];
@@ -42689,7 +43028,7 @@ async function gitRepos(root) {
42689
43028
  const m2 = line.trim().match(/^[-+U ]?[0-9a-f]{7,40}\s+(\S+)/);
42690
43029
  const subPath = m2?.[1];
42691
43030
  if (!subPath) continue;
42692
- const abs = resolve9(base, subPath);
43031
+ const abs = resolve10(base, subPath);
42693
43032
  if (!(abs === base || abs.startsWith(base + sep8))) continue;
42694
43033
  const remote = (await git(abs, ["remote", "get-url", "origin"])).trim();
42695
43034
  const branch = (await git(abs, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
@@ -42807,6 +43146,74 @@ function handleGitChannels(ctx, message, payload) {
42807
43146
  // src/core/ws-client/handlers/misc.ts
42808
43147
  import { join as join22 } from "path";
42809
43148
 
43149
+ // src/core/ai-assist.ts
43150
+ import { execFile as execFile9 } from "child_process";
43151
+ import { promisify as promisify9 } from "util";
43152
+ var run9 = promisify9(execFile9);
43153
+ function aiCli() {
43154
+ return process.env.AI_CLI || "claude";
43155
+ }
43156
+ function aiArgs(cli, prompt) {
43157
+ if (cli === "claude") return ["-p", prompt, "--output-format", "json", ...denySettingsArgs(cli, [process.env.CLAUDE_CONFIG_DIR])];
43158
+ if (cli === "codex") return ["exec", "--json", prompt];
43159
+ return ["-p", prompt];
43160
+ }
43161
+ function estimateTokens2(prompt, output) {
43162
+ return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
43163
+ }
43164
+ function parseAi(cli, stdout, prompt) {
43165
+ if (cli === "claude") {
43166
+ try {
43167
+ const j2 = JSON.parse(stdout);
43168
+ const text2 = (j2.result ?? "").trim();
43169
+ const tokens = (j2.usage?.input_tokens ?? 0) + (j2.usage?.output_tokens ?? 0);
43170
+ if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens2(prompt, text2) };
43171
+ } catch {
43172
+ }
43173
+ }
43174
+ if (cli === "codex") {
43175
+ try {
43176
+ let text2 = "";
43177
+ let tokens = 0;
43178
+ for (const line of stdout.split("\n").filter(Boolean)) {
43179
+ const obj = JSON.parse(line);
43180
+ if (obj.text) text2 = obj.text;
43181
+ else if (obj.message) text2 = obj.message;
43182
+ if (obj.usage) {
43183
+ tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
43184
+ }
43185
+ }
43186
+ if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens2(prompt, text2) };
43187
+ } catch {
43188
+ }
43189
+ }
43190
+ const text = stdout.trim();
43191
+ return { text, tokens: estimateTokens2(prompt, text) };
43192
+ }
43193
+ async function runAi(prompt) {
43194
+ const cli = aiCli();
43195
+ const { stdout } = await run9(cli, aiArgs(cli, prompt), {
43196
+ timeout: 12e4,
43197
+ maxBuffer: 8 * 1024 * 1024
43198
+ });
43199
+ return parseAi(cli, stdout, prompt);
43200
+ }
43201
+ async function aiOutboundReview(input, allowedRepos, projectPath) {
43202
+ const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
43203
+ ${input}`;
43204
+ const { text, tokens } = await runAi(prompt);
43205
+ const start = text.indexOf("{");
43206
+ const end = text.lastIndexOf("}");
43207
+ if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
43208
+ try {
43209
+ const j2 = JSON.parse(text.slice(start, end + 1));
43210
+ const reasons = Array.isArray(j2.reasons) ? j2.reasons.filter((r) => typeof r === "string") : [];
43211
+ return { ok: j2.ok === true && reasons.length === 0, reasons, tokens };
43212
+ } catch {
43213
+ return { ok: false, reasons: ["ai_parse"], tokens };
43214
+ }
43215
+ }
43216
+
42810
43217
  // src/core/outbound-review.ts
42811
43218
  function compilePatterns(sources, fallback) {
42812
43219
  const list = sources ?? fallback;
@@ -42918,8 +43325,74 @@ function handleMiscChannels(ctx, message, payload) {
42918
43325
  }
42919
43326
 
42920
43327
  // src/core/ws-client/handlers/project.ts
42921
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, renameSync, rmSync as rmSync6 } from "fs";
43328
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, renameSync, rmSync as rmSync7 } from "fs";
42922
43329
  import { join as join23 } from "path";
43330
+
43331
+ // src/core/ai-task.ts
43332
+ import { randomUUID as randomUUID2 } from "crypto";
43333
+ function createAiTaskRunner(ctx) {
43334
+ return {
43335
+ acquireSlot: (onQueued) => acquireRunSlot(ctx, onQueued),
43336
+ generate: (prompt, cwd2, label) => generate2(ctx, prompt, cwd2, label)
43337
+ };
43338
+ }
43339
+ async function generate2(ctx, prompt, cwd2, label) {
43340
+ const config = readProfileConfig(ctx.profileDir);
43341
+ const cmd = config.aiCli || "claude";
43342
+ const unified = isUnifiedConfig(config);
43343
+ const hint = unified ? {
43344
+ credential: getPinnedCredential(ctx.profileDir) ?? ((config.aiFailoverMode ?? "remember") === "remember" ? getWorkingCredential(ctx.profileDir) : null)
43345
+ } : { dir: getWorkingProfile(ctx.profileDir, cmd) };
43346
+ const plan = planAiRun(prompt, config, hint, /* @__PURE__ */ new Map(), true);
43347
+ const timeoutSec = config.projectAiRunTimeoutSec || config.aiRunTimeoutSec || 0;
43348
+ ctx.bus.push({ source: "system", kind: "aireq", text: `${plan.cmd} \u2039 ${label}` });
43349
+ let text = "";
43350
+ const handlers = {
43351
+ onChunk: (chunk) => {
43352
+ text += chunk;
43353
+ },
43354
+ onAttemptStart: (profile, index, total, attemptCmd) => {
43355
+ ctx.bus.push({ source: "system", kind: "log", text: `\u2192 trying ${attemptCmd} profile "${profile}" (${index + 1}/${total})\u2026` });
43356
+ if (index > 0) text = "";
43357
+ },
43358
+ onAttemptFail: (profile) => {
43359
+ ctx.bus.push({ source: "system", kind: "log", text: `profile "${profile}" failed \u2014 trying next`, level: "warn" });
43360
+ }
43361
+ };
43362
+ ctx.bus.startBusy(plan.cmd);
43363
+ let result;
43364
+ try {
43365
+ result = await runAiFailover(plan, `ai-task-${randomUUID2()}`, cwd2, handlers, timeoutSec * 1e3);
43366
+ } finally {
43367
+ ctx.bus.endBusy(plan.cmd);
43368
+ }
43369
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
43370
+ const events = [{ metric: UsageMetric.COMMANDS, amount: 1, occurredAt }];
43371
+ if (result.exitCode === 0 && result.usage.tokens > 0) {
43372
+ events.push({
43373
+ metric: UsageMetric.AI_TOKENS,
43374
+ amount: result.usage.tokens,
43375
+ occurredAt,
43376
+ inputTokens: result.usage.input,
43377
+ outputTokens: result.usage.output,
43378
+ cacheReadTokens: result.usage.cacheRead,
43379
+ cacheCreationTokens: result.usage.cacheCreation,
43380
+ provider: aiProviderOf(result.workedCmd ?? plan.cmd),
43381
+ authMode: resolveClaudeAuthMode(config)
43382
+ });
43383
+ }
43384
+ ctx.reportUsage(events, result.workedDir ? profileDisplayLabel(result.workedDir) : null);
43385
+ const ok = result.exitCode === 0 && text.trim().length > 0;
43386
+ ctx.bus.push({
43387
+ source: "system",
43388
+ kind: ok ? "aires" : "log",
43389
+ text: ok ? `${plan.cmd} \u203A ${label} \u2713` : `${label}: AI run failed (exit ${result.exitCode}) \u2014 using the fallback`,
43390
+ ...ok ? {} : { level: "warn" }
43391
+ });
43392
+ return ok ? text.trim() : null;
43393
+ }
43394
+
43395
+ // src/core/ws-client/handlers/project.ts
42923
43396
  function handleProjectChannels(ctx, message, payload) {
42924
43397
  switch (message.channel) {
42925
43398
  case WsChannels.PHYSIC_SYNC: {
@@ -42931,11 +43404,11 @@ function handleProjectChannels(ctx, message, payload) {
42931
43404
  const oldRoot = ctx.physicFolderPath(oldName);
42932
43405
  if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
42933
43406
  }
42934
- if (oldPath && oldName !== sync.newName && existsSync13(oldPath) && !existsSync13(newPath)) {
43407
+ if (oldPath && oldName !== sync.newName && existsSync14(oldPath) && !existsSync14(newPath)) {
42935
43408
  renameSync(oldPath, newPath);
42936
43409
  } else {
42937
- if (oldPath && oldName !== sync.newName && existsSync13(oldPath)) rmSync6(oldPath, { recursive: true, force: true });
42938
- mkdirSync6(newPath, { recursive: true });
43410
+ if (oldPath && oldName !== sync.newName && existsSync14(oldPath)) rmSync7(oldPath, { recursive: true, force: true });
43411
+ mkdirSync7(newPath, { recursive: true });
42939
43412
  }
42940
43413
  ctx.physicRoot = ctx.physicFolderPath(sync.newName);
42941
43414
  ctx.bus.setProject(sync.newName);
@@ -42948,7 +43421,7 @@ function handleProjectChannels(ctx, message, payload) {
42948
43421
  if (del.name) {
42949
43422
  const delRoot = ctx.physicFolderPath(del.name);
42950
43423
  if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
42951
- rmSync6(join23(ctx.profileDir, del.name), { recursive: true, force: true });
43424
+ rmSync7(join23(ctx.profileDir, del.name), { recursive: true, force: true });
42952
43425
  ctx.bus.log(t("project.folderDeleted", { name: del.name }));
42953
43426
  }
42954
43427
  void manageSshKey("delete", ctx.profileDir).catch(() => void 0);
@@ -42977,14 +43450,18 @@ function handleProjectChannels(ctx, message, payload) {
42977
43450
  void scaffoldProject(
42978
43451
  payload,
42979
43452
  ctx.profileDir,
42980
- (p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2)
43453
+ (p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2),
43454
+ // AI init on the standard AI path + one org run slot (ADR-0362).
43455
+ createAiTaskRunner(ctx)
42981
43456
  ).then((reply) => ctx.send(WsChannels.PROJECT_CREATE, reply, message.id));
42982
43457
  return true;
42983
43458
  case WsChannels.PROJECT_ADD:
42984
43459
  void addProject(
42985
43460
  payload,
42986
43461
  ctx.profileDir,
42987
- (p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2)
43462
+ (p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2),
43463
+ // An add-with-scaffold runs AI init too (ADR-0362).
43464
+ createAiTaskRunner(ctx)
42988
43465
  ).then((reply) => ctx.send(WsChannels.PROJECT_ADD, reply, message.id));
42989
43466
  return true;
42990
43467
  default:
@@ -43012,7 +43489,7 @@ Extra focus from the author: ${req.prompt}` : "";
43012
43489
  ].join("\n");
43013
43490
  }
43014
43491
  function runOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
43015
- return new Promise((resolve11, reject) => {
43492
+ return new Promise((resolve12, reject) => {
43016
43493
  const args = ["-p", ...profile?.model ? ["--model", profile.model] : [], ...denySettingsArgs(cmd, [...denyDirs, profile?.dir])];
43017
43494
  const env3 = {
43018
43495
  ...process.env,
@@ -43035,7 +43512,7 @@ function runOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
43035
43512
  });
43036
43513
  child.on("close", (code) => {
43037
43514
  clearTimeout(timer);
43038
- resolve11({ code: code ?? -1, out, err });
43515
+ resolve12({ code: code ?? -1, out, err });
43039
43516
  });
43040
43517
  child.stdin.write(prompt);
43041
43518
  child.stdin.end();
@@ -43066,8 +43543,8 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
43066
43543
 
43067
43544
  // src/core/faq-compose.ts
43068
43545
  import { spawn as spawn9, execFile as execFile10 } from "child_process";
43069
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, rmSync as rmSync7 } from "fs";
43070
- import { randomUUID as randomUUID2 } from "crypto";
43546
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, rmSync as rmSync8 } from "fs";
43547
+ import { randomUUID as randomUUID3 } from "crypto";
43071
43548
  import { join as join24 } from "path";
43072
43549
  import { tmpdir as tmpdir4 } from "os";
43073
43550
  import { promisify as promisify10 } from "util";
@@ -43136,7 +43613,7 @@ function parsePerTicket(text) {
43136
43613
  return [];
43137
43614
  }
43138
43615
  function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
43139
- return new Promise((resolve11, reject) => {
43616
+ return new Promise((resolve12, reject) => {
43140
43617
  const isClaude = cmd.includes("claude");
43141
43618
  const args = [
43142
43619
  "-p",
@@ -43169,7 +43646,7 @@ function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
43169
43646
  child.on("close", (code) => {
43170
43647
  clearTimeout(timer);
43171
43648
  out += parser.flush();
43172
- resolve11({ code: code ?? -1, out, err, usage: parser.usage() });
43649
+ resolve12({ code: code ?? -1, out, err, usage: parser.usage() });
43173
43650
  });
43174
43651
  child.stdin.write(prompt);
43175
43652
  child.stdin.end();
@@ -43189,11 +43666,11 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
43189
43666
  var MAX_OUTPUT_CHARS = 8e3;
43190
43667
  async function runFaqCompose(req, ai2) {
43191
43668
  if (!req.repo.token) return { prUrl: "", error: "no write token" };
43192
- const workDir = join24(tmpdir4(), "4pm-faq-sync", randomUUID2());
43669
+ const workDir = join24(tmpdir4(), "4pm-faq-sync", randomUUID3());
43193
43670
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
43194
43671
  const auth = gitAuthArgs("x-access-token", req.repo.token);
43195
43672
  try {
43196
- mkdirSync7(workDir, { recursive: true });
43673
+ mkdirSync8(workDir, { recursive: true });
43197
43674
  await execFileAsync2("git", [...auth, "clone", "--depth", "1", "--branch", req.repo.branch, req.repo.url, workDir], { env: gitEnv });
43198
43675
  await execFileAsync2("git", ["-C", workDir, "config", "user.name", "4PM FAQ Bot"], { env: gitEnv });
43199
43676
  await execFileAsync2("git", ["-C", workDir, "config", "user.email", "faq-bot@4pm.app"], { env: gitEnv });
@@ -43204,7 +43681,7 @@ async function runFaqCompose(req, ai2) {
43204
43681
  await execFileAsync2("git", ["-C", workDir, "add", "-A"], { env: gitEnv });
43205
43682
  const staged = await execFileAsync2("git", ["-C", workDir, "diff", "--cached", "--name-only"], { env: gitEnv });
43206
43683
  if (!staged.stdout.trim()) {
43207
- return { prUrl: "", output, perTicket, tokens: usage.tokens || estimateTokens2(text), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
43684
+ return { prUrl: "", output, perTicket, tokens: usage.tokens || estimateTokens(text), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
43208
43685
  }
43209
43686
  await execFileAsync2("git", ["-C", workDir, "commit", "-m", "docs(faq): synthesize from support tickets (4PM)"], { env: gitEnv });
43210
43687
  await execFileAsync2("git", [...auth, "-C", workDir, "push", "-u", "origin", req.headBranch], { env: gitEnv });
@@ -43233,9 +43710,9 @@ async function runFaqCompose(req, ai2) {
43233
43710
  logger.warn("faq.compose.failed", { error: String(err) });
43234
43711
  return { prUrl: "", error: String(err) };
43235
43712
  } finally {
43236
- if (existsSync14(workDir)) {
43713
+ if (existsSync15(workDir)) {
43237
43714
  try {
43238
- rmSync7(workDir, { recursive: true, force: true });
43715
+ rmSync8(workDir, { recursive: true, force: true });
43239
43716
  } catch {
43240
43717
  }
43241
43718
  }
@@ -43559,7 +44036,7 @@ async function writeSecrets(root, req) {
43559
44036
 
43560
44037
  // src/core/agent-tools.ts
43561
44038
  import { readFile as readFile12, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
43562
- import { dirname as dirname8, join as join27 } from "path";
44039
+ import { dirname as dirname9, join as join27 } from "path";
43563
44040
  var SHARED_REL = ".claude/settings.json";
43564
44041
  var LOCAL_REL = ".claude/settings.local.json";
43565
44042
  var SECRETS_DENY = "Read(./project.secrets.json)";
@@ -43590,7 +44067,7 @@ async function writeAgentTools(root, scope, permissions) {
43590
44067
  const next = normalizePermissions(permissions);
43591
44068
  if (scope === "shared" && !next.deny.includes(SECRETS_DENY)) next.deny = [...next.deny, SECRETS_DENY];
43592
44069
  obj.permissions = next;
43593
- await mkdir9(dirname8(path), { recursive: true });
44070
+ await mkdir9(dirname9(path), { recursive: true });
43594
44071
  await writeFile10(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
43595
44072
  return { ok: true };
43596
44073
  } catch (err) {
@@ -43601,7 +44078,7 @@ async function writeAgentTools(root, scope, permissions) {
43601
44078
  // src/core/packages.ts
43602
44079
  import { createHash as createHash4 } from "crypto";
43603
44080
  import { readFile as readFile13, writeFile as writeFile11, mkdir as mkdir10, readdir as readdir5, rm as rm6, stat as stat4 } from "fs/promises";
43604
- import { dirname as dirname9, join as join28, relative } from "path";
44081
+ import { dirname as dirname10, join as join28, relative } from "path";
43605
44082
  var AGENTS_REL2 = ".claude/agents";
43606
44083
  var SKILLS_REL2 = ".claude/skills";
43607
44084
  var MANIFEST_REL = ".claude/.4pm-packages.json";
@@ -43682,7 +44159,7 @@ async function readManifest(root) {
43682
44159
  }
43683
44160
  async function writeManifest(root, manifest) {
43684
44161
  const path = join28(root, MANIFEST_REL);
43685
- await mkdir10(dirname9(path), { recursive: true });
44162
+ await mkdir10(dirname10(path), { recursive: true });
43686
44163
  await writeFile11(path, JSON.stringify(manifest, null, 2), "utf8");
43687
44164
  }
43688
44165
  async function writeArtifactFiles(root, kind, slug, files) {
@@ -43708,7 +44185,7 @@ async function writeArtifactFiles(root, kind, slug, files) {
43708
44185
  for (const [rel, buf] of byPath) {
43709
44186
  if (rel.includes("..") || rel.startsWith("/")) continue;
43710
44187
  const dest = join28(base, rel);
43711
- await mkdir10(dirname9(dest), { recursive: true });
44188
+ await mkdir10(dirname10(dest), { recursive: true });
43712
44189
  await writeFile11(dest, buf);
43713
44190
  }
43714
44191
  }
@@ -43789,7 +44266,7 @@ async function removePackage(root, slug) {
43789
44266
 
43790
44267
  // src/core/graph.ts
43791
44268
  import { readFile as readFile14, readdir as readdir6 } from "fs/promises";
43792
- import { join as join29, relative as relative2, dirname as dirname10, resolve as resolve10, extname as extname2, basename as basename6 } from "path";
44269
+ import { join as join29, relative as relative2, dirname as dirname11, resolve as resolve11, extname as extname2, basename as basename6 } from "path";
43793
44270
  var MAX_FILES = 2e3;
43794
44271
  var MAX_NODES = 3e3;
43795
44272
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -43844,7 +44321,7 @@ async function buildDocs(root) {
43844
44321
  for (const m2 of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
43845
44322
  const href = (m2[1] ?? "").split("#")[0].trim();
43846
44323
  if (!href || !href.endsWith(".md") || href.startsWith("http")) continue;
43847
- const target = relative2(root, resolve10(dirname10(f2), href)).split("\\").join("/");
44324
+ const target = relative2(root, resolve11(dirname11(f2), href)).split("\\").join("/");
43848
44325
  if (ids.has(target) && target !== rel) {
43849
44326
  const key = `${rel}->${target}`;
43850
44327
  if (!seen.has(key)) {
@@ -43929,7 +44406,7 @@ async function buildGraph(root, mode) {
43929
44406
 
43930
44407
  // src/core/rag.ts
43931
44408
  import { readFile as readFile15, writeFile as writeFile12, statfs as statfs2, mkdir as mkdir11 } from "fs/promises";
43932
- import { existsSync as existsSync15 } from "fs";
44409
+ import { existsSync as existsSync16 } from "fs";
43933
44410
  import { execFile as execFile11, spawn as spawn10 } from "child_process";
43934
44411
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
43935
44412
  import { join as join30 } from "path";
@@ -44022,7 +44499,7 @@ async function ragStatus(root) {
44022
44499
  const modelPresent = modelName.length > 0;
44023
44500
  return {
44024
44501
  installed: fastembed && sqliteVec && modelPresent,
44025
- installing: existsSync15(join30(root, RAG_REL, ".installing")),
44502
+ installing: existsSync16(join30(root, RAG_REL, ".installing")),
44026
44503
  python: { found: py.found, version: py.version },
44027
44504
  deps: { pip, fastembed, sqliteVec },
44028
44505
  model: { present: modelPresent, name: modelName },
@@ -44033,7 +44510,7 @@ async function ragStatus(root) {
44033
44510
  };
44034
44511
  }
44035
44512
  async function readIndexMeta(root) {
44036
- const indexing = existsSync15(join30(root, RAG_REL, ".indexing"));
44513
+ const indexing = existsSync16(join30(root, RAG_REL, ".indexing"));
44037
44514
  try {
44038
44515
  const m2 = JSON.parse(await readFile15(join30(root, RAG_REL, ".index.json"), "utf8"));
44039
44516
  return {
@@ -44108,8 +44585,8 @@ async function ensureEngine(root) {
44108
44585
  async function ragReindex(root) {
44109
44586
  const py = await detectPython();
44110
44587
  if (!py.found) return { ok: false, started: false, error: "python3 not found" };
44111
- if (!existsSync15(join30(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
44112
- if (existsSync15(join30(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
44588
+ if (!existsSync16(join30(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
44589
+ if (existsSync16(join30(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
44113
44590
  await ensureEngine(root);
44114
44591
  const script = `
44115
44592
  set +e
@@ -44129,7 +44606,7 @@ rm -f "${RAG_REL}/.indexing"
44129
44606
  async function ragQuery(root, queryText, k2 = 8) {
44130
44607
  const py = await detectPython();
44131
44608
  if (!py.found) return { results: [], error: "python3 not found" };
44132
- if (!existsSync15(join30(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
44609
+ if (!existsSync16(join30(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
44133
44610
  await ensureEngine(root);
44134
44611
  try {
44135
44612
  const { stdout } = await run10(
@@ -44149,7 +44626,7 @@ async function ragInstall(root, model) {
44149
44626
  }
44150
44627
  const py = await detectPython();
44151
44628
  if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
44152
- if (existsSync15(join30(root, RAG_REL, ".installing"))) return { ok: true, started: true };
44629
+ if (existsSync16(join30(root, RAG_REL, ".installing"))) return { ok: true, started: true };
44153
44630
  try {
44154
44631
  await mkdir11(join30(root, RAG_REL), { recursive: true });
44155
44632
  } catch {
@@ -44178,7 +44655,7 @@ rm -f "${RAG_REL}/.installing"
44178
44655
  }
44179
44656
 
44180
44657
  // src/core/config-sync.ts
44181
- import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
44658
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
44182
44659
  import { join as join31 } from "path";
44183
44660
  var SERVER_MANAGED_KEYS = [
44184
44661
  "physicPath",
@@ -44197,7 +44674,7 @@ var SERVER_MANAGED_KEYS = [
44197
44674
  ];
44198
44675
  function readConfigText(profileDir2) {
44199
44676
  const path = join31(profileDir2, "config.json");
44200
- if (existsSync16(path)) {
44677
+ if (existsSync17(path)) {
44201
44678
  try {
44202
44679
  return readFileSync16(path, "utf8");
44203
44680
  } catch {
@@ -44683,14 +45160,14 @@ var WsClient = class {
44683
45160
  */
44684
45161
  awaitConnected(timeoutMs) {
44685
45162
  if (this.sessionKey && this.bus.status === "connected") return Promise.resolve(true);
44686
- return new Promise((resolve11) => {
45163
+ return new Promise((resolve12) => {
44687
45164
  const waiter = () => {
44688
45165
  clearTimeout(timer);
44689
- resolve11(true);
45166
+ resolve12(true);
44690
45167
  };
44691
45168
  const timer = setTimeout(() => {
44692
45169
  this.connectWaiters = this.connectWaiters.filter((w2) => w2 !== waiter);
44693
- resolve11(false);
45170
+ resolve12(false);
44694
45171
  }, timeoutMs);
44695
45172
  this.connectWaiters.push(waiter);
44696
45173
  });
@@ -44717,16 +45194,16 @@ var WsClient = class {
44717
45194
  this.forceReconnect = false;
44718
45195
  return Promise.resolve();
44719
45196
  }
44720
- return new Promise((resolve11) => {
45197
+ return new Promise((resolve12) => {
44721
45198
  const timer = setTimeout(() => {
44722
45199
  this.wakeReconnect = null;
44723
- resolve11();
45200
+ resolve12();
44724
45201
  }, ms2);
44725
45202
  this.wakeReconnect = () => {
44726
45203
  clearTimeout(timer);
44727
45204
  this.wakeReconnect = null;
44728
45205
  this.forceReconnect = false;
44729
- resolve11();
45206
+ resolve12();
44730
45207
  };
44731
45208
  });
44732
45209
  }
@@ -44780,7 +45257,7 @@ var WsClient = class {
44780
45257
  * One WS session: token → connect → handshake → handle messages until close.
44781
45258
  */
44782
45259
  connectOnce() {
44783
- return new Promise((resolve11, reject) => {
45260
+ return new Promise((resolve12, reject) => {
44784
45261
  void (async () => {
44785
45262
  this.sessionEstablished = false;
44786
45263
  this.bus.setStatus("connecting");
@@ -44822,7 +45299,12 @@ var WsClient = class {
44822
45299
  if (token.aiMemory && !this.aiMemory) this.aiMemory = token.aiMemory.text;
44823
45300
  this.outboundReviewEnabled = token.outboundReview?.enabled === true;
44824
45301
  this.restrictToFolder = token.aiRestrictToFolder === true;
44825
- applyGitAuth(token.gitAuth ?? null);
45302
+ configureGitAuth(
45303
+ token.gitAuth ?? null,
45304
+ token.gitAuthHost ?? null,
45305
+ this.context.profileDir,
45306
+ (req) => this.request(WsChannels.GIT_TOKEN, req)
45307
+ );
44826
45308
  this.machineUsername = token.machineUsername ?? "";
44827
45309
  this.servingRepos = token.repos ?? [];
44828
45310
  void this.ensureServingReposCloned();
@@ -44876,7 +45358,7 @@ var WsClient = class {
44876
45358
  if (code === CLOSE_DRAINING) {
44877
45359
  this.bus.log(t("error.serverDraining"));
44878
45360
  }
44879
- resolve11();
45361
+ resolve12();
44880
45362
  });
44881
45363
  socket.on("error", (err) => {
44882
45364
  this.clearHeartbeat();
@@ -44988,7 +45470,7 @@ var WsClient = class {
44988
45470
  }
44989
45471
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
44990
45472
  const envelope = {
44991
- id: randomUUID3(),
45473
+ id: randomUUID4(),
44992
45474
  replyTo,
44993
45475
  channel,
44994
45476
  wsToken: this.wsToken,
@@ -45002,14 +45484,14 @@ var WsClient = class {
45002
45484
  * Used for quota.check before spawning an AI cli (ADR-0020).
45003
45485
  */
45004
45486
  request(channel, data) {
45005
- return new Promise((resolve11, reject) => {
45487
+ return new Promise((resolve12, reject) => {
45006
45488
  if (!this.socket || !this.sessionKey) {
45007
45489
  reject(new Error("Not connected to the server."));
45008
45490
  return;
45009
45491
  }
45010
45492
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
45011
45493
  const envelope = {
45012
- id: randomUUID3(),
45494
+ id: randomUUID4(),
45013
45495
  replyTo: null,
45014
45496
  channel,
45015
45497
  wsToken: this.wsToken,
@@ -45022,7 +45504,7 @@ var WsClient = class {
45022
45504
  }, 15e3);
45023
45505
  timer.unref?.();
45024
45506
  this.pending.set(envelope.id, {
45025
- resolve: (value) => resolve11(value),
45507
+ resolve: (value) => resolve12(value),
45026
45508
  timer
45027
45509
  });
45028
45510
  this.socket.send(JSON.stringify(envelope));
@@ -45076,8 +45558,8 @@ var WsClient = class {
45076
45558
  /** Create the physic folder at an absolute path if missing (best-effort). */
45077
45559
  ensurePhysicFolderPath(folder) {
45078
45560
  try {
45079
- if (!existsSync17(folder)) {
45080
- mkdirSync8(folder, { recursive: true });
45561
+ if (!existsSync18(folder)) {
45562
+ mkdirSync9(folder, { recursive: true });
45081
45563
  this.bus.log(t("project.createdFolder", { folder }));
45082
45564
  }
45083
45565
  } catch (err) {
@@ -45106,7 +45588,7 @@ var WsClient = class {
45106
45588
  projects: [],
45107
45589
  version: CLI_VERSION,
45108
45590
  physicPath,
45109
- physicPathExists: physicPath ? existsSync17(physicPath) : void 0,
45591
+ physicPathExists: physicPath ? existsSync18(physicPath) : void 0,
45110
45592
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
45111
45593
  autonomousRunning: physicPath ? isAutonomousRunning(physicPath, this.context.profileDir) : false,
45112
45594
  // The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
@@ -45365,7 +45847,7 @@ var WsClient = class {
45365
45847
  };
45366
45848
 
45367
45849
  // src/core/instance-lock.ts
45368
- import { readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
45850
+ import { readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
45369
45851
  import { join as join33 } from "path";
45370
45852
  function lockPath(profileDir2) {
45371
45853
  return join33(profileDir2, "start.lock");
@@ -45394,14 +45876,14 @@ function acquireInstanceLock(profileDir2) {
45394
45876
  function releaseInstanceLock(profileDir2) {
45395
45877
  try {
45396
45878
  const pid = Number.parseInt(readFileSync17(lockPath(profileDir2), "utf8").trim(), 10);
45397
- if (pid === process.pid) rmSync8(lockPath(profileDir2), { force: true });
45879
+ if (pid === process.pid) rmSync9(lockPath(profileDir2), { force: true });
45398
45880
  } catch {
45399
45881
  }
45400
45882
  }
45401
45883
 
45402
45884
  // src/core/session-bus.ts
45403
45885
  import { EventEmitter } from "events";
45404
- import { randomUUID as randomUUID4 } from "crypto";
45886
+ import { randomUUID as randomUUID5 } from "crypto";
45405
45887
  var REPLAY_CAP = 500;
45406
45888
  var SessionBus = class {
45407
45889
  emitter = new EventEmitter();
@@ -45432,7 +45914,7 @@ var SessionBus = class {
45432
45914
  /** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
45433
45915
  push(input) {
45434
45916
  const entry = {
45435
- id: randomUUID4(),
45917
+ id: randomUUID5(),
45436
45918
  level: "info",
45437
45919
  ...input,
45438
45920
  ts: Date.now()
@@ -45689,7 +46171,7 @@ var SessionBus = class {
45689
46171
 
45690
46172
  // src/core/control-server.ts
45691
46173
  import { createServer } from "net";
45692
- import { chmodSync as chmodSync4, rmSync as rmSync9 } from "fs";
46174
+ import { chmodSync as chmodSync5, rmSync as rmSync10 } from "fs";
45693
46175
  import { join as join35 } from "path";
45694
46176
 
45695
46177
  // src/core/control-protocol.ts
@@ -45719,7 +46201,7 @@ function createFrameParser() {
45719
46201
  }
45720
46202
 
45721
46203
  // src/core/control-token.ts
45722
- import { chmodSync as chmodSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
46204
+ import { chmodSync as chmodSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
45723
46205
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
45724
46206
  import { join as join34 } from "path";
45725
46207
  function controlTokenPath(profileDir2) {
@@ -45730,7 +46212,7 @@ function writeControlToken(profileDir2) {
45730
46212
  const path = controlTokenPath(profileDir2);
45731
46213
  writeFileSync10(path, token, { mode: 384 });
45732
46214
  try {
45733
- chmodSync3(path, 384);
46215
+ chmodSync4(path, 384);
45734
46216
  } catch {
45735
46217
  }
45736
46218
  return token;
@@ -45754,7 +46236,7 @@ function tokenMatches(expected, given) {
45754
46236
  function startControlServer(bus, profileDir2, info) {
45755
46237
  const socketPath = join35(profileDir2, CONTROL_SOCKET_FILE);
45756
46238
  try {
45757
- rmSync9(socketPath, { force: true });
46239
+ rmSync10(socketPath, { force: true });
45758
46240
  } catch {
45759
46241
  }
45760
46242
  const clients = /* @__PURE__ */ new Set();
@@ -45807,8 +46289,10 @@ function startControlServer(bus, profileDir2, info) {
45807
46289
  if (!authed) {
45808
46290
  if (frame.t === "auth" && tokenMatches(token, frame.token)) {
45809
46291
  authed = true;
45810
- clients.add(sock);
45811
- sendSnapshot(sock);
46292
+ if (!frame.rpc) {
46293
+ clients.add(sock);
46294
+ sendSnapshot(sock);
46295
+ }
45812
46296
  } else {
45813
46297
  send(sock, { t: "authError" });
45814
46298
  sock.destroy();
@@ -45818,6 +46302,9 @@ function startControlServer(bus, profileDir2, info) {
45818
46302
  if (frame.t === "submit") bus.submitLocal(frame.input);
45819
46303
  else if (frame.t === "reconnect") bus.requestReconnect();
45820
46304
  else if (frame.t === "autonomousRun") bus.submitAutonomous();
46305
+ else if (frame.t === "gitToken") {
46306
+ void issueGitToken(frame.scope, frame.host, frame.path).then((tok) => send(sock, { t: "gitToken", token: tok }));
46307
+ }
45821
46308
  }
45822
46309
  });
45823
46310
  const drop = () => {
@@ -45830,7 +46317,7 @@ function startControlServer(bus, profileDir2, info) {
45830
46317
  });
45831
46318
  server.listen(socketPath, () => {
45832
46319
  try {
45833
- chmodSync4(socketPath, 384);
46320
+ chmodSync5(socketPath, 384);
45834
46321
  } catch {
45835
46322
  }
45836
46323
  });
@@ -45840,8 +46327,8 @@ function startControlServer(bus, profileDir2, info) {
45840
46327
  clients.clear();
45841
46328
  server.close();
45842
46329
  try {
45843
- rmSync9(socketPath, { force: true });
45844
- rmSync9(controlTokenPath(profileDir2), { force: true });
46330
+ rmSync10(socketPath, { force: true });
46331
+ rmSync10(controlTokenPath(profileDir2), { force: true });
45845
46332
  } catch {
45846
46333
  }
45847
46334
  };
@@ -47820,7 +48307,7 @@ var src_default = Yoga;
47820
48307
 
47821
48308
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
47822
48309
  var import_react_reconciler = __toESM(require_react_reconciler(), 1);
47823
- var import_constants11 = __toESM(require_constants2(), 1);
48310
+ var import_constants12 = __toESM(require_constants2(), 1);
47824
48311
  import process3 from "process";
47825
48312
 
47826
48313
  // ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
@@ -49062,7 +49549,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
49062
49549
  scheduleTimeout: setTimeout,
49063
49550
  cancelTimeout: clearTimeout,
49064
49551
  noTimeout: -1,
49065
- getCurrentEventPriority: () => import_constants11.DefaultEventPriority,
49552
+ getCurrentEventPriority: () => import_constants12.DefaultEventPriority,
49066
49553
  beforeActiveInstanceBlur() {
49067
49554
  },
49068
49555
  afterActiveInstanceBlur() {
@@ -50923,24 +51410,24 @@ var App = class extends import_react9.PureComponent {
50923
51410
  })
50924
51411
  }));
50925
51412
  };
50926
- findNextFocusable = (state) => {
50927
- const activeIndex = state.focusables.findIndex((focusable) => {
50928
- return focusable.id === state.activeFocusId;
51413
+ findNextFocusable = (state2) => {
51414
+ const activeIndex = state2.focusables.findIndex((focusable) => {
51415
+ return focusable.id === state2.activeFocusId;
50929
51416
  });
50930
- for (let index = activeIndex + 1; index < state.focusables.length; index++) {
50931
- const focusable = state.focusables[index];
51417
+ for (let index = activeIndex + 1; index < state2.focusables.length; index++) {
51418
+ const focusable = state2.focusables[index];
50932
51419
  if (focusable?.isActive) {
50933
51420
  return focusable.id;
50934
51421
  }
50935
51422
  }
50936
51423
  return void 0;
50937
51424
  };
50938
- findPreviousFocusable = (state) => {
50939
- const activeIndex = state.focusables.findIndex((focusable) => {
50940
- return focusable.id === state.activeFocusId;
51425
+ findPreviousFocusable = (state2) => {
51426
+ const activeIndex = state2.focusables.findIndex((focusable) => {
51427
+ return focusable.id === state2.activeFocusId;
50941
51428
  });
50942
51429
  for (let index = activeIndex - 1; index >= 0; index--) {
50943
- const focusable = state.focusables[index];
51430
+ const focusable = state2.focusables[index];
50944
51431
  if (focusable?.isActive) {
50945
51432
  return focusable.id;
50946
51433
  }
@@ -51136,8 +51623,8 @@ var Ink = class {
51136
51623
  }
51137
51624
  }
51138
51625
  async waitUntilExit() {
51139
- this.exitPromise ||= new Promise((resolve11, reject) => {
51140
- this.resolveExitPromise = resolve11;
51626
+ this.exitPromise ||= new Promise((resolve12, reject) => {
51627
+ this.resolveExitPromise = resolve12;
51141
51628
  this.rejectExitPromise = reject;
51142
51629
  });
51143
51630
  return this.exitPromise;
@@ -51504,7 +51991,7 @@ var import_react21 = __toESM(require_react(), 1);
51504
51991
  var import_react26 = __toESM(require_react(), 1);
51505
51992
 
51506
51993
  // src/core/input-history.ts
51507
- import { existsSync as existsSync19, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
51994
+ import { existsSync as existsSync20, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
51508
51995
  import { join as join36 } from "path";
51509
51996
  var MAX_HISTORY = 200;
51510
51997
  function historyPath(profileDir2) {
@@ -51512,7 +51999,7 @@ function historyPath(profileDir2) {
51512
51999
  }
51513
52000
  function loadInputHistory(profileDir2) {
51514
52001
  const path = historyPath(profileDir2);
51515
- if (!existsSync19(path)) return [];
52002
+ if (!existsSync20(path)) return [];
51516
52003
  try {
51517
52004
  const data = JSON.parse(readFileSync20(path, "utf8"));
51518
52005
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
@@ -52190,7 +52677,7 @@ async function runStart(profileDir2, profileName) {
52190
52677
  logger.error("ws.run.crash", { message: e.message, stack: e.stack });
52191
52678
  if (client.isStopped) return;
52192
52679
  bus.log(t("start.recovered", { message: e.message }), "error");
52193
- await new Promise((resolve11) => setTimeout(resolve11, 1e3));
52680
+ await new Promise((resolve12) => setTimeout(resolve12, 1e3));
52194
52681
  }
52195
52682
  }
52196
52683
  };
@@ -52225,6 +52712,7 @@ async function runStart(profileDir2, profileName) {
52225
52712
  }
52226
52713
  } finally {
52227
52714
  stopIdleClear?.();
52715
+ await revokeAll().catch(() => void 0);
52228
52716
  stopControl?.();
52229
52717
  release();
52230
52718
  }
@@ -52236,7 +52724,7 @@ import { join as join38 } from "path";
52236
52724
  // src/core/control-client.ts
52237
52725
  import { connect as connect2 } from "net";
52238
52726
  function connectControl(socketPath, bus, token) {
52239
- return new Promise((resolve11, reject) => {
52727
+ return new Promise((resolve12, reject) => {
52240
52728
  const socket = connect2(socketPath);
52241
52729
  socket.setEncoding("utf8");
52242
52730
  let ready = false;
@@ -52289,7 +52777,7 @@ function connectControl(socketPath, bus, token) {
52289
52777
  bus.setTokens(frame.tokens);
52290
52778
  for (const entry of frame.transcript) bus.pushEntry(entry);
52291
52779
  ready = true;
52292
- resolve11({ info: frame.info, close });
52780
+ resolve12({ info: frame.info, close });
52293
52781
  break;
52294
52782
  case "transcript":
52295
52783
  bus.pushEntry(frame.entry);
@@ -52377,7 +52865,7 @@ async function runAutoRun(profileDir2, profileName) {
52377
52865
  initI18n(readProfileConfig(profileDir2).locale);
52378
52866
  const socketPath = join39(profileDir2, CONTROL_SOCKET_FILE);
52379
52867
  const token = readControlToken(profileDir2);
52380
- const code = await new Promise((resolve11) => {
52868
+ const code = await new Promise((resolve12) => {
52381
52869
  let settled = false;
52382
52870
  const done = (c) => {
52383
52871
  if (settled) return;
@@ -52387,7 +52875,7 @@ async function runAutoRun(profileDir2, profileName) {
52387
52875
  socket.destroy();
52388
52876
  } catch {
52389
52877
  }
52390
- resolve11(c);
52878
+ resolve12(c);
52391
52879
  };
52392
52880
  const socket = connect3(socketPath);
52393
52881
  socket.setEncoding("utf8");
@@ -52430,21 +52918,91 @@ async function runAutoRun(profileDir2, profileName) {
52430
52918
  process.exitCode = code;
52431
52919
  }
52432
52920
 
52921
+ // src/commands/git-credential.ts
52922
+ import { connect as connect4 } from "net";
52923
+ import { join as join40 } from "path";
52924
+ var REQUEST_TIMEOUT_MS = 2e4;
52925
+ function requestToken(host, path) {
52926
+ const profileDir2 = process.env[PROFILE_DIR_ENV];
52927
+ if (!profileDir2) return Promise.resolve(null);
52928
+ const token = readControlToken(profileDir2);
52929
+ return new Promise((resolve12) => {
52930
+ let done = false;
52931
+ const finish = (value) => {
52932
+ if (done) return;
52933
+ done = true;
52934
+ clearTimeout(timer);
52935
+ socket.destroy();
52936
+ resolve12(value);
52937
+ };
52938
+ const socket = connect4(join40(profileDir2, CONTROL_SOCKET_FILE));
52939
+ socket.setEncoding("utf8");
52940
+ const timer = setTimeout(() => finish(null), REQUEST_TIMEOUT_MS);
52941
+ socket.on("connect", () => {
52942
+ try {
52943
+ socket.write(encodeFrame({ t: "auth", token: token ?? "", rpc: true }));
52944
+ socket.write(encodeFrame({ t: "gitToken", scope: process.env[JOB_ID_ENV] ?? "", host, path }));
52945
+ } catch {
52946
+ finish(null);
52947
+ }
52948
+ });
52949
+ const parse = createFrameParser();
52950
+ socket.on("data", (chunk) => {
52951
+ for (const frame of parse(chunk)) {
52952
+ if (frame.t === "gitToken") finish(frame.token);
52953
+ else if (frame.t === "authError") finish(null);
52954
+ }
52955
+ });
52956
+ socket.on("error", () => finish(null));
52957
+ socket.on("close", () => finish(null));
52958
+ });
52959
+ }
52960
+ async function readCredentialInput() {
52961
+ const chunks = [];
52962
+ for await (const chunk of process.stdin) chunks.push(chunk);
52963
+ const out = {};
52964
+ for (const line of Buffer.concat(chunks).toString("utf8").split("\n")) {
52965
+ const i = line.indexOf("=");
52966
+ if (i > 0) out[line.slice(0, i)] = line.slice(i + 1).trim();
52967
+ }
52968
+ return out;
52969
+ }
52970
+ async function runGitCredential(action) {
52971
+ if (action !== "get") {
52972
+ await readCredentialInput().catch(() => void 0);
52973
+ return;
52974
+ }
52975
+ const input = await readCredentialInput();
52976
+ const host = (input.host ?? "").toLowerCase();
52977
+ if (input.protocol !== "https" || !host || host !== (process.env[GIT_HOST_ENV] ?? "").toLowerCase()) return;
52978
+ const token = await requestToken(host, input.path ?? "");
52979
+ if (token) process.stdout.write(`username=x-access-token
52980
+ password=${token}
52981
+ `);
52982
+ }
52983
+ async function runGitToken() {
52984
+ const host = process.env[GIT_HOST_ENV] ?? "";
52985
+ if (!host) return;
52986
+ const token = await requestToken(host, "");
52987
+ if (token) process.stdout.write(`${token}
52988
+ `);
52989
+ }
52990
+
52433
52991
  // src/commands/ai-login.ts
52434
52992
  import { spawn as spawn12 } from "child_process";
52435
- import { mkdirSync as mkdirSync9 } from "fs";
52993
+ import { mkdirSync as mkdirSync10 } from "fs";
52436
52994
  import { basename as basename8 } from "path";
52437
52995
  function runProviderLogin(cmd, envVar, dir2) {
52438
- return new Promise((resolve11) => {
52996
+ return new Promise((resolve12) => {
52439
52997
  const child = spawn12(cmd, [], {
52440
52998
  stdio: "inherit",
52441
52999
  env: { ...process.env, [envVar]: dir2 }
52442
53000
  });
52443
53001
  child.on("error", (err) => {
52444
53002
  console.error(t("aiLogin.launchFailed", { cmd, message: err.message }));
52445
- resolve11(1);
53003
+ resolve12(1);
52446
53004
  });
52447
- child.on("exit", (code) => resolve11(code ?? 0));
53005
+ child.on("exit", (code) => resolve12(code ?? 0));
52448
53006
  });
52449
53007
  }
52450
53008
  async function runAiLogin(profileDir2, profileName, aiFilter) {
@@ -52469,7 +53027,7 @@ async function runAiLogin(profileDir2, profileName, aiFilter) {
52469
53027
  continue;
52470
53028
  }
52471
53029
  const dir2 = resolveHomePath(cred.profile);
52472
- mkdirSync9(dir2, { recursive: true });
53030
+ mkdirSync10(dir2, { recursive: true });
52473
53031
  console.log(t("aiLogin.loggingIn", { provider: cred.provider, profile: cred.profile, envVar, dir: dir2 }));
52474
53032
  console.log(t("aiLogin.completeSignIn"));
52475
53033
  const code = await runProviderLogin(cred.provider, envVar, dir2);
@@ -52481,7 +53039,7 @@ async function runAiLogin(profileDir2, profileName, aiFilter) {
52481
53039
  }
52482
53040
 
52483
53041
  // src/commands/unlink.ts
52484
- import { rmSync as rmSync10 } from "fs";
53042
+ import { rmSync as rmSync11 } from "fs";
52485
53043
  async function runUnlink(profileDir2, profileName) {
52486
53044
  initI18n(readProfileConfig(profileDir2).locale);
52487
53045
  const cred = readCredential(profileDir2, profileName);
@@ -52495,7 +53053,7 @@ async function runUnlink(profileDir2, profileName) {
52495
53053
  } catch (err) {
52496
53054
  console.log(t("unlink.revokeFailed", { message: err.message }));
52497
53055
  }
52498
- rmSync10(profileDir2, { recursive: true, force: true });
53056
+ rmSync11(profileDir2, { recursive: true, force: true });
52499
53057
  clearDefaultProfileIf(profileName);
52500
53058
  console.log(t("unlink.deleted"));
52501
53059
  }
@@ -52584,10 +53142,10 @@ function Picker({
52584
53142
  ] });
52585
53143
  }
52586
53144
  function selectFromList(items, title) {
52587
- return new Promise((resolve11) => {
53145
+ return new Promise((resolve12) => {
52588
53146
  let chosen = null;
52589
53147
  const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v2) => chosen = v2 }));
52590
- void app.waitUntilExit().then(() => resolve11(chosen));
53148
+ void app.waitUntilExit().then(() => resolve12(chosen));
52591
53149
  });
52592
53150
  }
52593
53151
 
@@ -52654,6 +53212,14 @@ async function main() {
52654
53212
  await runAttach(profileDir(name), name);
52655
53213
  break;
52656
53214
  }
53215
+ case "git-credential": {
53216
+ await runGitCredential(rest[0]);
53217
+ break;
53218
+ }
53219
+ case "git-token": {
53220
+ await runGitToken();
53221
+ break;
53222
+ }
52657
53223
  case "auto-run": {
52658
53224
  const name = await pickLinkedProfile(explicitProfile, "auto-run");
52659
53225
  if (!name) {