@4pm/cli 1.28.0-c → 1.30.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.
package/dist/index.js CHANGED
@@ -11606,7 +11606,7 @@ var require_react_reconciler_development = __commonJS({
11606
11606
  }
11607
11607
  }
11608
11608
  var assign = Object.assign;
11609
- function get2(key) {
11609
+ function get3(key) {
11610
11610
  return key._reactInternals;
11611
11611
  }
11612
11612
  function set(key, value) {
@@ -11961,7 +11961,7 @@ var require_react_reconciler_development = __commonJS({
11961
11961
  instance._warnedAboutRefsInRender = true;
11962
11962
  }
11963
11963
  }
11964
- var fiber = get2(component);
11964
+ var fiber = get3(component);
11965
11965
  if (!fiber) {
11966
11966
  return false;
11967
11967
  }
@@ -18301,7 +18301,7 @@ var require_react_reconciler_development = __commonJS({
18301
18301
  var classComponentUpdater = {
18302
18302
  isMounted,
18303
18303
  enqueueSetState: function(inst, payload, callback) {
18304
- var fiber = get2(inst);
18304
+ var fiber = get3(inst);
18305
18305
  var eventTime = requestEventTime();
18306
18306
  var lane = requestUpdateLane(fiber);
18307
18307
  var update = createUpdate(eventTime, lane);
@@ -18322,7 +18322,7 @@ var require_react_reconciler_development = __commonJS({
18322
18322
  }
18323
18323
  },
18324
18324
  enqueueReplaceState: function(inst, payload, callback) {
18325
- var fiber = get2(inst);
18325
+ var fiber = get3(inst);
18326
18326
  var eventTime = requestEventTime();
18327
18327
  var lane = requestUpdateLane(fiber);
18328
18328
  var update = createUpdate(eventTime, lane);
@@ -18344,7 +18344,7 @@ var require_react_reconciler_development = __commonJS({
18344
18344
  }
18345
18345
  },
18346
18346
  enqueueForceUpdate: function(inst, callback) {
18347
- var fiber = get2(inst);
18347
+ var fiber = get3(inst);
18348
18348
  var eventTime = requestEventTime();
18349
18349
  var lane = requestUpdateLane(fiber);
18350
18350
  var update = createUpdate(eventTime, lane);
@@ -26201,7 +26201,7 @@ var require_react_reconciler_development = __commonJS({
26201
26201
  if (!parentComponent) {
26202
26202
  return emptyContextObject;
26203
26203
  }
26204
- var fiber = get2(parentComponent);
26204
+ var fiber = get3(parentComponent);
26205
26205
  var parentContext = findCurrentUnmaskedContext(fiber);
26206
26206
  if (fiber.tag === ClassComponent) {
26207
26207
  var Component = fiber.type;
@@ -26212,7 +26212,7 @@ var require_react_reconciler_development = __commonJS({
26212
26212
  return parentContext;
26213
26213
  }
26214
26214
  function findHostInstance(component) {
26215
- var fiber = get2(component);
26215
+ var fiber = get3(component);
26216
26216
  if (fiber === void 0) {
26217
26217
  if (typeof component.render === "function") {
26218
26218
  throw new Error("Unable to find node on an unmounted component.");
@@ -26229,7 +26229,7 @@ var require_react_reconciler_development = __commonJS({
26229
26229
  }
26230
26230
  function findHostInstanceWithWarning(component, methodName) {
26231
26231
  {
26232
- var fiber = get2(component);
26232
+ var fiber = get3(component);
26233
26233
  if (fiber === void 0) {
26234
26234
  if (typeof component.render === "function") {
26235
26235
  throw new Error("Unable to find node on an unmounted component.");
@@ -27177,7 +27177,7 @@ var require_stack_utils = __commonJS({
27177
27177
  const evalCol = Number(match[6]);
27178
27178
  let file = match[7];
27179
27179
  const lnum = match[8];
27180
- const col = match[9];
27180
+ const col2 = match[9];
27181
27181
  const native = match[10] === "native";
27182
27182
  const closeParen = match[11] === ")";
27183
27183
  let method;
@@ -27185,8 +27185,8 @@ var require_stack_utils = __commonJS({
27185
27185
  if (lnum) {
27186
27186
  res.line = Number(lnum);
27187
27187
  }
27188
- if (col) {
27189
- res.column = Number(col);
27188
+ if (col2) {
27189
+ res.column = Number(col2);
27190
27190
  }
27191
27191
  if (closeParen && file) {
27192
27192
  let closes = 0;
@@ -28584,8 +28584,30 @@ function normalizeLocale(value) {
28584
28584
 
28585
28585
  // vendor/@4pm/constants/project-template.ts
28586
28586
  var PROJECT_TEMPLATE = {
28587
- version: "1.0.2",
28587
+ version: "1.1.0",
28588
28588
  changelog: [
28589
+ {
28590
+ version: "1.1.0",
28591
+ date: "2026-09-30",
28592
+ summary: "Multi-worker autonomous books: multi-row AI_PROGRESS claims, QA-gated task Depends, attempts sidecar (ADR-0371).",
28593
+ notes: [
28594
+ "AI_PROGRESS.md holds one row per claimed task: | Started | ID | Worker | Claim | Attempt | Task description | (was a single | Started | ID | Task description | row). Convert an existing single row into the new columns (Worker/Claim empty, Attempt 1).",
28595
+ "AI_TODO.md Depends may list QA-\u2026 ids (the task waits for that USER_QA answer); two or more TSK dependencies require their PRs to be merged into the base branch; Notes may carry size: S|M and from: <branch>.",
28596
+ "USER_QA.md notes that a question raised while implementing a task is listed in that task's Depends.",
28597
+ ".claude/AUTONOMOUS.md documents branches & claims; .claude/.autonomous.attempts.json (per-task attempts / split-pending) is committed with the books \u2014 make sure it is NOT gitignored."
28598
+ ],
28599
+ files: [
28600
+ "AI_PROGRESS.md",
28601
+ "AI_TODO.md",
28602
+ "USER_QA.md",
28603
+ ".claude/AUTONOMOUS.md",
28604
+ ".claude/templates/AI_PROGRESS.empty.md",
28605
+ ".claude/templates/AI_PROGRESS.sample.md",
28606
+ ".claude/templates/AI_TODO.empty.md",
28607
+ ".claude/templates/AI_TODO.sample.md",
28608
+ ".claude/templates/USER_QA.empty.md"
28609
+ ]
28610
+ },
28589
28611
  {
28590
28612
  version: "1.0.2",
28591
28613
  date: "2026-09-16",
@@ -29550,7 +29572,7 @@ async function runLink(serverUrl, explicitProfile, token = null) {
29550
29572
 
29551
29573
  // src/commands/start.ts
29552
29574
  import { spawn as spawn11 } from "child_process";
29553
- import { join as join38 } from "path";
29575
+ import { join as join42 } from "path";
29554
29576
 
29555
29577
  // src/core/update.ts
29556
29578
  import { execSync as execSync2 } from "child_process";
@@ -32769,8 +32791,8 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
32769
32791
  // src/core/ws-client.ts
32770
32792
  init_wrapper();
32771
32793
  import { randomUUID as randomUUID4 } from "crypto";
32772
- import { basename as basename7, join as join33 } from "path";
32773
- import { existsSync as existsSync18, mkdirSync as mkdirSync9 } from "fs";
32794
+ import { basename as basename7, join as join37 } from "path";
32795
+ import { existsSync as existsSync19, mkdirSync as mkdirSync9 } from "fs";
32774
32796
 
32775
32797
  // vendor/@4pm/utils/helpers.ts
32776
32798
  function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
@@ -37058,6 +37080,16 @@ var putGitCredentialRequestSchema = external_exports.object({
37058
37080
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["host"], message: "host is required for GitHub Enterprise" });
37059
37081
  }
37060
37082
  });
37083
+ function parseRepoUrl(url) {
37084
+ const raw = url.trim();
37085
+ const scp = /^[\w.-]+@([^:/\s]+):([^/\s]+)\/(.+?)(?:\.git)?\/?$/.exec(raw);
37086
+ if (scp) return { protocol: "ssh", host: scp[1].toLowerCase(), owner: scp[2], name: scp[3] };
37087
+ const std = /^(https?|ssh):\/\/(?:[^@/\s]+@)?([^/\s]+)\/([^/\s]+)\/(.+?)(?:\.git)?\/?$/.exec(raw);
37088
+ if (std) {
37089
+ return { protocol: std[1], host: std[2].toLowerCase(), owner: std[3], name: std[4] };
37090
+ }
37091
+ return null;
37092
+ }
37061
37093
  function githubHostKind(host) {
37062
37094
  const h = host.toLowerCase();
37063
37095
  if (h === "github.com") return "github";
@@ -37492,7 +37524,30 @@ var repoSpecSchema = external_exports.object({
37492
37524
  * worker clones with `-b <branch>` and the on-demand "update repos" action checks it out +
37493
37525
  * fast-forward pulls it.
37494
37526
  */
37495
- branch: external_exports.string().max(200).optional().default("")
37527
+ branch: external_exports.string().max(200).optional().default(""),
37528
+ /**
37529
+ * Where `branch` is created from when it does not exist on the remote yet (ADR-0370): the default
37530
+ * branch, another remote branch (`branch`, primary only), or `empty` — an orphan branch with no
37531
+ * history. Absent ⇒ `default`.
37532
+ */
37533
+ base: external_exports.object({
37534
+ kind: external_exports.enum(["default", "branch", "empty"]),
37535
+ branch: external_exports.string().max(200).optional()
37536
+ }).optional(),
37537
+ /**
37538
+ * Primary only (ADR-0370): reset the 4PM tracking files (AI_TODO / AI_DONE / AI_PROGRESS / USER_TODO /
37539
+ * USER_QA) to the template's empty versions after the scaffold copy. Absent ⇒ reset only when the
37540
+ * source already has a 4PM scaffold.
37541
+ */
37542
+ resetTracking: external_exports.boolean().optional()
37543
+ });
37544
+ var SCAFFOLD_TRACKING_FILES = ["AI_TODO.md", "AI_DONE.md", "AI_PROGRESS.md", "USER_TODO.md", "USER_QA.md"];
37545
+ var repoProbeRequestSchema = external_exports.object({
37546
+ url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
37547
+ branch: external_exports.string().max(200).optional().default(""),
37548
+ base: external_exports.object({ kind: external_exports.enum(["default", "branch", "empty"]), branch: external_exports.string().max(200).optional() }).optional(),
37549
+ /** The declared submodules to probe too — existence + protection (ADR-0371). */
37550
+ submodules: external_exports.array(external_exports.object({ subdir: external_exports.string().max(120), url: external_exports.string().max(500), branch: external_exports.string().max(200).optional().default("") })).max(20).optional()
37496
37551
  });
37497
37552
  function validateProjectRepos(repos, ctx, path = ["repos"]) {
37498
37553
  const list = repos ?? [];
@@ -37651,7 +37706,7 @@ var userCardsQuerySchema = external_exports.object({
37651
37706
  });
37652
37707
 
37653
37708
  // src/core/scaffold.ts
37654
- import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "fs/promises";
37709
+ import { cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "fs/promises";
37655
37710
  import { existsSync as existsSync4 } from "fs";
37656
37711
  import { execFile } from "child_process";
37657
37712
  import { tmpdir as tmpdir2 } from "os";
@@ -37670,65 +37725,82 @@ function submodulesOf(repos) {
37670
37725
  const primary = singleRepo(repos);
37671
37726
  return repos.filter((r) => r !== primary && !!r.url && !!(r.subdir ?? "").trim());
37672
37727
  }
37673
- async function attachSubmodules(root, submodules, emit, createMissingBranch = false) {
37674
- if (submodules.length === 0) return;
37675
- if (!existsSync4(join6(root, ".git"))) return;
37728
+ async function isRegisteredSubmodule(root, dir2) {
37729
+ if (existsSync4(join6(root, ".gitmodules"))) {
37730
+ const paths = await gitOut(root, ["config", "-f", ".gitmodules", "--get-regexp", "^submodule\\..*\\.path$"]);
37731
+ if (paths.split("\n").some((l) => l.split(" ").slice(1).join(" ") === dir2)) return true;
37732
+ }
37733
+ const staged = await gitOut(root, ["ls-files", "--stage", "--", dir2]);
37734
+ return staged.split("\n").some((l) => l.startsWith("160000 "));
37735
+ }
37736
+ async function listFiles(abs, rel = "") {
37737
+ const out = [];
37738
+ for (const e of await readdir(join6(abs, rel), { withFileTypes: true }).catch(() => [])) {
37739
+ if (e.name === ".git") continue;
37740
+ const r = rel ? `${rel}/${e.name}` : e.name;
37741
+ if (e.isDirectory()) out.push(...await listFiles(abs, r));
37742
+ else out.push(r);
37743
+ }
37744
+ return out;
37745
+ }
37746
+ async function prepareSubmodulePath(root, dir2, emit) {
37747
+ const abs = join6(root, dir2);
37748
+ const tracked = (await gitOut(root, ["ls-files", "--", dir2])).split("\n").filter(Boolean);
37749
+ const onDisk = existsSync4(abs) ? await listFiles(abs) : [];
37750
+ const trackedRel = new Set(tracked.map((f2) => f2.slice(dir2.length + 1)));
37751
+ const foreign = onDisk.filter((f2) => !trackedRel.has(f2) && !f2.endsWith(".gitkeep"));
37752
+ if (foreign.length > 0) {
37753
+ throw new Error(`"${dir2}" already holds ${foreign.length} file(s) that are not part of the repo (e.g. ${foreign[0]}) \u2014 move them away, then retry.`);
37754
+ }
37755
+ if (tracked.length > 0) {
37756
+ emit("submodule", `Untracking the placeholder files in ${dir2}\u2026`);
37757
+ await run("git", ["rm", "-r", "-q", "--cached", "--", dir2], { cwd: root, timeout: 3e4 });
37758
+ }
37759
+ const modules = join6(root, ".git", "modules", dir2);
37760
+ if (existsSync4(modules)) {
37761
+ emit("submodule", `Cleaning up a half-attached ${dir2}\u2026`);
37762
+ await rm(modules, { recursive: true, force: true });
37763
+ }
37764
+ if (existsSync4(abs)) await rm(abs, { recursive: true, force: true });
37765
+ }
37766
+ async function attachSubmodules(root, submodules, emit, opts = {}) {
37767
+ const outcomes = [];
37768
+ if (submodules.length === 0) return outcomes;
37769
+ if (!existsSync4(join6(root, ".git"))) return outcomes;
37676
37770
  let added = 0;
37677
37771
  for (const sub of submodules) {
37678
37772
  const dir2 = (sub.subdir ?? "").trim();
37679
37773
  if (!dir2 || !sub.url) continue;
37680
- if (existsSync4(join6(root, dir2, ".git")) || existsSync4(join6(root, ".git", "modules", dir2))) {
37681
- emit("submodule", `Submodule ${dir2} already present \u2014 updating\u2026`);
37682
- try {
37774
+ const b2 = (sub.branch ?? "").trim();
37775
+ try {
37776
+ if (await isRegisteredSubmodule(root, dir2)) {
37777
+ emit("submodule", `Submodule ${dir2} already registered \u2014 updating\u2026`);
37683
37778
  await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37684
- } catch {
37779
+ outcomes.push({ dir: dir2, ok: true, error: null });
37780
+ continue;
37685
37781
  }
37686
- continue;
37687
- }
37688
- const abs = join6(root, dir2);
37689
- if (existsSync4(abs)) {
37690
- emit("submodule", `Removing placeholder ${dir2} before attaching the submodule\u2026`);
37691
- await rm(abs, { recursive: true, force: true });
37692
- }
37693
- emit("submodule", `Adding submodule ${sub.url} \u2192 ${dir2}\u2026`);
37694
- const b2 = (sub.branch ?? "").trim();
37695
- if (b2 && createMissingBranch && !await ensureRemoteBranch(sub.url, b2, emit)) {
37696
- try {
37697
- await run("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 12e4 });
37782
+ await prepareSubmodulePath(root, dir2, emit);
37783
+ emit("submodule", `Adding submodule ${sub.url} \u2192 ${dir2}\u2026`);
37784
+ const base = { kind: opts.base?.kind === "empty" ? "empty" : "default" };
37785
+ const branchReady = !b2 || await remoteHasBranch(sub.url, b2) || !!opts.createMissingBranch && await ensureRemoteBranch(sub.url, b2, emit, base);
37786
+ if (branchReady) {
37787
+ await run("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 18e4 });
37788
+ } else {
37789
+ await run("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 18e4 });
37698
37790
  await run("git", ["config", "-f", ".gitmodules", `submodule.${dir2}.branch`, b2], { cwd: root, timeout: 3e4 });
37699
- try {
37700
- await run("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
37701
- await run("git", ["push", "-u", "origin", b2], { cwd: join6(root, dir2), timeout: 12e4 });
37702
- } catch (pushErr) {
37703
- const pmsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
37704
- emit("git-branch-push-failed", `Submodule ${dir2}: branch "${b2}" created locally but the push failed: ${pmsg}`);
37705
- }
37706
- added++;
37707
- continue;
37708
- } catch (err) {
37709
- const msg = err instanceof Error ? err.message : String(err);
37710
- if (!/already exists|already registered|in the index/i.test(msg)) {
37711
- throw new Error(`git submodule add ${dir2} failed: ${msg}`);
37712
- }
37791
+ await run("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
37792
+ emit("git-branch-push-failed", `Submodule ${dir2}: branch "${b2}" created locally only \u2014 push it when credentials are available.`);
37713
37793
  }
37714
- }
37715
- try {
37716
- await run("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
37717
37794
  added++;
37795
+ outcomes.push({ dir: dir2, ok: true, error: null });
37718
37796
  } catch (err) {
37719
- const msg = err instanceof Error ? err.message : String(err);
37720
- if (/already exists|already registered|in the index/i.test(msg)) {
37721
- try {
37722
- await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37723
- added++;
37724
- } catch {
37725
- }
37726
- } else {
37727
- throw new Error(`git submodule add ${dir2} failed: ${msg}`);
37728
- }
37797
+ const msg = errText(err);
37798
+ emit("submodule-failed", `Submodule ${dir2} was not attached: ${msg}`);
37799
+ outcomes.push({ dir: dir2, ok: false, error: msg });
37729
37800
  }
37730
37801
  }
37731
- if (added > 0) await commitAndPushSubmodules(root, submodules, emit);
37802
+ if (added > 0 && opts.commit) await commitAndPushSubmodules(root, submodules, emit);
37803
+ return outcomes;
37732
37804
  }
37733
37805
  async function commitAndPushSubmodules(root, submodules, emit) {
37734
37806
  const dirs = submodules.map((s3) => (s3.subdir ?? "").trim()).filter(Boolean);
@@ -37749,17 +37821,11 @@ async function commitAndPushSubmodules(root, submodules, emit) {
37749
37821
  }
37750
37822
  emit("submodule", "Pushing .gitmodules to the primary remote\u2026");
37751
37823
  try {
37752
- await run("git", ["push"], { cwd: root, timeout: 12e4 });
37824
+ await run("git", ["push", "-u", "origin", "HEAD"], { cwd: root, timeout: 12e4 });
37753
37825
  } catch (err) {
37754
- const msg = err instanceof Error ? err.message : String(err);
37755
- emit("submodule-push-failed", `Submodules committed locally but the push failed: ${msg}`);
37826
+ emit("submodule-push-failed", `Submodules committed locally but the push failed: ${errText(err)}`);
37756
37827
  }
37757
37828
  }
37758
- function providerSlug(url) {
37759
- const provider = /github\.com/i.test(url) ? "gh" : /gitlab\.com/i.test(url) ? "glab" : null;
37760
- const m2 = /(?:github|gitlab)\.com[/:]+([^/]+\/.+?)(?:\.git)?\/?$/i.exec(url);
37761
- return { provider, slug: m2?.[1] ?? "" };
37762
- }
37763
37829
  function errText(err) {
37764
37830
  const e = err;
37765
37831
  const raw = (e?.stderr || e?.message || String(err)).trim();
@@ -37772,7 +37838,7 @@ async function gitOut(cwd2, args) {
37772
37838
  return "";
37773
37839
  }
37774
37840
  }
37775
- async function commitAndOpenPr(root, repo, emit) {
37841
+ async function commitAndPush(root, repo, emit) {
37776
37842
  const result = { committed: false, pushed: false, branch: null, prUrl: null, step: null, error: null };
37777
37843
  const fail = (step, error) => {
37778
37844
  result.step = step;
@@ -37803,54 +37869,36 @@ async function commitAndOpenPr(root, repo, emit) {
37803
37869
  const head = await gitOut(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
37804
37870
  const branch = declared || (head && head !== "HEAD" ? head : "");
37805
37871
  result.branch = branch || null;
37806
- emit("push", "Pushing the scaffold branch\u2026");
37872
+ emit("push", `Pushing the scaffold to ${branch || "the current branch"}\u2026`);
37807
37873
  try {
37808
37874
  await run("git", ["push", "-u", "origin", branch || "HEAD"], { cwd: root, timeout: 12e4 });
37809
37875
  result.pushed = true;
37876
+ emit("push", `Pushed to ${branch || "the current branch"}.`);
37810
37877
  } catch (err) {
37811
37878
  const msg = errText(err);
37812
37879
  emit("push-failed", `Scaffold committed locally but the push failed: ${msg} \u2014 retry from the project page when credentials are available.`);
37813
37880
  return fail("push", msg);
37814
37881
  }
37815
- const remoteHead = (await gitOut(root, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).replace(/^origin\//, "");
37816
- if (branch && remoteHead && branch === remoteHead) {
37817
- emit("pr", `Pushed to the default branch (${branch}) \u2014 no pull request needed.`);
37818
- return result;
37819
- }
37820
- const url = (repo.url ?? "").trim() || await gitOut(root, ["remote", "get-url", "origin"]);
37821
- const { provider, slug } = providerSlug(url);
37822
- if (!provider) {
37823
- emit("pr", "Unknown git host \u2014 skipping the pull request.");
37824
- return result;
37825
- }
37826
- emit("pr", `Opening a pull request via ${provider}\u2026`);
37827
- try {
37828
- const args = provider === "glab" ? ["mr", "create", "--fill", "--yes", ...branch ? ["--source-branch", branch] : [], ...slug ? ["-R", slug] : []] : ["pr", "create", "--fill", ...branch ? ["--head", branch] : [], ...slug ? ["-R", slug] : []];
37829
- const { stdout } = await run(provider, args, { cwd: root, timeout: 12e4 });
37830
- const link2 = stdout.trim().split(/\s+/).find((x) => /^https?:\/\//.test(x)) ?? null;
37831
- result.prUrl = link2;
37832
- emit("pr", `Pull request opened${link2 ? `: ${link2}` : "."}`);
37833
- return result;
37834
- } catch (err) {
37835
- const msg = errText(err);
37836
- const existing = /already exists/i.test(msg) ? msg.match(/https?:\/\/\S+/)?.[0] ?? null : null;
37837
- if (existing) {
37838
- result.prUrl = existing;
37839
- emit("pr", `A pull request is already open: ${existing}`);
37840
- return result;
37841
- }
37842
- emit("pr-failed", `Could not open a pull request (${provider}): ${msg}`);
37843
- return fail("pr", msg);
37844
- }
37882
+ return result;
37845
37883
  }
37846
37884
  async function publishScaffold(root, emit) {
37847
- let repo = {};
37885
+ let repos = [];
37848
37886
  try {
37849
37887
  const spec = JSON.parse(await readFile(join6(root, "project.spec.json"), "utf8"));
37850
- repo = singleRepo(reposOf(spec)) ?? {};
37888
+ repos = reposOf(spec);
37851
37889
  } catch {
37852
37890
  }
37853
- return commitAndOpenPr(root, repo, emit);
37891
+ const repo = singleRepo(repos) ?? {};
37892
+ const submodules = await attachSubmodules(root, submodulesOf(repos), emit, { createMissingBranch: true, base: repo.base });
37893
+ return withSubmodules(await commitAndPush(root, repo, emit), submodules);
37894
+ }
37895
+ function withSubmodules(publish, submodules) {
37896
+ const failed = submodules.filter((x) => !x.ok);
37897
+ if (failed.length > 0 && !publish.step) {
37898
+ publish.step = "submodule";
37899
+ publish.error = failed.map((x) => `${x.dir}: ${x.error ?? "failed"}`).join("; ");
37900
+ }
37901
+ return { ...publish, submodules };
37854
37902
  }
37855
37903
  function cloneArgs(url, dest, branch) {
37856
37904
  const b2 = (branch ?? "").trim();
@@ -37864,18 +37912,27 @@ async function remoteHasBranch(url, branch) {
37864
37912
  return false;
37865
37913
  }
37866
37914
  }
37867
- async function ensureRemoteBranch(url, branch, emit) {
37915
+ async function ensureRemoteBranch(url, branch, emit, base = {}) {
37868
37916
  if (await remoteHasBranch(url, branch)) return true;
37869
37917
  const tmp = await mkdtemp(join6(tmpdir2(), "4pm-branch-"));
37870
37918
  try {
37871
- emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it from the default branch\u2026`);
37872
- await run("git", ["clone", "--depth", "1", url, tmp], { timeout: 12e4 });
37873
- await run("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
37919
+ if (base.kind === "empty") {
37920
+ emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it empty (no history)\u2026`);
37921
+ await run("git", ["clone", "--no-checkout", "--depth", "1", url, tmp], { timeout: 12e4 });
37922
+ await run("git", ["checkout", "--orphan", branch], { cwd: tmp, timeout: 3e4 });
37923
+ await run("git", ["rm", "-r", "-q", "--cached", "--ignore-unmatch", "."], { cwd: tmp, timeout: 3e4 });
37924
+ await run("git", ["clean", "-fdxq"], { cwd: tmp, timeout: 6e4 });
37925
+ await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", "commit", "--allow-empty", "-m", "chore: start branch (4PM)"], { cwd: tmp, timeout: 3e4 });
37926
+ } else {
37927
+ const from = base.kind === "branch" && base.branch ? base.branch : "";
37928
+ emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it from ${from || "the default branch"}\u2026`);
37929
+ await run("git", ["clone", "--depth", "1", ...from ? ["-b", from] : [], url, tmp], { timeout: 12e4 });
37930
+ await run("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
37931
+ }
37874
37932
  await run("git", ["push", "-u", "origin", branch], { cwd: tmp, timeout: 12e4 });
37875
37933
  return true;
37876
37934
  } catch (err) {
37877
- const msg = err instanceof Error ? err.message : String(err);
37878
- emit("git-branch-push-failed", `Could not create branch "${branch}" on ${url}: ${msg}`);
37935
+ emit("git-branch-push-failed", `Could not create branch "${branch}" on ${url}: ${errText(err)}`);
37879
37936
  return false;
37880
37937
  } finally {
37881
37938
  await rm(tmp, { recursive: true, force: true });
@@ -37912,15 +37969,24 @@ async function provisionRepo(target, repo, emit, opts = {}) {
37912
37969
  }
37913
37970
  const b2 = (repo.branch ?? "").trim();
37914
37971
  if (b2 && opts.createMissingBranch && !await remoteHasBranch(repo.url, b2)) {
37915
- emit("git", `Cloning ${repo.url} (default branch) to create "${b2}"\u2026`);
37916
- await run("git", ["clone", repo.url, target], { timeout: 12e4 });
37972
+ const kind = repo.base?.kind ?? "default";
37973
+ const from = kind === "branch" ? (repo.base?.branch ?? "").trim() : "";
37974
+ if (kind === "empty") {
37975
+ emit("git", `Creating "${b2}" as an empty branch (no history) in ${repo.url}\u2026`);
37976
+ await run("git", ["clone", "--no-checkout", repo.url, target], { timeout: 12e4 });
37977
+ await run("git", ["checkout", "--orphan", b2], { cwd: target, timeout: 3e4 });
37978
+ await run("git", ["rm", "-r", "-q", "--cached", "--ignore-unmatch", "."], { cwd: target, timeout: 3e4 });
37979
+ await run("git", ["clean", "-fdxq"], { cwd: target, timeout: 6e4 });
37980
+ return;
37981
+ }
37982
+ emit("git", `Cloning ${repo.url} (${from || "default branch"}) to create "${b2}"\u2026`);
37983
+ await run("git", cloneArgs(repo.url, target, from || void 0), { timeout: 12e4 });
37917
37984
  await run("git", ["checkout", "-b", b2], { cwd: target, timeout: 3e4 });
37918
37985
  try {
37919
37986
  await run("git", ["push", "-u", "origin", b2], { cwd: target, timeout: 12e4 });
37920
37987
  emit("git", `Created and pushed branch "${b2}".`);
37921
37988
  } catch (err) {
37922
- const msg = err instanceof Error ? err.message : String(err);
37923
- emit("git-branch-push-failed", `Branch "${b2}" created locally but the push failed: ${msg} \u2014 push it when credentials are available.`);
37989
+ emit("git-branch-push-failed", `Branch "${b2}" created locally but the push failed: ${errText(err)} \u2014 the publish step retries it.`);
37924
37990
  }
37925
37991
  return;
37926
37992
  }
@@ -37933,7 +37999,10 @@ async function ensureReposCloned(physicRoot, repos, emit) {
37933
37999
  if (!repo) return;
37934
38000
  const step = emit ?? (() => void 0);
37935
38001
  await provisionRepo(physicRoot, repo, step);
37936
- await attachSubmodules(physicRoot, submodulesOf(list), step);
38002
+ await attachSubmodules(physicRoot, submodulesOf(list), step, { commit: true });
38003
+ }
38004
+ function projectSampleDir() {
38005
+ return sampleDir();
37937
38006
  }
37938
38007
  function sampleDir() {
37939
38008
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
@@ -37959,11 +38028,16 @@ async function scaffoldProject(payload, profileDir2, onProgress, ai2, applyGitAu
37959
38028
  if (payload.gitAuth !== void 0) applyGitAuth2?.(payload.gitAuth ?? null, payload.gitAuthHost ?? null);
37960
38029
  emit("git", "Cloning repository\u2026");
37961
38030
  await provisionRepo(target, repo, emit, { createMissingBranch: true });
38031
+ const sourceHadScaffold = existsSync4(join6(target, ".4pm", ".4pm.json"));
37962
38032
  await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true, ai: ai2 });
37963
- await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit, true);
38033
+ if (repo.resetTracking ?? sourceHadScaffold) await resetTrackingFiles(target, emit);
38034
+ const submodules = await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit, {
38035
+ createMissingBranch: true,
38036
+ base: repo.base
38037
+ });
37964
38038
  emit("version", "Writing .4pm/.4pm.json\u2026");
37965
38039
  await writeTemplateMarker(target);
37966
- const publish = await commitAndOpenPr(target, repo, emit);
38040
+ const publish = withSubmodules(await commitAndPush(target, repo, emit), submodules);
37967
38041
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
37968
38042
  return { ok: true, path: target, publish };
37969
38043
  } catch (err) {
@@ -37988,6 +38062,17 @@ async function resetTemplateManagedClaude(dir2, label, emit) {
37988
38062
  await rm(join6(claude, "settings.json"), { force: true });
37989
38063
  await rm(join6(claude, "skills"), { recursive: true, force: true });
37990
38064
  }
38065
+ async function resetTrackingFiles(target, emit) {
38066
+ const templates = join6(sampleDir(), ".claude", "templates");
38067
+ const reset = [];
38068
+ for (const file of SCAFFOLD_TRACKING_FILES) {
38069
+ const empty = join6(templates, file.replace(/\.md$/, ".empty.md"));
38070
+ if (!existsSync4(empty)) continue;
38071
+ await cp(empty, join6(target, file), { force: true });
38072
+ reset.push(file);
38073
+ }
38074
+ if (reset.length > 0) emit("copy", `Reset the 4PM tracking files: ${reset.join(", ")}.`);
38075
+ }
37991
38076
  async function writeTemplateMarker(target) {
37992
38077
  const dir2 = join6(target, ".4pm");
37993
38078
  await mkdir(dir2, { recursive: true });
@@ -38088,7 +38173,9 @@ async function addProject(payload, profileDir2, onProgress, ai2) {
38088
38173
  if (payload.scaffoldRepos && payload.scaffoldRepos.length > 0) {
38089
38174
  await scaffoldRepo(target, payload.projectName, payload.spec, emit, { ai: ai2 });
38090
38175
  }
38091
- await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit);
38176
+ const outcomes = await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit, { commit: true });
38177
+ const failed = outcomes.filter((x) => !x.ok);
38178
+ if (failed.length > 0) throw new Error(failed.map((x) => `submodule ${x.dir}: ${x.error ?? "failed"}`).join("; "));
38092
38179
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
38093
38180
  return { ok: true, path: target };
38094
38181
  } catch (err) {
@@ -38478,6 +38565,9 @@ var WsChannels = {
38478
38565
  // server → cli (reply): retry the scaffold commit → push → PR in the project folder (ADR-0368,
38479
38566
  // project-0074); the reply carries the new outcome, stored on `projects.scaffold_publish`.
38480
38567
  PROJECT_PUBLISH: "project.publish",
38568
+ // server → cli (reply): probe a repo branch before creating a project (ADR-0370, project-0076) —
38569
+ // branch existence, remote branches, and whether the source ref already holds a 4PM scaffold.
38570
+ REPO_PROBE: "repo.probe",
38481
38571
  // server → cli: keep the physic folder name in sync with the project (folder =
38482
38572
  // project name — ADR-0064) when the project is renamed.
38483
38573
  PHYSIC_SYNC: "physic.sync",
@@ -38522,6 +38612,9 @@ var WsChannels = {
38522
38612
  // cli → server (reply): read / add the org's monthly autonomous-book counters — USER_TODO / USER_QA /
38523
38613
  // AI_TODO rows vs `autonomousRequestsPerMonth` (ADR-0365)
38524
38614
  AUTONOMOUS_BOOK_USAGE: "autonomous.bookUsage",
38615
+ // cli → server (one-way): an autonomous condition the project's managers must see (ADR-0371) —
38616
+ // protected base, lost claim, attempt limit, task split, task question. The server notifies them.
38617
+ AUTONOMOUS_ALERT: "autonomous.alert",
38525
38618
  USAGE_REPORT: "usage.report",
38526
38619
  // cli → server: Claude subscription usage snapshot (5h / weekly — ADR-0072)
38527
38620
  MACHINE_USAGE: "machine.usage",
@@ -38630,7 +38723,7 @@ var COMMAND_CANCELLED_EXIT_CODE = 130;
38630
38723
  import { spawn as spawn2 } from "child_process";
38631
38724
 
38632
38725
  // src/common/logger/logger.ts
38633
- import { appendFile, mkdir as mkdir2, readdir, unlink } from "fs/promises";
38726
+ import { appendFile, mkdir as mkdir2, readdir as readdir2, unlink } from "fs/promises";
38634
38727
  import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
38635
38728
  import { join as join7 } from "path";
38636
38729
  var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
@@ -38690,7 +38783,7 @@ var CliLogger = class {
38690
38783
  async cleanupOldLogs() {
38691
38784
  const dir2 = this.dir;
38692
38785
  if (!dir2) return;
38693
- const files = await readdir(dir2).catch(() => []);
38786
+ const files = await readdir2(dir2).catch(() => []);
38694
38787
  const cutoff = Date.now() - this.retentionDays * DAY_MS;
38695
38788
  for (const file of files) {
38696
38789
  const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
@@ -39049,9 +39142,9 @@ var UpdateScheduler = class {
39049
39142
  day: "2-digit",
39050
39143
  hour: "2-digit"
39051
39144
  }).formatToParts(/* @__PURE__ */ new Date());
39052
- const get2 = (type) => parts.find((p2) => p2.type === type)?.value ?? "";
39053
- const hour = Number.parseInt(get2("hour"), 10);
39054
- const dateKey = `${get2("year")}-${get2("month")}-${get2("day")}`;
39145
+ const get3 = (type) => parts.find((p2) => p2.type === type)?.value ?? "";
39146
+ const hour = Number.parseInt(get3("hour"), 10);
39147
+ const dateKey = `${get3("year")}-${get3("month")}-${get3("day")}`;
39055
39148
  if (Number.isNaN(hour)) return null;
39056
39149
  return { hour: hour % 24, dateKey };
39057
39150
  } catch {
@@ -40425,11 +40518,11 @@ function reportToolResult(tool, ok, message) {
40425
40518
  }
40426
40519
 
40427
40520
  // src/core/autonomous.ts
40428
- import { readFile as readFile5, writeFile as writeFile4, chmod } from "fs/promises";
40521
+ import { readFile as readFile7, writeFile as writeFile5, chmod } from "fs/promises";
40429
40522
  import { readFileSync as readFileSync14 } from "fs";
40430
- import { execFile as execFile3, spawn as spawn5 } from "child_process";
40431
- import { join as join17 } from "path";
40432
- import { promisify as promisify3 } from "util";
40523
+ import { execFile as execFile5, spawn as spawn5 } from "child_process";
40524
+ import { join as join19 } from "path";
40525
+ import { promisify as promisify5 } from "util";
40433
40526
 
40434
40527
  // src/core/autonomous-config.ts
40435
40528
  import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
@@ -40444,7 +40537,11 @@ var DEFAULT_AUTONOMOUS_CONFIG = {
40444
40537
  logRetentionDays: 14,
40445
40538
  model: "",
40446
40539
  maxSessionPct: 80,
40447
- maxWeeklyPct: 90
40540
+ maxWeeklyPct: 90,
40541
+ claimTtlHours: 6,
40542
+ claimCheckMinutes: 10,
40543
+ maxTaskAttempts: 5,
40544
+ taskSizeHints: { sMaxFiles: 5, sMaxLines: 300, mMaxFiles: 20, mMaxLines: 1500 }
40448
40545
  };
40449
40546
  function autonomousConfigPath(profileDir2) {
40450
40547
  return join15(profileDir2, "autonomous.config.json");
@@ -40463,7 +40560,20 @@ function coerce2(raw) {
40463
40560
  logRetentionDays: num(o.logRetentionDays, d.logRetentionDays),
40464
40561
  model: str(o.model, d.model),
40465
40562
  maxSessionPct: num(o.maxSessionPct, d.maxSessionPct),
40466
- maxWeeklyPct: num(o.maxWeeklyPct, d.maxWeeklyPct)
40563
+ maxWeeklyPct: num(o.maxWeeklyPct, d.maxWeeklyPct),
40564
+ claimTtlHours: Math.max(1, num(o.claimTtlHours, d.claimTtlHours)),
40565
+ claimCheckMinutes: Math.max(1, num(o.claimCheckMinutes, d.claimCheckMinutes)),
40566
+ maxTaskAttempts: Math.max(1, num(o.maxTaskAttempts, d.maxTaskAttempts)),
40567
+ taskSizeHints: (() => {
40568
+ const h = o.taskSizeHints && typeof o.taskSizeHints === "object" ? o.taskSizeHints : {};
40569
+ const dh = d.taskSizeHints;
40570
+ return {
40571
+ sMaxFiles: num(h.sMaxFiles, dh.sMaxFiles),
40572
+ sMaxLines: num(h.sMaxLines, dh.sMaxLines),
40573
+ mMaxFiles: num(h.mMaxFiles, dh.mMaxFiles),
40574
+ mMaxLines: num(h.mMaxLines, dh.mMaxLines)
40575
+ };
40576
+ })()
40467
40577
  };
40468
40578
  }
40469
40579
  async function readAutonomousConfig(profileDir2) {
@@ -40539,8 +40649,481 @@ function pushRecord(h, rec) {
40539
40649
  return h;
40540
40650
  }
40541
40651
 
40542
- // src/core/autonomous.ts
40652
+ // src/core/autonomous-books.ts
40653
+ import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
40654
+ import { join as join17 } from "path";
40655
+ function splitRow(line) {
40656
+ const body = line.trim().replace(/^\|/, "").replace(/\|$/, "");
40657
+ return body.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"));
40658
+ }
40659
+ function cell(value) {
40660
+ return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
40661
+ }
40662
+ function findTables(md) {
40663
+ const lines = md.split("\n");
40664
+ const out = [];
40665
+ let i = 0;
40666
+ while (i < lines.length) {
40667
+ const l = lines[i].trim();
40668
+ const next = lines[i + 1]?.trim() ?? "";
40669
+ if (l.startsWith("|") && /^\|[\s:|-]+\|?$/.test(next)) {
40670
+ const header = splitRow(l);
40671
+ let j2 = i + 2;
40672
+ const rows = [];
40673
+ while (j2 < lines.length && lines[j2].trim().startsWith("|")) {
40674
+ const cells = splitRow(lines[j2]);
40675
+ if (cells.some((c) => c.length > 0)) rows.push(cells);
40676
+ j2++;
40677
+ }
40678
+ out.push({ start: i, end: j2, header, rows });
40679
+ i = j2;
40680
+ } else i++;
40681
+ }
40682
+ return out;
40683
+ }
40684
+ function replaceRows(md, table, rows) {
40685
+ const lines = md.split("\n");
40686
+ const width = table.header.length;
40687
+ const sep9 = `|${table.header.map(() => "---").join("|")}|`;
40688
+ const body = (rows.length ? rows : [table.header.map(() => "")]).map(
40689
+ (r) => `| ${Array.from({ length: width }, (_2, k2) => cell(r[k2] ?? "")).join(" | ")} |`
40690
+ );
40691
+ const head = `| ${table.header.join(" | ")} |`;
40692
+ lines.splice(table.start, table.end - table.start, head, sep9, ...body);
40693
+ return lines.join("\n");
40694
+ }
40695
+ function col(header, name) {
40696
+ return header.findIndex((h) => h.toLowerCase() === name.toLowerCase());
40697
+ }
40698
+ function get(t2, row, name) {
40699
+ const i = col(t2.header, name);
40700
+ return i >= 0 ? row[i] ?? "" : "";
40701
+ }
40702
+ function build(header, values) {
40703
+ return header.map((h) => values[h.toLowerCase()] ?? "");
40704
+ }
40705
+ function splitIds(v2) {
40706
+ return v2.split(/[,\s]+/).map((x) => x.trim()).filter((x) => /^(TSK|QA)-/.test(x));
40707
+ }
40708
+ function parseTasks(md) {
40709
+ const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Depends") >= 0 && col(t2.header, "Priority") >= 0) ?? null;
40710
+ if (!table) return { table: null, tasks: [] };
40711
+ const tasks = table.rows.map((r) => ({
40712
+ id: get(table, r, "ID"),
40713
+ priority: get(table, r, "Priority"),
40714
+ tag: get(table, r, "Tag"),
40715
+ depends: splitIds(get(table, r, "Depends")),
40716
+ group: get(table, r, "Group"),
40717
+ desc: get(table, r, "Task description"),
40718
+ notes: get(table, r, "Notes")
40719
+ })).filter((t2) => /^TSK-/.test(t2.id));
40720
+ return { table, tasks };
40721
+ }
40722
+ function writeTasks(md, tasks) {
40723
+ const { table } = parseTasks(md);
40724
+ if (!table) return md;
40725
+ return replaceRows(
40726
+ md,
40727
+ table,
40728
+ tasks.map(
40729
+ (t2) => build(table.header, {
40730
+ id: t2.id,
40731
+ priority: t2.priority,
40732
+ tag: t2.tag,
40733
+ depends: t2.depends.join(", "),
40734
+ group: t2.group,
40735
+ "task description": t2.desc,
40736
+ notes: t2.notes
40737
+ })
40738
+ )
40739
+ );
40740
+ }
40741
+ var PROGRESS_HEADER = ["Started", "ID", "Worker", "Claim", "Attempt", "Task description"];
40742
+ function parseClaims(md) {
40743
+ const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Started") >= 0) ?? null;
40744
+ if (!table) return { table: null, claims: [] };
40745
+ const claims = table.rows.map((r) => ({
40746
+ started: get(table, r, "Started"),
40747
+ id: get(table, r, "ID"),
40748
+ worker: get(table, r, "Worker"),
40749
+ claim: get(table, r, "Claim"),
40750
+ attempt: Number(get(table, r, "Attempt")) || 1,
40751
+ desc: get(table, r, "Task description")
40752
+ })).filter((c) => /^TSK-/.test(c.id));
40753
+ return { table, claims };
40754
+ }
40755
+ function writeClaims(md, claims) {
40756
+ const found = parseClaims(md).table;
40757
+ const table = found ? { ...found, header: PROGRESS_HEADER } : { start: md.split("\n").length, end: md.split("\n").length, header: PROGRESS_HEADER, rows: [] };
40758
+ const src = found ? md : `${md.replace(/\n*$/, "")}
40759
+
40760
+ `;
40761
+ return replaceRows(
40762
+ src,
40763
+ table,
40764
+ claims.map((c) => [c.started, c.id, c.worker, c.claim, String(c.attempt), c.desc])
40765
+ );
40766
+ }
40767
+ function appendDone(md, row) {
40768
+ const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Timestamp") >= 0);
40769
+ if (!table) return md;
40770
+ const values = { timestamp: stamp(), id: row.id, "task description": row.desc, files: row.files, notes: row.notes };
40771
+ return replaceRows(md, table, [...table.rows, build(table.header, values)]);
40772
+ }
40773
+ function appendIncident(md, note) {
40774
+ const table = findTables(md).find((t2) => col(t2.header, "Note") >= 0 && col(t2.header, "Timestamp") >= 0 && col(t2.header, "ID") < 0);
40775
+ if (!table) return md;
40776
+ return replaceRows(md, table, [...table.rows, build(table.header, { timestamp: stamp(), note })]);
40777
+ }
40778
+ function doneRows(md) {
40779
+ const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Timestamp") >= 0);
40780
+ const out = /* @__PURE__ */ new Map();
40781
+ if (table) for (const r of table.rows) out.set(get(table, r, "ID"), get(table, r, "Notes"));
40782
+ return out;
40783
+ }
40784
+ function parseQa(md) {
40785
+ const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Answer") >= 0) ?? null;
40786
+ if (!table) return { table: null, rows: [] };
40787
+ const rows = table.rows.map((r) => ({
40788
+ id: get(table, r, "ID"),
40789
+ group: get(table, r, "Group"),
40790
+ depends: get(table, r, "Depends"),
40791
+ original: get(table, r, "Original request"),
40792
+ question: get(table, r, "Question / options"),
40793
+ answer: get(table, r, "Answer")
40794
+ })).filter((q2) => /^QA-/.test(q2.id));
40795
+ return { table, rows };
40796
+ }
40797
+ function writeQa(md, rows) {
40798
+ const { table } = parseQa(md);
40799
+ if (!table) return md;
40800
+ return replaceRows(
40801
+ md,
40802
+ table,
40803
+ rows.map(
40804
+ (q2) => build(table.header, {
40805
+ id: q2.id,
40806
+ group: q2.group,
40807
+ depends: q2.depends,
40808
+ "original request": q2.original,
40809
+ "question / options": q2.question,
40810
+ answer: q2.answer
40811
+ })
40812
+ )
40813
+ );
40814
+ }
40815
+ var ATTEMPTS_REL = ".claude/.autonomous.attempts.json";
40816
+ async function readAttempts(root) {
40817
+ try {
40818
+ return JSON.parse(await readFile5(join17(root, ATTEMPTS_REL), "utf8"));
40819
+ } catch {
40820
+ return {};
40821
+ }
40822
+ }
40823
+ async function writeAttempts(root, map) {
40824
+ const sorted = Object.fromEntries(Object.entries(map).sort(([a], [b2]) => a.localeCompare(b2)));
40825
+ await writeFile4(join17(root, ATTEMPTS_REL), `${JSON.stringify(sorted, null, 2)}
40826
+ `, "utf8");
40827
+ }
40828
+ function stamp(d = /* @__PURE__ */ new Date()) {
40829
+ return d.toISOString().slice(0, 19).replace("T", " ");
40830
+ }
40831
+ function parseStamp(v2) {
40832
+ const iso = v2.trim().replace(" ", "T");
40833
+ return Date.parse(/[zZ]|[+-]\d\d:?\d\d$/.test(iso) ? iso : `${iso}Z`);
40834
+ }
40835
+ async function readBook(root, rel) {
40836
+ try {
40837
+ return await readFile5(join17(root, rel), "utf8");
40838
+ } catch {
40839
+ return "";
40840
+ }
40841
+ }
40842
+ async function writeBook(root, rel, text) {
40843
+ await writeFile4(join17(root, rel), text, "utf8");
40844
+ }
40845
+
40846
+ // src/core/autonomous-git.ts
40847
+ import { execFile as execFile4 } from "child_process";
40848
+ import { existsSync as existsSync11 } from "fs";
40849
+ import { readFile as readFile6 } from "fs/promises";
40850
+ import { join as join18 } from "path";
40851
+ import { promisify as promisify4 } from "util";
40852
+
40853
+ // src/core/git-host.ts
40854
+ import { execFile as execFile3 } from "child_process";
40855
+ import { promisify as promisify3 } from "util";
40543
40856
  var run3 = promisify3(execFile3);
40857
+ function repoHost(url) {
40858
+ const p2 = parseRepoUrl(url);
40859
+ if (!p2) return { provider: null, host: "", slug: "" };
40860
+ const slug = `${p2.owner}/${p2.name}`;
40861
+ const provider = /gitlab/i.test(p2.host) ? "glab" : /github|ghe\.com/i.test(p2.host) ? "gh" : null;
40862
+ return { provider, host: p2.host, slug };
40863
+ }
40864
+ function ghHostArgs(h) {
40865
+ return h.host && h.host !== "github.com" ? ["--hostname", h.host] : [];
40866
+ }
40867
+ function errText2(err) {
40868
+ const e = err;
40869
+ return (e?.stderr || e?.message || String(err)).trim().split("\n").filter(Boolean).slice(-2).join(" ").slice(0, 400);
40870
+ }
40871
+ async function branchProtection(url, branch) {
40872
+ const h = repoHost(url);
40873
+ if (!h.provider || !branch) return null;
40874
+ try {
40875
+ if (h.provider === "gh") {
40876
+ const { stdout } = await run3("gh", ["api", ...ghHostArgs(h), `repos/${h.slug}/branches/${encodeURIComponent(branch)}`, "--jq", ".protected"], { timeout: 3e4 });
40877
+ return stdout.trim() === "true";
40878
+ }
40879
+ const project = encodeURIComponent(h.slug);
40880
+ try {
40881
+ await run3("glab", ["api", `projects/${project}/protected_branches/${encodeURIComponent(branch)}`], { timeout: 3e4 });
40882
+ return true;
40883
+ } catch (err) {
40884
+ return /404|not found/i.test(errText2(err)) ? false : null;
40885
+ }
40886
+ } catch {
40887
+ return null;
40888
+ }
40889
+ }
40890
+ async function prStateOf(url, branch) {
40891
+ const h = repoHost(url);
40892
+ if (!h.provider) return null;
40893
+ try {
40894
+ if (h.provider === "gh") {
40895
+ const { stdout: stdout2 } = await run3(
40896
+ "gh",
40897
+ ["pr", "list", "-R", h.host && h.host !== "github.com" ? `${h.host}/${h.slug}` : h.slug, "--head", branch, "--state", "all", "--json", "state", "-L", "1"],
40898
+ { timeout: 3e4 }
40899
+ );
40900
+ const s4 = JSON.parse(stdout2)[0]?.state?.toLowerCase();
40901
+ return s4 === "open" || s4 === "merged" || s4 === "closed" ? s4 : null;
40902
+ }
40903
+ const { stdout } = await run3("glab", ["mr", "list", "-R", h.slug, "--source-branch", branch, "--all", "-F", "json", "-P", "1"], { timeout: 3e4 });
40904
+ const s3 = JSON.parse(stdout)[0]?.state?.toLowerCase();
40905
+ return s3 === "opened" ? "open" : s3 === "merged" ? "merged" : s3 === "closed" ? "closed" : null;
40906
+ } catch {
40907
+ return null;
40908
+ }
40909
+ }
40910
+ async function openPullRequest(opts) {
40911
+ const h = repoHost(opts.url);
40912
+ if (!h.provider) return { url: null, error: "Unknown git host \u2014 no pull request opened." };
40913
+ const args = h.provider === "gh" ? ["pr", "create", "-R", h.host && h.host !== "github.com" ? `${h.host}/${h.slug}` : h.slug, "--base", opts.base, "--head", opts.head, "--title", opts.title, "--body", opts.body] : ["mr", "create", "-R", h.slug, "--target-branch", opts.base, "--source-branch", opts.head, "--title", opts.title, "--description", opts.body, "--yes"];
40914
+ try {
40915
+ const { stdout } = await run3(h.provider, args, { cwd: opts.cwd, timeout: 12e4 });
40916
+ return { url: stdout.trim().split(/\s+/).find((x) => /^https?:\/\//.test(x)) ?? null, error: null };
40917
+ } catch (err) {
40918
+ const msg = errText2(err);
40919
+ const existing = /already exists/i.test(msg) ? msg.match(/https?:\/\/\S+/)?.[0] ?? null : null;
40920
+ return existing ? { url: existing, error: null } : { url: null, error: msg };
40921
+ }
40922
+ }
40923
+
40924
+ // src/core/autonomous-git.ts
40925
+ var run4 = promisify4(execFile4);
40926
+ var BOOK_PATHS = [
40927
+ "AI_TODO.md",
40928
+ "AI_DONE.md",
40929
+ "AI_PROGRESS.md",
40930
+ "USER_TODO.md",
40931
+ "USER_QA.md",
40932
+ ".claude/.autonomous.approvals.json",
40933
+ ".claude/.autonomous.authors.json",
40934
+ ATTEMPTS_REL
40935
+ ];
40936
+ async function git(cwd2, args, timeout = 12e4) {
40937
+ try {
40938
+ const { stdout } = await run4("git", args, { cwd: cwd2, timeout, maxBuffer: 32 * 1024 * 1024 });
40939
+ return stdout;
40940
+ } catch (err) {
40941
+ const e = err;
40942
+ throw new Error((e.stderr || e.message || String(err)).trim().split("\n").slice(-3).join(" "));
40943
+ }
40944
+ }
40945
+ async function gitQuiet(cwd2, args, timeout = 6e4) {
40946
+ try {
40947
+ return await git(cwd2, args, timeout);
40948
+ } catch {
40949
+ return "";
40950
+ }
40951
+ }
40952
+ async function remoteDefault(cwd2) {
40953
+ const ref = (await gitQuiet(cwd2, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).trim();
40954
+ if (ref) return ref.replace(/^origin\//, "");
40955
+ const out = await gitQuiet(cwd2, ["ls-remote", "--symref", "origin", "HEAD"]);
40956
+ return /ref: refs\/heads\/(\S+)\s+HEAD/.exec(out)?.[1] ?? "";
40957
+ }
40958
+ async function resolveBases(root) {
40959
+ let declared = "";
40960
+ let url = "";
40961
+ try {
40962
+ const spec = JSON.parse(await readFile6(join18(root, "project.spec.json"), "utf8"));
40963
+ const primary = spec.repos?.find((r) => r.primary) ?? spec.repos?.[0];
40964
+ declared = primary?.branch?.trim() ?? "";
40965
+ url = primary?.url?.trim() ?? "";
40966
+ } catch {
40967
+ }
40968
+ if (!url) url = (await gitQuiet(root, ["remote", "get-url", "origin"])).trim();
40969
+ const rootBase = { dir: "", url, base: declared || await remoteDefault(root) };
40970
+ const subs = [];
40971
+ if (existsSync11(join18(root, ".gitmodules"))) {
40972
+ const paths = (await gitQuiet(root, ["config", "-f", ".gitmodules", "--get-regexp", "^submodule\\..*\\.path$"])).split("\n").filter(Boolean);
40973
+ for (const line of paths) {
40974
+ const [key = "", ...rest] = line.split(" ");
40975
+ const name = key.replace(/^submodule\./, "").replace(/\.path$/, "");
40976
+ const dir2 = rest.join(" ");
40977
+ const subUrl = (await gitQuiet(root, ["config", "-f", ".gitmodules", `submodule.${name}.url`])).trim();
40978
+ const b2 = (await gitQuiet(root, ["config", "-f", ".gitmodules", `submodule.${name}.branch`])).trim();
40979
+ const abs = join18(root, dir2);
40980
+ subs.push({ dir: dir2, url: subUrl, base: b2 || (existsSync11(join18(abs, ".git")) ? await remoteDefault(abs) : "") });
40981
+ }
40982
+ }
40983
+ return { root: rootBase, subs };
40984
+ }
40985
+ async function hasRemoteBranch(cwd2, branch) {
40986
+ return (await gitQuiet(cwd2, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}`])).trim().length > 0;
40987
+ }
40988
+ async function currentBranch(cwd2) {
40989
+ const b2 = (await gitQuiet(cwd2, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
40990
+ return b2 === "HEAD" ? "" : b2;
40991
+ }
40992
+ async function wipCommit(cwd2, message) {
40993
+ await gitQuiet(cwd2, ["add", "-A"]);
40994
+ const staged = (await gitQuiet(cwd2, ["diff", "--cached", "--name-only"])).trim();
40995
+ if (!staged) return false;
40996
+ await git(cwd2, ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", "commit", "-q", "-m", message]).catch(() => void 0);
40997
+ return true;
40998
+ }
40999
+ async function syncBase(root, base) {
41000
+ const cur = await currentBranch(root);
41001
+ if ((await gitQuiet(root, ["status", "--porcelain"])).trim()) {
41002
+ if (cur.includes("/dev/")) await wipCommit(root, "wip: autosave before sync (4PM)");
41003
+ else await gitQuiet(root, ["stash", "push", "-u", "-m", "4PM autosave"]);
41004
+ }
41005
+ await git(root, ["fetch", "-q", "--prune", "origin"]);
41006
+ if (await hasRemoteBranch(root, base)) await git(root, ["checkout", "-q", "-B", base, `origin/${base}`]);
41007
+ else await git(root, ["checkout", "-q", base]);
41008
+ await gitQuiet(root, ["submodule", "update", "--init", "-q"], 3e5);
41009
+ }
41010
+ async function publishBooks(root, base, message, change, opts = {}) {
41011
+ for (let attempt = 1; attempt <= 5; attempt++) {
41012
+ await git(root, ["fetch", "-q", "origin", base]).catch(() => void 0);
41013
+ if (await hasRemoteBranch(root, base)) {
41014
+ if (opts.detach) await git(root, ["checkout", "-q", "--detach", `origin/${base}`]);
41015
+ else await git(root, ["checkout", "-q", "-B", base, `origin/${base}`]);
41016
+ }
41017
+ if (!await change()) return "aborted";
41018
+ await gitQuiet(root, ["add", "--", ...BOOK_PATHS.filter((p2) => existsSync11(join18(root, p2)))]);
41019
+ const staged = (await gitQuiet(root, ["diff", "--cached", "--name-only"])).trim();
41020
+ if (!staged) return "unchanged";
41021
+ await git(root, ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", "commit", "-q", "-m", message]);
41022
+ try {
41023
+ await git(root, ["push", "-q", "origin", `HEAD:refs/heads/${base}`]);
41024
+ return "pushed";
41025
+ } catch (err) {
41026
+ const msg = String(err);
41027
+ if (!/non-fast-forward|fetch first|rejected|stale info/i.test(msg)) return "failed";
41028
+ await new Promise((r) => setTimeout(r, 500 + Math.random() * 2e3 * attempt));
41029
+ }
41030
+ }
41031
+ return "failed";
41032
+ }
41033
+ async function publishIntake(root, base, message) {
41034
+ await gitQuiet(root, ["add", "--", ...BOOK_PATHS.filter((p2) => existsSync11(join18(root, p2)))]);
41035
+ const staged = (await gitQuiet(root, ["diff", "--cached", "--name-only"])).trim();
41036
+ await gitQuiet(root, ["checkout", "--", "."]);
41037
+ await gitQuiet(root, ["clean", "-fdq", "--", "."]);
41038
+ if (!staged) return "unchanged";
41039
+ await git(root, ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", "commit", "-q", "-m", message]);
41040
+ for (let attempt = 1; attempt <= 5; attempt++) {
41041
+ try {
41042
+ await git(root, ["push", "-q", "origin", `HEAD:refs/heads/${base}`]);
41043
+ return "pushed";
41044
+ } catch {
41045
+ await gitQuiet(root, ["fetch", "-q", "origin", base]);
41046
+ try {
41047
+ await git(root, ["rebase", "-q", `origin/${base}`]);
41048
+ } catch {
41049
+ await gitQuiet(root, ["rebase", "--abort"]);
41050
+ await gitQuiet(root, ["reset", "-q", "--hard", `origin/${base}`]);
41051
+ return "aborted";
41052
+ }
41053
+ }
41054
+ }
41055
+ return "failed";
41056
+ }
41057
+ async function remoteBook(cwd2, base, rel) {
41058
+ await gitQuiet(cwd2, ["fetch", "-q", "origin", base]);
41059
+ return gitQuiet(cwd2, ["show", `origin/${base}:${rel}`]);
41060
+ }
41061
+ async function isMerged(cwd2, url, branch, base) {
41062
+ const state3 = await prStateOf(url, branch);
41063
+ if (state3 === "merged") return true;
41064
+ if (state3 === "open" || state3 === "closed") return false;
41065
+ if (!await hasRemoteBranch(cwd2, branch)) return true;
41066
+ try {
41067
+ await git(cwd2, ["merge-base", "--is-ancestor", `origin/${branch}`, `origin/${base}`]);
41068
+ return true;
41069
+ } catch {
41070
+ return false;
41071
+ }
41072
+ }
41073
+ async function checkoutTaskBranch(cwd2, branch, base, from) {
41074
+ await git(cwd2, ["fetch", "-q", "--prune", "origin"]);
41075
+ const local = (await gitQuiet(cwd2, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`])).trim();
41076
+ if (await hasRemoteBranch(cwd2, branch)) {
41077
+ if (local && await gitQuiet(cwd2, ["merge-base", "--is-ancestor", `origin/${branch}`, branch]).then(() => true, () => false)) {
41078
+ await git(cwd2, ["checkout", "-q", branch]);
41079
+ } else await git(cwd2, ["checkout", "-q", "-B", branch, `origin/${branch}`]);
41080
+ return;
41081
+ }
41082
+ if (local) {
41083
+ await git(cwd2, ["checkout", "-q", branch]);
41084
+ return;
41085
+ }
41086
+ const start = from && await hasRemoteBranch(cwd2, from) ? `origin/${from}` : `origin/${base}`;
41087
+ await git(cwd2, ["checkout", "-q", "-B", branch, start]);
41088
+ }
41089
+ async function aheadOf(cwd2, branch, base) {
41090
+ return Number((await gitQuiet(cwd2, ["rev-list", "--count", `origin/${base}..${branch}`])).trim()) || 0;
41091
+ }
41092
+ async function pushBranch(cwd2, branch) {
41093
+ await git(cwd2, ["push", "-q", "-u", "origin", `${branch}:refs/heads/${branch}`], 18e4);
41094
+ }
41095
+ async function deliverPr(cwd2, url, branch, base, title, body) {
41096
+ return openPullRequest({ url, cwd: cwd2, base, head: branch, title, body });
41097
+ }
41098
+ async function mutateBooksOnBase(root, base, message, fn) {
41099
+ await git(root, ["fetch", "-q", "origin", base]).catch(() => void 0);
41100
+ if (!await hasRemoteBranch(root, base)) return "unchanged";
41101
+ const dir2 = join18(root, ".git", `4pm-books-${process.pid}-${Date.now()}`);
41102
+ await git(root, ["worktree", "add", "-q", "--detach", dir2, `origin/${base}`]);
41103
+ try {
41104
+ return await publishBooks(dir2, base, message, () => fn(dir2), { detach: true });
41105
+ } finally {
41106
+ await gitQuiet(root, ["worktree", "remove", "--force", dir2]);
41107
+ await gitQuiet(root, ["worktree", "prune"]);
41108
+ }
41109
+ }
41110
+ var lastFetch = /* @__PURE__ */ new Map();
41111
+ async function readBaseFile(root, base, rel) {
41112
+ const key = `${root}\0${base}`;
41113
+ if (Date.now() - (lastFetch.get(key) ?? 0) > 15e3) {
41114
+ await gitQuiet(root, ["fetch", "-q", "origin", base]);
41115
+ lastFetch.set(key, Date.now());
41116
+ }
41117
+ if (!await hasRemoteBranch(root, base)) return null;
41118
+ try {
41119
+ return await git(root, ["show", `origin/${base}:${rel}`]);
41120
+ } catch {
41121
+ return "";
41122
+ }
41123
+ }
41124
+
41125
+ // src/core/autonomous.ts
41126
+ var run5 = promisify5(execFile5);
40544
41127
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
40545
41128
  var AUTHORS_REL = ".claude/.autonomous.authors.json";
40546
41129
  var TICK_REL = ".claude/hooks/autonomous-tick.sh";
@@ -40554,7 +41137,7 @@ var BOOK_FILES = {
40554
41137
  };
40555
41138
  async function readText(path, fallback = "") {
40556
41139
  try {
40557
- return await readFile5(path, "utf8");
41140
+ return await readFile7(path, "utf8");
40558
41141
  } catch {
40559
41142
  return fallback;
40560
41143
  }
@@ -40603,16 +41186,16 @@ var CAPPED_BOOK_FILES = { USER_TODO: "USER_TODO.md", USER_QA: "USER_QA.md", AI_T
40603
41186
  async function cappedBookIds(root) {
40604
41187
  const out = {};
40605
41188
  for (const [book, file] of Object.entries(CAPPED_BOOK_FILES)) {
40606
- out[book] = new Set(tableRowsById(await readText(join17(root, file))).keys());
41189
+ out[book] = new Set(tableRowsById(await readText(join19(root, file))).keys());
40607
41190
  }
40608
41191
  return out;
40609
41192
  }
40610
41193
  function tickScript(root) {
40611
- return join17(root, TICK_REL);
41194
+ return join19(root, TICK_REL);
40612
41195
  }
40613
41196
  async function crontabList() {
40614
41197
  try {
40615
- const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
41198
+ const { stdout } = await run5("crontab", ["-l"], { timeout: 1e4 });
40616
41199
  return stdout;
40617
41200
  } catch {
40618
41201
  return "";
@@ -40645,13 +41228,15 @@ async function getAutonomousStatus(root, profileDir2) {
40645
41228
  lastTickAt: last?.ts ?? null,
40646
41229
  lastResult: last?.status ?? null,
40647
41230
  consecutiveFails: hist.consecutiveFails,
40648
- todayTicks: hist.ticks.day === today ? hist.ticks.count : 0
41231
+ todayTicks: hist.ticks.day === today ? hist.ticks.count : 0,
41232
+ // The last tick stopped on a protected base (ADR-0371 phase 0) — "<branch> (<repo>)".
41233
+ baseProtected: /^base-protected: (.+)$/.exec(last?.note ?? "")?.[1] ?? null
40649
41234
  };
40650
41235
  }
40651
41236
  function isAutonomousRunning(root, profileDir2) {
40652
41237
  try {
40653
41238
  if (readAutonomousConfigSync(profileDir2).paused) return false;
40654
- const h = JSON.parse(readFileSync14(join17(root, ".claude/.autonomous.histories.json"), "utf8"));
41239
+ const h = JSON.parse(readFileSync14(join19(root, ".claude/.autonomous.histories.json"), "utf8"));
40655
41240
  const ts2 = h.records?.[h.records.length - 1]?.ts;
40656
41241
  if (!ts2) return false;
40657
41242
  const t2 = Date.parse(ts2.replace(" ", "T"));
@@ -40661,21 +41246,27 @@ function isAutonomousRunning(root, profileDir2) {
40661
41246
  }
40662
41247
  }
40663
41248
  async function readAutonomous(root, profileDir2) {
40664
- const [settings, approvals, authors, status, ...books] = await Promise.all([
41249
+ const base = (await resolveBases(root).catch(() => null))?.root.base ?? "";
41250
+ const read = async (rel, fallback = "") => {
41251
+ const remote = base ? await readBaseFile(root, base, rel) : null;
41252
+ return remote === null ? readText(join19(root, rel), fallback) : remote || fallback;
41253
+ };
41254
+ const [settings, approvals, authors, attempts, status, ...books] = await Promise.all([
40665
41255
  readAutonomousConfigText(profileDir2),
40666
- readText(join17(root, APPROVALS_REL), "{}"),
40667
- readText(join17(root, AUTHORS_REL), "{}"),
41256
+ read(APPROVALS_REL, "{}"),
41257
+ read(AUTHORS_REL, "{}"),
41258
+ read(ATTEMPTS_REL, "{}"),
40668
41259
  getAutonomousStatus(root, profileDir2),
40669
- ...Object.values(BOOK_FILES).map((f2) => readText(join17(root, f2)))
41260
+ ...Object.values(BOOK_FILES).map((f2) => read(f2))
40670
41261
  ]);
40671
41262
  const keys = Object.keys(BOOK_FILES);
40672
41263
  const bookMap = {};
40673
41264
  keys.forEach((k2, i) => bookMap[k2] = books[i] ?? "");
40674
- return { settings, status, books: bookMap, approvals, authors };
41265
+ return { settings, status, books: bookMap, approvals, authors, attempts };
40675
41266
  }
40676
41267
  async function readAutonomousLogs(root, date) {
40677
41268
  const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
40678
- const text = await readText(join17(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
41269
+ const text = await readText(join19(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
40679
41270
  const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
40680
41271
  return { date: day, lines };
40681
41272
  }
@@ -40697,79 +41288,88 @@ async function uninstallCron(root) {
40697
41288
  const kept = cur.split("\n").filter((l) => l.trim() && !l.includes(script));
40698
41289
  await crontabSet(kept.join("\n"));
40699
41290
  }
41291
+ async function applyWebWrite(dir2, req, by) {
41292
+ switch (req.kind) {
41293
+ case "approvals": {
41294
+ if (req.approved && !req.byIsAdmin) {
41295
+ const authors = await readJsonMap(join19(dir2, AUTHORS_REL));
41296
+ if (entryBy(authors, req.taskId) === by) return { ok: false, code: "APPROVAL_SELF", failedId: req.taskId, error: "self-approval blocked" };
41297
+ }
41298
+ const map = parseJson(await readText(join19(dir2, APPROVALS_REL), "{}")) ?? {};
41299
+ if (req.approved) map[req.taskId] = { approved: true, by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
41300
+ else delete map[req.taskId];
41301
+ await writeFile5(join19(dir2, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
41302
+ return { ok: true };
41303
+ }
41304
+ case "approvalsBatch": {
41305
+ if (!req.byIsAdmin) {
41306
+ const authors = await readJsonMap(join19(dir2, AUTHORS_REL));
41307
+ const selfId = req.approve.find((id) => entryBy(authors, id) === by);
41308
+ if (selfId) return { ok: false, code: "APPROVAL_SELF", failedId: selfId, error: "self-approval blocked" };
41309
+ }
41310
+ const map = parseJson(await readText(join19(dir2, APPROVALS_REL), "{}")) ?? {};
41311
+ const at2 = (/* @__PURE__ */ new Date()).toISOString();
41312
+ const byLabel = req.byLabel ?? by;
41313
+ for (const taskId of req.approve) map[taskId] = { approved: true, by, byLabel, at: at2 };
41314
+ for (const taskId of req.unapprove) delete map[taskId];
41315
+ await writeFile5(join19(dir2, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
41316
+ return { ok: true };
41317
+ }
41318
+ case "userTodo": {
41319
+ const cur = await readText(join19(dir2, BOOK_FILES.userTodo));
41320
+ const ids = [...cur.matchAll(/REQ-(\d{4})-(\d{4})/g)];
41321
+ const maxGroup = ids.reduce((m2, g2) => Math.max(m2, Number(g2[1])), 0);
41322
+ const id = `REQ-${String(maxGroup + 1).padStart(4, "0")}-0001`;
41323
+ const row = `| ${id} | | | ${tableCell(req.content)} |
41324
+ `;
41325
+ await writeFile5(join19(dir2, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
41326
+ const authors = await readJsonMap(join19(dir2, AUTHORS_REL));
41327
+ authors[id] = { by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
41328
+ await writeFile5(join19(dir2, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
41329
+ return { ok: true, added: 1 };
41330
+ }
41331
+ case "bookSave": {
41332
+ const file = BOOK_FILES[req.book];
41333
+ const prev = tableRowsById(await readText(join19(dir2, file)));
41334
+ const next = tableRowsById(req.content);
41335
+ const authors = await readJsonMap(join19(dir2, AUTHORS_REL));
41336
+ const at2 = (/* @__PURE__ */ new Date()).toISOString();
41337
+ const byLabel = req.byLabel ?? by;
41338
+ let added = 0;
41339
+ for (const [id, cells] of next) {
41340
+ if (prev.get(id) !== cells) authors[id] = { by, byLabel, at: at2 };
41341
+ if (!prev.has(id)) added += 1;
41342
+ }
41343
+ await writeFile5(join19(dir2, file), req.content, "utf8");
41344
+ await writeFile5(join19(dir2, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
41345
+ return { ok: true, added };
41346
+ }
41347
+ default:
41348
+ return { ok: false, error: "not a book write" };
41349
+ }
41350
+ }
40700
41351
  async function writeAutonomous(root, profileDir2, req, by) {
40701
41352
  let added;
40702
41353
  try {
40703
- switch (req.kind) {
40704
- case "settings": {
40705
- const parsed = parseJson(req.settings);
40706
- if (!parsed) return { ok: false, error: "settings is not valid JSON" };
40707
- await writeAutonomousConfig(profileDir2, parsed);
40708
- await syncCron(root, profileDir2);
40709
- break;
40710
- }
40711
- case "approvals": {
40712
- if (req.approved && !req.byIsAdmin) {
40713
- const authors = await readJsonMap(join17(root, AUTHORS_REL));
40714
- if (entryBy(authors, req.taskId) === by) {
40715
- return { ok: false, code: "APPROVAL_SELF", failedId: req.taskId, error: "self-approval blocked" };
40716
- }
40717
- }
40718
- const map = parseJson(await readText(join17(root, APPROVALS_REL), "{}")) ?? {};
40719
- if (req.approved) map[req.taskId] = { approved: true, by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
40720
- else delete map[req.taskId];
40721
- await writeFile4(join17(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
40722
- break;
40723
- }
40724
- case "approvalsBatch": {
40725
- if (!req.byIsAdmin) {
40726
- const authors = await readJsonMap(join17(root, AUTHORS_REL));
40727
- const selfId = req.approve.find((id) => entryBy(authors, id) === by);
40728
- if (selfId) return { ok: false, code: "APPROVAL_SELF", failedId: selfId, error: "self-approval blocked" };
40729
- }
40730
- const map = parseJson(await readText(join17(root, APPROVALS_REL), "{}")) ?? {};
40731
- const at2 = (/* @__PURE__ */ new Date()).toISOString();
40732
- const byLabel = req.byLabel ?? by;
40733
- for (const taskId of req.approve) map[taskId] = { approved: true, by, byLabel, at: at2 };
40734
- for (const taskId of req.unapprove) delete map[taskId];
40735
- await writeFile4(join17(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
40736
- break;
40737
- }
40738
- case "userTodo": {
40739
- const cur = await readText(join17(root, BOOK_FILES.userTodo));
40740
- const ids = [...cur.matchAll(/REQ-(\d{4})-(\d{4})/g)];
40741
- const maxGroup = ids.reduce((m2, g2) => Math.max(m2, Number(g2[1])), 0);
40742
- const id = `REQ-${String(maxGroup + 1).padStart(4, "0")}-0001`;
40743
- const row = `| ${id} | | | ${tableCell(req.content)} |
40744
- `;
40745
- await writeFile4(join17(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
40746
- added = 1;
40747
- const authors = await readJsonMap(join17(root, AUTHORS_REL));
40748
- authors[id] = { by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
40749
- await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
40750
- break;
40751
- }
40752
- case "bookSave": {
40753
- const file = BOOK_FILES[req.book];
40754
- const prev = tableRowsById(await readText(join17(root, file)));
40755
- const next = tableRowsById(req.content);
40756
- const authors = await readJsonMap(join17(root, AUTHORS_REL));
40757
- const at2 = (/* @__PURE__ */ new Date()).toISOString();
40758
- const byLabel = req.byLabel ?? by;
40759
- added = 0;
40760
- for (const [id, cells] of next) {
40761
- if (prev.get(id) !== cells) authors[id] = { by, byLabel, at: at2 };
40762
- if (!prev.has(id)) added += 1;
40763
- }
40764
- await writeFile4(join17(root, file), req.content, "utf8");
40765
- await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
40766
- break;
40767
- }
40768
- case "cron": {
40769
- if (req.action === "install") await installCron(root, profileDir2);
40770
- else await uninstallCron(root);
40771
- break;
40772
- }
41354
+ if (req.kind === "settings") {
41355
+ const parsed = parseJson(req.settings);
41356
+ if (!parsed) return { ok: false, error: "settings is not valid JSON" };
41357
+ await writeAutonomousConfig(profileDir2, parsed);
41358
+ await syncCron(root, profileDir2);
41359
+ } else if (req.kind === "cron") {
41360
+ if (req.action === "install") await installCron(root, profileDir2);
41361
+ else await uninstallCron(root);
41362
+ } else {
41363
+ const base = (await resolveBases(root).catch(() => null))?.root.base ?? "";
41364
+ let res;
41365
+ const pub = base ? await mutateBooksOnBase(root, base, `chore(web): ${req.kind} by ${req.byLabel ?? by}`, async (dir2) => {
41366
+ res = await applyWebWrite(dir2, req, by);
41367
+ return res.ok;
41368
+ }) : "unchanged";
41369
+ if (res === void 0) res = await applyWebWrite(root, req, by);
41370
+ else if (pub === "failed") return { ok: false, error: "could not push the change to the base branch (retry)" };
41371
+ if (!res.ok) return { ok: false, ...res.code ? { code: res.code } : {}, ...res.failedId ? { failedId: res.failedId } : {}, error: res.error };
41372
+ added = res.added;
40773
41373
  }
40774
41374
  return { ok: true, status: await getAutonomousStatus(root, profileDir2), ...added !== void 0 ? { added } : {} };
40775
41375
  } catch (err) {
@@ -40778,7 +41378,7 @@ async function writeAutonomous(root, profileDir2, req, by) {
40778
41378
  }
40779
41379
 
40780
41380
  // src/core/network-probe.ts
40781
- import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
41381
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
40782
41382
  import { connect } from "net";
40783
41383
  var CANARY_TARGETS = [
40784
41384
  { host: "1.1.1.1", port: 443 },
@@ -40787,7 +41387,7 @@ var CANARY_TARGETS = [
40787
41387
  var PROBE_TIMEOUT_MS = 2500;
40788
41388
  function detectContainerized() {
40789
41389
  try {
40790
- if (existsSync11("/.dockerenv")) return true;
41390
+ if (existsSync12("/.dockerenv")) return true;
40791
41391
  const cgroup = readFileSync15("/proc/1/cgroup", "utf8");
40792
41392
  return /docker|containerd|kubepods|libpod/.test(cgroup);
40793
41393
  } catch {
@@ -40855,9 +41455,9 @@ async function probeNetwork() {
40855
41455
  }
40856
41456
 
40857
41457
  // src/core/git-auth.ts
40858
- import { chmodSync as chmodSync3, existsSync as existsSync12, mkdirSync as mkdirSync6, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
41458
+ import { chmodSync as chmodSync3, existsSync as existsSync13, mkdirSync as mkdirSync6, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
40859
41459
  import { request as httpsRequest } from "https";
40860
- import { delimiter, dirname as dirname4, join as join18, resolve as resolve3 } from "path";
41460
+ import { delimiter, dirname as dirname4, join as join20, resolve as resolve3 } from "path";
40861
41461
  var JOB_ID_ENV = "FOURPM_JOB_ID";
40862
41462
  var PROFILE_DIR_ENV = "FOURPM_PROFILE_DIR";
40863
41463
  var GIT_HOST_ENV = "FOURPM_GIT_HOST";
@@ -40877,8 +41477,8 @@ function helperCommand() {
40877
41477
  function findRealGh(pathValue, shimDir) {
40878
41478
  for (const dir2 of pathValue.split(delimiter)) {
40879
41479
  if (!dir2 || dir2 === shimDir) continue;
40880
- const candidate = join18(dir2, process.platform === "win32" ? "gh.exe" : "gh");
40881
- if (existsSync12(candidate)) return candidate;
41480
+ const candidate = join20(dir2, process.platform === "win32" ? "gh.exe" : "gh");
41481
+ if (existsSync13(candidate)) return candidate;
40882
41482
  }
40883
41483
  return null;
40884
41484
  }
@@ -40939,22 +41539,22 @@ function configureGitAuth(method, host, profileDir2, transport) {
40939
41539
  process.env[PROFILE_DIR_ENV] = resolve3(profileDir2);
40940
41540
  const script = process.argv[1];
40941
41541
  const devTsconfig = script ? resolve3(dirname4(script), "..", "tsconfig.json") : "";
40942
- if (process.execArgv.some((a) => a.includes("tsx")) && devTsconfig && existsSync12(devTsconfig) && !process.env.TSX_TSCONFIG_PATH) {
41542
+ if (process.execArgv.some((a) => a.includes("tsx")) && devTsconfig && existsSync13(devTsconfig) && !process.env.TSX_TSCONFIG_PATH) {
40943
41543
  process.env.TSX_TSCONFIG_PATH = devTsconfig;
40944
41544
  }
40945
41545
  process.env[GIT_HOST_ENV] = host;
40946
41546
  process.env[GIT_HOST_KIND_ENV] = githubHostKind(host);
40947
41547
  if (process.platform !== "win32") {
40948
- const shimDir = join18(profileDir2, "bin");
41548
+ const shimDir = join20(profileDir2, "bin");
40949
41549
  const realGh = findRealGh(state.baseEnv.path, shimDir);
40950
41550
  if (realGh) {
40951
41551
  mkdirSync6(shimDir, { recursive: true });
40952
- const shim = join18(shimDir, "gh");
41552
+ const shim = join20(shimDir, "gh");
40953
41553
  writeFileSync8(shim, shimScript(realGh, cliInvocation()), { encoding: "utf8", mode: 493 });
40954
41554
  chmodSync3(shim, 493);
40955
41555
  process.env.PATH = `${shimDir}${delimiter}${state.baseEnv.path}`;
40956
41556
  } else {
40957
- rmSync6(join18(shimDir, "gh"), { force: true });
41557
+ rmSync6(join20(shimDir, "gh"), { force: true });
40958
41558
  }
40959
41559
  }
40960
41560
  state.host = host;
@@ -41028,16 +41628,16 @@ async function revokeAll() {
41028
41628
  }
41029
41629
 
41030
41630
  // src/core/git-snapshot.ts
41031
- import { execFile as execFile6 } from "child_process";
41631
+ import { execFile as execFile8 } from "child_process";
41032
41632
  import { stat } from "fs/promises";
41033
- import { join as join20, resolve as resolve5 } from "path";
41034
- import { promisify as promisify6 } from "util";
41633
+ import { join as join22, resolve as resolve5 } from "path";
41634
+ import { promisify as promisify8 } from "util";
41035
41635
 
41036
41636
  // src/core/git-history.ts
41037
- import { execFile as execFile4 } from "child_process";
41637
+ import { execFile as execFile6 } from "child_process";
41038
41638
  import { resolve as resolve4, sep as sep2 } from "path";
41039
- import { promisify as promisify4 } from "util";
41040
- var run4 = promisify4(execFile4);
41639
+ import { promisify as promisify6 } from "util";
41640
+ var run6 = promisify6(execFile6);
41041
41641
  var FS = "";
41042
41642
  var RS = "";
41043
41643
  function resolveRepo(root, repo) {
@@ -41046,9 +41646,9 @@ function resolveRepo(root, repo) {
41046
41646
  const requested = resolve4(base, repo && repo.trim() ? repo : ".");
41047
41647
  return requested === base || requested.startsWith(base + sep2) ? requested : base;
41048
41648
  }
41049
- async function git(cwd2, args) {
41649
+ async function git2(cwd2, args) {
41050
41650
  try {
41051
- const { stdout } = await run4("git", args, {
41651
+ const { stdout } = await run6("git", args, {
41052
41652
  cwd: cwd2,
41053
41653
  timeout: 15e3,
41054
41654
  maxBuffer: 8 * 1024 * 1024
@@ -41061,21 +41661,21 @@ async function git(cwd2, args) {
41061
41661
  async function gitRepos(root) {
41062
41662
  if (!root) return { repos: [] };
41063
41663
  const base = resolve4(root);
41064
- const top = (await git(base, ["rev-parse", "--show-toplevel"])).trim();
41664
+ const top = (await git2(base, ["rev-parse", "--show-toplevel"])).trim();
41065
41665
  if (!top || !(top === base || top.startsWith(base + sep2))) return { repos: [] };
41066
41666
  const repos = [];
41067
- const rootRemote = (await git(base, ["remote", "get-url", "origin"])).trim();
41068
- const rootBranch = (await git(base, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
41667
+ const rootRemote = (await git2(base, ["remote", "get-url", "origin"])).trim();
41668
+ const rootBranch = (await git2(base, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
41069
41669
  repos.push({ subdir: "", name: "", remote: rootRemote || null, branch: rootBranch || null });
41070
- const subOut = await git(base, ["submodule", "status"]);
41670
+ const subOut = await git2(base, ["submodule", "status"]);
41071
41671
  for (const line of subOut.split("\n")) {
41072
41672
  const m2 = line.trim().match(/^[-+U ]?[0-9a-f]{7,40}\s+(\S+)/);
41073
41673
  const subPath = m2?.[1];
41074
41674
  if (!subPath) continue;
41075
41675
  const abs = resolve4(base, subPath);
41076
41676
  if (!(abs === base || abs.startsWith(base + sep2))) continue;
41077
- const remote = (await git(abs, ["remote", "get-url", "origin"])).trim();
41078
- const branch = (await git(abs, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
41677
+ const remote = (await git2(abs, ["remote", "get-url", "origin"])).trim();
41678
+ const branch = (await git2(abs, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
41079
41679
  repos.push({ subdir: subPath, name: subPath, remote: remote || null, branch: branch || null });
41080
41680
  }
41081
41681
  return { repos };
@@ -41085,7 +41685,7 @@ async function gitLog(root, repo, skip, limit) {
41085
41685
  if (!cwd2) return { entries: [], hasMore: false };
41086
41686
  const capped = Math.min(Math.max(limit, 1), 200);
41087
41687
  const format = ["%H", "%h", "%an", "%aI", "%s"].join(FS) + RS;
41088
- const stdout = await git(cwd2, [
41688
+ const stdout = await git2(cwd2, [
41089
41689
  "log",
41090
41690
  `--skip=${Math.max(skip, 0)}`,
41091
41691
  `-n`,
@@ -41104,7 +41704,7 @@ async function gitCommit(root, repo, hash) {
41104
41704
  const empty = { hash, author: "", date: "", subject: "", files: [] };
41105
41705
  if (!cwd2 || !hash) return empty;
41106
41706
  const format = ["%H", "%an", "%aI", "%s"].join(FS);
41107
- const stdout = await git(cwd2, [
41707
+ const stdout = await git2(cwd2, [
41108
41708
  "show",
41109
41709
  "--name-status",
41110
41710
  "-M",
@@ -41131,8 +41731,8 @@ async function gitCommit(root, repo, hash) {
41131
41731
  async function gitCommitDiff(root, repo, hash, path) {
41132
41732
  const cwd2 = resolveRepo(root, repo);
41133
41733
  if (!cwd2 || !hash || !path) return { path, oldContent: "", newContent: "" };
41134
- const oldContent = await git(cwd2, ["show", `${hash}^:${path}`]);
41135
- const newContent = await git(cwd2, ["show", `${hash}:${path}`]);
41734
+ const oldContent = await git2(cwd2, ["show", `${hash}^:${path}`]);
41735
+ const newContent = await git2(cwd2, ["show", `${hash}:${path}`]);
41136
41736
  return { path, oldContent, newContent };
41137
41737
  }
41138
41738
 
@@ -41352,7 +41952,7 @@ function summarizeAiFailure(captured, exitCode) {
41352
41952
 
41353
41953
  // src/utils/time.ts
41354
41954
  function formatTimestampInZone(date, timeZone) {
41355
- const build = (tz) => {
41955
+ const build2 = (tz) => {
41356
41956
  const parts = new Intl.DateTimeFormat("en-CA", {
41357
41957
  ...tz ? { timeZone: tz } : {},
41358
41958
  hour12: false,
@@ -41363,20 +41963,20 @@ function formatTimestampInZone(date, timeZone) {
41363
41963
  minute: "2-digit",
41364
41964
  second: "2-digit"
41365
41965
  }).formatToParts(date);
41366
- const get2 = (type) => parts.find((p2) => p2.type === type)?.value ?? "";
41367
- const hour = get2("hour") === "24" ? "00" : get2("hour");
41368
- return `${get2("year")}/${get2("month")}/${get2("day")} ${hour}:${get2("minute")}:${get2("second")}`;
41966
+ const get3 = (type) => parts.find((p2) => p2.type === type)?.value ?? "";
41967
+ const hour = get3("hour") === "24" ? "00" : get3("hour");
41968
+ return `${get3("year")}/${get3("month")}/${get3("day")} ${hour}:${get3("minute")}:${get3("second")}`;
41369
41969
  };
41370
41970
  try {
41371
- return build(timeZone);
41971
+ return build2(timeZone);
41372
41972
  } catch {
41373
- return build();
41973
+ return build2();
41374
41974
  }
41375
41975
  }
41376
41976
 
41377
41977
  // src/ui/slash-commands.ts
41378
- import { existsSync as existsSync13 } from "fs";
41379
- import { join as join19 } from "path";
41978
+ import { existsSync as existsSync14 } from "fs";
41979
+ import { join as join21 } from "path";
41380
41980
  var CONFIG_KEYS = [
41381
41981
  "aiCli",
41382
41982
  "claudeHome",
@@ -41388,7 +41988,7 @@ var CONFIG_KEYS = [
41388
41988
  "aiEnv"
41389
41989
  ];
41390
41990
  function configPath(profileDir2) {
41391
- return join19(profileDir2, "config.json");
41991
+ return join21(profileDir2, "config.json");
41392
41992
  }
41393
41993
  function parseConfigAssignment(key, valueTokens) {
41394
41994
  const value = valueTokens.join(" ").trim();
@@ -41438,7 +42038,7 @@ function runConfig(ctx) {
41438
42038
  ctx.print(t("slash.config.wroteDefault", { verb, path }));
41439
42039
  ctx.print(t("slash.config.addClaudeHint"));
41440
42040
  };
41441
- if (existsSync13(path)) {
42041
+ if (existsSync14(path)) {
41442
42042
  ctx.confirm(
41443
42043
  t("slash.config.replaceConfirm", { path }),
41444
42044
  () => writeDefaults(t("slash.config.verbReplaced"))
@@ -41523,7 +42123,7 @@ function runOutput(ctx) {
41523
42123
  }
41524
42124
  function runLogs(ctx) {
41525
42125
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
41526
- const dir2 = join19(ctx.info.profileDir, "logs");
42126
+ const dir2 = join21(ctx.info.profileDir, "logs");
41527
42127
  const lines = readRecentLogLines(dir2, n);
41528
42128
  if (lines.length === 0) {
41529
42129
  ctx.print(t("slash.logs.none"));
@@ -41870,13 +42470,13 @@ async function runMemoryCompaction(ai2, cwd2, input) {
41870
42470
  }
41871
42471
 
41872
42472
  // src/core/git-commit-identity.ts
41873
- import { execFile as execFile5 } from "child_process";
41874
- import { promisify as promisify5 } from "util";
41875
- var run5 = promisify5(execFile5);
42473
+ import { execFile as execFile7 } from "child_process";
42474
+ import { promisify as promisify7 } from "util";
42475
+ var run7 = promisify7(execFile7);
41876
42476
  async function setCommitAuthor(cwd2, machineUsername) {
41877
42477
  if (!machineUsername) return;
41878
42478
  try {
41879
- await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
42479
+ await run7("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
41880
42480
  } catch {
41881
42481
  }
41882
42482
  }
@@ -41983,10 +42583,10 @@ function activeAiRunCount() {
41983
42583
  return activeRuns.size;
41984
42584
  }
41985
42585
  function cancelActiveRun(commandId) {
41986
- const run12 = activeRuns.get(commandId);
41987
- if (!run12) return { ok: false, state: "unknown" };
41988
- run12.controller.abort();
41989
- return { ok: true, state: run12.state };
42586
+ const run15 = activeRuns.get(commandId);
42587
+ if (!run15) return { ok: false, state: "unknown" };
42588
+ run15.controller.abort();
42589
+ return { ok: true, state: run15.state };
41990
42590
  }
41991
42591
  function handleCommandChannels(ctx, message, payload) {
41992
42592
  if (message.channel === WsChannels.COMMAND_CANCEL) {
@@ -42082,17 +42682,18 @@ async function runLocalCommand(ctx, input) {
42082
42682
  const commandId = randomCommandId();
42083
42683
  await runAiPrompt(ctx, prompt, commandId, "local");
42084
42684
  }
42685
+ var REFUSED = { exitCode: 1, cancelled: false, exhausted: false, neverStarted: true, output: "" };
42085
42686
  async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
42086
- const run12 = { controller: new AbortController(), state: "queued" };
42087
- activeRuns.set(commandId, run12);
42687
+ const run15 = { controller: new AbortController(), state: "queued" };
42688
+ activeRuns.set(commandId, run15);
42088
42689
  try {
42089
- await runAiPromptInner(ctx, prompt, commandId, origin, run12, oneShot, images, aiConfig, readOnly, bypass);
42690
+ return await runAiPromptInner(ctx, prompt, commandId, origin, run15, oneShot, images, aiConfig, readOnly, bypass);
42090
42691
  } finally {
42091
42692
  activeRuns.delete(commandId);
42092
42693
  if (!readOnly) requestGitSnapshot("ai-run");
42093
42694
  }
42094
42695
  }
42095
- async function runAiPromptInner(ctx, prompt, commandId, origin, run12, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
42696
+ async function runAiPromptInner(ctx, prompt, commandId, origin, run15, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
42096
42697
  const config = readProfileConfig(ctx.profileDir);
42097
42698
  const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
42098
42699
  const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
@@ -42103,7 +42704,7 @@ async function runAiPromptInner(ctx, prompt, commandId, origin, run12, oneShot =
42103
42704
  const estimated = estimateTokens(prompt);
42104
42705
  if (estimated > perPromptLimit) {
42105
42706
  rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
42106
- return;
42707
+ return REFUSED;
42107
42708
  }
42108
42709
  }
42109
42710
  if (ctx.outboundReviewEnabled) {
@@ -42118,7 +42719,7 @@ async function runAiPromptInner(ctx, prompt, commandId, origin, run12, oneShot =
42118
42719
  }
42119
42720
  if (!verdict.ok) {
42120
42721
  rejectByReview(ctx, commandId, origin, prompt, verdict.reasons);
42121
- return;
42722
+ return REFUSED;
42122
42723
  }
42123
42724
  }
42124
42725
  const cwd2 = ctx.physicRoot ?? config.physicPath ?? process.cwd();
@@ -42209,6 +42810,7 @@ ${guardedPrompt}`;
42209
42810
  let resultId = null;
42210
42811
  let resultBuf = "";
42211
42812
  let answerText = "";
42813
+ const failReasons = [];
42212
42814
  const handlers = {
42213
42815
  onChunk: (text) => {
42214
42816
  if (!responseHeaderShown) {
@@ -42250,6 +42852,7 @@ ${guardedPrompt}`;
42250
42852
  `, log: true });
42251
42853
  },
42252
42854
  onAttemptFail: (label, reason) => {
42855
+ failReasons.push(String(reason));
42253
42856
  const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : reason === "credits" ? `profile "${label}" is out of usage credits \u2014 trying next` : reason === "auth" ? `profile "${label}" failed to authenticate \u2014 trying next` : reason === "timeout" ? `profile "${label}" timed out \u2014 trying next` : `profile "${label}" run failed \u2014 trying next`;
42254
42857
  ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
42255
42858
  ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
@@ -42273,7 +42876,7 @@ ${guardedPrompt}`;
42273
42876
  queue: { position, limit, running: running2 }
42274
42877
  });
42275
42878
  },
42276
- run12.controller.signal
42879
+ run15.controller.signal
42277
42880
  );
42278
42881
  if (slot.kind === "timeout") {
42279
42882
  neverStarted = true;
@@ -42294,9 +42897,9 @@ ${guardedPrompt}`;
42294
42897
  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 };
42295
42898
  } else {
42296
42899
  if (slot.queued) ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", log: true, queue: null });
42297
- run12.state = "running";
42900
+ run15.state = "running";
42298
42901
  try {
42299
- result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run12.controller.signal);
42902
+ result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run15.controller.signal);
42300
42903
  } finally {
42301
42904
  slot.handle.release();
42302
42905
  }
@@ -42389,6 +42992,13 @@ ${guardedPrompt}`;
42389
42992
  ctx.sessionIdByKey.clear();
42390
42993
  ctx.sessionMetaByKey.clear();
42391
42994
  }
42995
+ return {
42996
+ exitCode: result.exitCode,
42997
+ cancelled: !!result.cancelled,
42998
+ exhausted: result.exitCode !== 0 && !result.cancelled && failReasons.length > 0 && failReasons.every((r) => r === "limit" || r === "credits"),
42999
+ neverStarted,
43000
+ output: answerText
43001
+ };
42392
43002
  }
42393
43003
  function pruneResumableSessions(ctx, policy, now) {
42394
43004
  for (const key of [...ctx.sessionIdByKey.keys()]) {
@@ -42608,14 +43218,14 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
42608
43218
  }
42609
43219
 
42610
43220
  // src/core/git-snapshot.ts
42611
- var run6 = promisify6(execFile6);
43221
+ var run8 = promisify8(execFile8);
42612
43222
  var US = "";
42613
43223
  var RS2 = "";
42614
43224
  var MIN_SPACING_MS = 15e3;
42615
43225
  var DEFAULT_INTERVAL_MIN = 10;
42616
- async function git2(cwd2, args, timeout = 2e4) {
43226
+ async function git3(cwd2, args, timeout = 2e4) {
42617
43227
  try {
42618
- const { stdout } = await run6("git", args, { cwd: cwd2, timeout, maxBuffer: 16 * 1024 * 1024 });
43228
+ const { stdout } = await run8("git", args, { cwd: cwd2, timeout, maxBuffer: 16 * 1024 * 1024 });
42619
43229
  return stdout;
42620
43230
  } catch {
42621
43231
  return "";
@@ -42670,13 +43280,13 @@ async function buildRepoSnapshot(dir2, subdir) {
42670
43280
  const L2 = GIT_SNAPSHOT_LIMITS;
42671
43281
  const fmt = `--format=${RS2}%H${US}%P${US}%an${US}%aI${US}%s`;
42672
43282
  const [abbrev, headSha, heads, remotes, remoteHead, status, gitDir] = await Promise.all([
42673
- git2(dir2, ["rev-parse", "--abbrev-ref", "HEAD"]),
42674
- git2(dir2, ["rev-parse", "--verify", "HEAD"]),
42675
- git2(dir2, ["for-each-ref", "refs/heads", `--format=%(refname:short)${US}%(objectname)${US}%(upstream:short)${US}%(upstream:track,nobracket)`]),
42676
- git2(dir2, ["for-each-ref", "refs/remotes/origin", `--format=%(refname:short)${US}%(objectname)${US}%(symref)`]),
42677
- git2(dir2, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]),
42678
- git2(dir2, ["status", "--porcelain=v2", "--untracked-files=all"]),
42679
- git2(dir2, ["rev-parse", "--absolute-git-dir"])
43283
+ git3(dir2, ["rev-parse", "--abbrev-ref", "HEAD"]),
43284
+ git3(dir2, ["rev-parse", "--verify", "HEAD"]),
43285
+ git3(dir2, ["for-each-ref", "refs/heads", `--format=%(refname:short)${US}%(objectname)${US}%(upstream:short)${US}%(upstream:track,nobracket)`]),
43286
+ git3(dir2, ["for-each-ref", "refs/remotes/origin", `--format=%(refname:short)${US}%(objectname)${US}%(symref)`]),
43287
+ git3(dir2, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]),
43288
+ git3(dir2, ["status", "--porcelain=v2", "--untracked-files=all"]),
43289
+ git3(dir2, ["rev-parse", "--absolute-git-dir"])
42680
43290
  ]);
42681
43291
  const branchName = abbrev.trim();
42682
43292
  const detached = branchName === "HEAD";
@@ -42687,8 +43297,8 @@ async function buildRepoSnapshot(dir2, subdir) {
42687
43297
  const remoteBranches = remotes.split("\n").filter(Boolean).map((l) => l.split(US)).filter(([, , symref]) => !symref).map(([name = "", sha = ""]) => ({ name: name.replace(/^origin\//, ""), sha })).filter((b2) => b2.name && b2.name !== "origin");
42688
43298
  const { dirty, files } = parseStatus(status);
42689
43299
  const [localOut, remoteOut] = await Promise.all([
42690
- git2(dir2, ["log", "--branches", "--not", "--remotes", "-n", String(L2.localCommits + 1), "--name-status", fmt], 3e4),
42691
- git2(dir2, ["log", "--remotes", "--date-order", "-n", String(L2.remoteCommits), fmt], 3e4)
43300
+ git3(dir2, ["log", "--branches", "--not", "--remotes", "-n", String(L2.localCommits + 1), "--name-status", fmt], 3e4),
43301
+ git3(dir2, ["log", "--remotes", "--date-order", "-n", String(L2.remoteCommits), fmt], 3e4)
42692
43302
  ]);
42693
43303
  const localAll = parseLog(localOut, true);
42694
43304
  const localCommits = localAll.slice(0, L2.localCommits).map((c) => ({
@@ -42701,10 +43311,10 @@ async function buildRepoSnapshot(dir2, subdir) {
42701
43311
  const defaultBranch = remoteHead.trim().replace(/^origin\//, "") || null;
42702
43312
  const upstream = branches.find((b2) => b2.name === branchName)?.upstream;
42703
43313
  const baseRef = upstream ?? (defaultBranch ? `origin/${defaultBranch}` : null);
42704
- const baseSha = headSha.trim() && baseRef ? (await git2(dir2, ["merge-base", "HEAD", baseRef])).trim() || null : null;
43314
+ const baseSha = headSha.trim() && baseRef ? (await git3(dir2, ["merge-base", "HEAD", baseRef])).trim() || null : null;
42705
43315
  let lastFetchAt = null;
42706
43316
  try {
42707
- if (gitDir.trim()) lastFetchAt = (await stat(join20(gitDir.trim(), "FETCH_HEAD"))).mtime.toISOString();
43317
+ if (gitDir.trim()) lastFetchAt = (await stat(join22(gitDir.trim(), "FETCH_HEAD"))).mtime.toISOString();
42708
43318
  } catch {
42709
43319
  lastFetchAt = null;
42710
43320
  }
@@ -42732,7 +43342,7 @@ async function buildGitSnapshot(root) {
42732
43342
  async function fetchAll(root) {
42733
43343
  const base = resolve5(root);
42734
43344
  const { repos } = await gitRepos(base);
42735
- for (const r of repos) await git2(r.subdir ? resolve5(base, r.subdir) : base, ["fetch", "--prune", "--quiet", "origin"], 6e4);
43345
+ for (const r of repos) await git3(r.subdir ? resolve5(base, r.subdir) : base, ["fetch", "--prune", "--quiet", "origin"], 6e4);
42736
43346
  }
42737
43347
  var state2 = { host: null, lastSentAt: 0, pending: null, periodic: null, running: false };
42738
43348
  async function reportNow(fetchFirst) {
@@ -42799,96 +43409,394 @@ function envReconnectMaxSec() {
42799
43409
 
42800
43410
  // src/core/autonomous-cycle.ts
42801
43411
  import { randomUUID } from "crypto";
42802
- import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile6, readdir as readdir2, rm as rm2, stat as stat2 } from "fs/promises";
42803
- import { join as join21 } from "path";
42804
- function buildAutonomousCyclePrompt(opts = {}) {
42805
- const capNote = opts.intakeBlocked ? `
42806
-
42807
- **Step 2 is DISABLED this cycle** \u2014 the organization reached its monthly plan limit for new \`AI_TODO\` /
42808
- \`USER_QA\` rows. Do NOT add any row to \`AI_TODO.md\` or \`USER_QA.md\`; skip Step 2 entirely and continue
42809
- with the already-approved work.` : "";
42810
- return `You are running UNATTENDED as the 4PM autonomous cycle (headless, one tick). Do EXACTLY ONE small
42811
- work cycle with the steps below, then STOP. Keep each cycle to ONE small task so you never run out of
42812
- tokens mid-way. If a step fails, append a row to the **Incidents** table of \`AI_DONE.md\` and STOP \u2014
42813
- do not push on. Write all books/docs/code in the language the project's \`CLAUDE.md\` specifies (default
42814
- English).${capNote}
42815
-
42816
- ## Books & approval (MUST read the approvals file)
42817
- The five "book" files at the project root \u2014 \`USER_TODO.md\`, \`USER_QA.md\`, \`AI_TODO.md\`,
43412
+ import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile9, readdir as readdir3, rm as rm3, stat as stat2 } from "fs/promises";
43413
+ import { join as join24 } from "path";
43414
+
43415
+ // src/core/autonomous-tasks.ts
43416
+ import { readFile as readFile8, rm as rm2, writeFile as writeFile6 } from "fs/promises";
43417
+ import { join as join23 } from "path";
43418
+ var APPROVALS_REL2 = ".claude/.autonomous.approvals.json";
43419
+ function claimPath(profileDir2) {
43420
+ return join23(profileDir2, "autonomous-claim.json");
43421
+ }
43422
+ async function readLocalClaim(profileDir2) {
43423
+ try {
43424
+ return JSON.parse(await readFile8(claimPath(profileDir2), "utf8"));
43425
+ } catch {
43426
+ return null;
43427
+ }
43428
+ }
43429
+ async function writeLocalClaim(profileDir2, c) {
43430
+ if (c) await writeFile6(claimPath(profileDir2), `${JSON.stringify(c, null, 2)}
43431
+ `, "utf8");
43432
+ else await rm2(claimPath(profileDir2), { force: true });
43433
+ }
43434
+ function groupOf(taskId) {
43435
+ const m2 = /^TSK-(\d+)-\d+/.exec(taskId);
43436
+ return m2 ? `GRP-${m2[1]}` : "GRP-0000";
43437
+ }
43438
+ function taskBranch(base, taskId) {
43439
+ return `dev/${base}/${groupOf(taskId)}/${taskId}`;
43440
+ }
43441
+ function fromHint(notes) {
43442
+ return /(?:^|;\s*)from:\s*(\S+)/.exec(notes)?.[1] ?? null;
43443
+ }
43444
+ function doneBranch(notes) {
43445
+ return /branch:\s*([^\s;]+)/.exec(notes)?.[1] ?? null;
43446
+ }
43447
+ async function approvedIds(root) {
43448
+ try {
43449
+ const map = JSON.parse(await readFile8(join23(root, APPROVALS_REL2), "utf8"));
43450
+ return new Set(Object.entries(map).filter(([, v2]) => v2?.approved === true).map(([k2]) => k2));
43451
+ } catch {
43452
+ return /* @__PURE__ */ new Set();
43453
+ }
43454
+ }
43455
+ function claimAgeHours(c) {
43456
+ const t2 = parseStamp(c.started);
43457
+ return Number.isFinite(t2) ? (Date.now() - t2) / 36e5 : Infinity;
43458
+ }
43459
+ async function claimState(root, mine, ttlHours) {
43460
+ const row = parseClaims(await readBook(root, "AI_PROGRESS.md")).claims.find((c) => c.id === mine.id);
43461
+ if (!row || row.claim !== mine.claim) return "lost";
43462
+ return claimAgeHours(row) >= ttlHours ? "expired" : "live";
43463
+ }
43464
+ async function pickTask(root, rootBase, opts) {
43465
+ const [todo, prog, done, qa, attempts, approved] = await Promise.all([
43466
+ readBook(root, "AI_TODO.md"),
43467
+ readBook(root, "AI_PROGRESS.md"),
43468
+ readBook(root, "AI_DONE.md"),
43469
+ readBook(root, "USER_QA.md"),
43470
+ readAttempts(root),
43471
+ approvedIds(root)
43472
+ ]);
43473
+ const claims = parseClaims(prog).claims;
43474
+ const skip = opts.skip ?? /* @__PURE__ */ new Set();
43475
+ for (const c of claims) {
43476
+ if (skip.has(c.id) || c.worker === opts.worker || claimAgeHours(c) < opts.ttlHours) continue;
43477
+ const snap = attempts[c.id]?.task;
43478
+ const task = snap ?? { id: c.id, priority: "Medium", tag: "", depends: [], group: groupOf(c.id), desc: c.desc, notes: "" };
43479
+ return { task, takeover: c, from: null };
43480
+ }
43481
+ const doneMap = doneRows(done);
43482
+ const qaRows = new Map(parseQa(qa).rows.map((q2) => [q2.id, q2]));
43483
+ const claimed = new Set(claims.map((c) => c.id));
43484
+ const rank = (p2) => ({ high: 0, medium: 1, low: 2 })[p2.trim().toLowerCase()] ?? 1;
43485
+ const groupNum = (id) => Number(/^TSK-(\d+)/.exec(id)?.[1] ?? 9999);
43486
+ const tasks = parseTasks(todo).tasks.map((t2, i) => ({ t: t2, i })).sort((a, b2) => groupNum(a.t.id) - groupNum(b2.t.id) || rank(a.t.priority) - rank(b2.t.priority) || a.i - b2.i);
43487
+ for (const { t: t2 } of tasks) {
43488
+ if (skip.has(t2.id) || claimed.has(t2.id) || !approved.has(t2.id)) continue;
43489
+ const a = attempts[t2.id];
43490
+ if (a && (a.splitPending || a.count >= opts.maxAttempts)) continue;
43491
+ const qaOk = t2.depends.filter((d) => d.startsWith("QA-")).every((d) => {
43492
+ const q2 = qaRows.get(d);
43493
+ return !q2 || q2.answer.trim().length > 0 && approved.has(d);
43494
+ });
43495
+ if (!qaOk) continue;
43496
+ const deps = t2.depends.filter((d) => d.startsWith("TSK-"));
43497
+ if (deps.some((d) => !doneMap.has(d))) continue;
43498
+ let from = null;
43499
+ if (deps.length === 1) {
43500
+ const dep = deps[0];
43501
+ const branch = doneBranch(doneMap.get(dep) ?? "") ?? taskBranch(rootBase.base, dep);
43502
+ if (!await isMerged(root, rootBase.url, branch, rootBase.base)) from = branch;
43503
+ } else if (deps.length >= 2) {
43504
+ let allMerged = true;
43505
+ for (const dep of deps) {
43506
+ const branch = doneBranch(doneMap.get(dep) ?? "") ?? taskBranch(rootBase.base, dep);
43507
+ if (!await isMerged(root, rootBase.url, branch, rootBase.base)) {
43508
+ allMerged = false;
43509
+ break;
43510
+ }
43511
+ }
43512
+ if (!allMerged) continue;
43513
+ }
43514
+ return { task: t2, takeover: null, from: fromHint(t2.notes) ?? from };
43515
+ }
43516
+ return null;
43517
+ }
43518
+ async function applyClaim(root, cand, mine, ttlHours) {
43519
+ const [todo, prog, done, attempts] = await Promise.all([
43520
+ readBook(root, "AI_TODO.md"),
43521
+ readBook(root, "AI_PROGRESS.md"),
43522
+ readBook(root, "AI_DONE.md"),
43523
+ readAttempts(root)
43524
+ ]);
43525
+ const claims = parseClaims(prog).claims;
43526
+ const existing = claims.find((c) => c.id === cand.task.id);
43527
+ let incident = null;
43528
+ if (cand.takeover) {
43529
+ if (!existing || existing.claim !== cand.takeover.claim || claimAgeHours(existing) < ttlHours) return false;
43530
+ incident = `Claim of ${cand.task.id} by ${existing.worker} expired (${ttlHours} h) \u2014 taken over by ${mine.worker}.`;
43531
+ attempts[cand.task.id] = { ...attempts[cand.task.id] ?? {}, count: (attempts[cand.task.id]?.count ?? 0) + 1, last: "claim expired (taken over)", at: (/* @__PURE__ */ new Date()).toISOString() };
43532
+ } else {
43533
+ if (existing) return false;
43534
+ const { tasks } = parseTasks(todo);
43535
+ if (!tasks.some((t2) => t2.id === cand.task.id)) return false;
43536
+ await writeBook(root, "AI_TODO.md", writeTasks(todo, tasks.filter((t2) => t2.id !== cand.task.id)));
43537
+ attempts[cand.task.id] = { ...attempts[cand.task.id] ?? { count: 0, last: "", at: "" }, task: cand.task };
43538
+ }
43539
+ const row = { started: mine.started, id: cand.task.id, worker: mine.worker, claim: mine.claim, attempt: mine.attempt, desc: cand.task.desc };
43540
+ await writeBook(root, "AI_PROGRESS.md", writeClaims(prog, [...claims.filter((c) => c.id !== cand.task.id), row]));
43541
+ if (cand.takeover) attempts[cand.task.id] = { ...attempts[cand.task.id], task: attempts[cand.task.id]?.task ?? cand.task };
43542
+ await writeAttempts(root, attempts);
43543
+ if (incident) await writeBook(root, "AI_DONE.md", appendIncident(done, incident));
43544
+ return true;
43545
+ }
43546
+ async function dropClaim(root, mine) {
43547
+ const prog = await readBook(root, "AI_PROGRESS.md");
43548
+ const claims = parseClaims(prog).claims;
43549
+ const row = claims.find((c) => c.id === mine.id);
43550
+ if (!row || row.claim !== mine.claim) return false;
43551
+ await writeBook(root, "AI_PROGRESS.md", writeClaims(prog, claims.filter((c) => c.id !== mine.id)));
43552
+ return true;
43553
+ }
43554
+ async function applyFinish(root, mine, done) {
43555
+ if (!await dropClaim(root, mine)) return false;
43556
+ await writeBook(root, "AI_DONE.md", appendDone(await readBook(root, "AI_DONE.md"), { id: mine.id, desc: mine.task.desc, files: done.files, notes: done.notes }));
43557
+ const attempts = await readAttempts(root);
43558
+ delete attempts[mine.id];
43559
+ await writeAttempts(root, attempts);
43560
+ return true;
43561
+ }
43562
+ async function applyRelease(root, mine, reason, maxAttempts) {
43563
+ if (!await dropClaim(root, mine)) return null;
43564
+ const attempts = await readAttempts(root);
43565
+ const count = (attempts[mine.id]?.count ?? 0) + 1;
43566
+ attempts[mine.id] = { ...attempts[mine.id] ?? {}, count, last: reason.slice(0, 300), at: (/* @__PURE__ */ new Date()).toISOString(), ...count >= maxAttempts ? { splitPending: true } : {} };
43567
+ await writeAttempts(root, attempts);
43568
+ const todo = await readBook(root, "AI_TODO.md");
43569
+ const { tasks } = parseTasks(todo);
43570
+ const back = { ...mine.task, notes: appendNote(mine.task.notes, `attempt ${count} ended: ${reason.slice(0, 160)}; WIP on ${mine.branch}`) };
43571
+ await writeBook(root, "AI_TODO.md", writeTasks(todo, [...tasks.filter((t2) => t2.id !== mine.id), back]));
43572
+ await writeBook(root, "AI_DONE.md", appendIncident(await readBook(root, "AI_DONE.md"), `${mine.id} attempt ${count} by ${mine.worker}: ${reason.slice(0, 200)} \u2014 returned to AI_TODO (WIP pushed on ${mine.branch}).`));
43573
+ return count;
43574
+ }
43575
+ function nextId(prefix, group, texts) {
43576
+ const re2 = new RegExp(`${prefix}-${group}-(\\d+)`, "g");
43577
+ let max = 0;
43578
+ for (const t2 of texts) for (const m2 of t2.matchAll(re2)) max = Math.max(max, Number(m2[1]));
43579
+ return `${prefix}-${group}-${String(max + 1).padStart(4, "0")}`;
43580
+ }
43581
+ function appendNote(notes, add) {
43582
+ return notes.trim() ? `${notes.trim()}; ${add}` : add;
43583
+ }
43584
+ async function applyQuestion(root, mine, question, resetAttempts = false) {
43585
+ if (!await dropClaim(root, mine)) return null;
43586
+ if (resetAttempts) {
43587
+ const attempts = await readAttempts(root);
43588
+ delete attempts[mine.id];
43589
+ await writeAttempts(root, attempts);
43590
+ }
43591
+ const [todo, qa, done, attemptsText] = await Promise.all([
43592
+ readBook(root, "AI_TODO.md"),
43593
+ readBook(root, "USER_QA.md"),
43594
+ readBook(root, "AI_DONE.md"),
43595
+ readBook(root, ATTEMPTS_REL)
43596
+ ]);
43597
+ const g2 = /^TSK-(\d+)/.exec(mine.id)?.[1] ?? "0000";
43598
+ const qaId = nextId("QA", g2, [qa, todo, done, attemptsText]);
43599
+ const { rows } = parseQa(qa);
43600
+ const qaDeps = mine.task.depends.filter((d) => d.startsWith("QA-")).join(", ");
43601
+ await writeBook(root, "USER_QA.md", writeQa(qa, [...rows, { id: qaId, group: mine.task.group, depends: qaDeps, original: `${mine.id}: ${mine.task.desc}`, question, answer: "" }]));
43602
+ const { tasks } = parseTasks(todo);
43603
+ const back = { ...mine.task, depends: [...mine.task.depends.filter((d) => d !== qaId), qaId], notes: appendNote(mine.task.notes, `waiting for ${qaId}; WIP on ${mine.branch}`) };
43604
+ await writeBook(root, "AI_TODO.md", writeTasks(todo, [...tasks.filter((t2) => t2.id !== mine.id), back]));
43605
+ return qaId;
43606
+ }
43607
+ async function foldTaskAnswers(root) {
43608
+ const [todo, qa, approved] = await Promise.all([readBook(root, "AI_TODO.md"), readBook(root, "USER_QA.md"), approvedIds(root)]);
43609
+ const { tasks } = parseTasks(todo);
43610
+ const { rows } = parseQa(qa);
43611
+ const answered = rows.filter((q2) => q2.answer.trim() && approved.has(q2.id) && tasks.some((t2) => t2.depends.includes(q2.id)));
43612
+ if (answered.length === 0) return false;
43613
+ const byId = new Map(answered.map((q2) => [q2.id, q2]));
43614
+ const next = tasks.map((t2) => {
43615
+ const add = t2.depends.filter((d) => byId.has(d)).map((d) => `answer ${d}: ${byId.get(d).answer}`);
43616
+ return add.length ? { ...t2, notes: appendNote(t2.notes, add.join("; ")) } : t2;
43617
+ });
43618
+ await writeBook(root, "AI_TODO.md", writeTasks(todo, next));
43619
+ await writeBook(root, "USER_QA.md", writeQa(qa, rows.filter((q2) => !byId.has(q2.id))));
43620
+ return true;
43621
+ }
43622
+ async function hasIntakeWork(root) {
43623
+ const [userTodo, todo, qa, approved] = await Promise.all([
43624
+ readBook(root, "USER_TODO.md"),
43625
+ readBook(root, "AI_TODO.md"),
43626
+ readBook(root, "USER_QA.md"),
43627
+ approvedIds(root)
43628
+ ]);
43629
+ if ([...approved].some((id) => id.startsWith("REQ-") && userTodo.includes(id))) return true;
43630
+ const taskDeps = new Set(parseTasks(todo).tasks.flatMap((t2) => t2.depends));
43631
+ return parseQa(qa).rows.some((q2) => !taskDeps.has(q2.id) && q2.answer.trim() && approved.has(q2.id));
43632
+ }
43633
+ async function applySplit(root, mine, children) {
43634
+ if (!await dropClaim(root, mine)) return null;
43635
+ const [todo, done, prog] = await Promise.all([readBook(root, "AI_TODO.md"), readBook(root, "AI_DONE.md"), readBook(root, "AI_PROGRESS.md")]);
43636
+ const g2 = /^TSK-(\d+)/.exec(mine.id)?.[1] ?? "0000";
43637
+ const texts = [todo, done, prog, mine.id];
43638
+ const ids = [];
43639
+ const rows = children.map((c, i) => {
43640
+ const id = nextId("TSK", g2, [...texts, ...ids]);
43641
+ ids.push(id);
43642
+ const deps = i === 0 ? mine.task.depends : [ids[i - 1]];
43643
+ const notes = [c.size ? `size: ${c.size}` : "", c.notes ?? "", `split from ${mine.id}`, i === 0 ? `from: ${mine.branch}` : ""].filter(Boolean).join("; ");
43644
+ return { id, priority: c.priority || mine.task.priority || "Medium", tag: "", depends: deps, group: mine.task.group, desc: c.desc, notes };
43645
+ });
43646
+ const { tasks } = parseTasks(todo);
43647
+ await writeBook(root, "AI_TODO.md", writeTasks(todo, [...tasks, ...rows]));
43648
+ await writeBook(root, "AI_DONE.md", appendDone(done, { id: mine.id, desc: mine.task.desc, files: "", notes: `split into ${ids.join(", ")} (after repeated attempts); WIP on ${mine.branch}` }));
43649
+ const attempts = await readAttempts(root);
43650
+ delete attempts[mine.id];
43651
+ await writeAttempts(root, attempts);
43652
+ return ids;
43653
+ }
43654
+ function isSplitChild(task) {
43655
+ return /split from TSK-/.test(task.notes);
43656
+ }
43657
+
43658
+ // src/core/autonomous-cycle.ts
43659
+ var BOOKS_INTRO = `The five "book" files at the project root \u2014 \`USER_TODO.md\`, \`USER_QA.md\`, \`AI_TODO.md\`,
42818
43660
  \`AI_PROGRESS.md\`, \`AI_DONE.md\` \u2014 have canonical templates in \`.claude/templates/\` (\`<NAME>.empty.md\`
42819
- = EMPTY, \`<NAME>.sample.md\` = example WITH DATA). To tell empty/has-work, COMPARE a book to its
42820
- \`<NAME>.empty.md\` (equal after trimming \u21D2 empty). When clearing a book, overwrite it with EXACTLY the
42821
- empty template (\`cp .claude/templates/<NAME>.empty.md <NAME>\`).
42822
-
42823
- The single source of truth for approval is \`.claude/.autonomous.approvals.json\` \u2014 a map
42824
- \`{ "<ID>": { "approved": true, "by": "...", "at": "..." } }\` (the user ticks rows in the web grids). A
42825
- row is APPROVED only when its ID has \`"approved": true\`. Row IDs: \`USER_TODO\` = \`REQ-\u2026\`, \`USER_QA\`
42826
- = \`QA-\u2026\`, \`AI_TODO\` = \`TSK-\u2026\`. NEVER act on an unapproved row.
42827
-
42828
- ## Step 1 \u2014 Base branch (from the spec, ADR-0292)
42829
- The project root IS the primary repo (ADR-0314). Its currently checked-out branch is the base branch
42830
- (4PM cloned it with \`-b <branch>\`): \`BASE=$(git rev-parse --abbrev-ref HEAD)\`. Sync it:
42831
- \`git fetch origin\` then \`git pull --ff-only origin "$BASE"\` (skip the pull if the remote has no such
42832
- branch yet). Do NOT invent a separate integration/dev branch.
42833
-
42834
- ## Step 2 \u2014 Intake APPROVED user requests \u2192 tasks
42835
- Read \`USER_TODO.md\` (content-only columns \`ID | Group | Depends | Request\`; who/when wrote or approved a
42836
- row lives in \`.claude/.autonomous.authors.json\` / \`.autonomous.approvals.json\`, ADR-0320 \u2014 never add
42837
- provenance columns to the table). For each row that is APPROVED (its \`REQ-\u2026\` in the approvals file) AND
42838
- whose \`Depends\` \`REQ-\u2026\` are already handled:
42839
- - If the request is CLEAR: split it into small tasks and append them to \`AI_TODO.md\` (7 columns
42840
- \`ID | Priority | Tag | Depends | Group | Task description | Notes\`), each with an ID
42841
- \`TSK-{groupid:0000}-{taskid:0000}\`. **groupid** must be UNIQUE + INCREASING across all history \u2014 find
42842
- the largest ever used in git history of \`AI_TODO.md\`/\`AI_DONE.md\`/\`AI_PROGRESS.md\`, then use max+1;
42843
- **taskid** starts at 0001 within the group. Leave \`Tag\` blank unless a catalog tag applies; leave
42844
- \`Priority\` = High/Medium/Low (default Medium). Then REMOVE the analysed request row from
42845
- \`USER_TODO.md\` (if it becomes empty, reset it to the empty template).
42846
- - If the request is UNCLEAR (ambiguous/missing info/contradictory): do NOT guess. Append a row to
42847
- \`USER_QA.md\` (content-only columns \`ID | Group | Depends | Original request | Question / options |
42848
- Answer\`) with a new \`QA-{groupid:0000}-{qaid:0000}\` id, the original request and the question +
42849
- options; leave \`Answer\` blank for the user. Remove the request row from \`USER_TODO.md\`. (Do NOT
42850
- generate tasks for it.)
42851
- An unapproved request row is LEFT in place (wait for approval).
42852
-
42853
- ## Step 3 \u2014 Fold APPROVED answers back (USER_QA)
42854
- For each \`USER_QA.md\` row that has an \`Answer\` AND is APPROVED (its \`QA-\u2026\` in the approvals file):
42855
- re-analyse using the answer \u2014 append the resulting tasks to \`AI_TODO.md\` (same ID rules as Step 2) \u2014
42856
- then clear that QA row. An unanswered or unapproved QA row is left in place.
42857
-
42858
- ## Step 4 \u2014 Pick ONE approved task and start it
42859
- Read \`AI_TODO.md\` + the approvals file. Filter tasks that are BOTH approved (\`TSK-\u2026\` in approvals) AND
42860
- have every \`Depends\` \`TSK-\u2026\` already in \`AI_DONE.md\`. Run group by group (smallest eligible group
42861
- first), within a group High \u2192 Medium \u2192 Low, then line order. If NO eligible task: append a row to the
42862
- \`AI_DONE.md\` Incidents table (e.g. "only generated tasks / waiting for approval / waiting for deps")
42863
- and STOP. Otherwise move the chosen task into \`AI_PROGRESS.md\` (with a start timestamp), remove it from
42864
- \`AI_TODO.md\`, and commit on the base branch: \`git commit -am "chore(auto): start TSK-\u2026"\`.
42865
-
42866
- ## Step 5 \u2014 Implement on a task branch + test
42867
- \`git checkout -b task/TSK-\u2026\`. Implement following the project's architecture + \`CLAUDE.md\`; add/update
42868
- tests. Run the project's test command (per \`CLAUDE.md\` / the project's scripts); WAIT for it to finish
42869
- and check the result. Keep any produced report/evidence. Commit: \`git commit -am "feat(TSK-\u2026): <desc>"\`.
42870
-
42871
- ## Step 6 \u2014 Pull request into the base branch (NO direct merge \u2014 ADR-0319/0271)
42872
- 1. Rebase onto the freshest base to minimise conflicts: \`git fetch origin\` then
42873
- \`git rebase origin/"$BASE"\` (on conflict you can't resolve confidently, \`git rebase --abort\`, write a
42874
- \`USER_QA.md\` row, and STOP).
42875
- 2. Push the task branch: \`git push -u origin task/TSK-\u2026\`.
42876
- 3. Open a PR into the base branch with \`gh\` (GitHub) or \`glab\` (GitLab) \u2014 e.g.
42877
- \`gh pr create --base "$BASE" --head task/TSK-\u2026 --title "TSK-\u2026: <desc>" --body "<summary>"\`. Do NOT
42878
- merge it and do NOT fast-forward the base yourself \u2014 a human reviews the PR. If no remote / no
42879
- \`gh\`/\`glab\` auth is available, keep the local task-branch commit and append an Incidents row saying
42880
- the PR could not be opened.
42881
-
42882
- ## Step 7 \u2014 Update the books
42883
- On the base branch, append a row to the \`AI_DONE.md\` Done table (Timestamp, ID, Task description, Files,
42884
- Notes \u2014 include the PR link if opened) and REMOVE the task from \`AI_PROGRESS.md\` (reset it to the empty
42885
- template if nothing is in progress). Commit + push the base branch:
42886
- \`git commit -am "chore(auto): finish TSK-\u2026 (PR opened)" && git push origin "$BASE"\` (a push rejected as
42887
- non-fast-forward \u21D2 \`git pull --rebase origin "$BASE"\` then push again).
42888
-
42889
- ## Step 8 \u2014 Stop
42890
- Print a one-line summary (task id + PR link or incident). STOP \u2014 the next cron tick runs the next cycle.
42891
- The 4PM cli serializes cycles (one at a time); you do NOT manage any run lock.`;
43661
+ = EMPTY, \`<NAME>.sample.md\` = example WITH DATA). When clearing a book, overwrite it with EXACTLY the
43662
+ empty template. The single source of truth for approval is \`.claude/.autonomous.approvals.json\`
43663
+ (\`{ "<ID>": { "approved": true, \u2026 } }\`); a row is APPROVED only when its id has \`"approved": true\`.
43664
+ NEVER act on an unapproved row.`;
43665
+ function buildIntakePrompt(cfg) {
43666
+ const h = cfg.taskSizeHints;
43667
+ return `You are the 4PM autonomous INTAKE step (headless). Edit ONLY \`USER_TODO.md\`, \`USER_QA.md\` and
43668
+ \`AI_TODO.md\`. Do NOT run git, do NOT touch \`AI_PROGRESS.md\` / \`AI_DONE.md\` / any code \u2014 the 4PM cli
43669
+ commits and publishes your book edits. Write in the language the project's \`CLAUDE.md\` specifies (default English).
43670
+
43671
+ ${BOOKS_INTRO}
43672
+
43673
+ ## 1 \u2014 Approved user requests \u2192 tasks
43674
+ Read \`USER_TODO.md\` (columns \`ID | Group | Depends | Request\`). For each APPROVED \`REQ-\u2026\` whose \`Depends\`
43675
+ are already handled:
43676
+ - CLEAR request \u21D2 split it into tasks appended to \`AI_TODO.md\` (columns
43677
+ \`ID | Priority | Tag | Depends | Group | Task description | Notes\`), ids \`TSK-{groupid:0000}-{taskid:0000}\`
43678
+ (groupid UNIQUE + INCREASING across all history \u2014 the largest ever used in \`AI_TODO.md\`/\`AI_DONE.md\`/
43679
+ \`AI_PROGRESS.md\`/their git history, +1; taskid from 0001). \`Depends\` = the \`TSK-\u2026\` it needs.
43680
+ Remove the analysed request row (empty \u21D2 reset to the empty template).
43681
+ - UNCLEAR request \u21D2 do NOT guess: append a \`USER_QA.md\` row (\`ID | Group | Depends | Original request |
43682
+ Question / options | Answer\`) with a new \`QA-{groupid:0000}-{qaid:0000}\`, blank \`Answer\`; remove the request row.
43683
+ An unapproved request stays.
43684
+
43685
+ ## 2 \u2014 Answered intake questions
43686
+ For each \`USER_QA.md\` row with an \`Answer\`, APPROVED, and whose id is NOT in any \`AI_TODO.md\` \`Depends\`
43687
+ (those are task questions \u2014 the cli handles them; leave them alone): re-analyse the original request with the
43688
+ answer into tasks (same rules), then remove that QA row.
43689
+
43690
+ ## Task size (MANDATORY \u2014 every task must be finishable in ONE run)
43691
+ Size each task by SCOPE and write \`size: S\` or \`size: M\` at the start of \`Notes\`:
43692
+ - **S** \u2014 one small change in one module/layer (\u2248 \u2264 ${h.sMaxFiles} files, \u2248 \u2264 ${h.sMaxLines} lines), e.g. fix a bug, add a field.
43693
+ - **M** \u2014 one feature slice across 2\u20133 layers with ONE purpose (\u2248 \u2264 ${h.mMaxFiles} files, \u2248 \u2264 ${h.mMaxLines} lines), e.g. one CRUD screen, one API + its UI + tests.
43694
+ - **L** \u2014 several features, a cross-cutting change, a big migration, a new subsystem, or an architecture decision.
43695
+ **Never write an L task**: split it into S/M tasks chained with \`Depends\`, or ask in \`USER_QA.md\` when the split needs a decision.
43696
+ The counts are hints, not hard limits. Each task's \`Task description\` states the acceptance criteria and how to test it.
43697
+
43698
+ When done, reply with one short line summarising what you added.`;
43699
+ }
43700
+ function buildImplementPrompt(mine, subs, resumed) {
43701
+ const subLines = subs.length ? subs.map((s3) => `- submodule \`${s3.dir}\` is on branch \`${s3.branch}\` \u2014 commit changes to it INSIDE \`${s3.dir}\``).join("\n") : "- (no submodules)";
43702
+ return `You are the 4PM autonomous IMPLEMENT step (headless). Implement exactly ONE task, then STOP.
43703
+
43704
+ Task ${mine.id} (group ${mine.task.group}, attempt ${mine.attempt}):
43705
+ ${mine.task.desc}
43706
+ Notes: ${mine.task.notes || "(none)"}
43707
+ ${resumed ? "\nThis is a CONTINUATION: earlier work for this task is already committed on the branches below \u2014 review it (git log / git diff against the base) and continue from it; do not start over.\n" : ""}
43708
+ Branches (already checked out by the 4PM cli \u2014 stay on them):
43709
+ - project root is on \`${mine.branch}\`
43710
+ ${subLines}
43711
+
43712
+ Rules:
43713
+ - Follow the project's architecture and \`CLAUDE.md\`. Add/update tests; run the project's test command and WAIT for the result.
43714
+ - Commit your work (\`git add\` + \`git commit\`) in the root and/or inside each submodule you changed.
43715
+ - Do NOT push, do NOT open pull requests, do NOT switch/create branches, do NOT edit the book files
43716
+ (USER_TODO/USER_QA/AI_TODO/AI_PROGRESS/AI_DONE) \u2014 the 4PM cli delivers and records everything.
43717
+ - If you need a human DECISION to continue (ambiguous requirement, missing information, a choice with real
43718
+ trade-offs), do NOT guess: commit what you have and report \`needs-input\` with the question + options.
43719
+
43720
+ Finish with ONLY one fenced \`json\` block:
43721
+ {"status": "done" | "needs-input" | "failed", "summary": "<what you did / why it failed>", "question": "<only for needs-input: the question and the options>"}`;
43722
+ }
43723
+ function buildSplitPrompt(mine, reasons, diff2) {
43724
+ return `You are the 4PM autonomous SPLIT step (read-only \u2014 do not change any file). Task ${mine.id} failed to
43725
+ finish after repeated attempts, so it is probably too big for one run. Split it into 2\u20136 smaller child tasks
43726
+ that are each finishable in ONE run (size S or M), in execution order (each depends on the previous).
43727
+
43728
+ Task: ${mine.task.desc}
43729
+ Notes: ${mine.task.notes || "(none)"}
43730
+ Why the attempts ended: ${reasons || "(unknown)"}
43731
+ Work already done on its WIP branch \`${mine.branch}\` (diff against the base, may be truncated):
43732
+ \`\`\`
43733
+ ${diff2 || "(no changes yet)"}
43734
+ \`\`\`
43735
+ The first child continues from that WIP branch: note which parts are already done so they are not redone.
43736
+ Each child description must state its acceptance criteria and how to test it.
43737
+
43738
+ Reply with ONLY one fenced \`json\` block:
43739
+ {"children": [{"desc": "<task + acceptance criteria>", "priority": "High|Medium|Low", "size": "S|M", "notes": "<optional>"}]}`;
43740
+ }
43741
+ function parseJsonReply(text) {
43742
+ const fenced = [...text.matchAll(/```json\s*([\s\S]*?)```/gi)].map((m2) => m2[1].trim());
43743
+ const candidates = fenced.length ? fenced.reverse() : [text.slice(text.lastIndexOf("{"), text.lastIndexOf("}") + 1)];
43744
+ for (const c of candidates) {
43745
+ try {
43746
+ return JSON.parse(c);
43747
+ } catch {
43748
+ }
43749
+ }
43750
+ return null;
43751
+ }
43752
+ var watch = {
43753
+ commandId: null,
43754
+ claim: null,
43755
+ root: null,
43756
+ lost: false
43757
+ };
43758
+ async function verifyHeldClaim(ctx) {
43759
+ const { claim, root, commandId } = watch;
43760
+ if (!claim || !root || !commandId || watch.lost) return;
43761
+ const prog = await remoteBook(root, claim.base, "AI_PROGRESS.md");
43762
+ if (!prog) return;
43763
+ const row = parseClaims(prog).claims.find((c) => c.id === claim.id);
43764
+ if (!row || row.claim !== claim.claim) {
43765
+ watch.lost = true;
43766
+ ctx.bus.log(`autonomous: claim of ${claim.id} was taken over \u2014 stopping this run`, "warn");
43767
+ cancelActiveRun(commandId);
43768
+ }
43769
+ }
43770
+ function onReconnectVerifyClaim(ctx) {
43771
+ void verifyHeldClaim(ctx).catch(() => void 0);
43772
+ }
43773
+ function alert(ctx, payload) {
43774
+ try {
43775
+ ctx.send(WsChannels.AUTONOMOUS_ALERT, payload);
43776
+ } catch {
43777
+ }
43778
+ }
43779
+ function subBranches(subs, taskId, rootFrom) {
43780
+ const fromTask = rootFrom ? rootFrom.split("/").pop() ?? null : null;
43781
+ return subs.filter((s3) => s3.base).map((s3) => ({ sub: s3, branch: taskBranch(s3.base, taskId), from: fromTask ? taskBranch(s3.base, fromTask) : null }));
43782
+ }
43783
+ async function pushWip(root, mine, subs, note) {
43784
+ for (const s3 of subs) {
43785
+ const abs = join24(root, s3.sub.dir);
43786
+ if (await currentBranch(abs) !== s3.branch) continue;
43787
+ await wipCommit(abs, `wip(${mine.id}): ${note}`);
43788
+ if (await aheadOf(abs, s3.branch, s3.sub.base) > 0) await pushBranch(abs, s3.branch).catch(() => void 0);
43789
+ }
43790
+ if (await currentBranch(root) === mine.branch) {
43791
+ await wipCommit(root, `wip(${mine.id}): ${note}`);
43792
+ await pushBranch(root, mine.branch).catch(() => void 0);
43793
+ }
43794
+ }
43795
+ async function wipDiff(root, mine) {
43796
+ const stat6 = await gitQuiet(root, ["diff", "--stat", `origin/${mine.base}...${mine.branch}`]);
43797
+ const patch = await gitQuiet(root, ["diff", `origin/${mine.base}...${mine.branch}`]);
43798
+ return `${stat6}
43799
+ ${patch}`.slice(0, 2e4);
42892
43800
  }
42893
43801
  var running = /* @__PURE__ */ new Set();
42894
43802
  async function runAutonomousCycle(ctx) {
@@ -42908,6 +43816,7 @@ async function runAutonomousCycle(ctx) {
42908
43816
  const cfg = await readAutonomousConfig(profileDir2);
42909
43817
  await pruneLogs(root, cfg.logRetentionDays);
42910
43818
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
43819
+ const worker = ctx.machineUsername || "worker";
42911
43820
  if (!ctx.isReady) {
42912
43821
  await log("[skip] cli offline (no WS connection)");
42913
43822
  ctx.bus.autonomousDone(false, "cli offline");
@@ -42929,13 +43838,53 @@ async function runAutonomousCycle(ctx) {
42929
43838
  ctx.bus.autonomousDone(false, "max ticks reached");
42930
43839
  return;
42931
43840
  }
42932
- if (!await hasWork(root)) {
42933
- await log("[skip] no approved USER_TODO/USER_QA/AI_TODO work, AI_PROGRESS empty");
43841
+ const bases = await resolveBases(root);
43842
+ const rootBase = bases.root;
43843
+ if (!rootBase.base) {
43844
+ await log("[skip] base branch unknown (no spec branch, no origin/HEAD)");
43845
+ ctx.bus.autonomousDone(false, "base branch unknown");
43846
+ return;
43847
+ }
43848
+ for (const r of [rootBase, ...bases.subs]) {
43849
+ if (!r.base || !r.url) continue;
43850
+ if (await branchProtection(r.url, r.base) === true) {
43851
+ const repo = r.dir || "root";
43852
+ await log(`[stop] base branch "${r.base}" is protected in ${repo} \u2014 the books cannot be pushed`);
43853
+ alert(ctx, { kind: "base-protected", repo, branch: r.base, message: `The base branch "${r.base}" (${repo}) is protected; autonomous cannot record its tasks there.` });
43854
+ await recordRun(root, hist, "skip", `base-protected: ${r.base} (${repo})`, today);
43855
+ ctx.bus.autonomousDone(false, "base branch protected");
43856
+ return;
43857
+ }
43858
+ }
43859
+ await syncBase(root, rootBase.base);
43860
+ let mine = await readLocalClaim(profileDir2);
43861
+ if (mine && mine.base !== rootBase.base) mine = null;
43862
+ if (mine) {
43863
+ const state3 = await claimState(root, mine, cfg.claimTtlHours);
43864
+ if (state3 === "lost") {
43865
+ await log(`[lost] claim of ${mine.id} was taken over \u2014 dropping local work (kept on ${mine.branch} locally)`);
43866
+ await publishBooks(root, rootBase.base, `chore(auto): incident \u2014 ${mine.id} claim lost by ${worker}`, async () => {
43867
+ await writeBook(root, "AI_DONE.md", appendIncident(await readBook(root, "AI_DONE.md"), `${worker} lost its claim of ${mine.id} (taken over); its local work was not pushed.`));
43868
+ return true;
43869
+ });
43870
+ alert(ctx, { kind: "claim-lost", task: mine.id, message: `${worker} lost its claim of ${mine.id}.` });
43871
+ await writeLocalClaim(profileDir2, null);
43872
+ mine = null;
43873
+ } else if (state3 === "expired") {
43874
+ await log(`[release] claim of ${mine.id} reached ${cfg.claimTtlHours} h \u2014 pushing WIP and returning the task`);
43875
+ await checkoutTaskBranch(root, mine.branch, rootBase.base, null).catch(() => void 0);
43876
+ await releaseTask(ctx, root, rootBase, bases.subs, mine, `not finished within ${cfg.claimTtlHours} h`, cfg);
43877
+ ctx.bus.autonomousDone(false, "claim expired \u2014 task returned");
43878
+ return;
43879
+ }
43880
+ }
43881
+ if (!mine && !await hasWork(root)) {
43882
+ await log("[skip] no approved USER_TODO/USER_QA/AI_TODO work");
42934
43883
  ctx.bus.autonomousDone(false, "no work");
42935
43884
  return;
42936
43885
  }
42937
43886
  if (await allAiProfilesExhausted(ctx, cfg)) {
42938
- await log(`[skip] all AI profiles over usage limit (\u2265 ${cfg.maxSessionPct}%/${cfg.maxWeeklyPct}%)`);
43887
+ await log(`[skip] all AI profiles over usage limit (\u2265 ${cfg.maxSessionPct}%/${cfg.maxWeeklyPct}%)${mine ? ` \u2014 ${mine.id} kept` : ""}`);
42939
43888
  await recordRun(root, hist, "skip", "all AI profiles over usage limit", today);
42940
43889
  ctx.bus.autonomousDone(false, "usage limit");
42941
43890
  return;
@@ -42943,42 +43892,244 @@ async function runAutonomousCycle(ctx) {
42943
43892
  hist.ticks = { day: today, count: todayTickCount(hist, today) + 1 };
42944
43893
  await writeHistories(root, hist);
42945
43894
  await log(`[run] cycle (tick ${hist.ticks.count}/${today})`);
42946
- const commandId = randomUUID();
42947
43895
  const override = cfg.model ? { model: cfg.model } : void 0;
42948
- const usage = await readBookUsage(ctx);
42949
- const intakeBlocked = ["AI_TODO", "USER_QA"].some((b2) => {
42950
- const c = usage?.books[b2];
42951
- return !!c && c.limit !== null && c.used >= c.limit;
42952
- });
42953
- if (intakeBlocked) await log("[cap] monthly AI_TODO/USER_QA limit reached \u2014 intake skipped this cycle");
42954
- const booksBefore = await cappedBookIds(root);
43896
+ let resumed = !!mine;
43897
+ if (!mine) {
43898
+ const folded = await publishBooks(root, rootBase.base, "chore(auto): fold answered task questions", () => foldTaskAnswers(root));
43899
+ if (folded === "pushed") await log("[fold] task-question answers folded into their tasks");
43900
+ const usage = await readBookUsage(ctx);
43901
+ const intakeBlocked = ["AI_TODO", "USER_QA"].some((b2) => {
43902
+ const c = usage?.books[b2];
43903
+ return !!c && c.limit !== null && c.used >= c.limit;
43904
+ });
43905
+ if (intakeBlocked) await log("[cap] monthly AI_TODO/USER_QA limit reached \u2014 intake skipped this cycle");
43906
+ else if (await hasIntakeWork(root)) {
43907
+ const before = await cappedBookIds(root);
43908
+ const out2 = await runAiPrompt(ctx, buildIntakePrompt(cfg), randomUUID(), "local", false, void 0, override, false, true);
43909
+ if (out2.exhausted) {
43910
+ await gitQuiet(root, ["checkout", "--", "."]);
43911
+ await log("[skip] usage limit during intake");
43912
+ ctx.bus.autonomousDone(false, "usage limit");
43913
+ return;
43914
+ }
43915
+ const res = await publishIntake(root, rootBase.base, "chore(auto): intake requests into tasks");
43916
+ await log(`[intake] ${res}`);
43917
+ if (res === "pushed") await reportNewBookRows(ctx, root, before);
43918
+ await syncBase(root, rootBase.base);
43919
+ }
43920
+ const skip = /* @__PURE__ */ new Set();
43921
+ for (let i = 0; i < 5 && !mine; i++) {
43922
+ const cand = await pickTask(root, rootBase, { worker, ttlHours: cfg.claimTtlHours, maxAttempts: cfg.maxTaskAttempts, skip });
43923
+ if (!cand) break;
43924
+ const attempts = await readAttempts(root);
43925
+ const claim = {
43926
+ id: cand.task.id,
43927
+ claim: randomUUID(),
43928
+ worker,
43929
+ started: stamp(),
43930
+ attempt: (attempts[cand.task.id]?.count ?? 0) + 1 + (cand.takeover ? 1 : 0),
43931
+ branch: taskBranch(rootBase.base, cand.task.id),
43932
+ base: rootBase.base,
43933
+ task: cand.task
43934
+ };
43935
+ const res = await publishBooks(
43936
+ root,
43937
+ rootBase.base,
43938
+ `chore(auto): claim ${cand.task.id} by ${worker}`,
43939
+ () => applyClaim(root, cand, claim, cfg.claimTtlHours)
43940
+ );
43941
+ if (res === "pushed") {
43942
+ mine = { ...claim, from: cand.from };
43943
+ await writeLocalClaim(profileDir2, mine);
43944
+ await log(`[claim] ${mine.id}${cand.takeover ? ` (taken over from ${cand.takeover.worker})` : ""} attempt ${mine.attempt}`);
43945
+ } else skip.add(cand.task.id);
43946
+ }
43947
+ if (!mine) {
43948
+ await log("[idle] no eligible task (waiting for approval / dependencies / answers)");
43949
+ hist = await readHistories(root);
43950
+ await recordRun(root, hist, "success", "no eligible task", today);
43951
+ ctx.bus.autonomousDone(true);
43952
+ return;
43953
+ }
43954
+ resumed = false;
43955
+ }
43956
+ const from = mine.from ?? null;
43957
+ await checkoutTaskBranch(root, mine.branch, rootBase.base, from);
43958
+ const hadWork = await aheadOf(root, mine.branch, rootBase.base) > 0;
43959
+ await gitQuiet(root, ["submodule", "update", "--init", "-q"], 3e5);
43960
+ const subs = subBranches(bases.subs, mine.id, from);
43961
+ for (const s3 of subs) await checkoutTaskBranch(join24(root, s3.sub.dir), s3.branch, s3.sub.base, s3.from).catch((e) => log(`[warn] submodule ${s3.sub.dir}: ${String(e)}`));
43962
+ const commandId = randomUUID();
43963
+ Object.assign(watch, { commandId, claim: mine, root, lost: false });
43964
+ const timer = setInterval(() => void verifyHeldClaim(ctx).catch(() => void 0), cfg.claimCheckMinutes * 6e4);
43965
+ timer.unref?.();
43966
+ let out;
42955
43967
  try {
42956
- await runAiPrompt(ctx, buildAutonomousCyclePrompt({ intakeBlocked }), commandId, "local", false, void 0, override, false, true);
42957
- await reportNewBookRows(ctx, root, booksBefore);
43968
+ out = await runAiPrompt(ctx, buildImplementPrompt(mine, subs.map((s3) => ({ dir: s3.sub.dir, branch: s3.branch })), resumed || hadWork), commandId, "local", false, void 0, override, false, true);
43969
+ } finally {
43970
+ clearInterval(timer);
43971
+ }
43972
+ const lost = watch.lost;
43973
+ Object.assign(watch, { commandId: null, claim: null, root: null, lost: false });
43974
+ if (await currentBranch(root) !== mine.branch) await gitQuiet(root, ["checkout", "-q", mine.branch]);
43975
+ if (lost) {
43976
+ await log(`[lost] ${mine.id} was taken over while running \u2014 stopped, nothing pushed`);
43977
+ alert(ctx, { kind: "claim-lost", task: mine.id, message: `${worker} lost its claim of ${mine.id} while working; its work was not pushed.` });
43978
+ await writeLocalClaim(profileDir2, null);
43979
+ await syncBase(root, rootBase.base);
43980
+ ctx.bus.autonomousDone(false, "claim lost");
43981
+ return;
43982
+ }
43983
+ if (out.exhausted) {
43984
+ for (const s3 of subs) await wipCommit(join24(root, s3.sub.dir), `wip(${mine.id}): out of tokens`);
43985
+ await wipCommit(root, `wip(${mine.id}): out of tokens`);
43986
+ await log(`[wait] ${mine.id}: usage limit \u2014 claim kept, resumes after the reset`);
43987
+ ctx.bus.autonomousDone(false, "usage limit \u2014 task kept");
43988
+ return;
43989
+ }
43990
+ const reply = parseJsonReply(out.output);
43991
+ const status = out.cancelled ? "failed" : out.exitCode !== 0 ? "failed" : reply?.status ?? "failed";
43992
+ if (status === "needs-input") {
43993
+ await pushWip(root, mine, subs, "waiting for an answer");
43994
+ await syncBase(root, rootBase.base);
43995
+ const question = reply?.question?.trim() || reply?.summary?.trim() || "The agent needs a decision to continue.";
43996
+ let qaId = null;
43997
+ await publishBooks(root, rootBase.base, `chore(auto): ${mine.id} asks a question`, async () => {
43998
+ qaId = await applyQuestion(root, mine, question);
43999
+ return qaId !== null;
44000
+ });
44001
+ await writeLocalClaim(profileDir2, null);
44002
+ await log(`[question] ${mine.id} parked on ${qaId ?? "?"}`);
44003
+ alert(ctx, { kind: "task-question", task: mine.id, message: `${mine.id} waits for an answer (${qaId ?? "QA"}): ${question.slice(0, 200)}` });
42958
44004
  hist = await readHistories(root);
42959
- hist.consecutiveFails = 0;
42960
- await recordRun(root, hist, "success", "cycle complete", today);
42961
- await log("[done] cycle complete");
44005
+ await recordRun(root, hist, "success", `${mine.id} needs input`, today);
42962
44006
  ctx.bus.autonomousDone(true);
42963
- } catch (err) {
42964
- const msg = err instanceof Error ? err.message : String(err);
42965
- await reportNewBookRows(ctx, root, booksBefore);
44007
+ return;
44008
+ }
44009
+ if (status !== "done") {
44010
+ const reason = reply?.summary?.trim() || (out.cancelled ? "run stopped" : `agent run failed (exit ${out.exitCode})`);
44011
+ await releaseTask(ctx, root, rootBase, bases.subs, mine, reason, cfg);
42966
44012
  hist = await readHistories(root);
42967
44013
  hist.consecutiveFails += 1;
42968
- await recordRun(root, hist, "failure", msg, today);
42969
- await log(`[warn] cycle failed: ${msg}`);
42970
- if (cfg.stopOnConsecutiveFailures > 0 && hist.consecutiveFails >= cfg.stopOnConsecutiveFailures) {
42971
- await writeAutonomousConfig(profileDir2, { ...cfg, paused: true });
42972
- await log(`[stop] ${hist.consecutiveFails} consecutive failures \u2192 paused`);
42973
- }
42974
- ctx.bus.autonomousDone(false, msg);
44014
+ await recordRun(root, hist, "failure", `${mine.id}: ${reason}`, today);
44015
+ await maybeAutoPause(root, profileDir2, cfg, hist, log);
44016
+ ctx.bus.autonomousDone(false, reason);
44017
+ return;
44018
+ }
44019
+ if (await remoteClaimLost(root, mine)) {
44020
+ await log(`[lost] ${mine.id} was taken over before delivery \u2014 nothing pushed`);
44021
+ alert(ctx, { kind: "claim-lost", task: mine.id, message: `${worker} lost its claim of ${mine.id} before delivering.` });
44022
+ await writeLocalClaim(profileDir2, null);
44023
+ await syncBase(root, rootBase.base);
44024
+ ctx.bus.autonomousDone(false, "claim lost");
44025
+ return;
44026
+ }
44027
+ let delivered;
44028
+ try {
44029
+ delivered = await deliver(root, rootBase, subs, mine, reply?.summary ?? "");
44030
+ } catch (err) {
44031
+ const reason = `delivery failed: ${String(err instanceof Error ? err.message : err)}`;
44032
+ await releaseTask(ctx, root, rootBase, bases.subs, mine, reason, cfg);
44033
+ ctx.bus.autonomousDone(false, reason);
44034
+ return;
42975
44035
  }
44036
+ await syncBase(root, rootBase.base);
44037
+ const fin = await publishBooks(root, rootBase.base, `chore(auto): finish ${mine.id}`, () => applyFinish(root, mine, delivered));
44038
+ await writeLocalClaim(profileDir2, null);
44039
+ await log(`[done] ${mine.id} \u2014 ${delivered.notes} (${fin})`);
44040
+ hist = await readHistories(root);
44041
+ hist.consecutiveFails = 0;
44042
+ await recordRun(root, hist, "success", `${mine.id} delivered`, today);
44043
+ ctx.bus.autonomousDone(true);
42976
44044
  } catch (err) {
44045
+ await dayLog(root, `[warn] cycle error: ${err instanceof Error ? err.message : String(err)}`).catch(() => void 0);
42977
44046
  ctx.bus.autonomousDone(false, err instanceof Error ? err.message : String(err));
42978
44047
  } finally {
42979
44048
  running.delete(root);
42980
44049
  }
42981
44050
  }
44051
+ async function remoteClaimLost(root, mine) {
44052
+ const prog = await remoteBook(root, mine.base, "AI_PROGRESS.md");
44053
+ if (!prog) return false;
44054
+ const row = parseClaims(prog).claims.find((c) => c.id === mine.id);
44055
+ return !row || row.claim !== mine.claim;
44056
+ }
44057
+ async function deliver(root, rootBase, subs, mine, summary) {
44058
+ const title = `${mine.id}: ${mine.task.desc}`.replace(/\s+/g, " ").slice(0, 100);
44059
+ const subPrs = [];
44060
+ for (const s3 of subs) {
44061
+ const abs = join24(root, s3.sub.dir);
44062
+ if (await currentBranch(abs) !== s3.branch) continue;
44063
+ await wipCommit(abs, `feat(${mine.id}): ${summary || mine.task.desc}`.slice(0, 200));
44064
+ if (await aheadOf(abs, s3.branch, s3.sub.base) === 0) continue;
44065
+ await pushBranch(abs, s3.branch);
44066
+ const pr3 = await deliverPr(abs, s3.sub.url, s3.branch, s3.sub.base, title, `${summary}
44067
+
44068
+ Task ${mine.id} (4PM autonomous).`);
44069
+ if (pr3.error) throw new Error(`submodule ${s3.sub.dir} PR: ${pr3.error}`);
44070
+ subPrs.push(`${s3.sub.dir}: ${pr3.url ?? s3.branch}`);
44071
+ }
44072
+ await wipCommit(root, `feat(${mine.id}): ${summary || mine.task.desc}`.slice(0, 200));
44073
+ const files = (await gitQuiet(root, ["diff", "--name-only", `origin/${rootBase.base}...${mine.branch}`])).split("\n").filter(Boolean);
44074
+ if (await aheadOf(root, mine.branch, rootBase.base) === 0) {
44075
+ return { files: "", notes: `no changes needed; branch: ${mine.branch}${subPrs.length ? `; sub PRs: ${subPrs.join(", ")}` : ""}` };
44076
+ }
44077
+ await pushBranch(root, mine.branch);
44078
+ const body = `${summary}
44079
+
44080
+ Task ${mine.id} (4PM autonomous).${subPrs.length ? `
44081
+
44082
+ Submodule PRs:
44083
+ ${subPrs.map((p2) => `- ${p2}`).join("\n")}` : ""}`;
44084
+ const pr2 = await deliverPr(root, rootBase.url, mine.branch, rootBase.base, title, body);
44085
+ if (pr2.error) throw new Error(`PR: ${pr2.error}`);
44086
+ return {
44087
+ files: files.slice(0, 20).join(", ") + (files.length > 20 ? ` (+${files.length - 20})` : ""),
44088
+ notes: `branch: ${mine.branch}; PR: ${pr2.url ?? "opened"}${subPrs.length ? `; sub PRs: ${subPrs.join(", ")}` : ""}`
44089
+ };
44090
+ }
44091
+ async function releaseTask(ctx, root, rootBase, allSubs, mine, reason, cfg) {
44092
+ const subs = subBranches(allSubs, mine.id, null);
44093
+ await pushWip(root, mine, subs, reason.slice(0, 80));
44094
+ await syncBase(root, rootBase.base);
44095
+ const attempts = await readAttempts(root);
44096
+ const next = (attempts[mine.id]?.count ?? 0) + 1;
44097
+ const reasons = `${attempts[mine.id]?.last ?? ""}; ${reason}`;
44098
+ if (next >= cfg.maxTaskAttempts) {
44099
+ if (isSplitChild(mine.task)) {
44100
+ const q2 = `${mine.id} (a task already split from a bigger one) failed ${next} times \u2014 last: ${reason}. How should it be handled (narrow the scope, change the approach, or drop it)?`;
44101
+ await publishBooks(root, rootBase.base, `chore(auto): ${mine.id} needs a decision`, async () => await applyQuestion(root, mine, q2, true) !== null);
44102
+ alert(ctx, { kind: "task-failed-limit", task: mine.id, message: `${mine.id} failed ${next} times; a decision was requested in USER_QA.` });
44103
+ } else {
44104
+ const out = await runAiPrompt(ctx, buildSplitPrompt(mine, reasons, await wipDiff(root, mine)), randomUUID(), "local", false, void 0, cfg.model ? { model: cfg.model } : void 0, true, false);
44105
+ const children = parseJsonReply(out.output)?.children?.filter((c) => c?.desc?.trim()) ?? [];
44106
+ await syncBase(root, rootBase.base);
44107
+ if (children.length >= 2) {
44108
+ let ids = null;
44109
+ await publishBooks(root, rootBase.base, `chore(auto): split ${mine.id}`, async () => {
44110
+ ids = await applySplit(root, mine, children);
44111
+ return ids !== null;
44112
+ });
44113
+ alert(ctx, { kind: "task-split", task: mine.id, message: `${mine.id} was split into ${(ids ?? []).join(", ")} \u2014 approve the new tasks.` });
44114
+ } else {
44115
+ const q2 = `${mine.id} failed ${next} times and could not be split automatically \u2014 last: ${reason}. How should it be handled?`;
44116
+ await publishBooks(root, rootBase.base, `chore(auto): ${mine.id} needs a decision`, async () => await applyQuestion(root, mine, q2, true) !== null);
44117
+ alert(ctx, { kind: "task-failed-limit", task: mine.id, message: `${mine.id} failed ${next} times; a decision was requested in USER_QA.` });
44118
+ }
44119
+ }
44120
+ } else {
44121
+ await publishBooks(root, rootBase.base, `chore(auto): release ${mine.id} (attempt ${next})`, async () => await applyRelease(root, mine, reason, cfg.maxTaskAttempts) !== null);
44122
+ }
44123
+ await writeLocalClaim(ctx.profileDir, null);
44124
+ await dayLog(root, `[release] ${mine.id} attempt ${next}: ${reason}`);
44125
+ }
44126
+ async function maybeAutoPause(root, profileDir2, cfg, hist, log) {
44127
+ if (cfg.stopOnConsecutiveFailures > 0 && hist.consecutiveFails >= cfg.stopOnConsecutiveFailures) {
44128
+ await writeAutonomousConfig(profileDir2, { ...cfg, paused: true });
44129
+ await log(`[stop] ${hist.consecutiveFails} consecutive failures \u2192 paused`);
44130
+ }
44131
+ await writeHistories(root, hist);
44132
+ }
42982
44133
  async function readBookUsage(ctx) {
42983
44134
  try {
42984
44135
  return await ctx.request(WsChannels.AUTONOMOUS_BOOK_USAGE, {});
@@ -43000,11 +44151,11 @@ async function reportNewBookRows(ctx, root, before) {
43000
44151
  }
43001
44152
  async function dayLog(root, line) {
43002
44153
  try {
43003
- const dir2 = join21(root, ".claude", "logs");
44154
+ const dir2 = join24(root, ".claude", "logs");
43004
44155
  await mkdir3(dir2, { recursive: true });
43005
44156
  const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
43006
44157
  const ts2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
43007
- await appendFile2(join21(dir2, `autonomous-tick-${day}.log`), `${ts2} ${line}
44158
+ await appendFile2(join24(dir2, `autonomous-tick-${day}.log`), `${ts2} ${line}
43008
44159
  `, "utf8");
43009
44160
  } catch {
43010
44161
  }
@@ -43012,12 +44163,12 @@ async function dayLog(root, line) {
43012
44163
  async function pruneLogs(root, retentionDays) {
43013
44164
  if (!(retentionDays > 0)) return;
43014
44165
  try {
43015
- const dir2 = join21(root, ".claude", "logs");
44166
+ const dir2 = join24(root, ".claude", "logs");
43016
44167
  const cutoff = Date.now() - retentionDays * 864e5;
43017
- for (const f2 of await readdir2(dir2)) {
44168
+ for (const f2 of await readdir3(dir2)) {
43018
44169
  if (!/^autonomous-tick-.*\.log$/.test(f2)) continue;
43019
- const st2 = await stat2(join21(dir2, f2)).catch(() => null);
43020
- if (st2 && st2.mtimeMs < cutoff) await rm2(join21(dir2, f2), { force: true }).catch(() => void 0);
44170
+ const st2 = await stat2(join24(dir2, f2)).catch(() => null);
44171
+ if (st2 && st2.mtimeMs < cutoff) await rm3(join24(dir2, f2), { force: true }).catch(() => void 0);
43021
44172
  }
43022
44173
  } catch {
43023
44174
  }
@@ -43029,15 +44180,15 @@ async function recordRun(root, hist, status, note, today) {
43029
44180
  }
43030
44181
  async function readJsonObject(path) {
43031
44182
  try {
43032
- return JSON.parse(await readFile6(path, "utf8"));
44183
+ return JSON.parse(await readFile9(path, "utf8"));
43033
44184
  } catch {
43034
44185
  return {};
43035
44186
  }
43036
44187
  }
43037
44188
  async function hasWork(root) {
43038
- const approvals = await readJsonObject(join21(root, ".claude/.autonomous.approvals.json"));
44189
+ const approvals = await readJsonObject(join24(root, ".claude/.autonomous.approvals.json"));
43039
44190
  const approved = Object.entries(approvals).filter(([, v2]) => v2 && typeof v2 === "object" && v2.approved === true).map(([k2]) => k2);
43040
- const read = (f2) => readFile6(join21(root, f2), "utf8").catch(() => "");
44191
+ const read = (f2) => readFile9(join24(root, f2), "utf8").catch(() => "");
43041
44192
  const [userTodo, userQa, aiTodo, aiProgress] = await Promise.all([
43042
44193
  read("USER_TODO.md"),
43043
44194
  read("USER_QA.md"),
@@ -43074,7 +44225,7 @@ async function allAiProfilesExhausted(ctx, cfg) {
43074
44225
  }
43075
44226
 
43076
44227
  // src/core/fs-browse.ts
43077
- import { readdir as readdir3 } from "fs/promises";
44228
+ import { readdir as readdir4 } from "fs/promises";
43078
44229
  import { resolve as resolve6, sep as sep3 } from "path";
43079
44230
  async function listDir(path, root) {
43080
44231
  if (!root) return { path: "", entries: [] };
@@ -43082,7 +44233,7 @@ async function listDir(path, root) {
43082
44233
  const requested = resolve6(base, path && path.trim() ? path : ".");
43083
44234
  const target = requested === base || requested.startsWith(base + sep3) ? requested : base;
43084
44235
  try {
43085
- const dirents = await readdir3(target, { withFileTypes: true });
44236
+ const dirents = await readdir4(target, { withFileTypes: true });
43086
44237
  const entries2 = dirents.map((d) => ({
43087
44238
  name: d.name,
43088
44239
  type: d.isDirectory() ? "dir" : "file"
@@ -43097,7 +44248,7 @@ async function listDir(path, root) {
43097
44248
  }
43098
44249
 
43099
44250
  // src/core/fs-read.ts
43100
- import { readFile as readFile7 } from "fs/promises";
44251
+ import { readFile as readFile10 } from "fs/promises";
43101
44252
  import { resolve as resolve7, sep as sep4 } from "path";
43102
44253
  var MAX_BYTES = 256 * 1024;
43103
44254
  function resolveInRoot(root, relPath) {
@@ -43112,7 +44263,7 @@ async function readWorkerFile(root, path) {
43112
44263
  return { path, content: "", truncated: false };
43113
44264
  }
43114
44265
  try {
43115
- const buf = await readFile7(target);
44266
+ const buf = await readFile10(target);
43116
44267
  const truncated = buf.byteLength > MAX_BYTES;
43117
44268
  const content = buf.subarray(0, MAX_BYTES).toString("utf8");
43118
44269
  return { path: target, content, truncated };
@@ -43122,7 +44273,7 @@ async function readWorkerFile(root, path) {
43122
44273
  }
43123
44274
 
43124
44275
  // src/core/fs-write.ts
43125
- import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
44276
+ import { mkdir as mkdir4, writeFile as writeFile7 } from "fs/promises";
43126
44277
  import { dirname as dirname5, resolve as resolve8, sep as sep5 } from "path";
43127
44278
  var MAX_BYTES2 = 2 * 1024 * 1024;
43128
44279
  function resolveInRoot2(root, relPath) {
@@ -43142,7 +44293,7 @@ async function writeWorkerFile(root, relPath, content) {
43142
44293
  }
43143
44294
  try {
43144
44295
  await mkdir4(dirname5(target), { recursive: true });
43145
- await writeFile5(target, content, "utf8");
44296
+ await writeFile7(target, content, "utf8");
43146
44297
  return { ok: true, path: target, bytes };
43147
44298
  } catch (err) {
43148
44299
  return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
@@ -43150,7 +44301,7 @@ async function writeWorkerFile(root, relPath, content) {
43150
44301
  }
43151
44302
 
43152
44303
  // src/core/fs-mutate.ts
43153
- import { mkdir as mkdir5, writeFile as writeFile6, rename, rm as rm3, stat as stat3 } from "fs/promises";
44304
+ import { mkdir as mkdir5, writeFile as writeFile8, rename, rm as rm4, stat as stat3 } from "fs/promises";
43154
44305
  import { basename as basename3, dirname as dirname6, resolve as resolve9, sep as sep6 } from "path";
43155
44306
  var MAX_BYTES3 = 2 * 1024 * 1024;
43156
44307
  function resolveInRoot3(root, rel) {
@@ -43193,7 +44344,7 @@ async function mutateFs(root, req) {
43193
44344
  return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
43194
44345
  }
43195
44346
  await mkdir5(dirname6(target), { recursive: true });
43196
- await writeFile6(target, content, "utf8");
44347
+ await writeFile8(target, content, "utf8");
43197
44348
  return { ok: true, path: target };
43198
44349
  }
43199
44350
  case "move": {
@@ -43212,7 +44363,7 @@ async function mutateFs(root, req) {
43212
44363
  const target = resolveInRoot3(root, req.path ?? "");
43213
44364
  if (!target) return escaped(req.path ?? "");
43214
44365
  if (target === resolve9(root)) return { ok: false, path: target, error: "cannot delete the project root" };
43215
- await rm3(target, { recursive: true, force: true });
44366
+ await rm4(target, { recursive: true, force: true });
43216
44367
  return { ok: true, path: target };
43217
44368
  }
43218
44369
  default:
@@ -43224,7 +44375,7 @@ async function mutateFs(root, req) {
43224
44375
  }
43225
44376
 
43226
44377
  // src/core/fs-transfer.ts
43227
- import { mkdir as mkdir6, readFile as readFile8, stat as stat4, writeFile as writeFile7 } from "fs/promises";
44378
+ import { mkdir as mkdir6, readFile as readFile11, stat as stat4, writeFile as writeFile9 } from "fs/promises";
43228
44379
  import { basename as basename4, dirname as dirname7, extname, resolve as resolve10, sep as sep7 } from "path";
43229
44380
  function resolveInRoot4(root, relPath) {
43230
44381
  if (!root) return null;
@@ -43269,7 +44420,7 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
43269
44420
  }
43270
44421
  try {
43271
44422
  await mkdir6(dirname7(target), { recursive: true });
43272
- await writeFile7(target, buf);
44423
+ await writeFile9(target, buf);
43273
44424
  return { ok: true, path: target, bytes: buf.byteLength };
43274
44425
  } catch (err) {
43275
44426
  return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
@@ -43284,7 +44435,7 @@ async function downloadBinaryFile(root, relPath) {
43284
44435
  if (info.size > FS_TRANSFER_MAX_BYTES) {
43285
44436
  return { error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
43286
44437
  }
43287
- const buf = await readFile8(target);
44438
+ const buf = await readFile11(target);
43288
44439
  return {
43289
44440
  contentBase64: buf.toString("base64"),
43290
44441
  contentType: guessMime(target),
@@ -43343,11 +44494,11 @@ function handleFsChannels(ctx, message, payload) {
43343
44494
  }
43344
44495
 
43345
44496
  // src/core/git-diff.ts
43346
- import { readFile as readFile9 } from "fs/promises";
43347
- import { execFile as execFile7 } from "child_process";
44497
+ import { readFile as readFile12 } from "fs/promises";
44498
+ import { execFile as execFile9 } from "child_process";
43348
44499
  import { basename as basename5, dirname as dirname8, resolve as resolve11, sep as sep8 } from "path";
43349
- import { promisify as promisify7 } from "util";
43350
- var run7 = promisify7(execFile7);
44500
+ import { promisify as promisify9 } from "util";
44501
+ var run9 = promisify9(execFile9);
43351
44502
  function resolveInRoot5(root, relPath) {
43352
44503
  if (!root) return null;
43353
44504
  const base = resolve11(root);
@@ -43370,12 +44521,12 @@ async function gitDiff(root, path) {
43370
44521
  const base = basename5(target);
43371
44522
  let newBuf = null;
43372
44523
  try {
43373
- newBuf = await readFile9(target);
44524
+ newBuf = await readFile12(target);
43374
44525
  } catch {
43375
44526
  }
43376
44527
  let oldBuf = null;
43377
44528
  try {
43378
- const { stdout } = await run7("git", ["show", `HEAD:./${base}`], {
44529
+ const { stdout } = await run9("git", ["show", `HEAD:./${base}`], {
43379
44530
  cwd: dir2,
43380
44531
  timeout: 15e3,
43381
44532
  maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
@@ -43405,12 +44556,12 @@ async function gitDiff(root, path) {
43405
44556
  }
43406
44557
 
43407
44558
  // src/core/git-env.ts
43408
- import { execFile as execFile8 } from "child_process";
43409
- import { promisify as promisify8 } from "util";
43410
- var run8 = promisify8(execFile8);
44559
+ import { execFile as execFile10 } from "child_process";
44560
+ import { promisify as promisify10 } from "util";
44561
+ var run10 = promisify10(execFile10);
43411
44562
  async function hasBinary(bin, args = ["--version"]) {
43412
44563
  try {
43413
- await run8(bin, args, { timeout: 5e3 });
44564
+ await run10(bin, args, { timeout: 5e3 });
43414
44565
  return true;
43415
44566
  } catch {
43416
44567
  return false;
@@ -43423,7 +44574,7 @@ async function checkGitEnv(provider) {
43423
44574
  let account = null;
43424
44575
  if (installed) {
43425
44576
  try {
43426
- const { stdout, stderr } = await run8(bin, ["auth", "status"], { timeout: 8e3 });
44577
+ const { stdout, stderr } = await run10(bin, ["auth", "status"], { timeout: 8e3 });
43427
44578
  const out = `${stdout}
43428
44579
  ${stderr}`;
43429
44580
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -43436,33 +44587,33 @@ ${stderr}`;
43436
44587
  }
43437
44588
 
43438
44589
  // src/core/git-ssh-key.ts
43439
- import { execFile as execFile9 } from "child_process";
43440
- import { readFile as readFile10, rm as rm4, mkdir as mkdir7 } from "fs/promises";
43441
- import { join as join22 } from "path";
43442
- import { promisify as promisify9 } from "util";
43443
- var run9 = promisify9(execFile9);
44590
+ import { execFile as execFile11 } from "child_process";
44591
+ import { readFile as readFile13, rm as rm5, mkdir as mkdir7 } from "fs/promises";
44592
+ import { join as join25 } from "path";
44593
+ import { promisify as promisify11 } from "util";
44594
+ var run11 = promisify11(execFile11);
43444
44595
  function keyPaths(profileDir2) {
43445
- const dir2 = join22(profileDir2, ".ssh");
43446
- const key = join22(dir2, "id_4pm");
44596
+ const dir2 = join25(profileDir2, ".ssh");
44597
+ const key = join25(dir2, "id_4pm");
43447
44598
  return { dir: dir2, key, pub: `${key}.pub` };
43448
44599
  }
43449
44600
  async function readPublicKey(pub) {
43450
44601
  try {
43451
- return (await readFile10(pub, "utf8")).trim();
44602
+ return (await readFile13(pub, "utf8")).trim();
43452
44603
  } catch {
43453
44604
  return null;
43454
44605
  }
43455
44606
  }
43456
44607
  async function fingerprintOf(pub) {
43457
44608
  try {
43458
- const { stdout } = await run9("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
44609
+ const { stdout } = await run11("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
43459
44610
  return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
43460
44611
  } catch {
43461
44612
  return null;
43462
44613
  }
43463
44614
  }
43464
44615
  async function configureGit(key) {
43465
- await run9(
44616
+ await run11(
43466
44617
  "git",
43467
44618
  ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
43468
44619
  { timeout: 5e3 }
@@ -43471,9 +44622,9 @@ async function configureGit(key) {
43471
44622
  async function generate(profileDir2) {
43472
44623
  const { dir: dir2, key, pub } = keyPaths(profileDir2);
43473
44624
  await mkdir7(dir2, { recursive: true });
43474
- await rm4(key, { force: true });
43475
- await rm4(pub, { force: true });
43476
- await run9("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
44625
+ await rm5(key, { force: true });
44626
+ await rm5(pub, { force: true });
44627
+ await run11("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
43477
44628
  timeout: 15e3
43478
44629
  });
43479
44630
  await configureGit(key);
@@ -43481,14 +44632,14 @@ async function generate(profileDir2) {
43481
44632
  }
43482
44633
  async function remove(profileDir2) {
43483
44634
  const { key, pub } = keyPaths(profileDir2);
43484
- await rm4(key, { force: true });
43485
- await rm4(pub, { force: true });
43486
- await run9("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
44635
+ await rm5(key, { force: true });
44636
+ await rm5(pub, { force: true });
44637
+ await run11("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
43487
44638
  () => void 0
43488
44639
  );
43489
44640
  return { publicKey: null, fingerprint: null };
43490
44641
  }
43491
- async function get(profileDir2) {
44642
+ async function get2(profileDir2) {
43492
44643
  const { pub } = keyPaths(profileDir2);
43493
44644
  const publicKey = await readPublicKey(pub);
43494
44645
  return { publicKey, fingerprint: publicKey ? await fingerprintOf(pub) : null };
@@ -43497,7 +44648,7 @@ async function manageSshKey(op, profileDir2) {
43497
44648
  try {
43498
44649
  if (op === "generate") return await generate(profileDir2);
43499
44650
  if (op === "delete") return await remove(profileDir2);
43500
- return await get(profileDir2);
44651
+ return await get2(profileDir2);
43501
44652
  } catch {
43502
44653
  return { publicKey: null, fingerprint: null };
43503
44654
  }
@@ -43555,12 +44706,12 @@ function handleGitChannels(ctx, message, payload) {
43555
44706
  }
43556
44707
 
43557
44708
  // src/core/ws-client/handlers/misc.ts
43558
- import { join as join23 } from "path";
44709
+ import { join as join26 } from "path";
43559
44710
 
43560
44711
  // src/core/ai-assist.ts
43561
- import { execFile as execFile10 } from "child_process";
43562
- import { promisify as promisify10 } from "util";
43563
- var run10 = promisify10(execFile10);
44712
+ import { execFile as execFile12 } from "child_process";
44713
+ import { promisify as promisify12 } from "util";
44714
+ var run12 = promisify12(execFile12);
43564
44715
  function aiCli() {
43565
44716
  return process.env.AI_CLI || "claude";
43566
44717
  }
@@ -43603,7 +44754,7 @@ function parseAi(cli, stdout, prompt) {
43603
44754
  }
43604
44755
  async function runAi(prompt) {
43605
44756
  const cli = aiCli();
43606
- const { stdout } = await run10(cli, aiArgs(cli, prompt), {
44757
+ const { stdout } = await run12(cli, aiArgs(cli, prompt), {
43607
44758
  timeout: 12e4,
43608
44759
  maxBuffer: 8 * 1024 * 1024
43609
44760
  });
@@ -43705,7 +44856,7 @@ function handleMiscChannels(ctx, message, payload) {
43705
44856
  }
43706
44857
  case WsChannels.LOG_READ: {
43707
44858
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
43708
- const lines = readRecentLogLines(join23(ctx.profileDir, "logs"), limit);
44859
+ const lines = readRecentLogLines(join26(ctx.profileDir, "logs"), limit);
43709
44860
  ctx.send(WsChannels.LOG_READ, { lines }, message.id);
43710
44861
  return true;
43711
44862
  }
@@ -43736,8 +44887,92 @@ function handleMiscChannels(ctx, message, payload) {
43736
44887
  }
43737
44888
 
43738
44889
  // src/core/ws-client/handlers/project.ts
43739
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, renameSync, rmSync as rmSync7 } from "fs";
43740
- import { join as join24 } from "path";
44890
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, renameSync, rmSync as rmSync7 } from "fs";
44891
+ import { join as join28 } from "path";
44892
+
44893
+ // src/core/repo-probe.ts
44894
+ import { execFile as execFile13 } from "child_process";
44895
+ import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm6 } from "fs/promises";
44896
+ import { tmpdir as tmpdir4 } from "os";
44897
+ import { join as join27 } from "path";
44898
+ import { promisify as promisify13 } from "util";
44899
+ var run13 = promisify13(execFile13);
44900
+ var MAX_BRANCHES = 100;
44901
+ var REWRITTEN = ["project.spec.json", "README.md", "CLAUDE.md", "AGENT.md", ".4pm/.4pm.json", ".claude/settings.json", ".claude/skills/", ".claude/agents/"];
44902
+ async function git4(args, cwd2, timeout = 6e4) {
44903
+ const { stdout } = await run13("git", args, { cwd: cwd2, timeout, maxBuffer: 32 * 1024 * 1024 });
44904
+ return stdout;
44905
+ }
44906
+ async function templateEntries() {
44907
+ try {
44908
+ return new Set((await readdir5(projectSampleDir())).filter((n) => n !== ".git"));
44909
+ } catch {
44910
+ return /* @__PURE__ */ new Set();
44911
+ }
44912
+ }
44913
+ async function probeSubmodule(sub) {
44914
+ try {
44915
+ const out = await git4(["ls-remote", "--symref", sub.url, "HEAD", ...sub.branch ? [`refs/heads/${sub.branch}`] : []], void 0, 3e4);
44916
+ const def = /ref: refs\/heads\/(\S+)\s+HEAD/.exec(out)?.[1] ?? "";
44917
+ const branch = sub.branch || def;
44918
+ const exists2 = !sub.branch || out.includes(`refs/heads/${sub.branch}`);
44919
+ return { subdir: sub.subdir, url: sub.url, branch, exists: exists2, protected: exists2 ? await branchProtection(sub.url, branch) : null, error: null };
44920
+ } catch (err) {
44921
+ const e = err;
44922
+ return { subdir: sub.subdir, url: sub.url, branch: sub.branch, exists: false, protected: null, error: (e.stderr || e.message || String(err)).trim().slice(0, 300) };
44923
+ }
44924
+ }
44925
+ async function probeRepo(req) {
44926
+ const empty = { branchExists: false, defaultBranch: null, branches: [], source: null, protected: null, submodules: [], error: null };
44927
+ let tmp = null;
44928
+ try {
44929
+ const [symref, heads] = await Promise.all([
44930
+ git4(["ls-remote", "--symref", req.url, "HEAD"], void 0, 3e4),
44931
+ git4(["ls-remote", "--heads", req.url], void 0, 3e4)
44932
+ ]);
44933
+ const defaultBranch = /ref: refs\/heads\/(\S+)\s+HEAD/.exec(symref)?.[1] ?? null;
44934
+ const branches = heads.split("\n").map((l) => l.split(" ")[1] ?? "").filter((r) => r.startsWith("refs/heads/")).map((r) => r.slice("refs/heads/".length));
44935
+ const branch = (req.branch ?? "").trim();
44936
+ const branchExists = !branch || branches.includes(branch);
44937
+ const kind = req.base?.kind ?? "default";
44938
+ const ref = branchExists ? branch || defaultBranch : kind === "empty" ? null : kind === "branch" ? (req.base?.branch ?? "").trim() || null : defaultBranch;
44939
+ const [isProtected, submodules] = await Promise.all([
44940
+ branchExists ? branchProtection(req.url, branch || defaultBranch || "") : Promise.resolve(null),
44941
+ Promise.all((req.submodules ?? []).map(probeSubmodule))
44942
+ ]);
44943
+ const result = { ...empty, branchExists, defaultBranch, branches: branches.slice(0, MAX_BRANCHES), protected: isProtected, submodules };
44944
+ if (!ref) return result;
44945
+ if (!branches.includes(ref)) return { ...result, error: `Branch "${ref}" does not exist on the remote.` };
44946
+ tmp = await mkdtemp2(join27(tmpdir4(), "4pm-probe-"));
44947
+ await git4(["init", "-q"], tmp);
44948
+ await git4(["remote", "add", "origin", req.url], tmp);
44949
+ await git4(["fetch", "-q", "--depth", "1", "--filter=blob:none", "origin", ref], tmp, 12e4);
44950
+ const files = (await git4(["ls-tree", "-r", "--name-only", "FETCH_HEAD"], tmp)).split("\n").filter(Boolean);
44951
+ const hasScaffold = files.includes(".4pm/.4pm.json");
44952
+ let templateVersion = null;
44953
+ if (hasScaffold) {
44954
+ try {
44955
+ const marker = JSON.parse(await git4(["show", "FETCH_HEAD:.4pm/.4pm.json"], tmp));
44956
+ templateVersion = typeof marker.templateVersion === "string" ? marker.templateVersion : null;
44957
+ } catch {
44958
+ templateVersion = null;
44959
+ }
44960
+ }
44961
+ const isRewritten = (f2) => REWRITTEN.some((r) => r.endsWith("/") ? f2.startsWith(r) : f2 === r);
44962
+ const tracking = SCAFFOLD_TRACKING_FILES.filter((f2) => files.includes(f2));
44963
+ const template = await templateEntries();
44964
+ const overwrite = files.filter((f2) => isRewritten(f2) && !f2.endsWith(".gitkeep"));
44965
+ const keep = files.filter(
44966
+ (f2) => template.has(f2.split("/")[0] ?? "") && !isRewritten(f2) && !tracking.includes(f2) && !f2.endsWith(".gitkeep")
44967
+ );
44968
+ return { ...result, source: { ref, hasScaffold, templateVersion, keep: keep.slice(0, 200), overwrite, tracking: [...tracking] } };
44969
+ } catch (err) {
44970
+ const e = err;
44971
+ return { ...empty, error: (e.stderr || e.message || String(err)).trim().split("\n").slice(-2).join(" ").slice(0, 400) };
44972
+ } finally {
44973
+ if (tmp) await rm6(tmp, { recursive: true, force: true }).catch(() => void 0);
44974
+ }
44975
+ }
43741
44976
 
43742
44977
  // src/core/ai-task.ts
43743
44978
  import { randomUUID as randomUUID2 } from "crypto";
@@ -43809,16 +45044,16 @@ function handleProjectChannels(ctx, message, payload) {
43809
45044
  case WsChannels.PHYSIC_SYNC: {
43810
45045
  const sync = payload;
43811
45046
  const oldName = sync.oldName;
43812
- const oldPath = oldName ? join24(ctx.profileDir, oldName) : null;
43813
- const newPath = join24(ctx.profileDir, sync.newName);
45047
+ const oldPath = oldName ? join28(ctx.profileDir, oldName) : null;
45048
+ const newPath = join28(ctx.profileDir, sync.newName);
43814
45049
  if (oldName) {
43815
45050
  const oldRoot = ctx.physicFolderPath(oldName);
43816
45051
  if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
43817
45052
  }
43818
- if (oldPath && oldName !== sync.newName && existsSync14(oldPath) && !existsSync14(newPath)) {
45053
+ if (oldPath && oldName !== sync.newName && existsSync15(oldPath) && !existsSync15(newPath)) {
43819
45054
  renameSync(oldPath, newPath);
43820
45055
  } else {
43821
- if (oldPath && oldName !== sync.newName && existsSync14(oldPath)) rmSync7(oldPath, { recursive: true, force: true });
45056
+ if (oldPath && oldName !== sync.newName && existsSync15(oldPath)) rmSync7(oldPath, { recursive: true, force: true });
43822
45057
  mkdirSync7(newPath, { recursive: true });
43823
45058
  }
43824
45059
  ctx.physicRoot = ctx.physicFolderPath(sync.newName);
@@ -43833,7 +45068,7 @@ function handleProjectChannels(ctx, message, payload) {
43833
45068
  if (del.name) {
43834
45069
  const delRoot = ctx.physicFolderPath(del.name);
43835
45070
  if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
43836
- rmSync7(join24(ctx.profileDir, del.name), { recursive: true, force: true });
45071
+ rmSync7(join28(ctx.profileDir, del.name), { recursive: true, force: true });
43837
45072
  ctx.bus.log(t("project.folderDeleted", { name: del.name }));
43838
45073
  }
43839
45074
  void manageSshKey("delete", ctx.profileDir).catch(() => void 0);
@@ -43872,6 +45107,10 @@ function handleProjectChannels(ctx, message, payload) {
43872
45107
  requestGitSnapshot("scaffold");
43873
45108
  });
43874
45109
  return true;
45110
+ case WsChannels.REPO_PROBE: {
45111
+ void probeRepo(payload).then((reply) => ctx.send(WsChannels.REPO_PROBE, reply, message.id));
45112
+ return true;
45113
+ }
43875
45114
  case WsChannels.PROJECT_PUBLISH: {
43876
45115
  const root = ctx.physicRoot;
43877
45116
  if (!root) {
@@ -43985,13 +45224,13 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
43985
45224
  }
43986
45225
 
43987
45226
  // src/core/faq-compose.ts
43988
- import { spawn as spawn9, execFile as execFile11 } from "child_process";
43989
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, rmSync as rmSync8 } from "fs";
45227
+ import { spawn as spawn9, execFile as execFile14 } from "child_process";
45228
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, rmSync as rmSync8 } from "fs";
43990
45229
  import { randomUUID as randomUUID3 } from "crypto";
43991
- import { join as join25 } from "path";
43992
- import { tmpdir as tmpdir4 } from "os";
43993
- import { promisify as promisify11 } from "util";
43994
- var execFileAsync2 = promisify11(execFile11);
45230
+ import { join as join29 } from "path";
45231
+ import { tmpdir as tmpdir5 } from "os";
45232
+ import { promisify as promisify14 } from "util";
45233
+ var execFileAsync2 = promisify14(execFile14);
43995
45234
  var NO_USAGE2 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
43996
45235
  var COMPOSE_TIMEOUT_MS2 = 10 * 60 * 1e3;
43997
45236
  var AGENT_DENIED_TOOLS = ["Bash", "WebFetch", "WebSearch", "NotebookEdit", "Task"];
@@ -44109,7 +45348,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
44109
45348
  var MAX_OUTPUT_CHARS = 8e3;
44110
45349
  async function runFaqCompose(req, ai2) {
44111
45350
  if (!req.repo.token) return { prUrl: "", error: "no write token" };
44112
- const workDir = join25(tmpdir4(), "4pm-faq-sync", randomUUID3());
45351
+ const workDir = join29(tmpdir5(), "4pm-faq-sync", randomUUID3());
44113
45352
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
44114
45353
  const auth = gitAuthArgs("x-access-token", req.repo.token);
44115
45354
  try {
@@ -44153,7 +45392,7 @@ async function runFaqCompose(req, ai2) {
44153
45392
  logger.warn("faq.compose.failed", { error: String(err) });
44154
45393
  return { prUrl: "", error: String(err) };
44155
45394
  } finally {
44156
- if (existsSync15(workDir)) {
45395
+ if (existsSync16(workDir)) {
44157
45396
  try {
44158
45397
  rmSync8(workDir, { recursive: true, force: true });
44159
45398
  } catch {
@@ -44337,8 +45576,8 @@ function handleToolsChannels(ctx, message, payload) {
44337
45576
  }
44338
45577
 
44339
45578
  // src/core/agents.ts
44340
- import { readFile as readFile11, writeFile as writeFile8, mkdir as mkdir8, readdir as readdir4, rm as rm5 } from "fs/promises";
44341
- import { join as join26 } from "path";
45579
+ import { readFile as readFile14, writeFile as writeFile10, mkdir as mkdir8, readdir as readdir6, rm as rm7 } from "fs/promises";
45580
+ import { join as join30 } from "path";
44342
45581
  var AGENTS_REL = ".claude/agents";
44343
45582
  var SKILLS_REL = ".claude/skills";
44344
45583
  var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
@@ -44347,7 +45586,7 @@ function safeName(name) {
44347
45586
  }
44348
45587
  async function readText2(path, fallback = "") {
44349
45588
  try {
44350
- return await readFile11(path, "utf8");
45589
+ return await readFile14(path, "utf8");
44351
45590
  } catch {
44352
45591
  return fallback;
44353
45592
  }
@@ -44355,24 +45594,24 @@ async function readText2(path, fallback = "") {
44355
45594
  function frontmatter(text) {
44356
45595
  const m2 = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
44357
45596
  const body = m2?.[1] ?? "";
44358
- const get2 = (key) => body.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
44359
- return { name: get2("name"), description: get2("description") };
45597
+ const get3 = (key) => body.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
45598
+ return { name: get3("name"), description: get3("description") };
44360
45599
  }
44361
45600
  async function listAgents(root) {
44362
45601
  const subagents = [];
44363
45602
  const skills = [];
44364
45603
  try {
44365
- for (const e of await readdir4(join26(root, AGENTS_REL), { withFileTypes: true })) {
45604
+ for (const e of await readdir6(join30(root, AGENTS_REL), { withFileTypes: true })) {
44366
45605
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
44367
- const fm = frontmatter(await readText2(join26(root, AGENTS_REL, e.name)));
45606
+ const fm = frontmatter(await readText2(join30(root, AGENTS_REL, e.name)));
44368
45607
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
44369
45608
  }
44370
45609
  } catch {
44371
45610
  }
44372
45611
  try {
44373
- for (const e of await readdir4(join26(root, SKILLS_REL), { withFileTypes: true })) {
45612
+ for (const e of await readdir6(join30(root, SKILLS_REL), { withFileTypes: true })) {
44374
45613
  if (!e.isDirectory()) continue;
44375
- const fm = frontmatter(await readText2(join26(root, SKILLS_REL, e.name, "SKILL.md")));
45614
+ const fm = frontmatter(await readText2(join30(root, SKILLS_REL, e.name, "SKILL.md")));
44376
45615
  skills.push({ name: e.name, description: fm.description });
44377
45616
  }
44378
45617
  } catch {
@@ -44383,15 +45622,15 @@ async function listAgents(root) {
44383
45622
  }
44384
45623
  function itemPath(root, kind, name) {
44385
45624
  const n = safeName(name);
44386
- return kind === "subagent" ? join26(root, AGENTS_REL, `${n}.md`) : join26(root, SKILLS_REL, n, "SKILL.md");
45625
+ return kind === "subagent" ? join30(root, AGENTS_REL, `${n}.md`) : join30(root, SKILLS_REL, n, "SKILL.md");
44387
45626
  }
44388
45627
  async function readAgent(root, kind, name) {
44389
45628
  const content = await readText2(itemPath(root, kind, name));
44390
45629
  if (kind !== "subagent") return { content };
44391
45630
  const n = safeName(name);
44392
45631
  const [short, long] = await Promise.all([
44393
- readText2(join26(root, AGENTS_REL, "short-memory", `${n}.md`)),
44394
- readText2(join26(root, AGENTS_REL, "long-memory", `${n}.md`))
45632
+ readText2(join30(root, AGENTS_REL, "short-memory", `${n}.md`)),
45633
+ readText2(join30(root, AGENTS_REL, "long-memory", `${n}.md`))
44395
45634
  ]);
44396
45635
  return { content, memory: { short, long } };
44397
45636
  }
@@ -44400,14 +45639,14 @@ async function writeAgent(root, req) {
44400
45639
  if (!n) return { ok: false, error: "invalid name" };
44401
45640
  try {
44402
45641
  if (req.action === "delete") {
44403
- if (req.kind === "subagent") await rm5(itemPath(root, "subagent", n), { force: true });
44404
- else await rm5(join26(root, SKILLS_REL, n), { recursive: true, force: true });
45642
+ if (req.kind === "subagent") await rm7(itemPath(root, "subagent", n), { force: true });
45643
+ else await rm7(join30(root, SKILLS_REL, n), { recursive: true, force: true });
44405
45644
  return { ok: true };
44406
45645
  }
44407
45646
  if (req.content === void 0) return { ok: false, error: "content is required" };
44408
45647
  const path = itemPath(root, req.kind, n);
44409
- await mkdir8(join26(path, ".."), { recursive: true });
44410
- await writeFile8(path, req.content, "utf8");
45648
+ await mkdir8(join30(path, ".."), { recursive: true });
45649
+ await writeFile10(path, req.content, "utf8");
44411
45650
  return { ok: true };
44412
45651
  } catch (err) {
44413
45652
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
@@ -44415,21 +45654,21 @@ async function writeAgent(root, req) {
44415
45654
  }
44416
45655
 
44417
45656
  // src/core/secrets.ts
44418
- import { readFile as readFile12, writeFile as writeFile9 } from "fs/promises";
44419
- import { join as join27 } from "path";
45657
+ import { readFile as readFile15, writeFile as writeFile11 } from "fs/promises";
45658
+ import { join as join31 } from "path";
44420
45659
  var SECURITY_REL = "AI_SECURITY.md";
44421
45660
  var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
44422
45661
  var SECRETS_REL = "project.secrets.json";
44423
45662
  async function readText3(path, fallback = "") {
44424
45663
  try {
44425
- return await readFile12(path, "utf8");
45664
+ return await readFile15(path, "utf8");
44426
45665
  } catch {
44427
45666
  return fallback;
44428
45667
  }
44429
45668
  }
44430
45669
  async function readSecretsObject(root) {
44431
45670
  try {
44432
- const v2 = JSON.parse(await readFile12(join27(root, SECRETS_REL), "utf8"));
45671
+ const v2 = JSON.parse(await readFile15(join31(root, SECRETS_REL), "utf8"));
44433
45672
  return v2 && typeof v2 === "object" ? v2 : {};
44434
45673
  } catch {
44435
45674
  return {};
@@ -44448,8 +45687,8 @@ function placeholderKeys(text) {
44448
45687
  }
44449
45688
  async function readSecrets(root) {
44450
45689
  const [security, placeholder, secrets] = await Promise.all([
44451
- readText3(join27(root, SECURITY_REL)),
44452
- readText3(join27(root, PLACEHOLDER_REL)),
45690
+ readText3(join31(root, SECURITY_REL)),
45691
+ readText3(join31(root, PLACEHOLDER_REL)),
44453
45692
  readSecretsObject(root)
44454
45693
  ]);
44455
45694
  const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
@@ -44462,14 +45701,14 @@ async function readSecrets(root) {
44462
45701
  async function writeSecrets(root, req) {
44463
45702
  try {
44464
45703
  if (req.kind === "security") {
44465
- await writeFile9(join27(root, SECURITY_REL), req.content, "utf8");
45704
+ await writeFile11(join31(root, SECURITY_REL), req.content, "utf8");
44466
45705
  } else if (req.kind === "placeholder") {
44467
- await writeFile9(join27(root, PLACEHOLDER_REL), req.content, "utf8");
45706
+ await writeFile11(join31(root, PLACEHOLDER_REL), req.content, "utf8");
44468
45707
  } else {
44469
45708
  const secrets = await readSecretsObject(root);
44470
45709
  if (req.kind === "secret") secrets[req.key] = req.value;
44471
45710
  else delete secrets[req.key];
44472
- await writeFile9(join27(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
45711
+ await writeFile11(join31(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
44473
45712
  }
44474
45713
  return { ok: true };
44475
45714
  } catch (err) {
@@ -44478,17 +45717,17 @@ async function writeSecrets(root, req) {
44478
45717
  }
44479
45718
 
44480
45719
  // src/core/agent-tools.ts
44481
- import { readFile as readFile13, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
44482
- import { dirname as dirname9, join as join28 } from "path";
45720
+ import { readFile as readFile16, writeFile as writeFile12, mkdir as mkdir9 } from "fs/promises";
45721
+ import { dirname as dirname9, join as join32 } from "path";
44483
45722
  var SHARED_REL = ".claude/settings.json";
44484
45723
  var LOCAL_REL = ".claude/settings.local.json";
44485
45724
  var SECRETS_DENY = "Read(./project.secrets.json)";
44486
45725
  function fileFor(root, scope) {
44487
- return join28(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
45726
+ return join32(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
44488
45727
  }
44489
45728
  async function readSettingsObject(path) {
44490
45729
  try {
44491
- const v2 = JSON.parse(await readFile13(path, "utf8"));
45730
+ const v2 = JSON.parse(await readFile16(path, "utf8"));
44492
45731
  return v2 && typeof v2 === "object" ? v2 : {};
44493
45732
  } catch {
44494
45733
  return {};
@@ -44511,7 +45750,7 @@ async function writeAgentTools(root, scope, permissions) {
44511
45750
  if (scope === "shared" && !next.deny.includes(SECRETS_DENY)) next.deny = [...next.deny, SECRETS_DENY];
44512
45751
  obj.permissions = next;
44513
45752
  await mkdir9(dirname9(path), { recursive: true });
44514
- await writeFile10(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
45753
+ await writeFile12(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
44515
45754
  return { ok: true };
44516
45755
  } catch (err) {
44517
45756
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
@@ -44520,8 +45759,8 @@ async function writeAgentTools(root, scope, permissions) {
44520
45759
 
44521
45760
  // src/core/packages.ts
44522
45761
  import { createHash as createHash4 } from "crypto";
44523
- import { readFile as readFile14, writeFile as writeFile11, mkdir as mkdir10, readdir as readdir5, rm as rm6, stat as stat5 } from "fs/promises";
44524
- import { dirname as dirname10, join as join29, relative } from "path";
45762
+ import { readFile as readFile17, writeFile as writeFile13, mkdir as mkdir10, readdir as readdir7, rm as rm8, stat as stat5 } from "fs/promises";
45763
+ import { dirname as dirname10, join as join33, relative } from "path";
44525
45764
  var AGENTS_REL2 = ".claude/agents";
44526
45765
  var SKILLS_REL2 = ".claude/skills";
44527
45766
  var MANIFEST_REL = ".claude/.4pm-packages.json";
@@ -44530,7 +45769,7 @@ function safeName2(name) {
44530
45769
  }
44531
45770
  async function readMaybe(path) {
44532
45771
  try {
44533
- return await readFile14(path);
45772
+ return await readFile17(path);
44534
45773
  } catch {
44535
45774
  return null;
44536
45775
  }
@@ -44540,12 +45779,12 @@ async function listFilesRel(dir2) {
44540
45779
  async function walk2(d) {
44541
45780
  let entries2;
44542
45781
  try {
44543
- entries2 = await readdir5(d, { withFileTypes: true });
45782
+ entries2 = await readdir7(d, { withFileTypes: true });
44544
45783
  } catch {
44545
45784
  return;
44546
45785
  }
44547
45786
  for (const e of entries2) {
44548
- const full = join29(d, e.name);
45787
+ const full = join33(d, e.name);
44549
45788
  if (e.isDirectory()) await walk2(full);
44550
45789
  else if (e.isFile()) out.push(relative(dir2, full));
44551
45790
  }
@@ -44556,16 +45795,16 @@ async function listFilesRel(dir2) {
44556
45795
  async function readArtifactFiles(root, kind, slug) {
44557
45796
  const files = [];
44558
45797
  if (kind === "subagent") {
44559
- const agent = await readMaybe(join29(root, AGENTS_REL2, `${slug}.md`));
45798
+ const agent = await readMaybe(join33(root, AGENTS_REL2, `${slug}.md`));
44560
45799
  if (agent) files.push({ path: "agent.md", contentBase64: agent.toString("base64") });
44561
- const short = await readMaybe(join29(root, AGENTS_REL2, "short-memory", `${slug}.md`));
45800
+ const short = await readMaybe(join33(root, AGENTS_REL2, "short-memory", `${slug}.md`));
44562
45801
  if (short) files.push({ path: "short-memory.md", contentBase64: short.toString("base64") });
44563
- const long = await readMaybe(join29(root, AGENTS_REL2, "long-memory", `${slug}.md`));
45802
+ const long = await readMaybe(join33(root, AGENTS_REL2, "long-memory", `${slug}.md`));
44564
45803
  if (long) files.push({ path: "long-memory.md", contentBase64: long.toString("base64") });
44565
45804
  } else {
44566
- const base = join29(root, SKILLS_REL2, slug);
45805
+ const base = join33(root, SKILLS_REL2, slug);
44567
45806
  for (const rel of await listFilesRel(base)) {
44568
- const buf = await readMaybe(join29(base, rel));
45807
+ const buf = await readMaybe(join33(base, rel));
44569
45808
  if (buf) files.push({ path: rel.split("\\").join("/"), contentBase64: buf.toString("base64") });
44570
45809
  }
44571
45810
  }
@@ -44583,7 +45822,7 @@ async function artifactSha256(root, kind, slug) {
44583
45822
  return h.digest("hex");
44584
45823
  }
44585
45824
  async function artifactExists(root, kind, slug) {
44586
- const target = kind === "subagent" ? join29(root, AGENTS_REL2, `${slug}.md`) : join29(root, SKILLS_REL2, slug);
45825
+ const target = kind === "subagent" ? join33(root, AGENTS_REL2, `${slug}.md`) : join33(root, SKILLS_REL2, slug);
44587
45826
  try {
44588
45827
  await stat5(target);
44589
45828
  return true;
@@ -44592,7 +45831,7 @@ async function artifactExists(root, kind, slug) {
44592
45831
  }
44593
45832
  }
44594
45833
  async function readManifest(root) {
44595
- const buf = await readMaybe(join29(root, MANIFEST_REL));
45834
+ const buf = await readMaybe(join33(root, MANIFEST_REL));
44596
45835
  if (!buf) return {};
44597
45836
  try {
44598
45837
  return JSON.parse(buf.toString("utf8"));
@@ -44601,35 +45840,35 @@ async function readManifest(root) {
44601
45840
  }
44602
45841
  }
44603
45842
  async function writeManifest(root, manifest) {
44604
- const path = join29(root, MANIFEST_REL);
45843
+ const path = join33(root, MANIFEST_REL);
44605
45844
  await mkdir10(dirname10(path), { recursive: true });
44606
- await writeFile11(path, JSON.stringify(manifest, null, 2), "utf8");
45845
+ await writeFile13(path, JSON.stringify(manifest, null, 2), "utf8");
44607
45846
  }
44608
45847
  async function writeArtifactFiles(root, kind, slug, files) {
44609
45848
  const byPath = new Map(files.map((f2) => [f2.path, Buffer.from(f2.contentBase64, "base64")]));
44610
45849
  if (kind === "subagent") {
44611
45850
  const agent = byPath.get("agent.md");
44612
45851
  if (agent) {
44613
- await mkdir10(join29(root, AGENTS_REL2), { recursive: true });
44614
- await writeFile11(join29(root, AGENTS_REL2, `${slug}.md`), agent);
45852
+ await mkdir10(join33(root, AGENTS_REL2), { recursive: true });
45853
+ await writeFile13(join33(root, AGENTS_REL2, `${slug}.md`), agent);
44615
45854
  }
44616
45855
  const short = byPath.get("short-memory.md");
44617
45856
  if (short) {
44618
- await mkdir10(join29(root, AGENTS_REL2, "short-memory"), { recursive: true });
44619
- await writeFile11(join29(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
45857
+ await mkdir10(join33(root, AGENTS_REL2, "short-memory"), { recursive: true });
45858
+ await writeFile13(join33(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
44620
45859
  }
44621
45860
  const long = byPath.get("long-memory.md");
44622
45861
  if (long) {
44623
- await mkdir10(join29(root, AGENTS_REL2, "long-memory"), { recursive: true });
44624
- await writeFile11(join29(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
45862
+ await mkdir10(join33(root, AGENTS_REL2, "long-memory"), { recursive: true });
45863
+ await writeFile13(join33(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
44625
45864
  }
44626
45865
  } else {
44627
- const base = join29(root, SKILLS_REL2, slug);
45866
+ const base = join33(root, SKILLS_REL2, slug);
44628
45867
  for (const [rel, buf] of byPath) {
44629
45868
  if (rel.includes("..") || rel.startsWith("/")) continue;
44630
- const dest = join29(base, rel);
45869
+ const dest = join33(base, rel);
44631
45870
  await mkdir10(dirname10(dest), { recursive: true });
44632
- await writeFile11(dest, buf);
45871
+ await writeFile13(dest, buf);
44633
45872
  }
44634
45873
  }
44635
45874
  }
@@ -44693,11 +45932,11 @@ async function removePackage(root, slug) {
44693
45932
  const entry = manifest[n];
44694
45933
  if (!entry) return { ok: false, error: "not installed" };
44695
45934
  if (entry.kind === "subagent") {
44696
- await rm6(join29(root, AGENTS_REL2, `${n}.md`), { force: true });
44697
- await rm6(join29(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
44698
- await rm6(join29(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
45935
+ await rm8(join33(root, AGENTS_REL2, `${n}.md`), { force: true });
45936
+ await rm8(join33(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
45937
+ await rm8(join33(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
44699
45938
  } else {
44700
- await rm6(join29(root, SKILLS_REL2, n), { recursive: true, force: true });
45939
+ await rm8(join33(root, SKILLS_REL2, n), { recursive: true, force: true });
44701
45940
  }
44702
45941
  delete manifest[n];
44703
45942
  await writeManifest(root, manifest);
@@ -44708,8 +45947,8 @@ async function removePackage(root, slug) {
44708
45947
  }
44709
45948
 
44710
45949
  // src/core/graph.ts
44711
- import { readFile as readFile15, readdir as readdir6 } from "fs/promises";
44712
- import { join as join30, relative as relative2, dirname as dirname11, resolve as resolve12, extname as extname2, basename as basename6 } from "path";
45950
+ import { readFile as readFile18, readdir as readdir8 } from "fs/promises";
45951
+ import { join as join34, relative as relative2, dirname as dirname11, resolve as resolve12, extname as extname2, basename as basename6 } from "path";
44713
45952
  var MAX_FILES = 2e3;
44714
45953
  var MAX_NODES = 3e3;
44715
45954
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -44721,7 +45960,7 @@ async function walk(root, accept) {
44721
45960
  const dir2 = stack.pop();
44722
45961
  let entries2;
44723
45962
  try {
44724
- entries2 = await readdir6(dir2, { withFileTypes: true });
45963
+ entries2 = await readdir8(dir2, { withFileTypes: true });
44725
45964
  } catch {
44726
45965
  continue;
44727
45966
  }
@@ -44729,7 +45968,7 @@ async function walk(root, accept) {
44729
45968
  if (e.name.startsWith(".") && e.name !== ".claude") {
44730
45969
  if (e.isDirectory()) continue;
44731
45970
  }
44732
- const full = join30(dir2, e.name);
45971
+ const full = join34(dir2, e.name);
44733
45972
  if (e.isDirectory()) {
44734
45973
  if (!SKIP_DIRS.has(e.name)) stack.push(full);
44735
45974
  } else if (accept(full)) {
@@ -44757,7 +45996,7 @@ async function buildDocs(root) {
44757
45996
  const rel = relative2(root, f2).split("\\").join("/");
44758
45997
  let text = "";
44759
45998
  try {
44760
- text = await readFile15(f2, "utf8");
45999
+ text = await readFile18(f2, "utf8");
44761
46000
  } catch {
44762
46001
  continue;
44763
46002
  }
@@ -44796,7 +46035,7 @@ async function buildCode(root) {
44796
46035
  const rel = relative2(root, f2).split("\\").join("/");
44797
46036
  let text = "";
44798
46037
  try {
44799
- text = await readFile15(f2, "utf8");
46038
+ text = await readFile18(f2, "utf8");
44800
46039
  } catch {
44801
46040
  continue;
44802
46041
  }
@@ -44848,17 +46087,17 @@ async function buildGraph(root, mode) {
44848
46087
  }
44849
46088
 
44850
46089
  // src/core/rag.ts
44851
- import { readFile as readFile16, writeFile as writeFile12, statfs as statfs2, mkdir as mkdir11 } from "fs/promises";
44852
- import { existsSync as existsSync16 } from "fs";
44853
- import { execFile as execFile12, spawn as spawn10 } from "child_process";
46090
+ import { readFile as readFile19, writeFile as writeFile14, statfs as statfs2, mkdir as mkdir11 } from "fs/promises";
46091
+ import { existsSync as existsSync17 } from "fs";
46092
+ import { execFile as execFile15, spawn as spawn10 } from "child_process";
44854
46093
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
44855
- import { join as join31 } from "path";
44856
- import { promisify as promisify12 } from "util";
44857
- var run11 = promisify12(execFile12);
46094
+ import { join as join35 } from "path";
46095
+ import { promisify as promisify15 } from "util";
46096
+ var run14 = promisify15(execFile15);
44858
46097
  var RAG_REL = ".claude/rag";
44859
46098
  async function tryRun(cmd, args) {
44860
46099
  try {
44861
- const { stdout } = await run11(cmd, args, { timeout: 8e3 });
46100
+ const { stdout } = await run14(cmd, args, { timeout: 8e3 });
44862
46101
  return { ok: true, out: stdout.trim() };
44863
46102
  } catch {
44864
46103
  return { ok: false, out: "" };
@@ -44873,7 +46112,7 @@ async function detectPython() {
44873
46112
  }
44874
46113
  async function installLog(root) {
44875
46114
  try {
44876
- const text = await readFile16(join31(root, RAG_REL, "install.log"), "utf8");
46115
+ const text = await readFile19(join35(root, RAG_REL, "install.log"), "utf8");
44877
46116
  return text.split("\n").filter((l) => l.length > 0).slice(-200);
44878
46117
  } catch {
44879
46118
  return [];
@@ -44935,14 +46174,14 @@ async function ragStatus(root) {
44935
46174
  };
44936
46175
  let modelName = "";
44937
46176
  try {
44938
- modelName = (await readFile16(join31(root, RAG_REL, ".installed"), "utf8")).trim();
46177
+ modelName = (await readFile19(join35(root, RAG_REL, ".installed"), "utf8")).trim();
44939
46178
  } catch {
44940
46179
  modelName = "";
44941
46180
  }
44942
46181
  const modelPresent = modelName.length > 0;
44943
46182
  return {
44944
46183
  installed: fastembed && sqliteVec && modelPresent,
44945
- installing: existsSync16(join31(root, RAG_REL, ".installing")),
46184
+ installing: existsSync17(join35(root, RAG_REL, ".installing")),
44946
46185
  python: { found: py.found, version: py.version },
44947
46186
  deps: { pip, fastembed, sqliteVec },
44948
46187
  model: { present: modelPresent, name: modelName },
@@ -44953,9 +46192,9 @@ async function ragStatus(root) {
44953
46192
  };
44954
46193
  }
44955
46194
  async function readIndexMeta(root) {
44956
- const indexing = existsSync16(join31(root, RAG_REL, ".indexing"));
46195
+ const indexing = existsSync17(join35(root, RAG_REL, ".indexing"));
44957
46196
  try {
44958
- const m2 = JSON.parse(await readFile16(join31(root, RAG_REL, ".index.json"), "utf8"));
46197
+ const m2 = JSON.parse(await readFile19(join35(root, RAG_REL, ".index.json"), "utf8"));
44959
46198
  return {
44960
46199
  present: true,
44961
46200
  indexing,
@@ -45022,14 +46261,14 @@ if __name__ == "__main__":
45022
46261
  elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
45023
46262
  `;
45024
46263
  async function ensureEngine(root) {
45025
- await mkdir11(join31(root, RAG_REL), { recursive: true }).catch(() => void 0);
45026
- await writeFile12(join31(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
46264
+ await mkdir11(join35(root, RAG_REL), { recursive: true }).catch(() => void 0);
46265
+ await writeFile14(join35(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
45027
46266
  }
45028
46267
  async function ragReindex(root) {
45029
46268
  const py = await detectPython();
45030
46269
  if (!py.found) return { ok: false, started: false, error: "python3 not found" };
45031
- if (!existsSync16(join31(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
45032
- if (existsSync16(join31(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
46270
+ if (!existsSync17(join35(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
46271
+ if (existsSync17(join35(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
45033
46272
  await ensureEngine(root);
45034
46273
  const script = `
45035
46274
  set +e
@@ -45049,12 +46288,12 @@ rm -f "${RAG_REL}/.indexing"
45049
46288
  async function ragQuery(root, queryText, k2 = 8) {
45050
46289
  const py = await detectPython();
45051
46290
  if (!py.found) return { results: [], error: "python3 not found" };
45052
- if (!existsSync16(join31(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
46291
+ if (!existsSync17(join35(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
45053
46292
  await ensureEngine(root);
45054
46293
  try {
45055
- const { stdout } = await run11(
46294
+ const { stdout } = await run14(
45056
46295
  py.bin,
45057
- [join31(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k2, 1), 20))],
46296
+ [join35(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k2, 1), 20))],
45058
46297
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
45059
46298
  );
45060
46299
  const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
@@ -45069,9 +46308,9 @@ async function ragInstall(root, model) {
45069
46308
  }
45070
46309
  const py = await detectPython();
45071
46310
  if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
45072
- if (existsSync16(join31(root, RAG_REL, ".installing"))) return { ok: true, started: true };
46311
+ if (existsSync17(join35(root, RAG_REL, ".installing"))) return { ok: true, started: true };
45073
46312
  try {
45074
- await mkdir11(join31(root, RAG_REL), { recursive: true });
46313
+ await mkdir11(join35(root, RAG_REL), { recursive: true });
45075
46314
  } catch {
45076
46315
  }
45077
46316
  const script = `
@@ -45098,8 +46337,8 @@ rm -f "${RAG_REL}/.installing"
45098
46337
  }
45099
46338
 
45100
46339
  // src/core/config-sync.ts
45101
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
45102
- import { join as join32 } from "path";
46340
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
46341
+ import { join as join36 } from "path";
45103
46342
  var SERVER_MANAGED_KEYS = [
45104
46343
  "physicPath",
45105
46344
  "sessionSwitchPct",
@@ -45116,8 +46355,8 @@ var SERVER_MANAGED_KEYS = [
45116
46355
  "projectAiMemoryBudgetChars"
45117
46356
  ];
45118
46357
  function readConfigText(profileDir2) {
45119
- const path = join32(profileDir2, "config.json");
45120
- if (existsSync17(path)) {
46358
+ const path = join36(profileDir2, "config.json");
46359
+ if (existsSync18(path)) {
45121
46360
  try {
45122
46361
  return readFileSync16(path, "utf8");
45123
46362
  } catch {
@@ -45839,6 +47078,7 @@ var WsClient = class {
45839
47078
  this.startUsagePolling();
45840
47079
  this.startLogUpload();
45841
47080
  this.startKbRefresh();
47081
+ onReconnectVerifyClaim(this.hctx);
45842
47082
  const servedRoot = () => this.physicRoot;
45843
47083
  startGitSnapshots({
45844
47084
  get physicRoot() {
@@ -46009,7 +47249,7 @@ var WsClient = class {
46009
47249
  /** Create the physic folder at an absolute path if missing (best-effort). */
46010
47250
  ensurePhysicFolderPath(folder) {
46011
47251
  try {
46012
- if (!existsSync18(folder)) {
47252
+ if (!existsSync19(folder)) {
46013
47253
  mkdirSync9(folder, { recursive: true });
46014
47254
  this.bus.log(t("project.createdFolder", { folder }));
46015
47255
  }
@@ -46024,7 +47264,7 @@ var WsClient = class {
46024
47264
  */
46025
47265
  physicFolderPath(projectName) {
46026
47266
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
46027
- return safe ? join33(this.context.profileDir, safe) : null;
47267
+ return safe ? join37(this.context.profileDir, safe) : null;
46028
47268
  }
46029
47269
  /**
46030
47270
  * Report machine.status: version + the state of the physic project being served
@@ -46039,7 +47279,7 @@ var WsClient = class {
46039
47279
  projects: [],
46040
47280
  version: CLI_VERSION,
46041
47281
  physicPath,
46042
- physicPathExists: physicPath ? existsSync18(physicPath) : void 0,
47282
+ physicPathExists: physicPath ? existsSync19(physicPath) : void 0,
46043
47283
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
46044
47284
  autonomousRunning: physicPath ? isAutonomousRunning(physicPath, this.context.profileDir) : false,
46045
47285
  // The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
@@ -46136,7 +47376,7 @@ var WsClient = class {
46136
47376
  */
46137
47377
  uploadLog() {
46138
47378
  try {
46139
- const upload = readLogUpload(join33(this.context.profileDir, "logs"));
47379
+ const upload = readLogUpload(join37(this.context.profileDir, "logs"));
46140
47380
  if (!upload) return;
46141
47381
  const sig = `${upload.fileName}:${upload.totalBytes}`;
46142
47382
  if (sig === this.lastLogSig) return;
@@ -46299,9 +47539,9 @@ var WsClient = class {
46299
47539
 
46300
47540
  // src/core/instance-lock.ts
46301
47541
  import { readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
46302
- import { join as join34 } from "path";
47542
+ import { join as join38 } from "path";
46303
47543
  function lockPath(profileDir2) {
46304
- return join34(profileDir2, "start.lock");
47544
+ return join38(profileDir2, "start.lock");
46305
47545
  }
46306
47546
  function isAlive(pid) {
46307
47547
  try {
@@ -46623,7 +47863,7 @@ var SessionBus = class {
46623
47863
  // src/core/control-server.ts
46624
47864
  import { createServer } from "net";
46625
47865
  import { chmodSync as chmodSync5, rmSync as rmSync10 } from "fs";
46626
- import { join as join36 } from "path";
47866
+ import { join as join40 } from "path";
46627
47867
 
46628
47868
  // src/core/control-protocol.ts
46629
47869
  var CONTROL_SOCKET_FILE = "control.sock";
@@ -46654,9 +47894,9 @@ function createFrameParser() {
46654
47894
  // src/core/control-token.ts
46655
47895
  import { chmodSync as chmodSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
46656
47896
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
46657
- import { join as join35 } from "path";
47897
+ import { join as join39 } from "path";
46658
47898
  function controlTokenPath(profileDir2) {
46659
- return join35(profileDir2, CONTROL_TOKEN_FILE);
47899
+ return join39(profileDir2, CONTROL_TOKEN_FILE);
46660
47900
  }
46661
47901
  function writeControlToken(profileDir2) {
46662
47902
  const token = randomBytes3(32).toString("hex");
@@ -46685,7 +47925,7 @@ function tokenMatches(expected, given) {
46685
47925
 
46686
47926
  // src/core/control-server.ts
46687
47927
  function startControlServer(bus, profileDir2, info) {
46688
- const socketPath = join36(profileDir2, CONTROL_SOCKET_FILE);
47928
+ const socketPath = join40(profileDir2, CONTROL_SOCKET_FILE);
46689
47929
  try {
46690
47930
  rmSync10(socketPath, { force: true });
46691
47931
  } catch {
@@ -47071,11 +48311,11 @@ var isOldWindows = () => {
47071
48311
  }
47072
48312
  const parts = os2.release().split(".");
47073
48313
  const major = Number(parts[0]);
47074
- const build = Number(parts[2] ?? 0);
48314
+ const build2 = Number(parts[2] ?? 0);
47075
48315
  if (major < 10) {
47076
48316
  return true;
47077
48317
  }
47078
- if (major === 10 && build < 10586) {
48318
+ if (major === 10 && build2 < 10586) {
47079
48319
  return true;
47080
48320
  }
47081
48321
  return false;
@@ -52442,15 +53682,15 @@ var import_react21 = __toESM(require_react(), 1);
52442
53682
  var import_react26 = __toESM(require_react(), 1);
52443
53683
 
52444
53684
  // src/core/input-history.ts
52445
- import { existsSync as existsSync20, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
52446
- import { join as join37 } from "path";
53685
+ import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
53686
+ import { join as join41 } from "path";
52447
53687
  var MAX_HISTORY = 200;
52448
53688
  function historyPath(profileDir2) {
52449
- return join37(profileDir2, "input-history.json");
53689
+ return join41(profileDir2, "input-history.json");
52450
53690
  }
52451
53691
  function loadInputHistory(profileDir2) {
52452
53692
  const path = historyPath(profileDir2);
52453
- if (!existsSync20(path)) return [];
53693
+ if (!existsSync21(path)) return [];
52454
53694
  try {
52455
53695
  const data = JSON.parse(readFileSync20(path, "utf8"));
52456
53696
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
@@ -52573,10 +53813,10 @@ function AiMarkerLine({ line }) {
52573
53813
  const idx = line.text.indexOf(arrow);
52574
53814
  if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
52575
53815
  const before = line.text.slice(0, idx);
52576
- const stamp = line.kind === "aireq" ? START_STAMP_RE.exec(before)?.[1] ?? "" : "";
52577
- const name = stamp ? before.slice(stamp.length) : before;
53816
+ const stamp2 = line.kind === "aireq" ? START_STAMP_RE.exec(before)?.[1] ?? "" : "";
53817
+ const name = stamp2 ? before.slice(stamp2.length) : before;
52578
53818
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { children: [
52579
- stamp ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: stamp }) : null,
53819
+ stamp2 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: stamp2 }) : null,
52580
53820
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: name }),
52581
53821
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
52582
53822
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
@@ -53060,7 +54300,7 @@ async function runStart(profileDir2, profileName) {
53060
54300
  return;
53061
54301
  }
53062
54302
  logger.configure({
53063
- dir: join38(profileDir2, "logs"),
54303
+ dir: join42(profileDir2, "logs"),
53064
54304
  level: resolveLogLevel(),
53065
54305
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
53066
54306
  });
@@ -53170,7 +54410,7 @@ async function runStart(profileDir2, profileName) {
53170
54410
  }
53171
54411
 
53172
54412
  // src/commands/attach.ts
53173
- import { join as join39 } from "path";
54413
+ import { join as join43 } from "path";
53174
54414
 
53175
54415
  // src/core/control-client.ts
53176
54416
  import { connect as connect2 } from "net";
@@ -53279,7 +54519,7 @@ async function runAttach(profileDir2, profileName) {
53279
54519
  process.exitCode = 1;
53280
54520
  return;
53281
54521
  }
53282
- const socketPath = join39(profileDir2, CONTROL_SOCKET_FILE);
54522
+ const socketPath = join43(profileDir2, CONTROL_SOCKET_FILE);
53283
54523
  const bus = new SessionBus();
53284
54524
  let conn;
53285
54525
  try {
@@ -53310,11 +54550,11 @@ async function runAttach(profileDir2, profileName) {
53310
54550
 
53311
54551
  // src/commands/auto-run.ts
53312
54552
  import { connect as connect3 } from "net";
53313
- import { join as join40 } from "path";
54553
+ import { join as join44 } from "path";
53314
54554
  var AUTO_RUN_TIMEOUT_MS = 3 * 60 * 60 * 1e3;
53315
54555
  async function runAutoRun(profileDir2, profileName) {
53316
54556
  initI18n(readProfileConfig(profileDir2).locale);
53317
- const socketPath = join40(profileDir2, CONTROL_SOCKET_FILE);
54557
+ const socketPath = join44(profileDir2, CONTROL_SOCKET_FILE);
53318
54558
  const token = readControlToken(profileDir2);
53319
54559
  const code = await new Promise((resolve13) => {
53320
54560
  let settled = false;
@@ -53371,7 +54611,7 @@ async function runAutoRun(profileDir2, profileName) {
53371
54611
 
53372
54612
  // src/commands/git-credential.ts
53373
54613
  import { connect as connect4 } from "net";
53374
- import { join as join41 } from "path";
54614
+ import { join as join45 } from "path";
53375
54615
  var REQUEST_TIMEOUT_MS = 2e4;
53376
54616
  function requestToken(host, path) {
53377
54617
  const profileDir2 = process.env[PROFILE_DIR_ENV];
@@ -53386,7 +54626,7 @@ function requestToken(host, path) {
53386
54626
  socket.destroy();
53387
54627
  resolve13(value);
53388
54628
  };
53389
- const socket = connect4(join41(profileDir2, CONTROL_SOCKET_FILE));
54629
+ const socket = connect4(join45(profileDir2, CONTROL_SOCKET_FILE));
53390
54630
  socket.setEncoding("utf8");
53391
54631
  const timer = setTimeout(() => finish(null), REQUEST_TIMEOUT_MS);
53392
54632
  socket.on("connect", () => {