@4pm/cli 1.24.0 → 1.27.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 +561 -247
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28557,11 +28557,12 @@ function fetchWhoami(serverUrl, hashcode3) {
|
|
|
28557
28557
|
function selfUnlink(serverUrl, hashcode3) {
|
|
28558
28558
|
return post(serverUrl, "/machine-links/self-unlink", { hashcode3 });
|
|
28559
28559
|
}
|
|
28560
|
-
async function fetchCliVersion(serverUrl) {
|
|
28560
|
+
async function fetchCliVersion(serverUrl, version) {
|
|
28561
28561
|
const ctrl = new AbortController();
|
|
28562
28562
|
const timer = setTimeout(() => ctrl.abort(), 2e4);
|
|
28563
28563
|
try {
|
|
28564
|
-
const
|
|
28564
|
+
const query = version ? `?version=${encodeURIComponent(version)}` : "";
|
|
28565
|
+
const res = await fetch(`${serverUrl}/meta/cli-version${query}`, { signal: ctrl.signal });
|
|
28565
28566
|
return unwrap(res);
|
|
28566
28567
|
} finally {
|
|
28567
28568
|
clearTimeout(timer);
|
|
@@ -28798,6 +28799,8 @@ var en_default = {
|
|
|
28798
28799
|
"update.updating": "Updating {from} \u2192 {to}\u2026",
|
|
28799
28800
|
"update.checkVersionFailed": "Could not check cli version: {error}",
|
|
28800
28801
|
"update.newVersionAutoUpdateOff": "A new version {latest} is available (running {current}) \u2014 autoUpdate is off.",
|
|
28802
|
+
"update.unsupportedSoon": "cli {current} stops being supported on {date} \u2014 update before then (4pm update).",
|
|
28803
|
+
"update.policyNote": "Note from 4PM: {note}",
|
|
28801
28804
|
"update.applying": "{kind} cli update {from} \u2192 {to}\u2026",
|
|
28802
28805
|
"update.kindMandatory": "Mandatory",
|
|
28803
28806
|
"update.kindAutomatic": "Automatic",
|
|
@@ -28981,6 +28984,8 @@ var vi_default = {
|
|
|
28981
28984
|
"update.updating": "\u0110ang c\u1EADp nh\u1EADt {from} \u2192 {to}\u2026",
|
|
28982
28985
|
"update.checkVersionFailed": "Kh\xF4ng th\u1EC3 ki\u1EC3m tra phi\xEAn b\u1EA3n cli: {error}",
|
|
28983
28986
|
"update.newVersionAutoUpdateOff": "C\xF3 phi\xEAn b\u1EA3n m\u1EDBi {latest} (\u0111ang ch\u1EA1y {current}) \u2014 autoUpdate \u0111ang t\u1EAFt.",
|
|
28987
|
+
"update.unsupportedSoon": "cli {current} s\u1EBD ng\u1EEBng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3 t\u1EEB {date} \u2014 h\xE3y c\u1EADp nh\u1EADt tr\u01B0\u1EDBc ng\xE0y \u0111\xF3 (4pm update).",
|
|
28988
|
+
"update.policyNote": "Ghi ch\xFA t\u1EEB 4PM: {note}",
|
|
28984
28989
|
"update.applying": "C\u1EADp nh\u1EADt cli {kind} {from} \u2192 {to}\u2026",
|
|
28985
28990
|
"update.kindMandatory": "b\u1EAFt bu\u1ED9c",
|
|
28986
28991
|
"update.kindAutomatic": "t\u1EF1 \u0111\u1ED9ng",
|
|
@@ -29164,6 +29169,8 @@ var ja_default = {
|
|
|
29164
29169
|
"update.updating": "\u66F4\u65B0\u4E2D {from} \u2192 {to}\u2026",
|
|
29165
29170
|
"update.checkVersionFailed": "cli \u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F: {error}",
|
|
29166
29171
|
"update.newVersionAutoUpdateOff": "\u65B0\u3057\u3044\u30D0\u30FC\u30B8\u30E7\u30F3 {latest} \u304C\u5229\u7528\u53EF\u80FD\u3067\u3059\uFF08\u5B9F\u884C\u4E2D {current}\uFF09\u2014 autoUpdate \u306F\u30AA\u30D5\u3067\u3059\u3002",
|
|
29172
|
+
"update.unsupportedSoon": "cli {current} \u306F {date} \u306B\u30B5\u30DD\u30FC\u30C8\u304C\u7D42\u4E86\u3057\u307E\u3059 \u2014 \u305D\u308C\u307E\u3067\u306B\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\uFF084pm update\uFF09\u3002",
|
|
29173
|
+
"update.policyNote": "4PM \u304B\u3089\u306E\u304A\u77E5\u3089\u305B: {note}",
|
|
29167
29174
|
"update.applying": "{kind} \u306E cli \u66F4\u65B0 {from} \u2192 {to}\u2026",
|
|
29168
29175
|
"update.kindMandatory": "\u5FC5\u9808",
|
|
29169
29176
|
"update.kindAutomatic": "\u81EA\u52D5",
|
|
@@ -29347,6 +29354,8 @@ var zh_default = {
|
|
|
29347
29354
|
"update.updating": "\u6B63\u5728\u66F4\u65B0 {from} \u2192 {to}\u2026",
|
|
29348
29355
|
"update.checkVersionFailed": "\u65E0\u6CD5\u68C0\u67E5 cli \u7248\u672C\uFF1A{error}",
|
|
29349
29356
|
"update.newVersionAutoUpdateOff": "\u6709\u65B0\u7248\u672C {latest} \u53EF\u7528\uFF08\u5F53\u524D\u8FD0\u884C {current}\uFF09\u2014 autoUpdate \u5DF2\u5173\u95ED\u3002",
|
|
29357
|
+
"update.unsupportedSoon": "cli {current} \u5C06\u4E8E {date} \u505C\u6B62\u652F\u6301 \u2014 \u8BF7\u5728\u6B64\u4E4B\u524D\u66F4\u65B0\uFF084pm update\uFF09\u3002",
|
|
29358
|
+
"update.policyNote": "\u6765\u81EA 4PM \u7684\u8BF4\u660E\uFF1A{note}",
|
|
29350
29359
|
"update.applying": "{kind} cli \u66F4\u65B0 {from} \u2192 {to}\u2026",
|
|
29351
29360
|
"update.kindMandatory": "\u5F3A\u5236",
|
|
29352
29361
|
"update.kindAutomatic": "\u81EA\u52A8",
|
|
@@ -32701,12 +32710,18 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
32701
32710
|
if (CLI_VERSION.startsWith("0.0.0")) return { action: "none" };
|
|
32702
32711
|
let meta;
|
|
32703
32712
|
try {
|
|
32704
|
-
meta = await fetchCliVersion(serverUrl);
|
|
32713
|
+
meta = await fetchCliVersion(serverUrl, CLI_VERSION);
|
|
32705
32714
|
} catch (err) {
|
|
32706
32715
|
console.warn(t("update.checkVersionFailed", { error: String(err) }));
|
|
32707
32716
|
return { action: "none" };
|
|
32708
32717
|
}
|
|
32709
|
-
const mandatory = compareSemver(CLI_VERSION, meta.minSupported) < 0;
|
|
32718
|
+
const mandatory = compareSemver(CLI_VERSION, meta.minSupported) < 0 || meta.status === "unsupported";
|
|
32719
|
+
if (meta.status === "warning" && meta.unsupportedFrom) {
|
|
32720
|
+
console.warn(t("update.unsupportedSoon", { current: CLI_VERSION, date: meta.unsupportedFrom.slice(0, 10) }));
|
|
32721
|
+
if (meta.note) console.warn(t("update.policyNote", { note: meta.note }));
|
|
32722
|
+
} else if (meta.status === "unsupported" && meta.note) {
|
|
32723
|
+
console.warn(t("update.policyNote", { note: meta.note }));
|
|
32724
|
+
}
|
|
32710
32725
|
const outdated = shouldUpdateTo(CLI_VERSION, meta.latest);
|
|
32711
32726
|
if (!outdated) return { action: "none" };
|
|
32712
32727
|
if (!mandatory && !autoUpdate) {
|
|
@@ -32753,7 +32768,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
32753
32768
|
|
|
32754
32769
|
// src/core/ws-client.ts
|
|
32755
32770
|
init_wrapper();
|
|
32756
|
-
import { randomUUID as
|
|
32771
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
32757
32772
|
import { basename as basename7, join as join32 } from "path";
|
|
32758
32773
|
import { existsSync as existsSync18, mkdirSync as mkdirSync9 } from "fs";
|
|
32759
32774
|
|
|
@@ -37255,6 +37270,67 @@ var agentToolsWriteRequestSchema = external_exports.object({
|
|
|
37255
37270
|
permissions: agentToolsPermissionsSchema
|
|
37256
37271
|
});
|
|
37257
37272
|
|
|
37273
|
+
// vendor/@4pm/dto/meta.ts
|
|
37274
|
+
var cliVersionString = external_exports.string().trim().regex(/^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$/);
|
|
37275
|
+
var setCliMinVersionRequestSchema = external_exports.object({
|
|
37276
|
+
minSupported: cliVersionString.nullable(),
|
|
37277
|
+
disconnect: external_exports.boolean().optional()
|
|
37278
|
+
});
|
|
37279
|
+
var setCliVersionOverrideRequestSchema = external_exports.object({
|
|
37280
|
+
status: external_exports.enum(["supported", "warning", "unsupported"]),
|
|
37281
|
+
unsupportedFrom: external_exports.string().datetime({ offset: true }).nullable().optional(),
|
|
37282
|
+
note: external_exports.string().max(500).optional(),
|
|
37283
|
+
disconnect: external_exports.boolean().optional()
|
|
37284
|
+
});
|
|
37285
|
+
|
|
37286
|
+
// vendor/@4pm/dto/storage.ts
|
|
37287
|
+
var STORAGE_KINDS = [
|
|
37288
|
+
"messageText",
|
|
37289
|
+
"postText",
|
|
37290
|
+
"commentText",
|
|
37291
|
+
"memoText",
|
|
37292
|
+
"messageImages",
|
|
37293
|
+
"messageFiles",
|
|
37294
|
+
"postImages",
|
|
37295
|
+
"postFiles",
|
|
37296
|
+
"commentImages",
|
|
37297
|
+
"commentFiles",
|
|
37298
|
+
"memoImages",
|
|
37299
|
+
"memoFiles",
|
|
37300
|
+
"templates",
|
|
37301
|
+
"aiDocImages",
|
|
37302
|
+
"artifacts",
|
|
37303
|
+
"commandHistory"
|
|
37304
|
+
];
|
|
37305
|
+
var COMMUNITY_STORAGE_KINDS = [
|
|
37306
|
+
"messageText",
|
|
37307
|
+
"postText",
|
|
37308
|
+
"commentText",
|
|
37309
|
+
"messageImages",
|
|
37310
|
+
"messageFiles",
|
|
37311
|
+
"postImages",
|
|
37312
|
+
"postFiles",
|
|
37313
|
+
"commentImages",
|
|
37314
|
+
"commentFiles"
|
|
37315
|
+
];
|
|
37316
|
+
var SERVER_CLEARABLE_STORAGE_KINDS = [
|
|
37317
|
+
"memoText",
|
|
37318
|
+
"memoImages",
|
|
37319
|
+
"memoFiles",
|
|
37320
|
+
"templates",
|
|
37321
|
+
"commandHistory"
|
|
37322
|
+
];
|
|
37323
|
+
var CLEARABLE_STORAGE_KINDS = [
|
|
37324
|
+
...COMMUNITY_STORAGE_KINDS,
|
|
37325
|
+
...SERVER_CLEARABLE_STORAGE_KINDS
|
|
37326
|
+
];
|
|
37327
|
+
var storageClearRequestSchema = external_exports.object({
|
|
37328
|
+
kinds: external_exports.array(external_exports.enum(STORAGE_KINDS)).min(1).max(STORAGE_KINDS.length),
|
|
37329
|
+
before: external_exports.string().datetime({ offset: true }),
|
|
37330
|
+
/** org-0009 / community-0015 only — must equal the org name. */
|
|
37331
|
+
confirm: external_exports.string().max(200).optional()
|
|
37332
|
+
});
|
|
37333
|
+
|
|
37258
37334
|
// vendor/@4pm/dto/org.ts
|
|
37259
37335
|
var CLI_SETTINGS_BOUNDS = {
|
|
37260
37336
|
reconnectMaxBackoffSec: { min: 5, max: 60 },
|
|
@@ -37274,6 +37350,8 @@ var TEMPLATE_KINDS = ["report", "est", "wbs", "proposal", "feasibility"];
|
|
|
37274
37350
|
// vendor/@4pm/dto/project.ts
|
|
37275
37351
|
var listProjectsQuerySchema = baseRequestSchema.extend({
|
|
37276
37352
|
pinned: pinnedFilterSchema,
|
|
37353
|
+
/** Only projects this team is attached to (project-0001; the public v1 list — ADR-0364). */
|
|
37354
|
+
teamId: external_exports.string().uuid().optional(),
|
|
37277
37355
|
/** ADMIN-only Trash: list soft-deleted projects (ADR-0109). */
|
|
37278
37356
|
deleted: deletedFilterSchema
|
|
37279
37357
|
});
|
|
@@ -37564,116 +37642,12 @@ var userCardsQuerySchema = external_exports.object({
|
|
|
37564
37642
|
// src/core/scaffold.ts
|
|
37565
37643
|
import { cp, mkdir, mkdtemp, rm, writeFile } from "fs/promises";
|
|
37566
37644
|
import { existsSync as existsSync4 } from "fs";
|
|
37567
|
-
import { execFile
|
|
37645
|
+
import { execFile } from "child_process";
|
|
37568
37646
|
import { tmpdir as tmpdir2 } from "os";
|
|
37569
|
-
import { dirname as dirname3, join as join6, resolve
|
|
37647
|
+
import { dirname as dirname3, join as join6, resolve } from "path";
|
|
37570
37648
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
37571
|
-
import { promisify as promisify2 } from "util";
|
|
37572
|
-
|
|
37573
|
-
// src/core/ai-assist.ts
|
|
37574
|
-
import { execFile } from "child_process";
|
|
37575
37649
|
import { promisify } from "util";
|
|
37576
|
-
|
|
37577
|
-
// src/utils/agent-deny.ts
|
|
37578
|
-
import { isAbsolute, resolve } from "path";
|
|
37579
|
-
var HOME_SECRET_PATTERNS = [
|
|
37580
|
-
"~/.4pm/**",
|
|
37581
|
-
"~/.ssh/**",
|
|
37582
|
-
"~/.config/gh/**",
|
|
37583
|
-
"~/.git-credentials",
|
|
37584
|
-
"~/.claude*",
|
|
37585
|
-
"~/.claude*/**"
|
|
37586
|
-
];
|
|
37587
|
-
function absDirPattern(dir2) {
|
|
37588
|
-
const abs = (isAbsolute(dir2) ? dir2 : resolve(dir2)).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
37589
|
-
return `/${abs.startsWith("/") ? abs : `/${abs}`}/**`;
|
|
37590
|
-
}
|
|
37591
|
-
function agentDenyRules(profileDirs2 = []) {
|
|
37592
|
-
const patterns = new Set(HOME_SECRET_PATTERNS);
|
|
37593
|
-
for (const dir2 of profileDirs2) {
|
|
37594
|
-
if (dir2 && dir2.trim()) patterns.add(absDirPattern(dir2.trim()));
|
|
37595
|
-
}
|
|
37596
|
-
const rules = [];
|
|
37597
|
-
for (const p2 of patterns) rules.push(`Read(${p2})`, `Edit(${p2})`);
|
|
37598
|
-
return rules;
|
|
37599
|
-
}
|
|
37600
|
-
function denySettingsArgs(cmd, profileDirs2 = []) {
|
|
37601
|
-
if (!cmd.includes("claude")) return [];
|
|
37602
|
-
return ["--settings", JSON.stringify({ permissions: { deny: agentDenyRules(profileDirs2) } })];
|
|
37603
|
-
}
|
|
37604
|
-
|
|
37605
|
-
// src/core/ai-assist.ts
|
|
37606
37650
|
var run = promisify(execFile);
|
|
37607
|
-
function aiCli() {
|
|
37608
|
-
return process.env.AI_CLI || "claude";
|
|
37609
|
-
}
|
|
37610
|
-
function aiArgs(cli, prompt) {
|
|
37611
|
-
if (cli === "claude") return ["-p", prompt, "--output-format", "json", ...denySettingsArgs(cli, [process.env.CLAUDE_CONFIG_DIR])];
|
|
37612
|
-
if (cli === "codex") return ["exec", "--json", prompt];
|
|
37613
|
-
return ["-p", prompt];
|
|
37614
|
-
}
|
|
37615
|
-
function estimateTokens(prompt, output) {
|
|
37616
|
-
return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
|
|
37617
|
-
}
|
|
37618
|
-
function parseAi(cli, stdout, prompt) {
|
|
37619
|
-
if (cli === "claude") {
|
|
37620
|
-
try {
|
|
37621
|
-
const j2 = JSON.parse(stdout);
|
|
37622
|
-
const text2 = (j2.result ?? "").trim();
|
|
37623
|
-
const tokens = (j2.usage?.input_tokens ?? 0) + (j2.usage?.output_tokens ?? 0);
|
|
37624
|
-
if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens(prompt, text2) };
|
|
37625
|
-
} catch {
|
|
37626
|
-
}
|
|
37627
|
-
}
|
|
37628
|
-
if (cli === "codex") {
|
|
37629
|
-
try {
|
|
37630
|
-
let text2 = "";
|
|
37631
|
-
let tokens = 0;
|
|
37632
|
-
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
37633
|
-
const obj = JSON.parse(line);
|
|
37634
|
-
if (obj.text) text2 = obj.text;
|
|
37635
|
-
else if (obj.message) text2 = obj.message;
|
|
37636
|
-
if (obj.usage) {
|
|
37637
|
-
tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
|
|
37638
|
-
}
|
|
37639
|
-
}
|
|
37640
|
-
if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens(prompt, text2) };
|
|
37641
|
-
} catch {
|
|
37642
|
-
}
|
|
37643
|
-
}
|
|
37644
|
-
const text = stdout.trim();
|
|
37645
|
-
return { text, tokens: estimateTokens(prompt, text) };
|
|
37646
|
-
}
|
|
37647
|
-
async function runAi(prompt) {
|
|
37648
|
-
const cli = aiCli();
|
|
37649
|
-
const { stdout } = await run(cli, aiArgs(cli, prompt), {
|
|
37650
|
-
timeout: 12e4,
|
|
37651
|
-
maxBuffer: 8 * 1024 * 1024
|
|
37652
|
-
});
|
|
37653
|
-
return parseAi(cli, stdout, prompt);
|
|
37654
|
-
}
|
|
37655
|
-
async function aiGenerate(prompt) {
|
|
37656
|
-
const { text } = await runAi(prompt);
|
|
37657
|
-
return text;
|
|
37658
|
-
}
|
|
37659
|
-
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
37660
|
-
const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
|
|
37661
|
-
${input}`;
|
|
37662
|
-
const { text, tokens } = await runAi(prompt);
|
|
37663
|
-
const start = text.indexOf("{");
|
|
37664
|
-
const end = text.lastIndexOf("}");
|
|
37665
|
-
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
37666
|
-
try {
|
|
37667
|
-
const j2 = JSON.parse(text.slice(start, end + 1));
|
|
37668
|
-
const reasons = Array.isArray(j2.reasons) ? j2.reasons.filter((r) => typeof r === "string") : [];
|
|
37669
|
-
return { ok: j2.ok === true && reasons.length === 0, reasons, tokens };
|
|
37670
|
-
} catch {
|
|
37671
|
-
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
37672
|
-
}
|
|
37673
|
-
}
|
|
37674
|
-
|
|
37675
|
-
// src/core/scaffold.ts
|
|
37676
|
-
var run2 = promisify2(execFile2);
|
|
37677
37651
|
function reposOf(spec) {
|
|
37678
37652
|
const repos = spec?.repos;
|
|
37679
37653
|
return Array.isArray(repos) ? repos : [];
|
|
@@ -37695,7 +37669,7 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
|
|
|
37695
37669
|
if (existsSync4(join6(root, dir2, ".git")) || existsSync4(join6(root, ".git", "modules", dir2))) {
|
|
37696
37670
|
emit("submodule", `Submodule ${dir2} already present \u2014 updating\u2026`);
|
|
37697
37671
|
try {
|
|
37698
|
-
await
|
|
37672
|
+
await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37699
37673
|
} catch {
|
|
37700
37674
|
}
|
|
37701
37675
|
continue;
|
|
@@ -37709,11 +37683,11 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
|
|
|
37709
37683
|
const b2 = (sub.branch ?? "").trim();
|
|
37710
37684
|
if (b2 && createMissingBranch && !await ensureRemoteBranch(sub.url, b2, emit)) {
|
|
37711
37685
|
try {
|
|
37712
|
-
await
|
|
37713
|
-
await
|
|
37686
|
+
await run("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 12e4 });
|
|
37687
|
+
await run("git", ["config", "-f", ".gitmodules", `submodule.${dir2}.branch`, b2], { cwd: root, timeout: 3e4 });
|
|
37714
37688
|
try {
|
|
37715
|
-
await
|
|
37716
|
-
await
|
|
37689
|
+
await run("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
|
|
37690
|
+
await run("git", ["push", "-u", "origin", b2], { cwd: join6(root, dir2), timeout: 12e4 });
|
|
37717
37691
|
} catch (pushErr) {
|
|
37718
37692
|
const pmsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
|
|
37719
37693
|
emit("git-branch-push-failed", `Submodule ${dir2}: branch "${b2}" created locally but the push failed: ${pmsg}`);
|
|
@@ -37728,13 +37702,13 @@ async function attachSubmodules(root, submodules, emit, createMissingBranch = fa
|
|
|
37728
37702
|
}
|
|
37729
37703
|
}
|
|
37730
37704
|
try {
|
|
37731
|
-
await
|
|
37705
|
+
await run("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
|
|
37732
37706
|
added++;
|
|
37733
37707
|
} catch (err) {
|
|
37734
37708
|
const msg = err instanceof Error ? err.message : String(err);
|
|
37735
37709
|
if (/already exists|already registered|in the index/i.test(msg)) {
|
|
37736
37710
|
try {
|
|
37737
|
-
await
|
|
37711
|
+
await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37738
37712
|
added++;
|
|
37739
37713
|
} catch {
|
|
37740
37714
|
}
|
|
@@ -37753,18 +37727,18 @@ async function commitAndPushSubmodules(root, submodules, emit) {
|
|
|
37753
37727
|
];
|
|
37754
37728
|
if (addPaths.length === 0) return;
|
|
37755
37729
|
emit("submodule", "Committing .gitmodules\u2026");
|
|
37756
|
-
await
|
|
37757
|
-
const staged = (await
|
|
37730
|
+
await run("git", ["add", ...addPaths], { cwd: root, timeout: 6e4 });
|
|
37731
|
+
const staged = (await run("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
|
|
37758
37732
|
if (!staged) return;
|
|
37759
37733
|
const commitArgs = ["commit", "-m", "chore: add git submodules (4PM)"];
|
|
37760
37734
|
try {
|
|
37761
|
-
await
|
|
37735
|
+
await run("git", commitArgs, { cwd: root, timeout: 6e4 });
|
|
37762
37736
|
} catch {
|
|
37763
|
-
await
|
|
37737
|
+
await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
|
|
37764
37738
|
}
|
|
37765
37739
|
emit("submodule", "Pushing .gitmodules to the primary remote\u2026");
|
|
37766
37740
|
try {
|
|
37767
|
-
await
|
|
37741
|
+
await run("git", ["push"], { cwd: root, timeout: 12e4 });
|
|
37768
37742
|
} catch (err) {
|
|
37769
37743
|
const msg = err instanceof Error ? err.message : String(err);
|
|
37770
37744
|
emit("submodule-push-failed", `Submodules committed locally but the push failed: ${msg}`);
|
|
@@ -37777,22 +37751,22 @@ function providerSlug(url) {
|
|
|
37777
37751
|
}
|
|
37778
37752
|
async function commitAndOpenPr(root, repo, emit) {
|
|
37779
37753
|
emit("commit", "Committing the scaffolded project\u2026");
|
|
37780
|
-
await
|
|
37781
|
-
const staged = (await
|
|
37754
|
+
await run("git", ["add", "-A"], { cwd: root, timeout: 6e4 });
|
|
37755
|
+
const staged = (await run("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
|
|
37782
37756
|
if (!staged) {
|
|
37783
37757
|
emit("commit", "Nothing to commit \u2014 skipping the pull request.");
|
|
37784
37758
|
return;
|
|
37785
37759
|
}
|
|
37786
37760
|
const commitArgs = ["commit", "-m", "chore: scaffold project (4PM)"];
|
|
37787
37761
|
try {
|
|
37788
|
-
await
|
|
37762
|
+
await run("git", commitArgs, { cwd: root, timeout: 6e4 });
|
|
37789
37763
|
} catch {
|
|
37790
|
-
await
|
|
37764
|
+
await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
|
|
37791
37765
|
}
|
|
37792
37766
|
const branch = (repo.branch ?? "").trim();
|
|
37793
37767
|
emit("push", "Pushing the scaffold branch\u2026");
|
|
37794
37768
|
try {
|
|
37795
|
-
await
|
|
37769
|
+
await run("git", ["push", "-u", "origin", ...branch ? [branch] : ["HEAD"]], { cwd: root, timeout: 12e4 });
|
|
37796
37770
|
} catch (err) {
|
|
37797
37771
|
const msg = err instanceof Error ? err.message : String(err);
|
|
37798
37772
|
emit("push-failed", `Scaffold committed locally but the push failed: ${msg} \u2014 push it from the Git tab when credentials are available.`);
|
|
@@ -37806,7 +37780,7 @@ async function commitAndOpenPr(root, repo, emit) {
|
|
|
37806
37780
|
emit("pr", `Opening a pull request via ${provider}\u2026`);
|
|
37807
37781
|
try {
|
|
37808
37782
|
const args = provider === "glab" ? ["mr", "create", "--fill", ...slug ? ["-R", slug] : []] : ["pr", "create", "--fill", ...slug ? ["-R", slug] : []];
|
|
37809
|
-
const { stdout } = await
|
|
37783
|
+
const { stdout } = await run(provider, args, { cwd: root, timeout: 12e4 });
|
|
37810
37784
|
const link2 = stdout.trim().split(/\s+/).find((s3) => /^https?:\/\//.test(s3)) ?? stdout.trim().split("\n").pop() ?? "";
|
|
37811
37785
|
emit("pr", `Pull request opened${link2 ? `: ${link2}` : "."}`);
|
|
37812
37786
|
} catch (err) {
|
|
@@ -37820,7 +37794,7 @@ function cloneArgs(url, dest, branch) {
|
|
|
37820
37794
|
}
|
|
37821
37795
|
async function remoteHasBranch(url, branch) {
|
|
37822
37796
|
try {
|
|
37823
|
-
const { stdout } = await
|
|
37797
|
+
const { stdout } = await run("git", ["ls-remote", "--heads", url, branch], { timeout: 3e4 });
|
|
37824
37798
|
return stdout.trim().length > 0;
|
|
37825
37799
|
} catch {
|
|
37826
37800
|
return false;
|
|
@@ -37831,9 +37805,9 @@ async function ensureRemoteBranch(url, branch, emit) {
|
|
|
37831
37805
|
const tmp = await mkdtemp(join6(tmpdir2(), "4pm-branch-"));
|
|
37832
37806
|
try {
|
|
37833
37807
|
emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it from the default branch\u2026`);
|
|
37834
|
-
await
|
|
37835
|
-
await
|
|
37836
|
-
await
|
|
37808
|
+
await run("git", ["clone", "--depth", "1", url, tmp], { timeout: 12e4 });
|
|
37809
|
+
await run("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
|
|
37810
|
+
await run("git", ["push", "-u", "origin", branch], { cwd: tmp, timeout: 12e4 });
|
|
37837
37811
|
return true;
|
|
37838
37812
|
} catch (err) {
|
|
37839
37813
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -37846,9 +37820,9 @@ async function ensureRemoteBranch(url, branch, emit) {
|
|
|
37846
37820
|
async function updateRepo(dir2, branch, emit) {
|
|
37847
37821
|
const b2 = (branch ?? "").trim();
|
|
37848
37822
|
emit("git", `Updating repository (fetch + ${b2 ? `checkout ${b2} + ` : ""}fast-forward pull)\u2026`);
|
|
37849
|
-
await
|
|
37850
|
-
if (b2) await
|
|
37851
|
-
await
|
|
37823
|
+
await run("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
|
|
37824
|
+
if (b2) await run("git", ["checkout", b2], { cwd: dir2, timeout: 6e4 });
|
|
37825
|
+
await run("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
|
|
37852
37826
|
}
|
|
37853
37827
|
async function provisionRepo(target, repo, emit, opts = {}) {
|
|
37854
37828
|
const mode = opts.mode ?? "clone";
|
|
@@ -37856,7 +37830,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
|
|
|
37856
37830
|
if (!existsSync4(join6(target, ".git"))) {
|
|
37857
37831
|
emit("git", "Initializing repository\u2026");
|
|
37858
37832
|
await mkdir(target, { recursive: true });
|
|
37859
|
-
await
|
|
37833
|
+
await run("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
37860
37834
|
}
|
|
37861
37835
|
return;
|
|
37862
37836
|
}
|
|
@@ -37866,7 +37840,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
|
|
|
37866
37840
|
} else if (mode === "force") {
|
|
37867
37841
|
emit("git", `Re-cloning ${repo.url} (force)\u2026`);
|
|
37868
37842
|
await rm(target, { recursive: true, force: true });
|
|
37869
|
-
await
|
|
37843
|
+
await run("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
37870
37844
|
} else {
|
|
37871
37845
|
emit("git", "Repository already present \u2014 skipping clone.");
|
|
37872
37846
|
}
|
|
@@ -37875,10 +37849,10 @@ async function provisionRepo(target, repo, emit, opts = {}) {
|
|
|
37875
37849
|
const b2 = (repo.branch ?? "").trim();
|
|
37876
37850
|
if (b2 && opts.createMissingBranch && !await remoteHasBranch(repo.url, b2)) {
|
|
37877
37851
|
emit("git", `Cloning ${repo.url} (default branch) to create "${b2}"\u2026`);
|
|
37878
|
-
await
|
|
37879
|
-
await
|
|
37852
|
+
await run("git", ["clone", repo.url, target], { timeout: 12e4 });
|
|
37853
|
+
await run("git", ["checkout", "-b", b2], { cwd: target, timeout: 3e4 });
|
|
37880
37854
|
try {
|
|
37881
|
-
await
|
|
37855
|
+
await run("git", ["push", "-u", "origin", b2], { cwd: target, timeout: 12e4 });
|
|
37882
37856
|
emit("git", `Created and pushed branch "${b2}".`);
|
|
37883
37857
|
} catch (err) {
|
|
37884
37858
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -37887,7 +37861,7 @@ async function provisionRepo(target, repo, emit, opts = {}) {
|
|
|
37887
37861
|
return;
|
|
37888
37862
|
}
|
|
37889
37863
|
emit("git", `Cloning ${repo.url}\u2026`);
|
|
37890
|
-
await
|
|
37864
|
+
await run("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
37891
37865
|
}
|
|
37892
37866
|
async function ensureReposCloned(physicRoot, repos, emit) {
|
|
37893
37867
|
const list = repos;
|
|
@@ -37900,27 +37874,27 @@ async function ensureReposCloned(physicRoot, repos, emit) {
|
|
|
37900
37874
|
function sampleDir() {
|
|
37901
37875
|
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
37902
37876
|
const here = dirname3(fileURLToPath3(import.meta.url));
|
|
37903
|
-
const bundled =
|
|
37904
|
-
for (const candidate of [bundled,
|
|
37877
|
+
const bundled = resolve(here, "project-sample");
|
|
37878
|
+
for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
|
|
37905
37879
|
if (existsSync4(candidate)) return candidate;
|
|
37906
37880
|
}
|
|
37907
37881
|
return bundled;
|
|
37908
37882
|
}
|
|
37909
|
-
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
37883
|
+
async function scaffoldProject(payload, profileDir2, onProgress, ai2) {
|
|
37910
37884
|
let lastStep = "start";
|
|
37911
37885
|
const emit = (step, message) => {
|
|
37912
37886
|
lastStep = step;
|
|
37913
37887
|
onProgress?.({ projectId: payload.projectId, step, message });
|
|
37914
37888
|
};
|
|
37915
37889
|
try {
|
|
37916
|
-
const root = process.env.SCAFFOLD_ROOT ?
|
|
37890
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
37917
37891
|
const target = join6(root, payload.projectName);
|
|
37918
37892
|
await mkdir(target, { recursive: true });
|
|
37919
37893
|
const repo = singleRepo(reposOf(payload.spec));
|
|
37920
37894
|
if (!repo) throw new Error("A project must declare one repo (ADR-0314).");
|
|
37921
37895
|
emit("git", "Cloning repository\u2026");
|
|
37922
37896
|
await provisionRepo(target, repo, emit, { createMissingBranch: true });
|
|
37923
|
-
await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true });
|
|
37897
|
+
await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true, ai: ai2 });
|
|
37924
37898
|
await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit, true);
|
|
37925
37899
|
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
37926
37900
|
await writeTemplateMarker(target);
|
|
@@ -37939,7 +37913,7 @@ async function scaffoldRepo(dir2, label, spec, emit, opts = {}) {
|
|
|
37939
37913
|
if (spec) {
|
|
37940
37914
|
emit("spec", `Writing project.spec.json into ${label}\u2026`);
|
|
37941
37915
|
await writeFile(join6(dir2, "project.spec.json"), JSON.stringify(spec, null, 2), "utf8");
|
|
37942
|
-
await aiInit(dir2, spec, emit);
|
|
37916
|
+
await aiInit(dir2, spec, emit, opts.ai);
|
|
37943
37917
|
}
|
|
37944
37918
|
}
|
|
37945
37919
|
async function resetTemplateManagedClaude(dir2, label, emit) {
|
|
@@ -37965,7 +37939,7 @@ function readSpecField(spec, key) {
|
|
|
37965
37939
|
}
|
|
37966
37940
|
return typeof spec[key] === "string" ? spec[key] : "";
|
|
37967
37941
|
}
|
|
37968
|
-
async function aiInit(target, spec, emit) {
|
|
37942
|
+
async function aiInit(target, spec, emit, ai2) {
|
|
37969
37943
|
const guideFile = readSpecField(spec, "ai_guide_file") === "AGENT.md" ? "AGENT.md" : "CLAUDE.md";
|
|
37970
37944
|
const guideInstructions = readSpecField(spec, "ai_guide_instructions").trim();
|
|
37971
37945
|
emit("ai-init", `AI init: subagents, README, ${guideFile}\u2026`);
|
|
@@ -37985,25 +37959,47 @@ ${(sa.description || "").trim()}
|
|
|
37985
37959
|
}
|
|
37986
37960
|
}
|
|
37987
37961
|
const specJson = JSON.stringify(spec);
|
|
37988
|
-
|
|
37989
|
-
|
|
37990
|
-
|
|
37962
|
+
let generate3 = null;
|
|
37963
|
+
let releaseSlot = () => void 0;
|
|
37964
|
+
if (ai2) {
|
|
37965
|
+
const slot = await ai2.acquireSlot(
|
|
37966
|
+
(q2) => emit("ai-queued", `Waiting for an AI run slot \u2014 ${q2.position} ahead (${q2.running}/${q2.limit} parallel runs in use)\u2026`)
|
|
37967
|
+
);
|
|
37968
|
+
if (slot.kind === "granted") {
|
|
37969
|
+
releaseSlot = () => slot.handle.release();
|
|
37970
|
+
generate3 = (prompt, label) => ai2.generate(prompt, target, label);
|
|
37971
|
+
if (slot.queued) emit("ai-init", `AI init: subagents, README, ${guideFile}\u2026`);
|
|
37972
|
+
} else {
|
|
37973
|
+
emit("ai-skipped", "No AI run slot freed up in time \u2014 AI init skipped; fallback content written.");
|
|
37974
|
+
}
|
|
37975
|
+
}
|
|
37976
|
+
try {
|
|
37977
|
+
await generateFile(
|
|
37978
|
+
generate3,
|
|
37979
|
+
join6(target, "README.md"),
|
|
37980
|
+
"README.md",
|
|
37981
|
+
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
37991
37982
|
${specJson}`
|
|
37992
|
-
|
|
37993
|
-
|
|
37994
|
-
|
|
37995
|
-
|
|
37983
|
+
);
|
|
37984
|
+
await generateFile(
|
|
37985
|
+
generate3,
|
|
37986
|
+
join6(target, guideFile),
|
|
37987
|
+
guideFile,
|
|
37988
|
+
`Write a ${guideFile} (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
37996
37989
|
${specJson}` + (guideInstructions ? `
|
|
37997
37990
|
Also incorporate these additional instructions/content:
|
|
37998
37991
|
${guideInstructions}
|
|
37999
37992
|
` : ""),
|
|
38000
|
-
|
|
38001
|
-
|
|
37993
|
+
guideInstructions
|
|
37994
|
+
);
|
|
37995
|
+
} finally {
|
|
37996
|
+
releaseSlot();
|
|
37997
|
+
}
|
|
38002
37998
|
}
|
|
38003
|
-
async function generateFile(path, prompt, fallback) {
|
|
37999
|
+
async function generateFile(generate3, path, label, prompt, fallback) {
|
|
38004
38000
|
try {
|
|
38005
|
-
const text = await
|
|
38006
|
-
if (text.trim()) {
|
|
38001
|
+
const text = generate3 ? await generate3(prompt, label) : null;
|
|
38002
|
+
if (text && text.trim()) {
|
|
38007
38003
|
await writeFile(path, text.trim() + "\n", "utf8");
|
|
38008
38004
|
return;
|
|
38009
38005
|
}
|
|
@@ -38011,21 +38007,21 @@ async function generateFile(path, prompt, fallback) {
|
|
|
38011
38007
|
}
|
|
38012
38008
|
if (fallback && fallback.trim()) await writeFile(path, fallback.trim() + "\n", "utf8");
|
|
38013
38009
|
}
|
|
38014
|
-
async function addProject(payload, profileDir2, onProgress) {
|
|
38010
|
+
async function addProject(payload, profileDir2, onProgress, ai2) {
|
|
38015
38011
|
let lastStep = "start";
|
|
38016
38012
|
const emit = (step, message) => {
|
|
38017
38013
|
lastStep = step;
|
|
38018
38014
|
onProgress?.({ projectId: payload.projectId, step, message });
|
|
38019
38015
|
};
|
|
38020
38016
|
try {
|
|
38021
|
-
const root = process.env.SCAFFOLD_ROOT ?
|
|
38017
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
38022
38018
|
const target = join6(root, payload.projectName);
|
|
38023
38019
|
await mkdir(target, { recursive: true });
|
|
38024
38020
|
const repo = singleRepo(payload.repos ?? []);
|
|
38025
38021
|
const mode = payload.mode ?? "clone";
|
|
38026
38022
|
if (repo) await provisionRepo(target, repo, emit, { mode });
|
|
38027
38023
|
if (payload.scaffoldRepos && payload.scaffoldRepos.length > 0) {
|
|
38028
|
-
await scaffoldRepo(target, payload.projectName, payload.spec, emit);
|
|
38024
|
+
await scaffoldRepo(target, payload.projectName, payload.spec, emit, { ai: ai2 });
|
|
38029
38025
|
}
|
|
38030
38026
|
await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit);
|
|
38031
38027
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
@@ -38432,6 +38428,9 @@ var WsChannels = {
|
|
|
38432
38428
|
CLI_UPDATE_RESULT: "cli.update-result",
|
|
38433
38429
|
// command lifecycle
|
|
38434
38430
|
COMMAND_ANNOUNCE: "command.announce",
|
|
38431
|
+
// server → cli (reply): stop an AI run (ADR-0362) — queued ⇒ leave the run-slot queue, running ⇒ kill
|
|
38432
|
+
// the AI process with no failover; the cli then ends the stream with `command.output {done, cancelled}`.
|
|
38433
|
+
COMMAND_CANCEL: "command.cancel",
|
|
38435
38434
|
// console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
|
|
38436
38435
|
// transcript so the web Console renders it verbatim; gated by console.watch (server → cli:
|
|
38437
38436
|
// whether a web viewer is attached) so an unwatched cli syncs nothing.
|
|
@@ -38449,6 +38448,9 @@ var WsChannels = {
|
|
|
38449
38448
|
QUOTA_CHECK: "quota.check",
|
|
38450
38449
|
// cli → server (reply): per-org concurrent AI-run slot — acquire / renew / release (ADR-0359)
|
|
38451
38450
|
RUN_SLOT: "run.slot",
|
|
38451
|
+
// cli → server (reply): read / add the org's monthly autonomous-book counters — USER_TODO / USER_QA /
|
|
38452
|
+
// AI_TODO rows vs `autonomousRequestsPerMonth` (ADR-0365)
|
|
38453
|
+
AUTONOMOUS_BOOK_USAGE: "autonomous.bookUsage",
|
|
38452
38454
|
USAGE_REPORT: "usage.report",
|
|
38453
38455
|
// cli → server: Claude subscription usage snapshot (5h / weekly — ADR-0072)
|
|
38454
38456
|
MACHINE_USAGE: "machine.usage",
|
|
@@ -38551,6 +38553,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
38551
38553
|
var RUN_SLOT_RENEW_MS = 45e3;
|
|
38552
38554
|
var RUN_SLOT_RETRY_MS = 3e3;
|
|
38553
38555
|
var RUN_SLOT_MAX_WAIT_MS = 30 * 6e4;
|
|
38556
|
+
var COMMAND_CANCELLED_EXIT_CODE = 130;
|
|
38554
38557
|
|
|
38555
38558
|
// src/core/update-scheduler.ts
|
|
38556
38559
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -38696,7 +38699,7 @@ function classifyToolFailure(code, out) {
|
|
|
38696
38699
|
}
|
|
38697
38700
|
return { reason: "other", retryable: false };
|
|
38698
38701
|
}
|
|
38699
|
-
function
|
|
38702
|
+
function run2(cmd, args, onLine, timeoutMs = 18e4) {
|
|
38700
38703
|
return new Promise((resolve12) => {
|
|
38701
38704
|
let out = "";
|
|
38702
38705
|
let buf = "";
|
|
@@ -38736,13 +38739,13 @@ function run3(cmd, args, onLine, timeoutMs = 18e4) {
|
|
|
38736
38739
|
});
|
|
38737
38740
|
}
|
|
38738
38741
|
async function detectOne(cmd, versionArg) {
|
|
38739
|
-
const { code, out } = await
|
|
38742
|
+
const { code, out } = await run2(cmd, [versionArg], void 0, 15e3);
|
|
38740
38743
|
if (code !== 0) return null;
|
|
38741
38744
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
38742
38745
|
return first ?? "";
|
|
38743
38746
|
}
|
|
38744
38747
|
async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
|
|
38745
|
-
const { out } = await
|
|
38748
|
+
const { out } = await run2("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
|
|
38746
38749
|
if (!out.trim()) return [];
|
|
38747
38750
|
let deps = {};
|
|
38748
38751
|
try {
|
|
@@ -38818,7 +38821,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
|
|
|
38818
38821
|
return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
|
|
38819
38822
|
}
|
|
38820
38823
|
onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
|
|
38821
|
-
const { code, out } = await
|
|
38824
|
+
const { code, out } = await run2(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
|
|
38822
38825
|
if (code === 0) return { ok: true, exitCode: code };
|
|
38823
38826
|
const detail = extractToolError(out);
|
|
38824
38827
|
return { ok: false, exitCode: code, error: detail ? `${manager} exited ${code}: ${detail}` : `${manager} exited ${code}` };
|
|
@@ -38851,7 +38854,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
|
|
|
38851
38854
|
for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
|
|
38852
38855
|
const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
|
|
38853
38856
|
onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
|
|
38854
|
-
const { code, out } = await
|
|
38857
|
+
const { code, out } = await run2(manager, args, onLine, timeoutMs);
|
|
38855
38858
|
if (code === 0) return { ok: true };
|
|
38856
38859
|
const { reason, retryable } = classifyToolFailure(code, out);
|
|
38857
38860
|
if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
|
|
@@ -39135,6 +39138,36 @@ function clearPinnedCredential(profileDir2) {
|
|
|
39135
39138
|
import { readFileSync as readFileSync8 } from "fs";
|
|
39136
39139
|
import { homedir as homedir3 } from "os";
|
|
39137
39140
|
import { basename, isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
39141
|
+
|
|
39142
|
+
// src/utils/agent-deny.ts
|
|
39143
|
+
import { isAbsolute, resolve as resolve2 } from "path";
|
|
39144
|
+
var HOME_SECRET_PATTERNS = [
|
|
39145
|
+
"~/.4pm/**",
|
|
39146
|
+
"~/.ssh/**",
|
|
39147
|
+
"~/.config/gh/**",
|
|
39148
|
+
"~/.git-credentials",
|
|
39149
|
+
"~/.claude*",
|
|
39150
|
+
"~/.claude*/**"
|
|
39151
|
+
];
|
|
39152
|
+
function absDirPattern(dir2) {
|
|
39153
|
+
const abs = (isAbsolute(dir2) ? dir2 : resolve2(dir2)).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
39154
|
+
return `/${abs.startsWith("/") ? abs : `/${abs}`}/**`;
|
|
39155
|
+
}
|
|
39156
|
+
function agentDenyRules(profileDirs2 = []) {
|
|
39157
|
+
const patterns = new Set(HOME_SECRET_PATTERNS);
|
|
39158
|
+
for (const dir2 of profileDirs2) {
|
|
39159
|
+
if (dir2 && dir2.trim()) patterns.add(absDirPattern(dir2.trim()));
|
|
39160
|
+
}
|
|
39161
|
+
const rules = [];
|
|
39162
|
+
for (const p2 of patterns) rules.push(`Read(${p2})`, `Edit(${p2})`);
|
|
39163
|
+
return rules;
|
|
39164
|
+
}
|
|
39165
|
+
function denySettingsArgs(cmd, profileDirs2 = []) {
|
|
39166
|
+
if (!cmd.includes("claude")) return [];
|
|
39167
|
+
return ["--settings", JSON.stringify({ permissions: { deny: agentDenyRules(profileDirs2) } })];
|
|
39168
|
+
}
|
|
39169
|
+
|
|
39170
|
+
// src/utils/ai-cli.ts
|
|
39138
39171
|
var REQUIRED_AI_ARGS = {
|
|
39139
39172
|
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
39140
39173
|
codex: ["exec", "--json"],
|
|
@@ -39811,12 +39844,12 @@ function pruneCommandOutputByAge(days) {
|
|
|
39811
39844
|
|
|
39812
39845
|
// src/core/support-answer.ts
|
|
39813
39846
|
import { spawn as spawn4 } from "child_process";
|
|
39814
|
-
import { execFile as
|
|
39847
|
+
import { execFile as execFile2 } from "child_process";
|
|
39815
39848
|
import { createHash as createHash3 } from "crypto";
|
|
39816
39849
|
import { existsSync as existsSync10, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
39817
39850
|
import { join as join14 } from "path";
|
|
39818
39851
|
import { tmpdir as tmpdir3 } from "os";
|
|
39819
|
-
import { promisify as
|
|
39852
|
+
import { promisify as promisify2 } from "util";
|
|
39820
39853
|
|
|
39821
39854
|
// src/core/ai-stream.ts
|
|
39822
39855
|
function assistantText(ev) {
|
|
@@ -39921,7 +39954,7 @@ function aiProviderOf(cmd) {
|
|
|
39921
39954
|
if (cmd.includes("antigravity")) return "antigravity";
|
|
39922
39955
|
return "claude";
|
|
39923
39956
|
}
|
|
39924
|
-
function
|
|
39957
|
+
function estimateTokens(text) {
|
|
39925
39958
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
39926
39959
|
}
|
|
39927
39960
|
|
|
@@ -39977,7 +40010,7 @@ function gitAuthArgs(user, password) {
|
|
|
39977
40010
|
|
|
39978
40011
|
// src/core/support-answer.ts
|
|
39979
40012
|
var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
39980
|
-
var execFileAsync =
|
|
40013
|
+
var execFileAsync = promisify2(execFile2);
|
|
39981
40014
|
var MAX_CONTEXT_CHARS = 12e4;
|
|
39982
40015
|
var ANSWER_TIMEOUT_MS = 12e4;
|
|
39983
40016
|
var LEGAL_DRAFT_TIMEOUT_MS = 54e4;
|
|
@@ -40274,7 +40307,7 @@ async function runSupportAnswer(req, ai2, profileDir2) {
|
|
|
40274
40307
|
if (!text) return { body: "", error: "empty answer" };
|
|
40275
40308
|
const { body, moderation } = req.task ? { body: text.trim(), moderation: void 0 } : parseModeratedAnswer(text);
|
|
40276
40309
|
if (!body) return { body: "", error: "empty answer" };
|
|
40277
|
-
const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens:
|
|
40310
|
+
const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens(body) };
|
|
40278
40311
|
return {
|
|
40279
40312
|
body,
|
|
40280
40313
|
...moderation ? { moderation } : {},
|
|
@@ -40323,9 +40356,9 @@ function reportToolResult(tool, ok, message) {
|
|
|
40323
40356
|
// src/core/autonomous.ts
|
|
40324
40357
|
import { readFile as readFile4, writeFile as writeFile4, chmod } from "fs/promises";
|
|
40325
40358
|
import { readFileSync as readFileSync14 } from "fs";
|
|
40326
|
-
import { execFile as
|
|
40359
|
+
import { execFile as execFile3, spawn as spawn5 } from "child_process";
|
|
40327
40360
|
import { join as join17 } from "path";
|
|
40328
|
-
import { promisify as
|
|
40361
|
+
import { promisify as promisify3 } from "util";
|
|
40329
40362
|
|
|
40330
40363
|
// src/core/autonomous-config.ts
|
|
40331
40364
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -40436,7 +40469,7 @@ function pushRecord(h, rec) {
|
|
|
40436
40469
|
}
|
|
40437
40470
|
|
|
40438
40471
|
// src/core/autonomous.ts
|
|
40439
|
-
var
|
|
40472
|
+
var run3 = promisify3(execFile3);
|
|
40440
40473
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
40441
40474
|
var AUTHORS_REL = ".claude/.autonomous.authors.json";
|
|
40442
40475
|
var TICK_REL = ".claude/hooks/autonomous-tick.sh";
|
|
@@ -40495,12 +40528,20 @@ function tableRowsById(md) {
|
|
|
40495
40528
|
}
|
|
40496
40529
|
return rows;
|
|
40497
40530
|
}
|
|
40531
|
+
var CAPPED_BOOK_FILES = { USER_TODO: "USER_TODO.md", USER_QA: "USER_QA.md", AI_TODO: "AI_TODO.md" };
|
|
40532
|
+
async function cappedBookIds(root) {
|
|
40533
|
+
const out = {};
|
|
40534
|
+
for (const [book, file] of Object.entries(CAPPED_BOOK_FILES)) {
|
|
40535
|
+
out[book] = new Set(tableRowsById(await readText(join17(root, file))).keys());
|
|
40536
|
+
}
|
|
40537
|
+
return out;
|
|
40538
|
+
}
|
|
40498
40539
|
function tickScript(root) {
|
|
40499
40540
|
return join17(root, TICK_REL);
|
|
40500
40541
|
}
|
|
40501
40542
|
async function crontabList() {
|
|
40502
40543
|
try {
|
|
40503
|
-
const { stdout } = await
|
|
40544
|
+
const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
|
|
40504
40545
|
return stdout;
|
|
40505
40546
|
} catch {
|
|
40506
40547
|
return "";
|
|
@@ -40586,6 +40627,7 @@ async function uninstallCron(root) {
|
|
|
40586
40627
|
await crontabSet(kept.join("\n"));
|
|
40587
40628
|
}
|
|
40588
40629
|
async function writeAutonomous(root, profileDir2, req, by) {
|
|
40630
|
+
let added;
|
|
40589
40631
|
try {
|
|
40590
40632
|
switch (req.kind) {
|
|
40591
40633
|
case "settings": {
|
|
@@ -40630,6 +40672,7 @@ async function writeAutonomous(root, profileDir2, req, by) {
|
|
|
40630
40672
|
const row = `| ${id} | | | ${tableCell(req.content)} |
|
|
40631
40673
|
`;
|
|
40632
40674
|
await writeFile4(join17(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
|
|
40675
|
+
added = 1;
|
|
40633
40676
|
const authors = await readJsonMap(join17(root, AUTHORS_REL));
|
|
40634
40677
|
authors[id] = { by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
40635
40678
|
await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
@@ -40642,8 +40685,10 @@ async function writeAutonomous(root, profileDir2, req, by) {
|
|
|
40642
40685
|
const authors = await readJsonMap(join17(root, AUTHORS_REL));
|
|
40643
40686
|
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
40644
40687
|
const byLabel = req.byLabel ?? by;
|
|
40688
|
+
added = 0;
|
|
40645
40689
|
for (const [id, cells] of next) {
|
|
40646
40690
|
if (prev.get(id) !== cells) authors[id] = { by, byLabel, at: at2 };
|
|
40691
|
+
if (!prev.has(id)) added += 1;
|
|
40647
40692
|
}
|
|
40648
40693
|
await writeFile4(join17(root, file), req.content, "utf8");
|
|
40649
40694
|
await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
@@ -40655,7 +40700,7 @@ async function writeAutonomous(root, profileDir2, req, by) {
|
|
|
40655
40700
|
break;
|
|
40656
40701
|
}
|
|
40657
40702
|
}
|
|
40658
|
-
return { ok: true, status: await getAutonomousStatus(root, profileDir2) };
|
|
40703
|
+
return { ok: true, status: await getAutonomousStatus(root, profileDir2), ...added !== void 0 ? { added } : {} };
|
|
40659
40704
|
} catch (err) {
|
|
40660
40705
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
40661
40706
|
}
|
|
@@ -40988,9 +41033,20 @@ async function runCommand(dispatch, emitRaw, opts) {
|
|
|
40988
41033
|
hardKillTimer.unref?.();
|
|
40989
41034
|
}, timeoutMs) : null;
|
|
40990
41035
|
timeoutTimer?.unref?.();
|
|
41036
|
+
let cancelled = false;
|
|
41037
|
+
const onAbort = () => {
|
|
41038
|
+
if (cancelled) return;
|
|
41039
|
+
cancelled = true;
|
|
41040
|
+
child.kill("SIGTERM");
|
|
41041
|
+
hardKillTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
41042
|
+
hardKillTimer.unref?.();
|
|
41043
|
+
};
|
|
41044
|
+
if (opts?.signal?.aborted) onAbort();
|
|
41045
|
+
else opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
40991
41046
|
const clearTimers = () => {
|
|
40992
41047
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
40993
41048
|
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
41049
|
+
opts?.signal?.removeEventListener("abort", onAbort);
|
|
40994
41050
|
};
|
|
40995
41051
|
const batcher = new OutputBatcher({
|
|
40996
41052
|
onFlush: (chunk, truncated) => {
|
|
@@ -41017,9 +41073,10 @@ async function runCommand(dispatch, emitRaw, opts) {
|
|
|
41017
41073
|
seq: seq++,
|
|
41018
41074
|
chunk: "",
|
|
41019
41075
|
done: true,
|
|
41020
|
-
// 124 (the conventional timeout code) when we killed it
|
|
41021
|
-
// server settles the command as failed and the web surfaces the
|
|
41022
|
-
|
|
41076
|
+
// 130 when the run was stopped (ADR-0362); 124 (the conventional timeout code) when we killed it
|
|
41077
|
+
// for exceeding the limit, so the server settles the command as failed and the web surfaces the
|
|
41078
|
+
// note instead of spinning.
|
|
41079
|
+
exitCode: cancelled ? COMMAND_CANCELLED_EXIT_CODE : timedOut ? 124 : exitCode ?? -1
|
|
41023
41080
|
});
|
|
41024
41081
|
resolve12();
|
|
41025
41082
|
});
|
|
@@ -41055,7 +41112,7 @@ function isOutOfCredits(text) {
|
|
|
41055
41112
|
text
|
|
41056
41113
|
);
|
|
41057
41114
|
}
|
|
41058
|
-
function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
|
|
41115
|
+
function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs, signal) {
|
|
41059
41116
|
return new Promise((resolve12) => {
|
|
41060
41117
|
runCommand(
|
|
41061
41118
|
{ commandId, cmd, args, path: cwd2, env: env3 },
|
|
@@ -41063,16 +41120,17 @@ function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs)
|
|
|
41063
41120
|
if (out.chunk) onChunk(out.chunk);
|
|
41064
41121
|
if (out.done) resolve12(out.exitCode ?? -1);
|
|
41065
41122
|
},
|
|
41066
|
-
{ stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
|
|
41123
|
+
{ stdin, ...timeoutMs > 0 ? { timeoutMs } : {}, ...signal ? { signal } : {} }
|
|
41067
41124
|
).catch(() => resolve12(-1));
|
|
41068
41125
|
});
|
|
41069
41126
|
}
|
|
41070
|
-
async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
41127
|
+
async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0, signal) {
|
|
41071
41128
|
const total = plan.attempts.length;
|
|
41072
41129
|
let finalExit = -1;
|
|
41073
41130
|
let lastCmd = plan.attempts[0]?.cmd ?? "claude";
|
|
41074
41131
|
let lastCaptured = "";
|
|
41075
41132
|
for (let i = 0; i < total; i++) {
|
|
41133
|
+
if (signal?.aborted) return cancelledResult();
|
|
41076
41134
|
const attempt = plan.attempts[i];
|
|
41077
41135
|
lastCmd = attempt.cmd;
|
|
41078
41136
|
if (total > 1) handlers.onAttemptStart(attempt.label, i, total, attempt.cmd);
|
|
@@ -41085,17 +41143,18 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
41085
41143
|
handlers.onChunk(text);
|
|
41086
41144
|
}
|
|
41087
41145
|
};
|
|
41088
|
-
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs);
|
|
41146
|
+
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs, signal);
|
|
41089
41147
|
const tail = parser.flush();
|
|
41090
41148
|
if (tail) {
|
|
41091
41149
|
captured += tail;
|
|
41092
41150
|
handlers.onChunk(tail);
|
|
41093
41151
|
}
|
|
41152
|
+
if (signal?.aborted) return cancelledResult();
|
|
41094
41153
|
lastCaptured = captured;
|
|
41095
41154
|
const apiErr = parser.apiError();
|
|
41096
41155
|
if (finalExit === 0 && !apiErr.isError) {
|
|
41097
41156
|
const usage = parser.usage();
|
|
41098
|
-
if (usage.tokens === 0) usage.tokens =
|
|
41157
|
+
if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
|
|
41099
41158
|
reportToolResult(attempt.cmd, true);
|
|
41100
41159
|
return {
|
|
41101
41160
|
exitCode: 0,
|
|
@@ -41114,6 +41173,17 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
41114
41173
|
reportToolResult(lastCmd, false, summarizeAiFailure(lastCaptured, finalExit));
|
|
41115
41174
|
return { exitCode: finalExit, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
|
|
41116
41175
|
}
|
|
41176
|
+
function cancelledResult() {
|
|
41177
|
+
return {
|
|
41178
|
+
exitCode: COMMAND_CANCELLED_EXIT_CODE,
|
|
41179
|
+
workedDir: null,
|
|
41180
|
+
workedKey: null,
|
|
41181
|
+
workedCmd: null,
|
|
41182
|
+
usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 },
|
|
41183
|
+
sessionId: "",
|
|
41184
|
+
cancelled: true
|
|
41185
|
+
};
|
|
41186
|
+
}
|
|
41117
41187
|
function summarizeAiFailure(captured, exitCode) {
|
|
41118
41188
|
if (isAuthFailure(captured)) return "Not logged in";
|
|
41119
41189
|
if (isOutOfCredits(captured)) return "Out of usage credits";
|
|
@@ -41642,21 +41712,30 @@ async function runMemoryCompaction(ai2, cwd2, input) {
|
|
|
41642
41712
|
}
|
|
41643
41713
|
|
|
41644
41714
|
// src/core/git-commit-identity.ts
|
|
41645
|
-
import { execFile as
|
|
41646
|
-
import { promisify as
|
|
41647
|
-
var
|
|
41715
|
+
import { execFile as execFile4 } from "child_process";
|
|
41716
|
+
import { promisify as promisify4 } from "util";
|
|
41717
|
+
var run4 = promisify4(execFile4);
|
|
41648
41718
|
async function setCommitAuthor(cwd2, machineUsername) {
|
|
41649
41719
|
if (!machineUsername) return;
|
|
41650
41720
|
try {
|
|
41651
|
-
await
|
|
41721
|
+
await run4("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
41652
41722
|
} catch {
|
|
41653
41723
|
}
|
|
41654
41724
|
}
|
|
41655
41725
|
|
|
41656
41726
|
// src/core/ws-client/run-slot.ts
|
|
41657
41727
|
var NO_SLOT = { release: () => void 0 };
|
|
41658
|
-
function sleep2(ms2) {
|
|
41659
|
-
return new Promise((resolve12) =>
|
|
41728
|
+
function sleep2(ms2, signal) {
|
|
41729
|
+
return new Promise((resolve12) => {
|
|
41730
|
+
if (signal?.aborted) return resolve12();
|
|
41731
|
+
const timer = setTimeout(done, ms2);
|
|
41732
|
+
function done() {
|
|
41733
|
+
clearTimeout(timer);
|
|
41734
|
+
signal?.removeEventListener("abort", done);
|
|
41735
|
+
resolve12();
|
|
41736
|
+
}
|
|
41737
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
41738
|
+
});
|
|
41660
41739
|
}
|
|
41661
41740
|
function holdLease(ctx, lease) {
|
|
41662
41741
|
const timer = setInterval(() => {
|
|
@@ -41673,23 +41752,30 @@ function holdLease(ctx, lease) {
|
|
|
41673
41752
|
}
|
|
41674
41753
|
};
|
|
41675
41754
|
}
|
|
41676
|
-
async function acquireRunSlot(ctx, onQueued) {
|
|
41755
|
+
async function acquireRunSlot(ctx, onQueued, signal) {
|
|
41677
41756
|
const startedAt = Date.now();
|
|
41678
41757
|
let ticket;
|
|
41679
41758
|
let lastPosition = -1;
|
|
41680
41759
|
let limit = 0;
|
|
41681
41760
|
for (; ; ) {
|
|
41761
|
+
if (signal?.aborted) return leaveQueue(ctx, ticket);
|
|
41682
41762
|
let reply;
|
|
41683
41763
|
try {
|
|
41684
41764
|
reply = await ctx.request(WsChannels.RUN_SLOT, { op: "acquire", ...ticket ? { ticket } : {} });
|
|
41685
41765
|
} catch (err) {
|
|
41686
41766
|
logger.warn("run.slot.unavailable", { error: err.message });
|
|
41687
|
-
return { kind: "granted", handle: NO_SLOT };
|
|
41767
|
+
return { kind: "granted", handle: NO_SLOT, queued: ticket !== void 0 };
|
|
41688
41768
|
}
|
|
41689
|
-
if (!reply || !("granted" in reply)) return { kind: "granted", handle: NO_SLOT };
|
|
41769
|
+
if (!reply || !("granted" in reply)) return { kind: "granted", handle: NO_SLOT, queued: ticket !== void 0 };
|
|
41690
41770
|
if (reply.granted) {
|
|
41691
|
-
|
|
41771
|
+
const handle = reply.lease ? holdLease(ctx, reply.lease) : NO_SLOT;
|
|
41772
|
+
if (signal?.aborted) {
|
|
41773
|
+
handle.release();
|
|
41774
|
+
return { kind: "cancelled" };
|
|
41775
|
+
}
|
|
41776
|
+
return { kind: "granted", handle, queued: ticket !== void 0 };
|
|
41692
41777
|
}
|
|
41778
|
+
if ("denied" in reply) return { kind: "denied", reason: reply.denied };
|
|
41693
41779
|
ticket = reply.ticket;
|
|
41694
41780
|
limit = reply.limit;
|
|
41695
41781
|
if (reply.position !== lastPosition) {
|
|
@@ -41697,13 +41783,31 @@ async function acquireRunSlot(ctx, onQueued) {
|
|
|
41697
41783
|
onQueued({ position: reply.position, limit: reply.limit, running: reply.running });
|
|
41698
41784
|
}
|
|
41699
41785
|
if (Date.now() - startedAt >= RUN_SLOT_MAX_WAIT_MS) return { kind: "timeout", limit };
|
|
41700
|
-
await sleep2(reply.retryAfterMs > 0 ? reply.retryAfterMs : RUN_SLOT_RETRY_MS);
|
|
41786
|
+
await sleep2(reply.retryAfterMs > 0 ? reply.retryAfterMs : RUN_SLOT_RETRY_MS, signal);
|
|
41701
41787
|
}
|
|
41702
41788
|
}
|
|
41789
|
+
function leaveQueue(ctx, ticket) {
|
|
41790
|
+
if (ticket) ctx.request(WsChannels.RUN_SLOT, { op: "leave", ticket }).catch(() => void 0);
|
|
41791
|
+
return { kind: "cancelled" };
|
|
41792
|
+
}
|
|
41703
41793
|
|
|
41704
41794
|
// src/core/ws-client/command-dispatch.ts
|
|
41705
41795
|
var MEMORY_SEED_HEADER = "CONTEXT MEMORY from earlier in this conversation (may span prior sessions/accounts). Use it as background; do not repeat it back unless relevant:";
|
|
41796
|
+
var activeRuns = /* @__PURE__ */ new Map();
|
|
41797
|
+
function cancelActiveRun(commandId) {
|
|
41798
|
+
const run11 = activeRuns.get(commandId);
|
|
41799
|
+
if (!run11) return { ok: false, state: "unknown" };
|
|
41800
|
+
run11.controller.abort();
|
|
41801
|
+
return { ok: true, state: run11.state };
|
|
41802
|
+
}
|
|
41706
41803
|
function handleCommandChannels(ctx, message, payload) {
|
|
41804
|
+
if (message.channel === WsChannels.COMMAND_CANCEL) {
|
|
41805
|
+
const commandId = typeof payload.commandId === "string" ? payload.commandId : "";
|
|
41806
|
+
const reply = cancelActiveRun(commandId);
|
|
41807
|
+
logger.info("command.cancel", { commandId, ...reply });
|
|
41808
|
+
ctx.send(WsChannels.COMMAND_CANCEL, reply, message.id);
|
|
41809
|
+
return true;
|
|
41810
|
+
}
|
|
41707
41811
|
if (message.channel !== WsChannels.COMMAND_DISPATCH) return false;
|
|
41708
41812
|
const dispatch = payload;
|
|
41709
41813
|
if (ctx.hasHandledCommand(dispatch.commandId)) {
|
|
@@ -41790,6 +41894,15 @@ async function runLocalCommand(ctx, input) {
|
|
|
41790
41894
|
await runAiPrompt(ctx, prompt, commandId, "local");
|
|
41791
41895
|
}
|
|
41792
41896
|
async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
|
|
41897
|
+
const run11 = { controller: new AbortController(), state: "queued" };
|
|
41898
|
+
activeRuns.set(commandId, run11);
|
|
41899
|
+
try {
|
|
41900
|
+
await runAiPromptInner(ctx, prompt, commandId, origin, run11, oneShot, images, aiConfig, readOnly, bypass);
|
|
41901
|
+
} finally {
|
|
41902
|
+
activeRuns.delete(commandId);
|
|
41903
|
+
}
|
|
41904
|
+
}
|
|
41905
|
+
async function runAiPromptInner(ctx, prompt, commandId, origin, run11, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
|
|
41793
41906
|
const config = readProfileConfig(ctx.profileDir);
|
|
41794
41907
|
const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
|
|
41795
41908
|
const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
|
|
@@ -41797,7 +41910,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
|
|
|
41797
41910
|
const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
|
|
41798
41911
|
const perPromptLimit = config.perPromptTokenLimit ?? 0;
|
|
41799
41912
|
if (perPromptLimit > 0) {
|
|
41800
|
-
const estimated =
|
|
41913
|
+
const estimated = estimateTokens(prompt);
|
|
41801
41914
|
if (estimated > perPromptLimit) {
|
|
41802
41915
|
rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
|
|
41803
41916
|
return;
|
|
@@ -41954,24 +42067,46 @@ ${guardedPrompt}`;
|
|
|
41954
42067
|
}
|
|
41955
42068
|
};
|
|
41956
42069
|
let result;
|
|
41957
|
-
let
|
|
42070
|
+
let neverStarted = false;
|
|
41958
42071
|
try {
|
|
41959
|
-
const slot = await acquireRunSlot(
|
|
41960
|
-
|
|
41961
|
-
|
|
41962
|
-
|
|
41963
|
-
|
|
41964
|
-
|
|
42072
|
+
const slot = await acquireRunSlot(
|
|
42073
|
+
ctx,
|
|
42074
|
+
({ position, limit, running: running2 }) => {
|
|
42075
|
+
const text = `\u23F3 queued \u2014 ${position} ahead, ${running2}/${limit} parallel runs in use (plan limit)`;
|
|
42076
|
+
ctx.bus.push({ source: origin, kind: "log", text });
|
|
42077
|
+
ctx.send(WsChannels.COMMAND_OUTPUT, {
|
|
42078
|
+
commandId,
|
|
42079
|
+
seq: seq++,
|
|
42080
|
+
chunk: `${text}
|
|
42081
|
+
`,
|
|
42082
|
+
log: true,
|
|
42083
|
+
queue: { position, limit, running: running2 }
|
|
42084
|
+
});
|
|
42085
|
+
},
|
|
42086
|
+
run11.controller.signal
|
|
42087
|
+
);
|
|
41965
42088
|
if (slot.kind === "timeout") {
|
|
41966
|
-
|
|
42089
|
+
neverStarted = true;
|
|
41967
42090
|
const text = `RUN_QUEUE_TIMEOUT: waited 30 min for a free run slot (plan limit ${slot.limit} parallel runs) \u2014 not run`;
|
|
41968
42091
|
ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
|
|
41969
42092
|
ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
41970
42093
|
`, log: true });
|
|
41971
42094
|
result = { exitCode: 1, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
|
|
42095
|
+
} else if (slot.kind === "denied") {
|
|
42096
|
+
neverStarted = true;
|
|
42097
|
+
const text = "STORAGE_QUOTA_EXCEEDED: the organization's hosted storage is full \u2014 clear storage in Settings \u203A Storage, then retry";
|
|
42098
|
+
ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
|
|
42099
|
+
ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
42100
|
+
`, log: true });
|
|
42101
|
+
result = { exitCode: 1, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
|
|
42102
|
+
} else if (slot.kind === "cancelled") {
|
|
42103
|
+
neverStarted = true;
|
|
42104
|
+
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 };
|
|
41972
42105
|
} else {
|
|
42106
|
+
if (slot.queued) ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", log: true, queue: null });
|
|
42107
|
+
run11.state = "running";
|
|
41973
42108
|
try {
|
|
41974
|
-
result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs);
|
|
42109
|
+
result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run11.controller.signal);
|
|
41975
42110
|
} finally {
|
|
41976
42111
|
slot.handle.release();
|
|
41977
42112
|
}
|
|
@@ -41979,6 +42114,12 @@ ${guardedPrompt}`;
|
|
|
41979
42114
|
} finally {
|
|
41980
42115
|
ctx.bus.endBusy(plan.cmd);
|
|
41981
42116
|
}
|
|
42117
|
+
if (result.cancelled) {
|
|
42118
|
+
const text = "\u25A0 stopped";
|
|
42119
|
+
ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
|
|
42120
|
+
ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
42121
|
+
`, log: true });
|
|
42122
|
+
}
|
|
41982
42123
|
const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
|
|
41983
42124
|
if (result.workedDir) {
|
|
41984
42125
|
if (unified && result.workedKey) {
|
|
@@ -41993,14 +42134,16 @@ ${guardedPrompt}`;
|
|
|
41993
42134
|
seq: seq++,
|
|
41994
42135
|
chunk: "",
|
|
41995
42136
|
done: true,
|
|
41996
|
-
exitCode: result.exitCode
|
|
42137
|
+
exitCode: result.exitCode,
|
|
42138
|
+
// A stopped run (ADR-0362): the server settles it `cancelled` and the web applies nothing.
|
|
42139
|
+
...result.cancelled ? { cancelled: true } : {}
|
|
41997
42140
|
});
|
|
41998
42141
|
finishCommand(commandId, result.exitCode, result.usage);
|
|
41999
42142
|
ctx.send(WsChannels.COMMAND_HISTORY, {
|
|
42000
42143
|
commandId,
|
|
42001
42144
|
cmd: plan.cmd,
|
|
42002
42145
|
args: markerArgs,
|
|
42003
|
-
status: result.exitCode === 0 ? "done" : "failed",
|
|
42146
|
+
status: result.cancelled ? "cancelled" : result.exitCode === 0 ? "done" : "failed",
|
|
42004
42147
|
exitCode: result.exitCode,
|
|
42005
42148
|
tokens: result.usage.tokens,
|
|
42006
42149
|
// The ai_tokens split for this run (ADR-0145); attached only when we metered real tokens.
|
|
@@ -42036,13 +42179,13 @@ ${guardedPrompt}`;
|
|
|
42036
42179
|
});
|
|
42037
42180
|
ctx.bus.addTokens?.(result.usage.tokens);
|
|
42038
42181
|
}
|
|
42039
|
-
if (!
|
|
42182
|
+
if (!neverStarted) ctx.reportUsage(events, profileLabel);
|
|
42040
42183
|
void ctx.pollUsage();
|
|
42041
42184
|
ctx.bus.push({
|
|
42042
42185
|
source: origin,
|
|
42043
42186
|
kind: "exit",
|
|
42044
|
-
text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
|
|
42045
|
-
level: result.exitCode === 0 ? "info" : "error",
|
|
42187
|
+
text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : result.cancelled ? "\u25A0 stopped \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
|
|
42188
|
+
level: result.exitCode === 0 ? "info" : result.cancelled ? "warn" : "error",
|
|
42046
42189
|
durationMs: Date.now() - aiStartedMs
|
|
42047
42190
|
});
|
|
42048
42191
|
if (memCfg.enabled && result.exitCode === 0) {
|
|
@@ -42278,12 +42421,17 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
|
|
|
42278
42421
|
import { randomUUID } from "crypto";
|
|
42279
42422
|
import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile5, readdir as readdir2, rm as rm2, stat } from "fs/promises";
|
|
42280
42423
|
import { join as join20 } from "path";
|
|
42281
|
-
function buildAutonomousCyclePrompt() {
|
|
42424
|
+
function buildAutonomousCyclePrompt(opts = {}) {
|
|
42425
|
+
const capNote = opts.intakeBlocked ? `
|
|
42426
|
+
|
|
42427
|
+
**Step 2 is DISABLED this cycle** \u2014 the organization reached its monthly plan limit for new \`AI_TODO\` /
|
|
42428
|
+
\`USER_QA\` rows. Do NOT add any row to \`AI_TODO.md\` or \`USER_QA.md\`; skip Step 2 entirely and continue
|
|
42429
|
+
with the already-approved work.` : "";
|
|
42282
42430
|
return `You are running UNATTENDED as the 4PM autonomous cycle (headless, one tick). Do EXACTLY ONE small
|
|
42283
42431
|
work cycle with the steps below, then STOP. Keep each cycle to ONE small task so you never run out of
|
|
42284
42432
|
tokens mid-way. If a step fails, append a row to the **Incidents** table of \`AI_DONE.md\` and STOP \u2014
|
|
42285
42433
|
do not push on. Write all books/docs/code in the language the project's \`CLAUDE.md\` specifies (default
|
|
42286
|
-
English)
|
|
42434
|
+
English).${capNote}
|
|
42287
42435
|
|
|
42288
42436
|
## Books & approval (MUST read the approvals file)
|
|
42289
42437
|
The five "book" files at the project root \u2014 \`USER_TODO.md\`, \`USER_QA.md\`, \`AI_TODO.md\`,
|
|
@@ -42417,8 +42565,16 @@ async function runAutonomousCycle(ctx) {
|
|
|
42417
42565
|
await log(`[run] cycle (tick ${hist.ticks.count}/${today})`);
|
|
42418
42566
|
const commandId = randomUUID();
|
|
42419
42567
|
const override = cfg.model ? { model: cfg.model } : void 0;
|
|
42568
|
+
const usage = await readBookUsage(ctx);
|
|
42569
|
+
const intakeBlocked = ["AI_TODO", "USER_QA"].some((b2) => {
|
|
42570
|
+
const c = usage?.books[b2];
|
|
42571
|
+
return !!c && c.limit !== null && c.used >= c.limit;
|
|
42572
|
+
});
|
|
42573
|
+
if (intakeBlocked) await log("[cap] monthly AI_TODO/USER_QA limit reached \u2014 intake skipped this cycle");
|
|
42574
|
+
const booksBefore = await cappedBookIds(root);
|
|
42420
42575
|
try {
|
|
42421
|
-
await runAiPrompt(ctx, buildAutonomousCyclePrompt(), commandId, "local", false, void 0, override, false, true);
|
|
42576
|
+
await runAiPrompt(ctx, buildAutonomousCyclePrompt({ intakeBlocked }), commandId, "local", false, void 0, override, false, true);
|
|
42577
|
+
await reportNewBookRows(ctx, root, booksBefore);
|
|
42422
42578
|
hist = await readHistories(root);
|
|
42423
42579
|
hist.consecutiveFails = 0;
|
|
42424
42580
|
await recordRun(root, hist, "success", "cycle complete", today);
|
|
@@ -42426,6 +42582,7 @@ async function runAutonomousCycle(ctx) {
|
|
|
42426
42582
|
ctx.bus.autonomousDone(true);
|
|
42427
42583
|
} catch (err) {
|
|
42428
42584
|
const msg = err instanceof Error ? err.message : String(err);
|
|
42585
|
+
await reportNewBookRows(ctx, root, booksBefore);
|
|
42429
42586
|
hist = await readHistories(root);
|
|
42430
42587
|
hist.consecutiveFails += 1;
|
|
42431
42588
|
await recordRun(root, hist, "failure", msg, today);
|
|
@@ -42442,6 +42599,25 @@ async function runAutonomousCycle(ctx) {
|
|
|
42442
42599
|
running.delete(root);
|
|
42443
42600
|
}
|
|
42444
42601
|
}
|
|
42602
|
+
async function readBookUsage(ctx) {
|
|
42603
|
+
try {
|
|
42604
|
+
return await ctx.request(WsChannels.AUTONOMOUS_BOOK_USAGE, {});
|
|
42605
|
+
} catch {
|
|
42606
|
+
return null;
|
|
42607
|
+
}
|
|
42608
|
+
}
|
|
42609
|
+
async function reportNewBookRows(ctx, root, before) {
|
|
42610
|
+
try {
|
|
42611
|
+
const after = await cappedBookIds(root);
|
|
42612
|
+
const added = {};
|
|
42613
|
+
for (const book of Object.keys(after)) {
|
|
42614
|
+
const n = [...after[book]].filter((id) => !before[book].has(id)).length;
|
|
42615
|
+
if (n > 0) added[book] = n;
|
|
42616
|
+
}
|
|
42617
|
+
if (Object.keys(added).length > 0) await ctx.request(WsChannels.AUTONOMOUS_BOOK_USAGE, { added });
|
|
42618
|
+
} catch {
|
|
42619
|
+
}
|
|
42620
|
+
}
|
|
42445
42621
|
async function dayLog(root, line) {
|
|
42446
42622
|
try {
|
|
42447
42623
|
const dir2 = join20(root, ".claude", "logs");
|
|
@@ -42788,10 +42964,10 @@ function handleFsChannels(ctx, message, payload) {
|
|
|
42788
42964
|
|
|
42789
42965
|
// src/core/git-diff.ts
|
|
42790
42966
|
import { readFile as readFile8 } from "fs/promises";
|
|
42791
|
-
import { execFile as
|
|
42967
|
+
import { execFile as execFile5 } from "child_process";
|
|
42792
42968
|
import { basename as basename5, dirname as dirname8, resolve as resolve9, sep as sep7 } from "path";
|
|
42793
|
-
import { promisify as
|
|
42794
|
-
var
|
|
42969
|
+
import { promisify as promisify5 } from "util";
|
|
42970
|
+
var run5 = promisify5(execFile5);
|
|
42795
42971
|
function resolveInRoot5(root, relPath) {
|
|
42796
42972
|
if (!root) return null;
|
|
42797
42973
|
const base = resolve9(root);
|
|
@@ -42819,7 +42995,7 @@ async function gitDiff(root, path) {
|
|
|
42819
42995
|
}
|
|
42820
42996
|
let oldBuf = null;
|
|
42821
42997
|
try {
|
|
42822
|
-
const { stdout } = await
|
|
42998
|
+
const { stdout } = await run5("git", ["show", `HEAD:./${base}`], {
|
|
42823
42999
|
cwd: dir2,
|
|
42824
43000
|
timeout: 15e3,
|
|
42825
43001
|
maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
|
|
@@ -42849,12 +43025,12 @@ async function gitDiff(root, path) {
|
|
|
42849
43025
|
}
|
|
42850
43026
|
|
|
42851
43027
|
// src/core/git-env.ts
|
|
42852
|
-
import { execFile as
|
|
42853
|
-
import { promisify as
|
|
42854
|
-
var
|
|
43028
|
+
import { execFile as execFile6 } from "child_process";
|
|
43029
|
+
import { promisify as promisify6 } from "util";
|
|
43030
|
+
var run6 = promisify6(execFile6);
|
|
42855
43031
|
async function hasBinary(bin, args = ["--version"]) {
|
|
42856
43032
|
try {
|
|
42857
|
-
await
|
|
43033
|
+
await run6(bin, args, { timeout: 5e3 });
|
|
42858
43034
|
return true;
|
|
42859
43035
|
} catch {
|
|
42860
43036
|
return false;
|
|
@@ -42867,7 +43043,7 @@ async function checkGitEnv(provider) {
|
|
|
42867
43043
|
let account = null;
|
|
42868
43044
|
if (installed) {
|
|
42869
43045
|
try {
|
|
42870
|
-
const { stdout, stderr } = await
|
|
43046
|
+
const { stdout, stderr } = await run6(bin, ["auth", "status"], { timeout: 8e3 });
|
|
42871
43047
|
const out = `${stdout}
|
|
42872
43048
|
${stderr}`;
|
|
42873
43049
|
authenticated = /logged in|logged into|✓/i.test(out);
|
|
@@ -42880,11 +43056,11 @@ ${stderr}`;
|
|
|
42880
43056
|
}
|
|
42881
43057
|
|
|
42882
43058
|
// src/core/git-ssh-key.ts
|
|
42883
|
-
import { execFile as
|
|
43059
|
+
import { execFile as execFile7 } from "child_process";
|
|
42884
43060
|
import { readFile as readFile9, rm as rm4, mkdir as mkdir7 } from "fs/promises";
|
|
42885
43061
|
import { join as join21 } from "path";
|
|
42886
|
-
import { promisify as
|
|
42887
|
-
var
|
|
43062
|
+
import { promisify as promisify7 } from "util";
|
|
43063
|
+
var run7 = promisify7(execFile7);
|
|
42888
43064
|
function keyPaths(profileDir2) {
|
|
42889
43065
|
const dir2 = join21(profileDir2, ".ssh");
|
|
42890
43066
|
const key = join21(dir2, "id_4pm");
|
|
@@ -42899,14 +43075,14 @@ async function readPublicKey(pub) {
|
|
|
42899
43075
|
}
|
|
42900
43076
|
async function fingerprintOf(pub) {
|
|
42901
43077
|
try {
|
|
42902
|
-
const { stdout } = await
|
|
43078
|
+
const { stdout } = await run7("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
|
|
42903
43079
|
return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
|
|
42904
43080
|
} catch {
|
|
42905
43081
|
return null;
|
|
42906
43082
|
}
|
|
42907
43083
|
}
|
|
42908
43084
|
async function configureGit(key) {
|
|
42909
|
-
await
|
|
43085
|
+
await run7(
|
|
42910
43086
|
"git",
|
|
42911
43087
|
["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
|
|
42912
43088
|
{ timeout: 5e3 }
|
|
@@ -42917,7 +43093,7 @@ async function generate(profileDir2) {
|
|
|
42917
43093
|
await mkdir7(dir2, { recursive: true });
|
|
42918
43094
|
await rm4(key, { force: true });
|
|
42919
43095
|
await rm4(pub, { force: true });
|
|
42920
|
-
await
|
|
43096
|
+
await run7("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
|
|
42921
43097
|
timeout: 15e3
|
|
42922
43098
|
});
|
|
42923
43099
|
await configureGit(key);
|
|
@@ -42927,7 +43103,7 @@ async function remove(profileDir2) {
|
|
|
42927
43103
|
const { key, pub } = keyPaths(profileDir2);
|
|
42928
43104
|
await rm4(key, { force: true });
|
|
42929
43105
|
await rm4(pub, { force: true });
|
|
42930
|
-
await
|
|
43106
|
+
await run7("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
|
|
42931
43107
|
() => void 0
|
|
42932
43108
|
);
|
|
42933
43109
|
return { publicKey: null, fingerprint: null };
|
|
@@ -42948,10 +43124,10 @@ async function manageSshKey(op, profileDir2) {
|
|
|
42948
43124
|
}
|
|
42949
43125
|
|
|
42950
43126
|
// src/core/git-history.ts
|
|
42951
|
-
import { execFile as
|
|
43127
|
+
import { execFile as execFile8 } from "child_process";
|
|
42952
43128
|
import { resolve as resolve10, sep as sep8 } from "path";
|
|
42953
|
-
import { promisify as
|
|
42954
|
-
var
|
|
43129
|
+
import { promisify as promisify8 } from "util";
|
|
43130
|
+
var run8 = promisify8(execFile8);
|
|
42955
43131
|
var FS = "";
|
|
42956
43132
|
var RS = "";
|
|
42957
43133
|
function resolveRepo(root, repo) {
|
|
@@ -42962,7 +43138,7 @@ function resolveRepo(root, repo) {
|
|
|
42962
43138
|
}
|
|
42963
43139
|
async function git(cwd2, args) {
|
|
42964
43140
|
try {
|
|
42965
|
-
const { stdout } = await
|
|
43141
|
+
const { stdout } = await run8("git", args, {
|
|
42966
43142
|
cwd: cwd2,
|
|
42967
43143
|
timeout: 15e3,
|
|
42968
43144
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -43104,6 +43280,74 @@ function handleGitChannels(ctx, message, payload) {
|
|
|
43104
43280
|
// src/core/ws-client/handlers/misc.ts
|
|
43105
43281
|
import { join as join22 } from "path";
|
|
43106
43282
|
|
|
43283
|
+
// src/core/ai-assist.ts
|
|
43284
|
+
import { execFile as execFile9 } from "child_process";
|
|
43285
|
+
import { promisify as promisify9 } from "util";
|
|
43286
|
+
var run9 = promisify9(execFile9);
|
|
43287
|
+
function aiCli() {
|
|
43288
|
+
return process.env.AI_CLI || "claude";
|
|
43289
|
+
}
|
|
43290
|
+
function aiArgs(cli, prompt) {
|
|
43291
|
+
if (cli === "claude") return ["-p", prompt, "--output-format", "json", ...denySettingsArgs(cli, [process.env.CLAUDE_CONFIG_DIR])];
|
|
43292
|
+
if (cli === "codex") return ["exec", "--json", prompt];
|
|
43293
|
+
return ["-p", prompt];
|
|
43294
|
+
}
|
|
43295
|
+
function estimateTokens2(prompt, output) {
|
|
43296
|
+
return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
|
|
43297
|
+
}
|
|
43298
|
+
function parseAi(cli, stdout, prompt) {
|
|
43299
|
+
if (cli === "claude") {
|
|
43300
|
+
try {
|
|
43301
|
+
const j2 = JSON.parse(stdout);
|
|
43302
|
+
const text2 = (j2.result ?? "").trim();
|
|
43303
|
+
const tokens = (j2.usage?.input_tokens ?? 0) + (j2.usage?.output_tokens ?? 0);
|
|
43304
|
+
if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens2(prompt, text2) };
|
|
43305
|
+
} catch {
|
|
43306
|
+
}
|
|
43307
|
+
}
|
|
43308
|
+
if (cli === "codex") {
|
|
43309
|
+
try {
|
|
43310
|
+
let text2 = "";
|
|
43311
|
+
let tokens = 0;
|
|
43312
|
+
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
43313
|
+
const obj = JSON.parse(line);
|
|
43314
|
+
if (obj.text) text2 = obj.text;
|
|
43315
|
+
else if (obj.message) text2 = obj.message;
|
|
43316
|
+
if (obj.usage) {
|
|
43317
|
+
tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
|
|
43318
|
+
}
|
|
43319
|
+
}
|
|
43320
|
+
if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens2(prompt, text2) };
|
|
43321
|
+
} catch {
|
|
43322
|
+
}
|
|
43323
|
+
}
|
|
43324
|
+
const text = stdout.trim();
|
|
43325
|
+
return { text, tokens: estimateTokens2(prompt, text) };
|
|
43326
|
+
}
|
|
43327
|
+
async function runAi(prompt) {
|
|
43328
|
+
const cli = aiCli();
|
|
43329
|
+
const { stdout } = await run9(cli, aiArgs(cli, prompt), {
|
|
43330
|
+
timeout: 12e4,
|
|
43331
|
+
maxBuffer: 8 * 1024 * 1024
|
|
43332
|
+
});
|
|
43333
|
+
return parseAi(cli, stdout, prompt);
|
|
43334
|
+
}
|
|
43335
|
+
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
43336
|
+
const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
|
|
43337
|
+
${input}`;
|
|
43338
|
+
const { text, tokens } = await runAi(prompt);
|
|
43339
|
+
const start = text.indexOf("{");
|
|
43340
|
+
const end = text.lastIndexOf("}");
|
|
43341
|
+
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
43342
|
+
try {
|
|
43343
|
+
const j2 = JSON.parse(text.slice(start, end + 1));
|
|
43344
|
+
const reasons = Array.isArray(j2.reasons) ? j2.reasons.filter((r) => typeof r === "string") : [];
|
|
43345
|
+
return { ok: j2.ok === true && reasons.length === 0, reasons, tokens };
|
|
43346
|
+
} catch {
|
|
43347
|
+
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
43348
|
+
}
|
|
43349
|
+
}
|
|
43350
|
+
|
|
43107
43351
|
// src/core/outbound-review.ts
|
|
43108
43352
|
function compilePatterns(sources, fallback) {
|
|
43109
43353
|
const list = sources ?? fallback;
|
|
@@ -43217,6 +43461,72 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
43217
43461
|
// src/core/ws-client/handlers/project.ts
|
|
43218
43462
|
import { existsSync as existsSync14, mkdirSync as mkdirSync7, renameSync, rmSync as rmSync7 } from "fs";
|
|
43219
43463
|
import { join as join23 } from "path";
|
|
43464
|
+
|
|
43465
|
+
// src/core/ai-task.ts
|
|
43466
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
43467
|
+
function createAiTaskRunner(ctx) {
|
|
43468
|
+
return {
|
|
43469
|
+
acquireSlot: (onQueued) => acquireRunSlot(ctx, onQueued),
|
|
43470
|
+
generate: (prompt, cwd2, label) => generate2(ctx, prompt, cwd2, label)
|
|
43471
|
+
};
|
|
43472
|
+
}
|
|
43473
|
+
async function generate2(ctx, prompt, cwd2, label) {
|
|
43474
|
+
const config = readProfileConfig(ctx.profileDir);
|
|
43475
|
+
const cmd = config.aiCli || "claude";
|
|
43476
|
+
const unified = isUnifiedConfig(config);
|
|
43477
|
+
const hint = unified ? {
|
|
43478
|
+
credential: getPinnedCredential(ctx.profileDir) ?? ((config.aiFailoverMode ?? "remember") === "remember" ? getWorkingCredential(ctx.profileDir) : null)
|
|
43479
|
+
} : { dir: getWorkingProfile(ctx.profileDir, cmd) };
|
|
43480
|
+
const plan = planAiRun(prompt, config, hint, /* @__PURE__ */ new Map(), true);
|
|
43481
|
+
const timeoutSec = config.projectAiRunTimeoutSec || config.aiRunTimeoutSec || 0;
|
|
43482
|
+
ctx.bus.push({ source: "system", kind: "aireq", text: `${plan.cmd} \u2039 ${label}` });
|
|
43483
|
+
let text = "";
|
|
43484
|
+
const handlers = {
|
|
43485
|
+
onChunk: (chunk) => {
|
|
43486
|
+
text += chunk;
|
|
43487
|
+
},
|
|
43488
|
+
onAttemptStart: (profile, index, total, attemptCmd) => {
|
|
43489
|
+
ctx.bus.push({ source: "system", kind: "log", text: `\u2192 trying ${attemptCmd} profile "${profile}" (${index + 1}/${total})\u2026` });
|
|
43490
|
+
if (index > 0) text = "";
|
|
43491
|
+
},
|
|
43492
|
+
onAttemptFail: (profile) => {
|
|
43493
|
+
ctx.bus.push({ source: "system", kind: "log", text: `profile "${profile}" failed \u2014 trying next`, level: "warn" });
|
|
43494
|
+
}
|
|
43495
|
+
};
|
|
43496
|
+
ctx.bus.startBusy(plan.cmd);
|
|
43497
|
+
let result;
|
|
43498
|
+
try {
|
|
43499
|
+
result = await runAiFailover(plan, `ai-task-${randomUUID2()}`, cwd2, handlers, timeoutSec * 1e3);
|
|
43500
|
+
} finally {
|
|
43501
|
+
ctx.bus.endBusy(plan.cmd);
|
|
43502
|
+
}
|
|
43503
|
+
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
43504
|
+
const events = [{ metric: UsageMetric.COMMANDS, amount: 1, occurredAt }];
|
|
43505
|
+
if (result.exitCode === 0 && result.usage.tokens > 0) {
|
|
43506
|
+
events.push({
|
|
43507
|
+
metric: UsageMetric.AI_TOKENS,
|
|
43508
|
+
amount: result.usage.tokens,
|
|
43509
|
+
occurredAt,
|
|
43510
|
+
inputTokens: result.usage.input,
|
|
43511
|
+
outputTokens: result.usage.output,
|
|
43512
|
+
cacheReadTokens: result.usage.cacheRead,
|
|
43513
|
+
cacheCreationTokens: result.usage.cacheCreation,
|
|
43514
|
+
provider: aiProviderOf(result.workedCmd ?? plan.cmd),
|
|
43515
|
+
authMode: resolveClaudeAuthMode(config)
|
|
43516
|
+
});
|
|
43517
|
+
}
|
|
43518
|
+
ctx.reportUsage(events, result.workedDir ? profileDisplayLabel(result.workedDir) : null);
|
|
43519
|
+
const ok = result.exitCode === 0 && text.trim().length > 0;
|
|
43520
|
+
ctx.bus.push({
|
|
43521
|
+
source: "system",
|
|
43522
|
+
kind: ok ? "aires" : "log",
|
|
43523
|
+
text: ok ? `${plan.cmd} \u203A ${label} \u2713` : `${label}: AI run failed (exit ${result.exitCode}) \u2014 using the fallback`,
|
|
43524
|
+
...ok ? {} : { level: "warn" }
|
|
43525
|
+
});
|
|
43526
|
+
return ok ? text.trim() : null;
|
|
43527
|
+
}
|
|
43528
|
+
|
|
43529
|
+
// src/core/ws-client/handlers/project.ts
|
|
43220
43530
|
function handleProjectChannels(ctx, message, payload) {
|
|
43221
43531
|
switch (message.channel) {
|
|
43222
43532
|
case WsChannels.PHYSIC_SYNC: {
|
|
@@ -43274,14 +43584,18 @@ function handleProjectChannels(ctx, message, payload) {
|
|
|
43274
43584
|
void scaffoldProject(
|
|
43275
43585
|
payload,
|
|
43276
43586
|
ctx.profileDir,
|
|
43277
|
-
(p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2)
|
|
43587
|
+
(p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2),
|
|
43588
|
+
// AI init on the standard AI path + one org run slot (ADR-0362).
|
|
43589
|
+
createAiTaskRunner(ctx)
|
|
43278
43590
|
).then((reply) => ctx.send(WsChannels.PROJECT_CREATE, reply, message.id));
|
|
43279
43591
|
return true;
|
|
43280
43592
|
case WsChannels.PROJECT_ADD:
|
|
43281
43593
|
void addProject(
|
|
43282
43594
|
payload,
|
|
43283
43595
|
ctx.profileDir,
|
|
43284
|
-
(p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2)
|
|
43596
|
+
(p2) => ctx.send(WsChannels.PROJECT_PROGRESS, p2),
|
|
43597
|
+
// An add-with-scaffold runs AI init too (ADR-0362).
|
|
43598
|
+
createAiTaskRunner(ctx)
|
|
43285
43599
|
).then((reply) => ctx.send(WsChannels.PROJECT_ADD, reply, message.id));
|
|
43286
43600
|
return true;
|
|
43287
43601
|
default:
|
|
@@ -43364,7 +43678,7 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
|
|
|
43364
43678
|
// src/core/faq-compose.ts
|
|
43365
43679
|
import { spawn as spawn9, execFile as execFile10 } from "child_process";
|
|
43366
43680
|
import { existsSync as existsSync15, mkdirSync as mkdirSync8, rmSync as rmSync8 } from "fs";
|
|
43367
|
-
import { randomUUID as
|
|
43681
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
43368
43682
|
import { join as join24 } from "path";
|
|
43369
43683
|
import { tmpdir as tmpdir4 } from "os";
|
|
43370
43684
|
import { promisify as promisify10 } from "util";
|
|
@@ -43486,7 +43800,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
|
|
|
43486
43800
|
var MAX_OUTPUT_CHARS = 8e3;
|
|
43487
43801
|
async function runFaqCompose(req, ai2) {
|
|
43488
43802
|
if (!req.repo.token) return { prUrl: "", error: "no write token" };
|
|
43489
|
-
const workDir = join24(tmpdir4(), "4pm-faq-sync",
|
|
43803
|
+
const workDir = join24(tmpdir4(), "4pm-faq-sync", randomUUID3());
|
|
43490
43804
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
43491
43805
|
const auth = gitAuthArgs("x-access-token", req.repo.token);
|
|
43492
43806
|
try {
|
|
@@ -43501,7 +43815,7 @@ async function runFaqCompose(req, ai2) {
|
|
|
43501
43815
|
await execFileAsync2("git", ["-C", workDir, "add", "-A"], { env: gitEnv });
|
|
43502
43816
|
const staged = await execFileAsync2("git", ["-C", workDir, "diff", "--cached", "--name-only"], { env: gitEnv });
|
|
43503
43817
|
if (!staged.stdout.trim()) {
|
|
43504
|
-
return { prUrl: "", output, perTicket, tokens: usage.tokens ||
|
|
43818
|
+
return { prUrl: "", output, perTicket, tokens: usage.tokens || estimateTokens(text), finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
43505
43819
|
}
|
|
43506
43820
|
await execFileAsync2("git", ["-C", workDir, "commit", "-m", "docs(faq): synthesize from support tickets (4PM)"], { env: gitEnv });
|
|
43507
43821
|
await execFileAsync2("git", [...auth, "-C", workDir, "push", "-u", "origin", req.headBranch], { env: gitEnv });
|
|
@@ -45290,7 +45604,7 @@ var WsClient = class {
|
|
|
45290
45604
|
}
|
|
45291
45605
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
45292
45606
|
const envelope = {
|
|
45293
|
-
id:
|
|
45607
|
+
id: randomUUID4(),
|
|
45294
45608
|
replyTo,
|
|
45295
45609
|
channel,
|
|
45296
45610
|
wsToken: this.wsToken,
|
|
@@ -45311,7 +45625,7 @@ var WsClient = class {
|
|
|
45311
45625
|
}
|
|
45312
45626
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
45313
45627
|
const envelope = {
|
|
45314
|
-
id:
|
|
45628
|
+
id: randomUUID4(),
|
|
45315
45629
|
replyTo: null,
|
|
45316
45630
|
channel,
|
|
45317
45631
|
wsToken: this.wsToken,
|
|
@@ -45703,7 +46017,7 @@ function releaseInstanceLock(profileDir2) {
|
|
|
45703
46017
|
|
|
45704
46018
|
// src/core/session-bus.ts
|
|
45705
46019
|
import { EventEmitter } from "events";
|
|
45706
|
-
import { randomUUID as
|
|
46020
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
45707
46021
|
var REPLAY_CAP = 500;
|
|
45708
46022
|
var SessionBus = class {
|
|
45709
46023
|
emitter = new EventEmitter();
|
|
@@ -45734,7 +46048,7 @@ var SessionBus = class {
|
|
|
45734
46048
|
/** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
|
|
45735
46049
|
push(input) {
|
|
45736
46050
|
const entry = {
|
|
45737
|
-
id:
|
|
46051
|
+
id: randomUUID5(),
|
|
45738
46052
|
level: "info",
|
|
45739
46053
|
...input,
|
|
45740
46054
|
ts: Date.now()
|
|
@@ -48127,7 +48441,7 @@ var src_default = Yoga;
|
|
|
48127
48441
|
|
|
48128
48442
|
// ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
|
|
48129
48443
|
var import_react_reconciler = __toESM(require_react_reconciler(), 1);
|
|
48130
|
-
var
|
|
48444
|
+
var import_constants12 = __toESM(require_constants2(), 1);
|
|
48131
48445
|
import process3 from "process";
|
|
48132
48446
|
|
|
48133
48447
|
// ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
|
|
@@ -49369,7 +49683,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
|
|
|
49369
49683
|
scheduleTimeout: setTimeout,
|
|
49370
49684
|
cancelTimeout: clearTimeout,
|
|
49371
49685
|
noTimeout: -1,
|
|
49372
|
-
getCurrentEventPriority: () =>
|
|
49686
|
+
getCurrentEventPriority: () => import_constants12.DefaultEventPriority,
|
|
49373
49687
|
beforeActiveInstanceBlur() {
|
|
49374
49688
|
},
|
|
49375
49689
|
afterActiveInstanceBlur() {
|