@4pm/cli 1.14.0 → 1.18.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 +482 -167
- package/dist/project-sample/.claude/.autonomous.settings.json +9 -11
- package/dist/project-sample/.claude/AUTONOMOUS-CRON.md +5 -4
- package/dist/project-sample/.claude/AUTONOMOUS.md +26 -26
- package/dist/project-sample/.claude/commands/auto-cycle.md +16 -170
- package/dist/project-sample/.claude/hooks/autonomous-tick.sh +76 -193
- package/dist/project-sample/.claude/settings.json +1 -3
- package/dist/project-sample/.claude/templates/AI_TODO.empty.md +6 -4
- package/dist/project-sample/.claude/templates/AI_TODO.sample.md +9 -6
- package/dist/project-sample/.claude/templates/USER_QA.empty.md +13 -6
- package/dist/project-sample/.claude/templates/USER_QA.sample.md +6 -3
- package/dist/project-sample/.claude/templates/USER_TODO.empty.md +13 -6
- package/dist/project-sample/.claude/templates/USER_TODO.sample.md +7 -6
- package/dist/project-sample/.claude/templates/project.secrets.sample.json +0 -1
- package/dist/project-sample/AI_PLACEHOLDER.md +5 -5
- package/dist/project-sample/AI_TODO.md +6 -4
- package/dist/project-sample/USER_QA.md +13 -6
- package/dist/project-sample/USER_TODO.md +13 -6
- package/dist/project-sample/project.secrets.json.sample +0 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28873,7 +28873,11 @@ var en_default = {
|
|
|
28873
28873
|
"slash.claudeCmd.usage": "usage: /claude-cmd /context (forwards to the AI CLI)",
|
|
28874
28874
|
"start.connecting": "Connecting to {server} (profile: {profile})\u2026",
|
|
28875
28875
|
"start.recovered": "Recovered from an internal error \u2014 reconnecting: {message}",
|
|
28876
|
-
"attach.attached": 'Attached to profile "{profile}" \u2014 the worker runs headless; input is forwarded.'
|
|
28876
|
+
"attach.attached": 'Attached to profile "{profile}" \u2014 the worker runs headless; input is forwarded.',
|
|
28877
|
+
"autoRun.done": "Autonomous cycle complete.",
|
|
28878
|
+
"autoRun.failed": "Autonomous cycle failed to dispatch: {note}",
|
|
28879
|
+
"autoRun.noDaemon": 'No running daemon for profile "{profile}" \u2014 start it first: 4pm start --profile {profile}',
|
|
28880
|
+
"autoRun.timeout": "Autonomous cycle exceeded the time limit \u2014 aborting."
|
|
28877
28881
|
};
|
|
28878
28882
|
|
|
28879
28883
|
// src/i18n/messages/vi.json
|
|
@@ -29051,7 +29055,11 @@ var vi_default = {
|
|
|
29051
29055
|
"slash.claudeCmd.usage": "c\xE1ch d\xF9ng: /claude-cmd /context (chuy\u1EC3n ti\u1EBFp t\u1EDBi AI CLI)",
|
|
29052
29056
|
"start.connecting": "\u0110ang k\u1EBFt n\u1ED1i t\u1EDBi {server} (h\u1ED3 s\u01A1: {profile})\u2026",
|
|
29053
29057
|
"start.recovered": "\u0110\xE3 kh\xF4i ph\u1EE5c sau l\u1ED7i n\u1ED9i b\u1ED9 \u2014 \u0111ang k\u1EBFt n\u1ED1i l\u1EA1i: {message}",
|
|
29054
|
-
"attach.attached": '\u0110\xE3 g\u1EAFn v\xE0o h\u1ED3 s\u01A1 "{profile}" \u2014 worker ch\u1EA1y kh\xF4ng giao di\u1EC7n; \u0111\u1EA7u v\xE0o \u0111\u01B0\u1EE3c chuy\u1EC3n ti\u1EBFp.'
|
|
29058
|
+
"attach.attached": '\u0110\xE3 g\u1EAFn v\xE0o h\u1ED3 s\u01A1 "{profile}" \u2014 worker ch\u1EA1y kh\xF4ng giao di\u1EC7n; \u0111\u1EA7u v\xE0o \u0111\u01B0\u1EE3c chuy\u1EC3n ti\u1EBFp.',
|
|
29059
|
+
"autoRun.done": "Chu k\u1EF3 t\u1EF1 \u0111\u1ED9ng \u0111\xE3 ho\xE0n t\u1EA5t.",
|
|
29060
|
+
"autoRun.failed": "Kh\xF4ng \u0111i\u1EC1u ph\u1ED1i \u0111\u01B0\u1EE3c chu k\u1EF3 t\u1EF1 \u0111\u1ED9ng: {note}",
|
|
29061
|
+
"autoRun.noDaemon": 'Kh\xF4ng c\xF3 ti\u1EBFn tr\xECnh n\u1EC1n n\xE0o cho h\u1ED3 s\u01A1 "{profile}" \u2014 h\xE3y kh\u1EDFi \u0111\u1ED9ng tr\u01B0\u1EDBc: 4pm start --profile {profile}',
|
|
29062
|
+
"autoRun.timeout": "Chu k\u1EF3 t\u1EF1 \u0111\u1ED9ng v\u01B0\u1EE3t qu\xE1 gi\u1EDBi h\u1EA1n th\u1EDDi gian \u2014 \u0111ang h\u1EE7y."
|
|
29055
29063
|
};
|
|
29056
29064
|
|
|
29057
29065
|
// src/i18n/messages/ja.json
|
|
@@ -29229,7 +29237,11 @@ var ja_default = {
|
|
|
29229
29237
|
"slash.claudeCmd.usage": "\u4F7F\u3044\u65B9: /claude-cmd /context (AI CLI \u306B\u8EE2\u9001)",
|
|
29230
29238
|
"start.connecting": "{server} \u306B\u63A5\u7D9A\u4E2D\uFF08\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB: {profile}\uFF09\u2026",
|
|
29231
29239
|
"start.recovered": "\u5185\u90E8\u30A8\u30E9\u30FC\u304B\u3089\u56DE\u5FA9\u3057\u307E\u3057\u305F \u2014 \u518D\u63A5\u7D9A\u4E2D: {message}",
|
|
29232
|
-
"attach.attached": "\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB\u300C{profile}\u300D\u306B\u30A2\u30BF\u30C3\u30C1\u3057\u307E\u3057\u305F \u2014 worker \u306F\u30D8\u30C3\u30C9\u30EC\u30B9\u3067\u52D5\u4F5C\u3057\u3001\u5165\u529B\u306F\u8EE2\u9001\u3055\u308C\u307E\u3059\u3002"
|
|
29240
|
+
"attach.attached": "\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB\u300C{profile}\u300D\u306B\u30A2\u30BF\u30C3\u30C1\u3057\u307E\u3057\u305F \u2014 worker \u306F\u30D8\u30C3\u30C9\u30EC\u30B9\u3067\u52D5\u4F5C\u3057\u3001\u5165\u529B\u306F\u8EE2\u9001\u3055\u308C\u307E\u3059\u3002",
|
|
29241
|
+
"autoRun.done": "\u81EA\u5F8B\u30B5\u30A4\u30AF\u30EB\u304C\u5B8C\u4E86\u3057\u307E\u3057\u305F\u3002",
|
|
29242
|
+
"autoRun.failed": "\u81EA\u5F8B\u30B5\u30A4\u30AF\u30EB\u306E\u30C7\u30A3\u30B9\u30D1\u30C3\u30C1\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {note}",
|
|
29243
|
+
"autoRun.noDaemon": '\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB "{profile}" \u306E\u30C7\u30FC\u30E2\u30F3\u304C\u5B9F\u884C\u3055\u308C\u3066\u3044\u307E\u305B\u3093 \u2014 \u5148\u306B\u8D77\u52D5\u3057\u3066\u304F\u3060\u3055\u3044: 4pm start --profile {profile}',
|
|
29244
|
+
"autoRun.timeout": "\u81EA\u5F8B\u30B5\u30A4\u30AF\u30EB\u304C\u5236\u9650\u6642\u9593\u3092\u8D85\u3048\u307E\u3057\u305F \u2014 \u4E2D\u6B62\u3057\u307E\u3059\u3002"
|
|
29233
29245
|
};
|
|
29234
29246
|
|
|
29235
29247
|
// src/i18n/messages/zh.json
|
|
@@ -29407,7 +29419,11 @@ var zh_default = {
|
|
|
29407
29419
|
"slash.claudeCmd.usage": "\u7528\u6CD5\uFF1A/claude-cmd /context (\u8F6C\u53D1\u7ED9 AI CLI)",
|
|
29408
29420
|
"start.connecting": "\u6B63\u5728\u8FDE\u63A5\u5230 {server}\uFF08\u914D\u7F6E\u6587\u4EF6\uFF1A{profile}\uFF09\u2026",
|
|
29409
29421
|
"start.recovered": "\u5DF2\u4ECE\u5185\u90E8\u9519\u8BEF\u4E2D\u6062\u590D \u2014 \u6B63\u5728\u91CD\u65B0\u8FDE\u63A5\uFF1A{message}",
|
|
29410
|
-
"attach.attached": "\u5DF2\u9644\u52A0\u5230\u914D\u7F6E\u6587\u4EF6 \u201C{profile}\u201D \u2014 worker \u4EE5\u65E0\u754C\u9762\u65B9\u5F0F\u8FD0\u884C\uFF1B\u8F93\u5165\u5C06\u88AB\u8F6C\u53D1\u3002"
|
|
29422
|
+
"attach.attached": "\u5DF2\u9644\u52A0\u5230\u914D\u7F6E\u6587\u4EF6 \u201C{profile}\u201D \u2014 worker \u4EE5\u65E0\u754C\u9762\u65B9\u5F0F\u8FD0\u884C\uFF1B\u8F93\u5165\u5C06\u88AB\u8F6C\u53D1\u3002",
|
|
29423
|
+
"autoRun.done": "\u81EA\u4E3B\u5FAA\u73AF\u5DF2\u5B8C\u6210\u3002",
|
|
29424
|
+
"autoRun.failed": "\u81EA\u4E3B\u5FAA\u73AF\u8C03\u5EA6\u5931\u8D25\uFF1A{note}",
|
|
29425
|
+
"autoRun.noDaemon": "\u914D\u7F6E\u6587\u4EF6 \u201C{profile}\u201D \u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u5B88\u62A4\u8FDB\u7A0B \u2014 \u8BF7\u5148\u542F\u52A8\uFF1A4pm start --profile {profile}",
|
|
29426
|
+
"autoRun.timeout": "\u81EA\u4E3B\u5FAA\u73AF\u8D85\u8FC7\u65F6\u95F4\u9650\u5236 \u2014 \u6B63\u5728\u4E2D\u6B62\u3002"
|
|
29411
29427
|
};
|
|
29412
29428
|
|
|
29413
29429
|
// src/i18n/messages/index.ts
|
|
@@ -32706,7 +32722,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
32706
32722
|
|
|
32707
32723
|
// src/core/ws-client.ts
|
|
32708
32724
|
init_wrapper();
|
|
32709
|
-
import { randomUUID as
|
|
32725
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
32710
32726
|
import { basename as basename7, join as join28 } from "path";
|
|
32711
32727
|
import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
|
|
32712
32728
|
|
|
@@ -37098,10 +37114,32 @@ var taskApproveRequestSchema = external_exports.object({
|
|
|
37098
37114
|
taskId: external_exports.string().min(1).max(64),
|
|
37099
37115
|
approved: external_exports.boolean()
|
|
37100
37116
|
});
|
|
37117
|
+
var taskApproveEntrySchema = external_exports.object({
|
|
37118
|
+
taskId: external_exports.string().min(1).max(64),
|
|
37119
|
+
tags: external_exports.array(external_exports.string().min(1).max(64)).max(16).default([])
|
|
37120
|
+
});
|
|
37121
|
+
var taskApproveBatchRequestSchema = external_exports.object({
|
|
37122
|
+
approvals: external_exports.array(taskApproveEntrySchema).max(500).default([]),
|
|
37123
|
+
unapprovals: external_exports.array(external_exports.string().min(1).max(64)).max(500).default([])
|
|
37124
|
+
});
|
|
37101
37125
|
var autonomousWriteRequestSchema = external_exports.discriminatedUnion("kind", [
|
|
37102
37126
|
external_exports.object({ kind: external_exports.literal("settings"), settings: external_exports.string().max(64 * 1024) }),
|
|
37103
37127
|
external_exports.object({ kind: external_exports.literal("approvals"), taskId: external_exports.string().min(1).max(64), approved: external_exports.boolean() }),
|
|
37128
|
+
// Batched approvals (ADR-0311): commit many approve/unapprove ids in ONE approvals-file write, so a
|
|
37129
|
+
// Save's coupled batch lands atomically. Constructed by the server after its tag actions succeed.
|
|
37130
|
+
external_exports.object({
|
|
37131
|
+
kind: external_exports.literal("approvalsBatch"),
|
|
37132
|
+
approve: external_exports.array(external_exports.string().min(1).max(64)).max(500),
|
|
37133
|
+
unapprove: external_exports.array(external_exports.string().min(1).max(64)).max(500)
|
|
37134
|
+
}),
|
|
37104
37135
|
external_exports.object({ kind: external_exports.literal("userTodo"), content: external_exports.string().min(1).max(16 * 1024) }),
|
|
37136
|
+
// Traced book save (ADR-0320): the cli diffs rows by id + stamps `.autonomous.authors.json` so
|
|
37137
|
+
// authorship is server-filled, not a spoofable `.md` cell. `by`/author are added server-side.
|
|
37138
|
+
external_exports.object({
|
|
37139
|
+
kind: external_exports.literal("bookSave"),
|
|
37140
|
+
book: external_exports.enum(["userTodo", "userQa", "aiTodo"]),
|
|
37141
|
+
content: external_exports.string().max(256 * 1024)
|
|
37142
|
+
}),
|
|
37105
37143
|
external_exports.object({ kind: external_exports.literal("cron"), action: external_exports.enum(["install", "uninstall"]) })
|
|
37106
37144
|
]);
|
|
37107
37145
|
var autonomousLogsQuerySchema = external_exports.object({
|
|
@@ -37288,11 +37326,14 @@ var repoSpecSchema = external_exports.object({
|
|
|
37288
37326
|
role: external_exports.string().max(60).optional().default(""),
|
|
37289
37327
|
/** Free-form description of what this repo holds (optional). */
|
|
37290
37328
|
desc: external_exports.string().max(500).optional().default(""),
|
|
37291
|
-
/** Exactly one repo of the project is the primary. */
|
|
37329
|
+
/** Exactly one repo of the project is the primary (the root); the rest are submodules. */
|
|
37292
37330
|
primary: external_exports.boolean().optional().default(false),
|
|
37293
37331
|
/** Clone url — `https` or `ssh` (required; ADR-0172). */
|
|
37294
37332
|
url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
|
|
37295
|
-
/**
|
|
37333
|
+
/**
|
|
37334
|
+
* Folder under the root: **empty for the primary** (the root itself), **non-empty and unique for a
|
|
37335
|
+
* submodule** — the path `git submodule add` attaches it at (ADR-0316).
|
|
37336
|
+
*/
|
|
37296
37337
|
subdir: external_exports.string().max(120).optional().default(""),
|
|
37297
37338
|
/**
|
|
37298
37339
|
* Primary branch to clone / check out (ADR-0292). Empty ⇒ the repo's default branch. The
|
|
@@ -37301,9 +37342,38 @@ var repoSpecSchema = external_exports.object({
|
|
|
37301
37342
|
*/
|
|
37302
37343
|
branch: external_exports.string().max(200).optional().default("")
|
|
37303
37344
|
});
|
|
37304
|
-
function
|
|
37305
|
-
const
|
|
37306
|
-
|
|
37345
|
+
function validateProjectRepos(repos, ctx, path = ["repos"]) {
|
|
37346
|
+
const list = repos ?? [];
|
|
37347
|
+
const issue = (message) => ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path, message });
|
|
37348
|
+
if (list.length < 1) {
|
|
37349
|
+
issue("a project must declare at least one repo");
|
|
37350
|
+
return;
|
|
37351
|
+
}
|
|
37352
|
+
const primaries = list.filter((r) => r.primary);
|
|
37353
|
+
if (primaries.length !== 1) {
|
|
37354
|
+
issue("exactly one repo must be primary");
|
|
37355
|
+
}
|
|
37356
|
+
for (const r of list) {
|
|
37357
|
+
if (r.primary) {
|
|
37358
|
+
if ((r.subdir ?? "").trim() !== "") issue("the primary repo must be at the root (no subfolder)");
|
|
37359
|
+
} else if ((r.subdir ?? "").trim() === "") {
|
|
37360
|
+
issue("each submodule needs a folder path");
|
|
37361
|
+
}
|
|
37362
|
+
}
|
|
37363
|
+
const seenDir = /* @__PURE__ */ new Set();
|
|
37364
|
+
for (const r of list) {
|
|
37365
|
+
const dir2 = (r.subdir ?? "").trim().toLowerCase();
|
|
37366
|
+
if (!dir2) continue;
|
|
37367
|
+
if (seenDir.has(dir2)) issue("submodule folder paths must be unique");
|
|
37368
|
+
seenDir.add(dir2);
|
|
37369
|
+
}
|
|
37370
|
+
const seenUrl = /* @__PURE__ */ new Set();
|
|
37371
|
+
for (const r of list) {
|
|
37372
|
+
const url = (r.url ?? "").trim().toLowerCase();
|
|
37373
|
+
if (!url) continue;
|
|
37374
|
+
if (seenUrl.has(url)) issue("repo URLs must be unique");
|
|
37375
|
+
seenUrl.add(url);
|
|
37376
|
+
}
|
|
37307
37377
|
}
|
|
37308
37378
|
var specFieldEnvelopeSchema = external_exports.object({
|
|
37309
37379
|
id: external_exports.string().min(1),
|
|
@@ -37335,16 +37405,7 @@ var projectSpecSchema = external_exports.object({
|
|
|
37335
37405
|
});
|
|
37336
37406
|
}
|
|
37337
37407
|
}
|
|
37338
|
-
|
|
37339
|
-
const primaries = v2.repos.filter((r) => r.primary).length;
|
|
37340
|
-
if (primaries !== 1) {
|
|
37341
|
-
ctx.addIssue({
|
|
37342
|
-
code: external_exports.ZodIssueCode.custom,
|
|
37343
|
-
path: ["repos"],
|
|
37344
|
-
message: "exactly one primary repo is required"
|
|
37345
|
-
});
|
|
37346
|
-
}
|
|
37347
|
-
}
|
|
37408
|
+
validateProjectRepos(v2.repos, ctx);
|
|
37348
37409
|
});
|
|
37349
37410
|
var createFromSpecRequestSchema = external_exports.object({
|
|
37350
37411
|
projectId: external_exports.string().uuid(),
|
|
@@ -37354,14 +37415,7 @@ var addRepoRequestSchema = external_exports.object({
|
|
|
37354
37415
|
projectId: external_exports.string().uuid(),
|
|
37355
37416
|
repos: external_exports.array(repoSpecSchema).min(1)
|
|
37356
37417
|
}).superRefine((v2, ctx) => {
|
|
37357
|
-
|
|
37358
|
-
if (primaries !== 1) {
|
|
37359
|
-
ctx.addIssue({
|
|
37360
|
-
code: external_exports.ZodIssueCode.custom,
|
|
37361
|
-
path: ["repos"],
|
|
37362
|
-
message: "Exactly one repo must be primary"
|
|
37363
|
-
});
|
|
37364
|
-
}
|
|
37418
|
+
validateProjectRepos(v2.repos, ctx);
|
|
37365
37419
|
});
|
|
37366
37420
|
var addProjectRepoRequestSchema = external_exports.object({
|
|
37367
37421
|
/** Clone url — `https` or `ssh` (required; ADR-0172). */
|
|
@@ -37439,7 +37493,7 @@ var activityHeartbeatSchema = external_exports.object({
|
|
|
37439
37493
|
});
|
|
37440
37494
|
|
|
37441
37495
|
// src/core/scaffold.ts
|
|
37442
|
-
import { cp, mkdir,
|
|
37496
|
+
import { cp, mkdir, rm, writeFile } from "fs/promises";
|
|
37443
37497
|
import { existsSync as existsSync4 } from "fs";
|
|
37444
37498
|
import { execFile as execFile2 } from "child_process";
|
|
37445
37499
|
import { dirname as dirname3, join as join6, resolve } from "path";
|
|
@@ -37524,89 +37578,111 @@ function reposOf(spec) {
|
|
|
37524
37578
|
const repos = spec?.repos;
|
|
37525
37579
|
return Array.isArray(repos) ? repos : [];
|
|
37526
37580
|
}
|
|
37527
|
-
function
|
|
37528
|
-
|
|
37529
|
-
|
|
37530
|
-
|
|
37531
|
-
|
|
37532
|
-
|
|
37533
|
-
|
|
37534
|
-
|
|
37535
|
-
|
|
37536
|
-
|
|
37537
|
-
|
|
37538
|
-
|
|
37539
|
-
|
|
37540
|
-
|
|
37581
|
+
function singleRepo(repos) {
|
|
37582
|
+
return repos.find((r) => r.primary) ?? repos[0] ?? null;
|
|
37583
|
+
}
|
|
37584
|
+
function submodulesOf(repos) {
|
|
37585
|
+
const primary = singleRepo(repos);
|
|
37586
|
+
return repos.filter((r) => r !== primary && !!r.url && !!(r.subdir ?? "").trim());
|
|
37587
|
+
}
|
|
37588
|
+
async function attachSubmodules(root, submodules, emit) {
|
|
37589
|
+
if (submodules.length === 0) return;
|
|
37590
|
+
if (!existsSync4(join6(root, ".git"))) return;
|
|
37591
|
+
let added = 0;
|
|
37592
|
+
for (const sub of submodules) {
|
|
37593
|
+
const dir2 = (sub.subdir ?? "").trim();
|
|
37594
|
+
if (!dir2 || !sub.url) continue;
|
|
37595
|
+
if (existsSync4(join6(root, dir2, ".git")) || existsSync4(join6(root, ".git", "modules", dir2))) {
|
|
37596
|
+
emit("submodule", `Submodule ${dir2} already present \u2014 updating\u2026`);
|
|
37597
|
+
try {
|
|
37598
|
+
await run2("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37599
|
+
} catch {
|
|
37600
|
+
}
|
|
37601
|
+
continue;
|
|
37602
|
+
}
|
|
37603
|
+
emit("submodule", `Adding submodule ${sub.url} \u2192 ${dir2}\u2026`);
|
|
37604
|
+
const b2 = (sub.branch ?? "").trim();
|
|
37605
|
+
try {
|
|
37606
|
+
await run2("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
|
|
37607
|
+
added++;
|
|
37608
|
+
} catch (err) {
|
|
37609
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37610
|
+
if (/already exists|already registered|in the index/i.test(msg)) {
|
|
37611
|
+
try {
|
|
37612
|
+
await run2("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37613
|
+
added++;
|
|
37614
|
+
} catch {
|
|
37615
|
+
}
|
|
37616
|
+
} else {
|
|
37617
|
+
throw new Error(`git submodule add ${dir2} failed: ${msg}`);
|
|
37618
|
+
}
|
|
37619
|
+
}
|
|
37620
|
+
}
|
|
37621
|
+
if (added > 0) await commitAndPushSubmodules(root, submodules, emit);
|
|
37622
|
+
}
|
|
37623
|
+
async function commitAndPushSubmodules(root, submodules, emit) {
|
|
37624
|
+
const dirs = submodules.map((s3) => (s3.subdir ?? "").trim()).filter(Boolean);
|
|
37625
|
+
emit("submodule", "Committing .gitmodules\u2026");
|
|
37626
|
+
await run2("git", ["add", ".gitmodules", ...dirs], { cwd: root, timeout: 6e4 });
|
|
37627
|
+
const staged = (await run2("git", ["diff", "--cached", "--name-only"], { cwd: root, timeout: 3e4 })).stdout.trim();
|
|
37628
|
+
if (!staged) return;
|
|
37629
|
+
const commitArgs = ["commit", "-m", "chore: add git submodules (4PM)"];
|
|
37630
|
+
try {
|
|
37631
|
+
await run2("git", commitArgs, { cwd: root, timeout: 6e4 });
|
|
37632
|
+
} catch {
|
|
37633
|
+
await run2("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", ...commitArgs], { cwd: root, timeout: 6e4 });
|
|
37634
|
+
}
|
|
37635
|
+
emit("submodule", "Pushing .gitmodules to the primary remote\u2026");
|
|
37636
|
+
try {
|
|
37637
|
+
await run2("git", ["push"], { cwd: root, timeout: 12e4 });
|
|
37638
|
+
} catch (err) {
|
|
37639
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37640
|
+
emit("submodule-push-failed", `Submodules committed locally but the push failed: ${msg}`);
|
|
37641
|
+
}
|
|
37541
37642
|
}
|
|
37542
37643
|
function cloneArgs(url, dest, branch) {
|
|
37543
37644
|
const b2 = (branch ?? "").trim();
|
|
37544
37645
|
return b2 ? ["clone", "-b", b2, url, dest] : ["clone", url, dest];
|
|
37545
37646
|
}
|
|
37546
|
-
async function updateRepo(dir2,
|
|
37647
|
+
async function updateRepo(dir2, branch, emit) {
|
|
37547
37648
|
const b2 = (branch ?? "").trim();
|
|
37548
|
-
emit("git", `Updating
|
|
37649
|
+
emit("git", `Updating repository (fetch + ${b2 ? `checkout ${b2} + ` : ""}fast-forward pull)\u2026`);
|
|
37549
37650
|
await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
|
|
37550
37651
|
if (b2) await run2("git", ["checkout", b2], { cwd: dir2, timeout: 6e4 });
|
|
37551
37652
|
await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
|
|
37552
37653
|
}
|
|
37553
|
-
function
|
|
37554
|
-
if (repo.primary && legacyRoot) return target;
|
|
37555
|
-
return join6(target, folder);
|
|
37556
|
-
}
|
|
37557
|
-
async function provisionRepos(target, repos, emit, opts = {}) {
|
|
37654
|
+
async function provisionRepo(target, repo, emit, opts = {}) {
|
|
37558
37655
|
const mode = opts.mode ?? "clone";
|
|
37559
|
-
|
|
37560
|
-
|
|
37561
|
-
|
|
37562
|
-
|
|
37563
|
-
|
|
37564
|
-
for (const [i, repo] of repos.entries()) {
|
|
37565
|
-
const folder = folders[i];
|
|
37566
|
-
const atRoot = !!repo.primary && legacyRoot;
|
|
37567
|
-
const dir2 = repoDir(target, repo, folder, legacyRoot);
|
|
37568
|
-
const label = repo.primary ? "primary repo" : `repo ${folder}`;
|
|
37569
|
-
if (!repo.url) {
|
|
37570
|
-
if (!existsSync4(join6(dir2, ".git"))) {
|
|
37571
|
-
emit("git", `Initializing ${label}\u2026`);
|
|
37572
|
-
await mkdir(dir2, { recursive: true });
|
|
37573
|
-
await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
37574
|
-
}
|
|
37575
|
-
continue;
|
|
37656
|
+
if (!repo.url) {
|
|
37657
|
+
if (!existsSync4(join6(target, ".git"))) {
|
|
37658
|
+
emit("git", "Initializing repository\u2026");
|
|
37659
|
+
await mkdir(target, { recursive: true });
|
|
37660
|
+
await run2("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
37576
37661
|
}
|
|
37577
|
-
|
|
37578
|
-
|
|
37579
|
-
|
|
37580
|
-
|
|
37581
|
-
|
|
37582
|
-
|
|
37583
|
-
|
|
37584
|
-
|
|
37585
|
-
|
|
37586
|
-
|
|
37587
|
-
|
|
37588
|
-
}
|
|
37589
|
-
for (const entry of await readdir(tmp)) {
|
|
37590
|
-
if (foldersToKeep.has(entry)) continue;
|
|
37591
|
-
await rename(join6(tmp, entry), join6(target, entry));
|
|
37592
|
-
}
|
|
37593
|
-
await rm(tmp, { recursive: true, force: true });
|
|
37594
|
-
} else if (mode === "force") {
|
|
37595
|
-
emit("git", `Re-cloning ${repo.url} (force)\u2026`);
|
|
37596
|
-
await rm(dir2, { recursive: true, force: true });
|
|
37597
|
-
await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
|
|
37598
|
-
} else {
|
|
37599
|
-
emit("git", `${label} already present \u2014 skipping clone.`);
|
|
37600
|
-
}
|
|
37601
|
-
continue;
|
|
37662
|
+
return;
|
|
37663
|
+
}
|
|
37664
|
+
if (existsSync4(join6(target, ".git"))) {
|
|
37665
|
+
if (mode === "sync") {
|
|
37666
|
+
await updateRepo(target, repo.branch, emit);
|
|
37667
|
+
} else if (mode === "force") {
|
|
37668
|
+
emit("git", `Re-cloning ${repo.url} (force)\u2026`);
|
|
37669
|
+
await rm(target, { recursive: true, force: true });
|
|
37670
|
+
await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
37671
|
+
} else {
|
|
37672
|
+
emit("git", "Repository already present \u2014 skipping clone.");
|
|
37602
37673
|
}
|
|
37603
|
-
|
|
37604
|
-
await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
|
|
37674
|
+
return;
|
|
37605
37675
|
}
|
|
37676
|
+
emit("git", `Cloning ${repo.url}\u2026`);
|
|
37677
|
+
await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
|
|
37606
37678
|
}
|
|
37607
37679
|
async function ensureReposCloned(physicRoot, repos, emit) {
|
|
37608
|
-
|
|
37609
|
-
|
|
37680
|
+
const list = repos;
|
|
37681
|
+
const repo = singleRepo(list);
|
|
37682
|
+
if (!repo) return;
|
|
37683
|
+
const step = emit ?? (() => void 0);
|
|
37684
|
+
await provisionRepo(physicRoot, repo, step);
|
|
37685
|
+
await attachSubmodules(physicRoot, submodulesOf(list), step);
|
|
37610
37686
|
}
|
|
37611
37687
|
function sampleDir() {
|
|
37612
37688
|
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
@@ -37627,17 +37703,12 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
|
37627
37703
|
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
37628
37704
|
const target = join6(root, payload.projectName);
|
|
37629
37705
|
await mkdir(target, { recursive: true });
|
|
37630
|
-
const
|
|
37631
|
-
if (
|
|
37632
|
-
|
|
37633
|
-
|
|
37634
|
-
|
|
37635
|
-
|
|
37636
|
-
await scaffoldRepo(join6(target, folders[i]), folders[i], payload.spec, emit);
|
|
37637
|
-
}
|
|
37638
|
-
} else {
|
|
37639
|
-
await scaffoldRepo(target, payload.projectName, payload.spec, emit);
|
|
37640
|
-
}
|
|
37706
|
+
const repo = singleRepo(reposOf(payload.spec));
|
|
37707
|
+
if (!repo) throw new Error("A project must declare one repo (ADR-0314).");
|
|
37708
|
+
emit("git", "Cloning repository\u2026");
|
|
37709
|
+
await provisionRepo(target, repo, emit);
|
|
37710
|
+
await scaffoldRepo(target, payload.projectName, payload.spec, emit);
|
|
37711
|
+
await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit);
|
|
37641
37712
|
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
37642
37713
|
await writeTemplateMarker(target);
|
|
37643
37714
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
@@ -37723,12 +37794,13 @@ async function addProject(payload, profileDir2, onProgress) {
|
|
|
37723
37794
|
const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
|
|
37724
37795
|
const target = join6(root, payload.projectName);
|
|
37725
37796
|
await mkdir(target, { recursive: true });
|
|
37726
|
-
const
|
|
37797
|
+
const repo = singleRepo(payload.repos ?? []);
|
|
37727
37798
|
const mode = payload.mode ?? "clone";
|
|
37728
|
-
if (
|
|
37729
|
-
|
|
37730
|
-
await scaffoldRepo(
|
|
37799
|
+
if (repo) await provisionRepo(target, repo, emit, { mode });
|
|
37800
|
+
if (payload.scaffoldRepos && payload.scaffoldRepos.length > 0) {
|
|
37801
|
+
await scaffoldRepo(target, payload.projectName, payload.spec, emit);
|
|
37731
37802
|
}
|
|
37803
|
+
await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit);
|
|
37732
37804
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
37733
37805
|
return { ok: true, path: target };
|
|
37734
37806
|
} catch (err) {
|
|
@@ -38227,7 +38299,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
38227
38299
|
import { spawn as spawn2 } from "child_process";
|
|
38228
38300
|
|
|
38229
38301
|
// src/common/logger/logger.ts
|
|
38230
|
-
import { appendFile, mkdir as mkdir2, readdir
|
|
38302
|
+
import { appendFile, mkdir as mkdir2, readdir, unlink } from "fs/promises";
|
|
38231
38303
|
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
38232
38304
|
import { join as join7 } from "path";
|
|
38233
38305
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
@@ -38287,7 +38359,7 @@ var CliLogger = class {
|
|
|
38287
38359
|
async cleanupOldLogs() {
|
|
38288
38360
|
const dir2 = this.dir;
|
|
38289
38361
|
if (!dir2) return;
|
|
38290
|
-
const files = await
|
|
38362
|
+
const files = await readdir(dir2).catch(() => []);
|
|
38291
38363
|
const cutoff = Date.now() - this.retentionDays * DAY_MS;
|
|
38292
38364
|
for (const file of files) {
|
|
38293
38365
|
const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
|
|
@@ -38823,7 +38895,6 @@ var ONE_SHOT_DISALLOWED_CLAUDE_TOOLS = [
|
|
|
38823
38895
|
"WebSearch",
|
|
38824
38896
|
"WebFetch",
|
|
38825
38897
|
"Skill",
|
|
38826
|
-
"SlashCommand",
|
|
38827
38898
|
"ToolSearch",
|
|
38828
38899
|
"TaskCreate",
|
|
38829
38900
|
"TaskGet",
|
|
@@ -39564,7 +39635,7 @@ var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
|
|
|
39564
39635
|
function kbRoot(profileDir2) {
|
|
39565
39636
|
return join14(profileDir2, "support-kb");
|
|
39566
39637
|
}
|
|
39567
|
-
function
|
|
39638
|
+
function repoDir(profileDir2, url, branch) {
|
|
39568
39639
|
const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
|
|
39569
39640
|
return join14(kbRoot(profileDir2), key);
|
|
39570
39641
|
}
|
|
@@ -39574,7 +39645,7 @@ function authedUrl(url, token) {
|
|
|
39574
39645
|
return url;
|
|
39575
39646
|
}
|
|
39576
39647
|
async function ensureRepo(profileDir2, repo) {
|
|
39577
|
-
const dir2 =
|
|
39648
|
+
const dir2 = repoDir(profileDir2, repo.url, repo.branch);
|
|
39578
39649
|
if (existsSync10(join14(dir2, ".git"))) return dir2;
|
|
39579
39650
|
const url = authedUrl(repo.url, repo.token);
|
|
39580
39651
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
@@ -39825,6 +39896,7 @@ var run4 = promisify4(execFile4);
|
|
|
39825
39896
|
var SETTINGS_REL = ".claude/.autonomous.settings.json";
|
|
39826
39897
|
var HISTORIES_REL = ".claude/.autonomous.histories.json";
|
|
39827
39898
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
39899
|
+
var AUTHORS_REL = ".claude/.autonomous.authors.json";
|
|
39828
39900
|
var TICK_REL = ".claude/hooks/autonomous-tick.sh";
|
|
39829
39901
|
var LOG_DIR_REL = ".claude/logs";
|
|
39830
39902
|
var BOOK_FILES = {
|
|
@@ -39853,6 +39925,35 @@ function parseJson(text) {
|
|
|
39853
39925
|
function tableCell(value) {
|
|
39854
39926
|
return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
|
39855
39927
|
}
|
|
39928
|
+
async function readJsonMap(path) {
|
|
39929
|
+
return parseJson(await readText(path, "{}")) ?? {};
|
|
39930
|
+
}
|
|
39931
|
+
function entryBy(map, id) {
|
|
39932
|
+
const e = map[id];
|
|
39933
|
+
return e && typeof e === "object" && typeof e.by === "string" ? e.by : null;
|
|
39934
|
+
}
|
|
39935
|
+
function tableRowsById(md) {
|
|
39936
|
+
const rows = /* @__PURE__ */ new Map();
|
|
39937
|
+
let headerSeen = false;
|
|
39938
|
+
let inTable = false;
|
|
39939
|
+
for (const line of md.split(/\r?\n/)) {
|
|
39940
|
+
const s3 = line.trim();
|
|
39941
|
+
if (!s3.startsWith("|")) {
|
|
39942
|
+
if (inTable) break;
|
|
39943
|
+
continue;
|
|
39944
|
+
}
|
|
39945
|
+
inTable = true;
|
|
39946
|
+
if (!headerSeen) {
|
|
39947
|
+
headerSeen = true;
|
|
39948
|
+
continue;
|
|
39949
|
+
}
|
|
39950
|
+
if (/^\|[\s:|-]+\|?$/.test(s3)) continue;
|
|
39951
|
+
const cells = s3.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
39952
|
+
const id = cells[0] ?? "";
|
|
39953
|
+
if (id) rows.set(id, cells.join(""));
|
|
39954
|
+
}
|
|
39955
|
+
return rows;
|
|
39956
|
+
}
|
|
39856
39957
|
function tickScript(root) {
|
|
39857
39958
|
return join15(root, TICK_REL);
|
|
39858
39959
|
}
|
|
@@ -39907,16 +40008,17 @@ function isAutonomousRunning(root) {
|
|
|
39907
40008
|
}
|
|
39908
40009
|
}
|
|
39909
40010
|
async function readAutonomous(root) {
|
|
39910
|
-
const [settings, approvals, status, ...books] = await Promise.all([
|
|
40011
|
+
const [settings, approvals, authors, status, ...books] = await Promise.all([
|
|
39911
40012
|
readText(join15(root, SETTINGS_REL)),
|
|
39912
40013
|
readText(join15(root, APPROVALS_REL), "{}"),
|
|
40014
|
+
readText(join15(root, AUTHORS_REL), "{}"),
|
|
39913
40015
|
getAutonomousStatus(root),
|
|
39914
40016
|
...Object.values(BOOK_FILES).map((f2) => readText(join15(root, f2)))
|
|
39915
40017
|
]);
|
|
39916
40018
|
const keys = Object.keys(BOOK_FILES);
|
|
39917
40019
|
const bookMap = {};
|
|
39918
40020
|
keys.forEach((k2, i) => bookMap[k2] = books[i] ?? "");
|
|
39919
|
-
return { settings, status, books: bookMap, approvals };
|
|
40021
|
+
return { settings, status, books: bookMap, approvals, authors };
|
|
39920
40022
|
}
|
|
39921
40023
|
async function readAutonomousLogs(root, date) {
|
|
39922
40024
|
const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -39948,18 +40050,57 @@ async function writeAutonomous(root, req, by) {
|
|
|
39948
40050
|
break;
|
|
39949
40051
|
}
|
|
39950
40052
|
case "approvals": {
|
|
40053
|
+
if (req.approved && !req.byIsAdmin) {
|
|
40054
|
+
const authors = await readJsonMap(join15(root, AUTHORS_REL));
|
|
40055
|
+
if (entryBy(authors, req.taskId) === by) {
|
|
40056
|
+
return { ok: false, code: "APPROVAL_SELF", failedId: req.taskId, error: "self-approval blocked" };
|
|
40057
|
+
}
|
|
40058
|
+
}
|
|
39951
40059
|
const map = parseJson(await readText(join15(root, APPROVALS_REL), "{}")) ?? {};
|
|
39952
|
-
if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
40060
|
+
if (req.approved) map[req.taskId] = { approved: true, by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
39953
40061
|
else delete map[req.taskId];
|
|
39954
40062
|
await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
39955
40063
|
break;
|
|
39956
40064
|
}
|
|
40065
|
+
case "approvalsBatch": {
|
|
40066
|
+
if (!req.byIsAdmin) {
|
|
40067
|
+
const authors = await readJsonMap(join15(root, AUTHORS_REL));
|
|
40068
|
+
const selfId = req.approve.find((id) => entryBy(authors, id) === by);
|
|
40069
|
+
if (selfId) return { ok: false, code: "APPROVAL_SELF", failedId: selfId, error: "self-approval blocked" };
|
|
40070
|
+
}
|
|
40071
|
+
const map = parseJson(await readText(join15(root, APPROVALS_REL), "{}")) ?? {};
|
|
40072
|
+
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
40073
|
+
const byLabel = req.byLabel ?? by;
|
|
40074
|
+
for (const taskId of req.approve) map[taskId] = { approved: true, by, byLabel, at: at2 };
|
|
40075
|
+
for (const taskId of req.unapprove) delete map[taskId];
|
|
40076
|
+
await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
40077
|
+
break;
|
|
40078
|
+
}
|
|
39957
40079
|
case "userTodo": {
|
|
39958
40080
|
const cur = await readText(join15(root, BOOK_FILES.userTodo));
|
|
39959
|
-
const
|
|
39960
|
-
const
|
|
40081
|
+
const ids = [...cur.matchAll(/REQ-(\d{4})-(\d{4})/g)];
|
|
40082
|
+
const maxGroup = ids.reduce((m2, g2) => Math.max(m2, Number(g2[1])), 0);
|
|
40083
|
+
const id = `REQ-${String(maxGroup + 1).padStart(4, "0")}-0001`;
|
|
40084
|
+
const row = `| ${id} | | | ${tableCell(req.content)} |
|
|
39961
40085
|
`;
|
|
39962
40086
|
await writeFile2(join15(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
|
|
40087
|
+
const authors = await readJsonMap(join15(root, AUTHORS_REL));
|
|
40088
|
+
authors[id] = { by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
40089
|
+
await writeFile2(join15(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
40090
|
+
break;
|
|
40091
|
+
}
|
|
40092
|
+
case "bookSave": {
|
|
40093
|
+
const file = BOOK_FILES[req.book];
|
|
40094
|
+
const prev = tableRowsById(await readText(join15(root, file)));
|
|
40095
|
+
const next = tableRowsById(req.content);
|
|
40096
|
+
const authors = await readJsonMap(join15(root, AUTHORS_REL));
|
|
40097
|
+
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
40098
|
+
const byLabel = req.byLabel ?? by;
|
|
40099
|
+
for (const [id, cells] of next) {
|
|
40100
|
+
if (prev.get(id) !== cells) authors[id] = { by, byLabel, at: at2 };
|
|
40101
|
+
}
|
|
40102
|
+
await writeFile2(join15(root, file), req.content, "utf8");
|
|
40103
|
+
await writeFile2(join15(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
39963
40104
|
break;
|
|
39964
40105
|
}
|
|
39965
40106
|
case "cron": {
|
|
@@ -40828,8 +40969,10 @@ function handleCommandChannels(ctx, message, payload) {
|
|
|
40828
40969
|
text: `$ ${dispatch.cmd} ${dispatch.args.join(" ")}`.trimEnd()
|
|
40829
40970
|
});
|
|
40830
40971
|
ctx.bus.startBusy(dispatch.cmd);
|
|
40972
|
+
const cwd2 = dispatch.path ?? ctx.physicRoot ?? readProfileConfig(ctx.profileDir).physicPath ?? process.cwd();
|
|
40973
|
+
const dispatchInRoot = { ...dispatch, path: cwd2 };
|
|
40831
40974
|
const cmdStartMs = Date.now();
|
|
40832
|
-
void runCommand(
|
|
40975
|
+
void runCommand(dispatchInRoot, (out) => {
|
|
40833
40976
|
if (out.chunk) {
|
|
40834
40977
|
ctx.bus.push({ source: "server", kind: "out", text: out.chunk });
|
|
40835
40978
|
appendCommandOutput(out.commandId, out.chunk);
|
|
@@ -41293,8 +41436,104 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
|
|
|
41293
41436
|
});
|
|
41294
41437
|
}
|
|
41295
41438
|
|
|
41439
|
+
// src/core/autonomous-cycle.ts
|
|
41440
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
41441
|
+
function buildAutonomousCyclePrompt() {
|
|
41442
|
+
return `You are running UNATTENDED as the 4PM autonomous cycle (headless, one tick). Do EXACTLY ONE small
|
|
41443
|
+
work cycle with the steps below, then STOP. Keep each cycle to ONE small task so you never run out of
|
|
41444
|
+
tokens mid-way. If a step fails, append a row to the **Incidents** table of \`AI_DONE.md\` and STOP \u2014
|
|
41445
|
+
do not push on. Write all books/docs/code in the language the project's \`CLAUDE.md\` specifies (default
|
|
41446
|
+
English).
|
|
41447
|
+
|
|
41448
|
+
## Books & approval (MUST read the approvals file)
|
|
41449
|
+
The five "book" files at the project root \u2014 \`USER_TODO.md\`, \`USER_QA.md\`, \`AI_TODO.md\`,
|
|
41450
|
+
\`AI_PROGRESS.md\`, \`AI_DONE.md\` \u2014 have canonical templates in \`.claude/templates/\` (\`<NAME>.empty.md\`
|
|
41451
|
+
= EMPTY, \`<NAME>.sample.md\` = example WITH DATA). To tell empty/has-work, COMPARE a book to its
|
|
41452
|
+
\`<NAME>.empty.md\` (equal after trimming \u21D2 empty). When clearing a book, overwrite it with EXACTLY the
|
|
41453
|
+
empty template (\`cp .claude/templates/<NAME>.empty.md <NAME>\`).
|
|
41454
|
+
|
|
41455
|
+
The single source of truth for approval is \`.claude/.autonomous.approvals.json\` \u2014 a map
|
|
41456
|
+
\`{ "<ID>": { "approved": true, "by": "...", "at": "..." } }\` (the user ticks rows in the web grids). A
|
|
41457
|
+
row is APPROVED only when its ID has \`"approved": true\`. Row IDs: \`USER_TODO\` = \`REQ-\u2026\`, \`USER_QA\`
|
|
41458
|
+
= \`QA-\u2026\`, \`AI_TODO\` = \`TSK-\u2026\`. NEVER act on an unapproved row.
|
|
41459
|
+
|
|
41460
|
+
## Step 1 \u2014 Base branch (from the spec, ADR-0292)
|
|
41461
|
+
The project root IS the primary repo (ADR-0314). Its currently checked-out branch is the base branch
|
|
41462
|
+
(4PM cloned it with \`-b <branch>\`): \`BASE=$(git rev-parse --abbrev-ref HEAD)\`. Sync it:
|
|
41463
|
+
\`git fetch origin\` then \`git pull --ff-only origin "$BASE"\` (skip the pull if the remote has no such
|
|
41464
|
+
branch yet). Do NOT invent a separate integration/dev branch.
|
|
41465
|
+
|
|
41466
|
+
## Step 2 \u2014 Intake APPROVED user requests \u2192 tasks
|
|
41467
|
+
Read \`USER_TODO.md\` (content-only columns \`ID | Group | Depends | Request\`; who/when wrote or approved a
|
|
41468
|
+
row lives in \`.claude/.autonomous.authors.json\` / \`.autonomous.approvals.json\`, ADR-0320 \u2014 never add
|
|
41469
|
+
provenance columns to the table). For each row that is APPROVED (its \`REQ-\u2026\` in the approvals file) AND
|
|
41470
|
+
whose \`Depends\` \`REQ-\u2026\` are already handled:
|
|
41471
|
+
- If the request is CLEAR: split it into small tasks and append them to \`AI_TODO.md\` (7 columns
|
|
41472
|
+
\`ID | Priority | Tag | Depends | Group | Task description | Notes\`), each with an ID
|
|
41473
|
+
\`TSK-{groupid:0000}-{taskid:0000}\`. **groupid** must be UNIQUE + INCREASING across all history \u2014 find
|
|
41474
|
+
the largest ever used in git history of \`AI_TODO.md\`/\`AI_DONE.md\`/\`AI_PROGRESS.md\`, then use max+1;
|
|
41475
|
+
**taskid** starts at 0001 within the group. Leave \`Tag\` blank unless a catalog tag applies; leave
|
|
41476
|
+
\`Priority\` = High/Medium/Low (default Medium). Then REMOVE the analysed request row from
|
|
41477
|
+
\`USER_TODO.md\` (if it becomes empty, reset it to the empty template).
|
|
41478
|
+
- If the request is UNCLEAR (ambiguous/missing info/contradictory): do NOT guess. Append a row to
|
|
41479
|
+
\`USER_QA.md\` (content-only columns \`ID | Group | Depends | Original request | Question / options |
|
|
41480
|
+
Answer\`) with a new \`QA-{groupid:0000}-{qaid:0000}\` id, the original request and the question +
|
|
41481
|
+
options; leave \`Answer\` blank for the user. Remove the request row from \`USER_TODO.md\`. (Do NOT
|
|
41482
|
+
generate tasks for it.)
|
|
41483
|
+
An unapproved request row is LEFT in place (wait for approval).
|
|
41484
|
+
|
|
41485
|
+
## Step 3 \u2014 Fold APPROVED answers back (USER_QA)
|
|
41486
|
+
For each \`USER_QA.md\` row that has an \`Answer\` AND is APPROVED (its \`QA-\u2026\` in the approvals file):
|
|
41487
|
+
re-analyse using the answer \u2014 append the resulting tasks to \`AI_TODO.md\` (same ID rules as Step 2) \u2014
|
|
41488
|
+
then clear that QA row. An unanswered or unapproved QA row is left in place.
|
|
41489
|
+
|
|
41490
|
+
## Step 4 \u2014 Pick ONE approved task and start it
|
|
41491
|
+
Read \`AI_TODO.md\` + the approvals file. Filter tasks that are BOTH approved (\`TSK-\u2026\` in approvals) AND
|
|
41492
|
+
have every \`Depends\` \`TSK-\u2026\` already in \`AI_DONE.md\`. Run group by group (smallest eligible group
|
|
41493
|
+
first), within a group High \u2192 Medium \u2192 Low, then line order. If NO eligible task: append a row to the
|
|
41494
|
+
\`AI_DONE.md\` Incidents table (e.g. "only generated tasks / waiting for approval / waiting for deps")
|
|
41495
|
+
and STOP. Otherwise move the chosen task into \`AI_PROGRESS.md\` (with a start timestamp), remove it from
|
|
41496
|
+
\`AI_TODO.md\`, and commit on the base branch: \`git commit -am "chore(auto): start TSK-\u2026"\`.
|
|
41497
|
+
|
|
41498
|
+
## Step 5 \u2014 Implement on a task branch + test
|
|
41499
|
+
\`git checkout -b task/TSK-\u2026\`. Implement following the project's architecture + \`CLAUDE.md\`; add/update
|
|
41500
|
+
tests. Run the project's test command (per \`CLAUDE.md\` / the project's scripts); WAIT for it to finish
|
|
41501
|
+
and check the result. Keep any produced report/evidence. Commit: \`git commit -am "feat(TSK-\u2026): <desc>"\`.
|
|
41502
|
+
|
|
41503
|
+
## Step 6 \u2014 Pull request into the base branch (NO direct merge \u2014 ADR-0319/0271)
|
|
41504
|
+
1. Rebase onto the freshest base to minimise conflicts: \`git fetch origin\` then
|
|
41505
|
+
\`git rebase origin/"$BASE"\` (on conflict you can't resolve confidently, \`git rebase --abort\`, write a
|
|
41506
|
+
\`USER_QA.md\` row, and STOP).
|
|
41507
|
+
2. Push the task branch: \`git push -u origin task/TSK-\u2026\`.
|
|
41508
|
+
3. Open a PR into the base branch with \`gh\` (GitHub) or \`glab\` (GitLab) \u2014 e.g.
|
|
41509
|
+
\`gh pr create --base "$BASE" --head task/TSK-\u2026 --title "TSK-\u2026: <desc>" --body "<summary>"\`. Do NOT
|
|
41510
|
+
merge it and do NOT fast-forward the base yourself \u2014 a human reviews the PR. If no remote / no
|
|
41511
|
+
\`gh\`/\`glab\` auth is available, keep the local task-branch commit and append an Incidents row saying
|
|
41512
|
+
the PR could not be opened.
|
|
41513
|
+
|
|
41514
|
+
## Step 7 \u2014 Update the books
|
|
41515
|
+
On the base branch, append a row to the \`AI_DONE.md\` Done table (Timestamp, ID, Task description, Files,
|
|
41516
|
+
Notes \u2014 include the PR link if opened) and REMOVE the task from \`AI_PROGRESS.md\` (reset it to the empty
|
|
41517
|
+
template if nothing is in progress). Commit + push the base branch:
|
|
41518
|
+
\`git commit -am "chore(auto): finish TSK-\u2026 (PR opened)" && git push origin "$BASE"\` (a push rejected as
|
|
41519
|
+
non-fast-forward \u21D2 \`git pull --rebase origin "$BASE"\` then push again).
|
|
41520
|
+
|
|
41521
|
+
## Step 8 \u2014 Stop
|
|
41522
|
+
Print a one-line summary (task id + PR link or incident). STOP \u2014 the next cron tick runs the next cycle.
|
|
41523
|
+
The cron tick owns the run lock; you do NOT manage \`.claude/.autonomous.lock\`.`;
|
|
41524
|
+
}
|
|
41525
|
+
async function runAutonomousCycle(ctx) {
|
|
41526
|
+
const commandId = randomUUID2();
|
|
41527
|
+
try {
|
|
41528
|
+
await runAiPrompt(ctx, buildAutonomousCyclePrompt(), commandId, "local", false, void 0, void 0, false, true);
|
|
41529
|
+
ctx.bus.autonomousDone(true);
|
|
41530
|
+
} catch (err) {
|
|
41531
|
+
ctx.bus.autonomousDone(false, err instanceof Error ? err.message : String(err));
|
|
41532
|
+
}
|
|
41533
|
+
}
|
|
41534
|
+
|
|
41296
41535
|
// src/core/fs-browse.ts
|
|
41297
|
-
import { readdir as
|
|
41536
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
41298
41537
|
import { resolve as resolve2, sep as sep2 } from "path";
|
|
41299
41538
|
async function listDir(path, root) {
|
|
41300
41539
|
if (!root) return { path: "", entries: [] };
|
|
@@ -41302,7 +41541,7 @@ async function listDir(path, root) {
|
|
|
41302
41541
|
const requested = resolve2(base, path && path.trim() ? path : ".");
|
|
41303
41542
|
const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
|
|
41304
41543
|
try {
|
|
41305
|
-
const dirents = await
|
|
41544
|
+
const dirents = await readdir2(target, { withFileTypes: true });
|
|
41306
41545
|
const entries2 = dirents.map((d) => ({
|
|
41307
41546
|
name: d.name,
|
|
41308
41547
|
type: d.isDirectory() ? "dir" : "file"
|
|
@@ -41370,7 +41609,7 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
41370
41609
|
}
|
|
41371
41610
|
|
|
41372
41611
|
// src/core/fs-mutate.ts
|
|
41373
|
-
import { mkdir as mkdir4, writeFile as writeFile4, rename
|
|
41612
|
+
import { mkdir as mkdir4, writeFile as writeFile4, rename, rm as rm2, stat } from "fs/promises";
|
|
41374
41613
|
import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
|
|
41375
41614
|
var MAX_BYTES3 = 2 * 1024 * 1024;
|
|
41376
41615
|
function resolveInRoot3(root, rel) {
|
|
@@ -41425,7 +41664,7 @@ async function mutateFs(root, req) {
|
|
|
41425
41664
|
if (to2 !== resolve5(root) && !to2.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
|
|
41426
41665
|
if (await exists(to2)) return { ok: false, path: to2, error: "destination already exists" };
|
|
41427
41666
|
await mkdir4(dirname5(to2), { recursive: true });
|
|
41428
|
-
await
|
|
41667
|
+
await rename(from, to2);
|
|
41429
41668
|
return { ok: true, path: to2 };
|
|
41430
41669
|
}
|
|
41431
41670
|
case "delete": {
|
|
@@ -41724,7 +41963,6 @@ async function manageSshKey(op, profileDir2) {
|
|
|
41724
41963
|
}
|
|
41725
41964
|
|
|
41726
41965
|
// src/core/git-history.ts
|
|
41727
|
-
import { readdir as readdir4 } from "fs/promises";
|
|
41728
41966
|
import { execFile as execFile9 } from "child_process";
|
|
41729
41967
|
import { resolve as resolve8, sep as sep8 } from "path";
|
|
41730
41968
|
import { promisify as promisify9 } from "util";
|
|
@@ -41752,36 +41990,21 @@ async function git(cwd2, args) {
|
|
|
41752
41990
|
async function gitRepos(root) {
|
|
41753
41991
|
if (!root) return { repos: [] };
|
|
41754
41992
|
const base = resolve8(root);
|
|
41755
|
-
const
|
|
41993
|
+
const top = (await git(base, ["rev-parse", "--show-toplevel"])).trim();
|
|
41994
|
+
if (!top || !(top === base || top.startsWith(base + sep8))) return { repos: [] };
|
|
41756
41995
|
const repos = [];
|
|
41757
|
-
const
|
|
41758
|
-
|
|
41759
|
-
|
|
41760
|
-
|
|
41761
|
-
|
|
41762
|
-
const
|
|
41763
|
-
|
|
41764
|
-
|
|
41765
|
-
|
|
41766
|
-
|
|
41767
|
-
|
|
41768
|
-
const lvl1 = await readdir4(base, { withFileTypes: true });
|
|
41769
|
-
for (const d of lvl1) {
|
|
41770
|
-
if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
|
|
41771
|
-
const p1 = resolve8(base, d.name);
|
|
41772
|
-
await add(p1);
|
|
41773
|
-
try {
|
|
41774
|
-
const lvl2 = await readdir4(p1, { withFileTypes: true });
|
|
41775
|
-
for (const d2 of lvl2) {
|
|
41776
|
-
if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
|
|
41777
|
-
await add(resolve8(p1, d2.name));
|
|
41778
|
-
}
|
|
41779
|
-
} catch {
|
|
41780
|
-
}
|
|
41781
|
-
}
|
|
41782
|
-
} catch {
|
|
41996
|
+
const rootRemote = (await git(base, ["remote", "get-url", "origin"])).trim();
|
|
41997
|
+
repos.push({ subdir: "", name: "", remote: rootRemote || null });
|
|
41998
|
+
const subOut = await git(base, ["submodule", "status"]);
|
|
41999
|
+
for (const line of subOut.split("\n")) {
|
|
42000
|
+
const m2 = line.trim().match(/^[-+U ]?[0-9a-f]{7,40}\s+(\S+)/);
|
|
42001
|
+
const subPath = m2?.[1];
|
|
42002
|
+
if (!subPath) continue;
|
|
42003
|
+
const abs = resolve8(base, subPath);
|
|
42004
|
+
if (!(abs === base || abs.startsWith(base + sep8))) continue;
|
|
42005
|
+
const remote = (await git(abs, ["remote", "get-url", "origin"])).trim();
|
|
42006
|
+
repos.push({ subdir: subPath, name: subPath, remote: remote || null });
|
|
41783
42007
|
}
|
|
41784
|
-
repos.sort((a, b2) => a.subdir.localeCompare(b2.subdir));
|
|
41785
42008
|
return { repos };
|
|
41786
42009
|
}
|
|
41787
42010
|
async function gitLog(root, repo, skip, limit) {
|
|
@@ -42304,7 +42527,7 @@ function handleToolsChannels(ctx, message, payload) {
|
|
|
42304
42527
|
}
|
|
42305
42528
|
|
|
42306
42529
|
// src/core/agents.ts
|
|
42307
|
-
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as
|
|
42530
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir3, rm as rm4 } from "fs/promises";
|
|
42308
42531
|
import { join as join21 } from "path";
|
|
42309
42532
|
var AGENTS_REL = ".claude/agents";
|
|
42310
42533
|
var SKILLS_REL = ".claude/skills";
|
|
@@ -42329,7 +42552,7 @@ async function listAgents(root) {
|
|
|
42329
42552
|
const subagents = [];
|
|
42330
42553
|
const skills = [];
|
|
42331
42554
|
try {
|
|
42332
|
-
for (const e of await
|
|
42555
|
+
for (const e of await readdir3(join21(root, AGENTS_REL), { withFileTypes: true })) {
|
|
42333
42556
|
if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
|
|
42334
42557
|
const fm = frontmatter(await readText2(join21(root, AGENTS_REL, e.name)));
|
|
42335
42558
|
subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
|
|
@@ -42337,7 +42560,7 @@ async function listAgents(root) {
|
|
|
42337
42560
|
} catch {
|
|
42338
42561
|
}
|
|
42339
42562
|
try {
|
|
42340
|
-
for (const e of await
|
|
42563
|
+
for (const e of await readdir3(join21(root, SKILLS_REL), { withFileTypes: true })) {
|
|
42341
42564
|
if (!e.isDirectory()) continue;
|
|
42342
42565
|
const fm = frontmatter(await readText2(join21(root, SKILLS_REL, e.name, "SKILL.md")));
|
|
42343
42566
|
skills.push({ name: e.name, description: fm.description });
|
|
@@ -42489,7 +42712,7 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
42489
42712
|
|
|
42490
42713
|
// src/core/packages.ts
|
|
42491
42714
|
import { createHash as createHash4 } from "crypto";
|
|
42492
|
-
import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as
|
|
42715
|
+
import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir4, rm as rm5, stat as stat3 } from "fs/promises";
|
|
42493
42716
|
import { dirname as dirname9, join as join24, relative } from "path";
|
|
42494
42717
|
var AGENTS_REL2 = ".claude/agents";
|
|
42495
42718
|
var SKILLS_REL2 = ".claude/skills";
|
|
@@ -42509,7 +42732,7 @@ async function listFilesRel(dir2) {
|
|
|
42509
42732
|
async function walk2(d) {
|
|
42510
42733
|
let entries2;
|
|
42511
42734
|
try {
|
|
42512
|
-
entries2 = await
|
|
42735
|
+
entries2 = await readdir4(d, { withFileTypes: true });
|
|
42513
42736
|
} catch {
|
|
42514
42737
|
return;
|
|
42515
42738
|
}
|
|
@@ -42677,7 +42900,7 @@ async function removePackage(root, slug) {
|
|
|
42677
42900
|
}
|
|
42678
42901
|
|
|
42679
42902
|
// src/core/graph.ts
|
|
42680
|
-
import { readFile as readFile11, readdir as
|
|
42903
|
+
import { readFile as readFile11, readdir as readdir5 } from "fs/promises";
|
|
42681
42904
|
import { join as join25, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
|
|
42682
42905
|
var MAX_FILES = 2e3;
|
|
42683
42906
|
var MAX_NODES = 3e3;
|
|
@@ -42690,7 +42913,7 @@ async function walk(root, accept) {
|
|
|
42690
42913
|
const dir2 = stack.pop();
|
|
42691
42914
|
let entries2;
|
|
42692
42915
|
try {
|
|
42693
|
-
entries2 = await
|
|
42916
|
+
entries2 = await readdir5(dir2, { withFileTypes: true });
|
|
42694
42917
|
} catch {
|
|
42695
42918
|
continue;
|
|
42696
42919
|
}
|
|
@@ -43325,6 +43548,7 @@ var WsClient = class {
|
|
|
43325
43548
|
);
|
|
43326
43549
|
this.hctx = this.buildHandlerCtx();
|
|
43327
43550
|
context.bus.onLocalSubmit((input) => void runLocalCommand(this.hctx, input));
|
|
43551
|
+
context.bus.onAutonomousRun(() => void runAutonomousCycle(this.hctx));
|
|
43328
43552
|
context.bus.onReconnect(() => this.reconnectNow());
|
|
43329
43553
|
context.bus.onTranscript((entry) => this.emitConsole({ kind: "add", entry: toDtoEntry(entry) }));
|
|
43330
43554
|
context.bus.onTranscriptUpdate(
|
|
@@ -43874,7 +44098,7 @@ var WsClient = class {
|
|
|
43874
44098
|
}
|
|
43875
44099
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
43876
44100
|
const envelope = {
|
|
43877
|
-
id:
|
|
44101
|
+
id: randomUUID3(),
|
|
43878
44102
|
replyTo,
|
|
43879
44103
|
channel,
|
|
43880
44104
|
wsToken: this.wsToken,
|
|
@@ -43895,7 +44119,7 @@ var WsClient = class {
|
|
|
43895
44119
|
}
|
|
43896
44120
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
43897
44121
|
const envelope = {
|
|
43898
|
-
id:
|
|
44122
|
+
id: randomUUID3(),
|
|
43899
44123
|
replyTo: null,
|
|
43900
44124
|
channel,
|
|
43901
44125
|
wsToken: this.wsToken,
|
|
@@ -44284,7 +44508,7 @@ function releaseInstanceLock(profileDir2) {
|
|
|
44284
44508
|
|
|
44285
44509
|
// src/core/session-bus.ts
|
|
44286
44510
|
import { EventEmitter } from "events";
|
|
44287
|
-
import { randomUUID as
|
|
44511
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
44288
44512
|
var REPLAY_CAP = 500;
|
|
44289
44513
|
var SessionBus = class {
|
|
44290
44514
|
emitter = new EventEmitter();
|
|
@@ -44315,7 +44539,7 @@ var SessionBus = class {
|
|
|
44315
44539
|
/** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
|
|
44316
44540
|
push(input) {
|
|
44317
44541
|
const entry = {
|
|
44318
|
-
id:
|
|
44542
|
+
id: randomUUID4(),
|
|
44319
44543
|
level: "info",
|
|
44320
44544
|
...input,
|
|
44321
44545
|
ts: Date.now()
|
|
@@ -44546,6 +44770,28 @@ var SessionBus = class {
|
|
|
44546
44770
|
this.emitter.on("reconnect", fn);
|
|
44547
44771
|
return () => this.emitter.off("reconnect", fn);
|
|
44548
44772
|
}
|
|
44773
|
+
/**
|
|
44774
|
+
* Trigger → session: run ONE autonomous cycle (ADR-0319). Sent by `4pm auto-run` over the control
|
|
44775
|
+
* socket so the cron tick runs the cycle through THIS daemon's live WS session — reusing its
|
|
44776
|
+
* profile-failover, token metering (ADR-0072) and folder-scope (ADR-0181) — instead of a raw `claude -p`.
|
|
44777
|
+
*/
|
|
44778
|
+
submitAutonomous() {
|
|
44779
|
+
this.emitter.emit("autonomous-run");
|
|
44780
|
+
}
|
|
44781
|
+
/** Subscribe to autonomous-cycle triggers. */
|
|
44782
|
+
onAutonomousRun(fn) {
|
|
44783
|
+
this.emitter.on("autonomous-run", fn);
|
|
44784
|
+
return () => this.emitter.off("autonomous-run", fn);
|
|
44785
|
+
}
|
|
44786
|
+
/** Session → trigger: the autonomous cycle settled (ok + an optional note). */
|
|
44787
|
+
autonomousDone(ok, note) {
|
|
44788
|
+
this.emitter.emit("autonomous-done", ok, note);
|
|
44789
|
+
}
|
|
44790
|
+
/** Subscribe to autonomous-cycle completion. */
|
|
44791
|
+
onAutonomousDone(fn) {
|
|
44792
|
+
this.emitter.on("autonomous-done", fn);
|
|
44793
|
+
return () => this.emitter.off("autonomous-done", fn);
|
|
44794
|
+
}
|
|
44549
44795
|
};
|
|
44550
44796
|
|
|
44551
44797
|
// src/core/control-server.ts
|
|
@@ -44608,7 +44854,9 @@ function startControlServer(bus, profileDir2, info) {
|
|
|
44608
44854
|
bus.onWorker((worker) => broadcast({ t: "worker", worker })),
|
|
44609
44855
|
bus.onProject((project) => broadcast({ t: "project", project })),
|
|
44610
44856
|
bus.onUsage((usage) => broadcast({ t: "usage", usage })),
|
|
44611
|
-
bus.onTokens((total) => broadcast({ t: "tokens", total }))
|
|
44857
|
+
bus.onTokens((total) => broadcast({ t: "tokens", total })),
|
|
44858
|
+
// Report an autonomous cycle's completion back to the `4pm auto-run` client (ADR-0319).
|
|
44859
|
+
bus.onAutonomousDone((ok, note) => broadcast({ t: "autonomousDone", ok, note }))
|
|
44612
44860
|
];
|
|
44613
44861
|
const server = createServer((sock) => {
|
|
44614
44862
|
clients.add(sock);
|
|
@@ -44631,6 +44879,7 @@ function startControlServer(bus, profileDir2, info) {
|
|
|
44631
44879
|
for (const frame of parse(chunk)) {
|
|
44632
44880
|
if (frame.t === "submit") bus.submitLocal(frame.input);
|
|
44633
44881
|
else if (frame.t === "reconnect") bus.requestReconnect();
|
|
44882
|
+
else if (frame.t === "autonomousRun") bus.submitAutonomous();
|
|
44634
44883
|
}
|
|
44635
44884
|
});
|
|
44636
44885
|
const drop = () => {
|
|
@@ -51167,6 +51416,62 @@ async function runAttach(profileDir2, profileName) {
|
|
|
51167
51416
|
}
|
|
51168
51417
|
}
|
|
51169
51418
|
|
|
51419
|
+
// src/commands/auto-run.ts
|
|
51420
|
+
import { connect as connect3 } from "net";
|
|
51421
|
+
import { join as join34 } from "path";
|
|
51422
|
+
var AUTO_RUN_TIMEOUT_MS = 3 * 60 * 60 * 1e3;
|
|
51423
|
+
async function runAutoRun(profileDir2, profileName) {
|
|
51424
|
+
initI18n(readProfileConfig(profileDir2).locale);
|
|
51425
|
+
const socketPath = join34(profileDir2, CONTROL_SOCKET_FILE);
|
|
51426
|
+
const code = await new Promise((resolve10) => {
|
|
51427
|
+
let settled = false;
|
|
51428
|
+
const done = (c) => {
|
|
51429
|
+
if (settled) return;
|
|
51430
|
+
settled = true;
|
|
51431
|
+
clearTimeout(timer);
|
|
51432
|
+
try {
|
|
51433
|
+
socket.destroy();
|
|
51434
|
+
} catch {
|
|
51435
|
+
}
|
|
51436
|
+
resolve10(c);
|
|
51437
|
+
};
|
|
51438
|
+
const socket = connect3(socketPath);
|
|
51439
|
+
socket.setEncoding("utf8");
|
|
51440
|
+
const timer = setTimeout(() => {
|
|
51441
|
+
console.error(t("autoRun.timeout"));
|
|
51442
|
+
done(1);
|
|
51443
|
+
}, AUTO_RUN_TIMEOUT_MS);
|
|
51444
|
+
timer.unref?.();
|
|
51445
|
+
socket.on("connect", () => {
|
|
51446
|
+
try {
|
|
51447
|
+
socket.write(encodeFrame({ t: "autonomousRun" }));
|
|
51448
|
+
} catch {
|
|
51449
|
+
done(1);
|
|
51450
|
+
}
|
|
51451
|
+
});
|
|
51452
|
+
const parse = createFrameParser();
|
|
51453
|
+
socket.on("data", (chunk) => {
|
|
51454
|
+
for (const frame of parse(chunk)) {
|
|
51455
|
+
if (frame.t === "transcript") {
|
|
51456
|
+
if (frame.entry.text) console.log(frame.entry.text);
|
|
51457
|
+
} else if (frame.t === "update") {
|
|
51458
|
+
if (frame.text) console.log(frame.text);
|
|
51459
|
+
} else if (frame.t === "autonomousDone") {
|
|
51460
|
+
if (frame.ok) console.log(t("autoRun.done"));
|
|
51461
|
+
else console.error(t("autoRun.failed", { note: frame.note ?? "" }));
|
|
51462
|
+
done(frame.ok ? 0 : 1);
|
|
51463
|
+
}
|
|
51464
|
+
}
|
|
51465
|
+
});
|
|
51466
|
+
socket.on("error", () => {
|
|
51467
|
+
console.error(t("autoRun.noDaemon", { profile: profileName }));
|
|
51468
|
+
done(1);
|
|
51469
|
+
});
|
|
51470
|
+
socket.on("close", () => done(1));
|
|
51471
|
+
});
|
|
51472
|
+
process.exitCode = code;
|
|
51473
|
+
}
|
|
51474
|
+
|
|
51170
51475
|
// src/commands/ai-login.ts
|
|
51171
51476
|
import { spawn as spawn11 } from "child_process";
|
|
51172
51477
|
import { mkdirSync as mkdirSync8 } from "fs";
|
|
@@ -51391,6 +51696,15 @@ async function main() {
|
|
|
51391
51696
|
await runAttach(profileDir(name), name);
|
|
51392
51697
|
break;
|
|
51393
51698
|
}
|
|
51699
|
+
case "auto-run": {
|
|
51700
|
+
const name = await pickLinkedProfile(explicitProfile, "auto-run");
|
|
51701
|
+
if (!name) {
|
|
51702
|
+
process.exitCode = 1;
|
|
51703
|
+
break;
|
|
51704
|
+
}
|
|
51705
|
+
await runAutoRun(profileDir(name), name);
|
|
51706
|
+
break;
|
|
51707
|
+
}
|
|
51394
51708
|
case "ai-login": {
|
|
51395
51709
|
const name = await pickLinkedProfile(explicitProfile, "ai-login");
|
|
51396
51710
|
if (!name) {
|
|
@@ -51431,6 +51745,7 @@ async function main() {
|
|
|
51431
51745
|
" 4pm link Pair with the server (pairing \u2192 confirmation code)",
|
|
51432
51746
|
" 4pm start Connect WS, receive commands from the server",
|
|
51433
51747
|
" 4pm attach Open the TUI against a running headless daemon (ADR-0192)",
|
|
51748
|
+
" 4pm auto-run Run one autonomous cycle via the running daemon (cron \u2014 ADR-0319)",
|
|
51434
51749
|
" 4pm ai-login Log into a profile's AI CLI (claude/codex) in place (ADR-0199)",
|
|
51435
51750
|
" 4pm unlink Delete a link (pick a profile if several)",
|
|
51436
51751
|
" 4pm version Show the installed version (+ latest from the server)",
|