@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 +1826 -586
- package/dist/project-sample/.claude/AUTONOMOUS.md +16 -3
- package/dist/project-sample/.claude/templates/AI_PROGRESS.empty.md +10 -6
- package/dist/project-sample/.claude/templates/AI_PROGRESS.sample.md +5 -4
- package/dist/project-sample/.claude/templates/AI_TODO.empty.md +9 -3
- package/dist/project-sample/.claude/templates/AI_TODO.sample.md +4 -2
- package/dist/project-sample/.claude/templates/USER_QA.empty.md +3 -0
- package/dist/project-sample/AI_PROGRESS.md +10 -6
- package/dist/project-sample/AI_TODO.md +9 -3
- package/dist/project-sample/USER_QA.md +3 -0
- package/package.json +1 -1
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (
|
|
27189
|
-
res.column = Number(
|
|
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
|
|
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
|
|
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
|
|
32773
|
-
import { existsSync as
|
|
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
|
|
37674
|
-
if (
|
|
37675
|
-
|
|
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
|
-
|
|
37681
|
-
|
|
37682
|
-
|
|
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
|
-
|
|
37779
|
+
outcomes.push({ dir: dir2, ok: true, error: null });
|
|
37780
|
+
continue;
|
|
37685
37781
|
}
|
|
37686
|
-
|
|
37687
|
-
|
|
37688
|
-
|
|
37689
|
-
|
|
37690
|
-
|
|
37691
|
-
|
|
37692
|
-
|
|
37693
|
-
|
|
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
|
-
|
|
37700
|
-
|
|
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 =
|
|
37720
|
-
|
|
37721
|
-
|
|
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
|
-
|
|
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
|
|
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",
|
|
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
|
-
|
|
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
|
|
37885
|
+
let repos = [];
|
|
37848
37886
|
try {
|
|
37849
37887
|
const spec = JSON.parse(await readFile(join6(root, "project.spec.json"), "utf8"));
|
|
37850
|
-
|
|
37888
|
+
repos = reposOf(spec);
|
|
37851
37889
|
} catch {
|
|
37852
37890
|
}
|
|
37853
|
-
|
|
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
|
-
|
|
37872
|
-
|
|
37873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37916
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
39053
|
-
const hour = Number.parseInt(
|
|
39054
|
-
const dateKey = `${
|
|
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
|
|
40521
|
+
import { readFile as readFile7, writeFile as writeFile5, chmod } from "fs/promises";
|
|
40429
40522
|
import { readFileSync as readFileSync14 } from "fs";
|
|
40430
|
-
import { execFile as
|
|
40431
|
-
import { join as
|
|
40432
|
-
import { promisify as
|
|
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
|
|
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(
|
|
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
|
|
41194
|
+
return join19(root, TICK_REL);
|
|
40612
41195
|
}
|
|
40613
41196
|
async function crontabList() {
|
|
40614
41197
|
try {
|
|
40615
|
-
const { stdout } = await
|
|
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(
|
|
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
|
|
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
|
-
|
|
40667
|
-
|
|
41256
|
+
read(APPROVALS_REL, "{}"),
|
|
41257
|
+
read(AUTHORS_REL, "{}"),
|
|
41258
|
+
read(ATTEMPTS_REL, "{}"),
|
|
40668
41259
|
getAutonomousStatus(root, profileDir2),
|
|
40669
|
-
...Object.values(BOOK_FILES).map((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(
|
|
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
|
-
|
|
40704
|
-
|
|
40705
|
-
|
|
40706
|
-
|
|
40707
|
-
|
|
40708
|
-
|
|
40709
|
-
|
|
40710
|
-
|
|
40711
|
-
|
|
40712
|
-
|
|
40713
|
-
|
|
40714
|
-
|
|
40715
|
-
|
|
40716
|
-
|
|
40717
|
-
|
|
40718
|
-
|
|
40719
|
-
|
|
40720
|
-
|
|
40721
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 =
|
|
40881
|
-
if (
|
|
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 &&
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
41631
|
+
import { execFile as execFile8 } from "child_process";
|
|
41032
41632
|
import { stat } from "fs/promises";
|
|
41033
|
-
import { join as
|
|
41034
|
-
import { promisify as
|
|
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
|
|
41637
|
+
import { execFile as execFile6 } from "child_process";
|
|
41038
41638
|
import { resolve as resolve4, sep as sep2 } from "path";
|
|
41039
|
-
import { promisify as
|
|
41040
|
-
var
|
|
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
|
|
41649
|
+
async function git2(cwd2, args) {
|
|
41050
41650
|
try {
|
|
41051
|
-
const { stdout } = await
|
|
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
|
|
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
|
|
41068
|
-
const rootBranch = (await
|
|
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
|
|
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
|
|
41078
|
-
const branch = (await
|
|
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
|
|
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
|
|
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
|
|
41135
|
-
const newContent = await
|
|
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
|
|
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
|
|
41367
|
-
const hour =
|
|
41368
|
-
return `${
|
|
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
|
|
41971
|
+
return build2(timeZone);
|
|
41372
41972
|
} catch {
|
|
41373
|
-
return
|
|
41973
|
+
return build2();
|
|
41374
41974
|
}
|
|
41375
41975
|
}
|
|
41376
41976
|
|
|
41377
41977
|
// src/ui/slash-commands.ts
|
|
41378
|
-
import { existsSync as
|
|
41379
|
-
import { join as
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
|
41874
|
-
import { promisify as
|
|
41875
|
-
var
|
|
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
|
|
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
|
|
41987
|
-
if (!
|
|
41988
|
-
|
|
41989
|
-
return { ok: true, 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
|
|
42087
|
-
activeRuns.set(commandId,
|
|
42687
|
+
const run15 = { controller: new AbortController(), state: "queued" };
|
|
42688
|
+
activeRuns.set(commandId, run15);
|
|
42088
42689
|
try {
|
|
42089
|
-
await runAiPromptInner(ctx, prompt, commandId, origin,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
42900
|
+
run15.state = "running";
|
|
42298
42901
|
try {
|
|
42299
|
-
result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs,
|
|
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
|
|
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
|
|
43226
|
+
async function git3(cwd2, args, timeout = 2e4) {
|
|
42617
43227
|
try {
|
|
42618
|
-
const { stdout } = await
|
|
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
|
-
|
|
42674
|
-
|
|
42675
|
-
|
|
42676
|
-
|
|
42677
|
-
|
|
42678
|
-
|
|
42679
|
-
|
|
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
|
-
|
|
42691
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
42803
|
-
import { join as
|
|
42804
|
-
|
|
42805
|
-
|
|
42806
|
-
|
|
42807
|
-
|
|
42808
|
-
|
|
42809
|
-
|
|
42810
|
-
return
|
|
42811
|
-
|
|
42812
|
-
|
|
42813
|
-
|
|
42814
|
-
|
|
42815
|
-
|
|
42816
|
-
|
|
42817
|
-
|
|
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).
|
|
42820
|
-
|
|
42821
|
-
|
|
42822
|
-
|
|
42823
|
-
|
|
42824
|
-
|
|
42825
|
-
|
|
42826
|
-
|
|
42827
|
-
|
|
42828
|
-
|
|
42829
|
-
|
|
42830
|
-
|
|
42831
|
-
|
|
42832
|
-
|
|
42833
|
-
|
|
42834
|
-
|
|
42835
|
-
|
|
42836
|
-
|
|
42837
|
-
|
|
42838
|
-
|
|
42839
|
-
-
|
|
42840
|
-
|
|
42841
|
-
|
|
42842
|
-
|
|
42843
|
-
|
|
42844
|
-
|
|
42845
|
-
|
|
42846
|
-
|
|
42847
|
-
|
|
42848
|
-
|
|
42849
|
-
|
|
42850
|
-
|
|
42851
|
-
|
|
42852
|
-
|
|
42853
|
-
|
|
42854
|
-
|
|
42855
|
-
|
|
42856
|
-
|
|
42857
|
-
|
|
42858
|
-
|
|
42859
|
-
|
|
42860
|
-
|
|
42861
|
-
|
|
42862
|
-
|
|
42863
|
-
|
|
42864
|
-
|
|
42865
|
-
|
|
42866
|
-
|
|
42867
|
-
|
|
42868
|
-
|
|
42869
|
-
|
|
42870
|
-
|
|
42871
|
-
|
|
42872
|
-
|
|
42873
|
-
|
|
42874
|
-
|
|
42875
|
-
|
|
42876
|
-
|
|
42877
|
-
|
|
42878
|
-
|
|
42879
|
-
|
|
42880
|
-
|
|
42881
|
-
|
|
42882
|
-
|
|
42883
|
-
|
|
42884
|
-
|
|
42885
|
-
|
|
42886
|
-
|
|
42887
|
-
|
|
42888
|
-
|
|
42889
|
-
|
|
42890
|
-
|
|
42891
|
-
|
|
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
|
-
|
|
42933
|
-
|
|
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
|
-
|
|
42949
|
-
|
|
42950
|
-
const
|
|
42951
|
-
|
|
42952
|
-
|
|
42953
|
-
|
|
42954
|
-
|
|
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,
|
|
42957
|
-
|
|
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.
|
|
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
|
-
|
|
42964
|
-
|
|
42965
|
-
|
|
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",
|
|
42969
|
-
await
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
|
|
42973
|
-
|
|
42974
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
44166
|
+
const dir2 = join24(root, ".claude", "logs");
|
|
43016
44167
|
const cutoff = Date.now() - retentionDays * 864e5;
|
|
43017
|
-
for (const f2 of await
|
|
44168
|
+
for (const f2 of await readdir3(dir2)) {
|
|
43018
44169
|
if (!/^autonomous-tick-.*\.log$/.test(f2)) continue;
|
|
43019
|
-
const st2 = await stat2(
|
|
43020
|
-
if (st2 && st2.mtimeMs < cutoff) await
|
|
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
|
|
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(
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
43347
|
-
import { execFile as
|
|
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
|
|
43350
|
-
var
|
|
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
|
|
44524
|
+
newBuf = await readFile12(target);
|
|
43374
44525
|
} catch {
|
|
43375
44526
|
}
|
|
43376
44527
|
let oldBuf = null;
|
|
43377
44528
|
try {
|
|
43378
|
-
const { stdout } = await
|
|
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
|
|
43409
|
-
import { promisify as
|
|
43410
|
-
var
|
|
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
|
|
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
|
|
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
|
|
43440
|
-
import { readFile as
|
|
43441
|
-
import { join as
|
|
43442
|
-
import { promisify as
|
|
43443
|
-
var
|
|
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 =
|
|
43446
|
-
const key =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
43475
|
-
await
|
|
43476
|
-
await
|
|
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
|
|
43485
|
-
await
|
|
43486
|
-
await
|
|
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
|
|
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
|
|
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
|
|
44709
|
+
import { join as join26 } from "path";
|
|
43559
44710
|
|
|
43560
44711
|
// src/core/ai-assist.ts
|
|
43561
|
-
import { execFile as
|
|
43562
|
-
import { promisify as
|
|
43563
|
-
var
|
|
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
|
|
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(
|
|
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
|
|
43740
|
-
import { join as
|
|
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 ?
|
|
43813
|
-
const newPath =
|
|
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 &&
|
|
45053
|
+
if (oldPath && oldName !== sync.newName && existsSync15(oldPath) && !existsSync15(newPath)) {
|
|
43819
45054
|
renameSync(oldPath, newPath);
|
|
43820
45055
|
} else {
|
|
43821
|
-
if (oldPath && oldName !== sync.newName &&
|
|
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(
|
|
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
|
|
43989
|
-
import { existsSync as
|
|
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
|
|
43992
|
-
import { tmpdir as
|
|
43993
|
-
import { promisify as
|
|
43994
|
-
var execFileAsync2 =
|
|
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 =
|
|
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 (
|
|
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
|
|
44341
|
-
import { join as
|
|
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
|
|
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
|
|
44359
|
-
return { name:
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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" ?
|
|
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(
|
|
44394
|
-
readText2(
|
|
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
|
|
44404
|
-
else await
|
|
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(
|
|
44410
|
-
await
|
|
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
|
|
44419
|
-
import { join as
|
|
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
|
|
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
|
|
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(
|
|
44452
|
-
readText3(
|
|
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
|
|
45704
|
+
await writeFile11(join31(root, SECURITY_REL), req.content, "utf8");
|
|
44466
45705
|
} else if (req.kind === "placeholder") {
|
|
44467
|
-
await
|
|
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
|
|
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
|
|
44482
|
-
import { dirname as dirname9, join as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
44524
|
-
import { dirname as dirname10, join as
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
45805
|
+
const base = join33(root, SKILLS_REL2, slug);
|
|
44567
45806
|
for (const rel of await listFilesRel(base)) {
|
|
44568
|
-
const buf = await readMaybe(
|
|
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" ?
|
|
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(
|
|
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 =
|
|
45843
|
+
const path = join33(root, MANIFEST_REL);
|
|
44605
45844
|
await mkdir10(dirname10(path), { recursive: true });
|
|
44606
|
-
await
|
|
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(
|
|
44614
|
-
await
|
|
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(
|
|
44619
|
-
await
|
|
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(
|
|
44624
|
-
await
|
|
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 =
|
|
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 =
|
|
45869
|
+
const dest = join33(base, rel);
|
|
44631
45870
|
await mkdir10(dirname10(dest), { recursive: true });
|
|
44632
|
-
await
|
|
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
|
|
44697
|
-
await
|
|
44698
|
-
await
|
|
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
|
|
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
|
|
44712
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
44852
|
-
import { existsSync as
|
|
44853
|
-
import { execFile as
|
|
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
|
|
44856
|
-
import { promisify as
|
|
44857
|
-
var
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
46195
|
+
const indexing = existsSync17(join35(root, RAG_REL, ".indexing"));
|
|
44957
46196
|
try {
|
|
44958
|
-
const m2 = JSON.parse(await
|
|
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(
|
|
45026
|
-
await
|
|
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 (!
|
|
45032
|
-
if (
|
|
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 (!
|
|
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
|
|
46294
|
+
const { stdout } = await run14(
|
|
45056
46295
|
py.bin,
|
|
45057
|
-
[
|
|
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 (
|
|
46311
|
+
if (existsSync17(join35(root, RAG_REL, ".installing"))) return { ok: true, started: true };
|
|
45073
46312
|
try {
|
|
45074
|
-
await mkdir11(
|
|
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
|
|
45102
|
-
import { join as
|
|
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 =
|
|
45120
|
-
if (
|
|
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 (!
|
|
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 ?
|
|
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 ?
|
|
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(
|
|
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
|
|
47542
|
+
import { join as join38 } from "path";
|
|
46303
47543
|
function lockPath(profileDir2) {
|
|
46304
|
-
return
|
|
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
|
|
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
|
|
47897
|
+
import { join as join39 } from "path";
|
|
46658
47898
|
function controlTokenPath(profileDir2) {
|
|
46659
|
-
return
|
|
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 =
|
|
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
|
|
48314
|
+
const build2 = Number(parts[2] ?? 0);
|
|
47075
48315
|
if (major < 10) {
|
|
47076
48316
|
return true;
|
|
47077
48317
|
}
|
|
47078
|
-
if (major === 10 &&
|
|
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
|
|
52446
|
-
import { join as
|
|
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
|
|
53689
|
+
return join41(profileDir2, "input-history.json");
|
|
52450
53690
|
}
|
|
52451
53691
|
function loadInputHistory(profileDir2) {
|
|
52452
53692
|
const path = historyPath(profileDir2);
|
|
52453
|
-
if (!
|
|
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
|
|
52577
|
-
const name =
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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", () => {
|