@4pm/cli 1.28.0 → 1.31.0-b
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 +2193 -595
- package/dist/project-sample/.claude/AUTONOMOUS.md +16 -3
- package/dist/project-sample/.claude/templates/AI_PROGRESS.empty.md +10 -6
- package/dist/project-sample/.claude/templates/AI_PROGRESS.sample.md +5 -4
- package/dist/project-sample/.claude/templates/AI_TODO.empty.md +9 -3
- package/dist/project-sample/.claude/templates/AI_TODO.sample.md +4 -2
- package/dist/project-sample/.claude/templates/USER_QA.empty.md +3 -0
- package/dist/project-sample/AI_PROGRESS.md +10 -6
- package/dist/project-sample/AI_TODO.md +9 -3
- package/dist/project-sample/USER_QA.md +3 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11606,7 +11606,7 @@ var require_react_reconciler_development = __commonJS({
|
|
|
11606
11606
|
}
|
|
11607
11607
|
}
|
|
11608
11608
|
var assign = Object.assign;
|
|
11609
|
-
function
|
|
11609
|
+
function get3(key) {
|
|
11610
11610
|
return key._reactInternals;
|
|
11611
11611
|
}
|
|
11612
11612
|
function set(key, value) {
|
|
@@ -11961,7 +11961,7 @@ var require_react_reconciler_development = __commonJS({
|
|
|
11961
11961
|
instance._warnedAboutRefsInRender = true;
|
|
11962
11962
|
}
|
|
11963
11963
|
}
|
|
11964
|
-
var fiber =
|
|
11964
|
+
var fiber = get3(component);
|
|
11965
11965
|
if (!fiber) {
|
|
11966
11966
|
return false;
|
|
11967
11967
|
}
|
|
@@ -18301,7 +18301,7 @@ var require_react_reconciler_development = __commonJS({
|
|
|
18301
18301
|
var classComponentUpdater = {
|
|
18302
18302
|
isMounted,
|
|
18303
18303
|
enqueueSetState: function(inst, payload, callback) {
|
|
18304
|
-
var fiber =
|
|
18304
|
+
var fiber = get3(inst);
|
|
18305
18305
|
var eventTime = requestEventTime();
|
|
18306
18306
|
var lane = requestUpdateLane(fiber);
|
|
18307
18307
|
var update = createUpdate(eventTime, lane);
|
|
@@ -18322,7 +18322,7 @@ var require_react_reconciler_development = __commonJS({
|
|
|
18322
18322
|
}
|
|
18323
18323
|
},
|
|
18324
18324
|
enqueueReplaceState: function(inst, payload, callback) {
|
|
18325
|
-
var fiber =
|
|
18325
|
+
var fiber = get3(inst);
|
|
18326
18326
|
var eventTime = requestEventTime();
|
|
18327
18327
|
var lane = requestUpdateLane(fiber);
|
|
18328
18328
|
var update = createUpdate(eventTime, lane);
|
|
@@ -18344,7 +18344,7 @@ var require_react_reconciler_development = __commonJS({
|
|
|
18344
18344
|
}
|
|
18345
18345
|
},
|
|
18346
18346
|
enqueueForceUpdate: function(inst, callback) {
|
|
18347
|
-
var fiber =
|
|
18347
|
+
var fiber = get3(inst);
|
|
18348
18348
|
var eventTime = requestEventTime();
|
|
18349
18349
|
var lane = requestUpdateLane(fiber);
|
|
18350
18350
|
var update = createUpdate(eventTime, lane);
|
|
@@ -26201,7 +26201,7 @@ var require_react_reconciler_development = __commonJS({
|
|
|
26201
26201
|
if (!parentComponent) {
|
|
26202
26202
|
return emptyContextObject;
|
|
26203
26203
|
}
|
|
26204
|
-
var fiber =
|
|
26204
|
+
var fiber = get3(parentComponent);
|
|
26205
26205
|
var parentContext = findCurrentUnmaskedContext(fiber);
|
|
26206
26206
|
if (fiber.tag === ClassComponent) {
|
|
26207
26207
|
var Component = fiber.type;
|
|
@@ -26212,7 +26212,7 @@ var require_react_reconciler_development = __commonJS({
|
|
|
26212
26212
|
return parentContext;
|
|
26213
26213
|
}
|
|
26214
26214
|
function findHostInstance(component) {
|
|
26215
|
-
var fiber =
|
|
26215
|
+
var fiber = get3(component);
|
|
26216
26216
|
if (fiber === void 0) {
|
|
26217
26217
|
if (typeof component.render === "function") {
|
|
26218
26218
|
throw new Error("Unable to find node on an unmounted component.");
|
|
@@ -26229,7 +26229,7 @@ var require_react_reconciler_development = __commonJS({
|
|
|
26229
26229
|
}
|
|
26230
26230
|
function findHostInstanceWithWarning(component, methodName) {
|
|
26231
26231
|
{
|
|
26232
|
-
var fiber =
|
|
26232
|
+
var fiber = get3(component);
|
|
26233
26233
|
if (fiber === void 0) {
|
|
26234
26234
|
if (typeof component.render === "function") {
|
|
26235
26235
|
throw new Error("Unable to find node on an unmounted component.");
|
|
@@ -27177,7 +27177,7 @@ var require_stack_utils = __commonJS({
|
|
|
27177
27177
|
const evalCol = Number(match[6]);
|
|
27178
27178
|
let file = match[7];
|
|
27179
27179
|
const lnum = match[8];
|
|
27180
|
-
const
|
|
27180
|
+
const col2 = match[9];
|
|
27181
27181
|
const native = match[10] === "native";
|
|
27182
27182
|
const closeParen = match[11] === ")";
|
|
27183
27183
|
let method;
|
|
@@ -27185,8 +27185,8 @@ var require_stack_utils = __commonJS({
|
|
|
27185
27185
|
if (lnum) {
|
|
27186
27186
|
res.line = Number(lnum);
|
|
27187
27187
|
}
|
|
27188
|
-
if (
|
|
27189
|
-
res.column = Number(
|
|
27188
|
+
if (col2) {
|
|
27189
|
+
res.column = Number(col2);
|
|
27190
27190
|
}
|
|
27191
27191
|
if (closeParen && file) {
|
|
27192
27192
|
let closes = 0;
|
|
@@ -28569,6 +28569,344 @@ async function fetchCliVersion(serverUrl, version) {
|
|
|
28569
28569
|
}
|
|
28570
28570
|
}
|
|
28571
28571
|
|
|
28572
|
+
// vendor/@4pm/constants/error-codes.ts
|
|
28573
|
+
var ErrorCode = {
|
|
28574
|
+
// Common
|
|
28575
|
+
VALIDATION_FAILED: "VALIDATION_FAILED",
|
|
28576
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
28577
|
+
FORBIDDEN: "FORBIDDEN",
|
|
28578
|
+
IP_NOT_ALLOWED: "IP_NOT_ALLOWED",
|
|
28579
|
+
// A user's IP allowlist entry falls outside the org allowlist (ADR-0044).
|
|
28580
|
+
IP_OUTSIDE_ORG_ALLOWLIST: "IP_OUTSIDE_ORG_ALLOWLIST",
|
|
28581
|
+
// An allowlist entry falls outside its parent tier: a team/project outside the
|
|
28582
|
+
// org, or a user outside the union of their teams'/projects' allowlists (ADR-0050).
|
|
28583
|
+
IP_OUTSIDE_PARENT_ALLOWLIST: "IP_OUTSIDE_PARENT_ALLOWLIST",
|
|
28584
|
+
NOT_FOUND: "NOT_FOUND",
|
|
28585
|
+
CONFLICT: "CONFLICT",
|
|
28586
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
28587
|
+
FEATURE_NOT_AVAILABLE: "FEATURE_NOT_AVAILABLE",
|
|
28588
|
+
// Request body exceeded the HTTP gateway's JSON size limit (see HTTP_JSON_BODY_LIMIT_BYTES).
|
|
28589
|
+
PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE",
|
|
28590
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
28591
|
+
// auth
|
|
28592
|
+
INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
|
|
28593
|
+
EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED",
|
|
28594
|
+
ACCOUNT_LOCKED: "ACCOUNT_LOCKED",
|
|
28595
|
+
TOKEN_INVALID: "TOKEN_INVALID",
|
|
28596
|
+
TOKEN_EXPIRED: "TOKEN_EXPIRED",
|
|
28597
|
+
TOKEN_USED: "TOKEN_USED",
|
|
28598
|
+
TOKEN_REVOKED: "TOKEN_REVOKED",
|
|
28599
|
+
/** The session is idle-locked (ADR-0204) — re-authenticate to continue (HTTP 423). */
|
|
28600
|
+
SESSION_LOCKED: "SESSION_LOCKED",
|
|
28601
|
+
PASSWORD_POLICY_VIOLATION: "PASSWORD_POLICY_VIOLATION",
|
|
28602
|
+
PASSWORD_CONFIRM_MISMATCH: "PASSWORD_CONFIRM_MISMATCH",
|
|
28603
|
+
PASSWORD_CURRENT_INVALID: "PASSWORD_CURRENT_INVALID",
|
|
28604
|
+
IDENTITY_REQUIRED: "IDENTITY_REQUIRED",
|
|
28605
|
+
MFA_CODE_INVALID: "MFA_CODE_INVALID",
|
|
28606
|
+
MFA_CODE_EXPIRED: "MFA_CODE_EXPIRED",
|
|
28607
|
+
// org · user · team · project
|
|
28608
|
+
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
28609
|
+
TEAM_NOT_FOUND: "TEAM_NOT_FOUND",
|
|
28610
|
+
PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
|
|
28611
|
+
ORG_NOT_FOUND: "ORG_NOT_FOUND",
|
|
28612
|
+
// org login alias (ADR-0111): alias already used by another org, or a reserved word
|
|
28613
|
+
ORG_ALIAS_TAKEN: "ORG_ALIAS_TAKEN",
|
|
28614
|
+
// org template files (ADR-0113)
|
|
28615
|
+
TEMPLATE_NOT_FOUND: "TEMPLATE_NOT_FOUND",
|
|
28616
|
+
TEMPLATE_QUOTA_EXCEEDED: "TEMPLATE_QUOTA_EXCEEDED",
|
|
28617
|
+
TEMPLATE_FILE_TOO_LARGE: "TEMPLATE_FILE_TOO_LARGE",
|
|
28618
|
+
// skill/subagent marketplace (ADR-0185)
|
|
28619
|
+
MARKET_PACKAGE_NOT_FOUND: "MARKET_PACKAGE_NOT_FOUND",
|
|
28620
|
+
MARKET_VERSION_NOT_FOUND: "MARKET_VERSION_NOT_FOUND",
|
|
28621
|
+
MARKET_PACKAGE_EXISTS: "MARKET_PACKAGE_EXISTS",
|
|
28622
|
+
MARKET_VERSION_EXISTS: "MARKET_VERSION_EXISTS",
|
|
28623
|
+
MARKET_INVALID_PACKAGE: "MARKET_INVALID_PACKAGE",
|
|
28624
|
+
MARKET_SCOPE_FORBIDDEN: "MARKET_SCOPE_FORBIDDEN",
|
|
28625
|
+
MARKET_SCOPE_UNSUPPORTED: "MARKET_SCOPE_UNSUPPORTED",
|
|
28626
|
+
MARKET_VERSION_NOT_APPROVED: "MARKET_VERSION_NOT_APPROVED",
|
|
28627
|
+
MARKET_NOT_MODERATABLE: "MARKET_NOT_MODERATABLE",
|
|
28628
|
+
MARKET_UNAVAILABLE: "MARKET_UNAVAILABLE",
|
|
28629
|
+
MARKET_REVIEW_NOT_ELIGIBLE: "MARKET_REVIEW_NOT_ELIGIBLE",
|
|
28630
|
+
MARKET_PAYMENT_REQUIRED: "MARKET_PAYMENT_REQUIRED",
|
|
28631
|
+
MARKET_ORDER_NOT_FOUND: "MARKET_ORDER_NOT_FOUND",
|
|
28632
|
+
MARKET_ALREADY_PURCHASED: "MARKET_ALREADY_PURCHASED",
|
|
28633
|
+
KNOWLEDGE_POST_NOT_FOUND: "KNOWLEDGE_POST_NOT_FOUND",
|
|
28634
|
+
KNOWLEDGE_SCOPE_FORBIDDEN: "KNOWLEDGE_SCOPE_FORBIDDEN",
|
|
28635
|
+
// 4rum post / Messenger message missing or not visible — also a cursor anchor (ADR-0341/0361)
|
|
28636
|
+
POST_NOT_FOUND: "POST_NOT_FOUND",
|
|
28637
|
+
MESSAGE_NOT_FOUND: "MESSAGE_NOT_FOUND",
|
|
28638
|
+
PACKAGE_ALREADY_INSTALLED: "PACKAGE_ALREADY_INSTALLED",
|
|
28639
|
+
// org total hosted storage cap (ADR-0122/0365) — blocks text writes, uploads, memo/book writes and new AI runs
|
|
28640
|
+
STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED",
|
|
28641
|
+
// Self-managed storage clear (ADR-0365): typed org-name confirmation mismatch; a kind the service
|
|
28642
|
+
// cannot clear; a clear already running for the org.
|
|
28643
|
+
STORAGE_CLEAR_CONFIRM_MISMATCH: "STORAGE_CLEAR_CONFIRM_MISMATCH",
|
|
28644
|
+
STORAGE_CLEAR_KIND_INVALID: "STORAGE_CLEAR_KIND_INVALID",
|
|
28645
|
+
STORAGE_CLEAR_RUNNING: "STORAGE_CLEAR_RUNNING",
|
|
28646
|
+
// project lifecycle (ADR-0092): invalid transition / operation on a paused|closed project
|
|
28647
|
+
PROJECT_STATE_INVALID: "PROJECT_STATE_INVALID",
|
|
28648
|
+
PROJECT_NOT_ACTIVE: "PROJECT_NOT_ACTIVE",
|
|
28649
|
+
// user pause (ADR-0093): user is paused (blocks login/API/token/WS) / invalid pause transition
|
|
28650
|
+
USER_PAUSED: "USER_PAUSED",
|
|
28651
|
+
USER_STATE_INVALID: "USER_STATE_INVALID",
|
|
28652
|
+
// org pause (ADR-0094 admin-0013): whole org paused ⇒ blocks all its users' login/API/token
|
|
28653
|
+
ORG_PAUSED: "ORG_PAUSED",
|
|
28654
|
+
// project-membership pause (ADR-0093): invalid membership pause transition
|
|
28655
|
+
MEMBER_STATE_INVALID: "MEMBER_STATE_INVALID",
|
|
28656
|
+
// soft-delete restore (ADR-0109): restore a record that is not currently deleted
|
|
28657
|
+
PROJECT_NOT_DELETED: "PROJECT_NOT_DELETED",
|
|
28658
|
+
TEAM_NOT_DELETED: "TEAM_NOT_DELETED",
|
|
28659
|
+
USER_NOT_DELETED: "USER_NOT_DELETED",
|
|
28660
|
+
USERNAME_TAKEN: "USERNAME_TAKEN",
|
|
28661
|
+
EMAIL_TAKEN: "EMAIL_TAKEN",
|
|
28662
|
+
// email omitted while the org disallows sub-accounts without email (ADR-0039)
|
|
28663
|
+
EMAIL_REQUIRED: "EMAIL_REQUIRED",
|
|
28664
|
+
// contact-change (self email/phone) verification (ADR-0046)
|
|
28665
|
+
// email and phone cannot be changed in the same request
|
|
28666
|
+
CONTACT_CHANGE_BOTH: "CONTACT_CHANGE_BOTH",
|
|
28667
|
+
// a contact-change is already pending — resolve it before editing again
|
|
28668
|
+
CONTACT_CHANGE_PENDING: "CONTACT_CHANGE_PENDING",
|
|
28669
|
+
// the verifying channel is missing (e.g. no phone on file to OTP an email change)
|
|
28670
|
+
CONTACT_CHANGE_CHANNEL_MISSING: "CONTACT_CHANGE_CHANNEL_MISSING",
|
|
28671
|
+
// the submitted OTP / link token is invalid or expired
|
|
28672
|
+
CONTACT_CHANGE_INVALID: "CONTACT_CHANGE_INVALID",
|
|
28673
|
+
// self email/phone must go through the contact-change flow (not a direct update)
|
|
28674
|
+
CONTACT_CHANGE_REQUIRED: "CONTACT_CHANGE_REQUIRED",
|
|
28675
|
+
ROLE_EXCLUSIVE: "ROLE_EXCLUSIVE",
|
|
28676
|
+
ROOT_IMMUTABLE: "ROOT_IMMUTABLE",
|
|
28677
|
+
// ADMIN role is reserved for the org root; it cannot be granted to sub-accounts
|
|
28678
|
+
ADMIN_ROLE_RESERVED: "ADMIN_ROLE_RESERVED",
|
|
28679
|
+
// a PM may only assign roles below PM (never PM/ADMIN/MACHINE) — anti-escalation (ADR-0051)
|
|
28680
|
+
USER_ROLE_ASSIGN_FORBIDDEN: "USER_ROLE_ASSIGN_FORBIDDEN",
|
|
28681
|
+
// an ADMIN account manages the org only — it cannot be attached to teams/projects
|
|
28682
|
+
ADMIN_NOT_ASSIGNABLE: "ADMIN_NOT_ASSIGNABLE",
|
|
28683
|
+
// a MACHINE account is a worker — it cannot be a project manager (ADR-0041)
|
|
28684
|
+
MACHINE_NOT_MANAGER: "MACHINE_NOT_MANAGER",
|
|
28685
|
+
// a MACHINE account may be a member of at most one project (ADR-0041)
|
|
28686
|
+
MACHINE_ALREADY_IN_PROJECT: "MACHINE_ALREADY_IN_PROJECT",
|
|
28687
|
+
// a MACHINE account is a worker, not a human member — it cannot join a team (ADR-0041)
|
|
28688
|
+
MACHINE_NOT_TEAM_MEMBER: "MACHINE_NOT_TEAM_MEMBER",
|
|
28689
|
+
ALREADY_ATTACHED: "ALREADY_ATTACHED",
|
|
28690
|
+
NOT_ATTACHED: "NOT_ATTACHED",
|
|
28691
|
+
SELF_DELETE_FORBIDDEN: "SELF_DELETE_FORBIDDEN",
|
|
28692
|
+
SELF_SET_PASSWORD_FORBIDDEN: "SELF_SET_PASSWORD_FORBIDDEN",
|
|
28693
|
+
AVATAR_INVALID_TYPE: "AVATAR_INVALID_TYPE",
|
|
28694
|
+
AVATAR_TOO_LARGE: "AVATAR_TOO_LARGE",
|
|
28695
|
+
// permission
|
|
28696
|
+
PERMISSION_CODE_INVALID: "PERMISSION_CODE_INVALID",
|
|
28697
|
+
SUBJECT_INVALID: "SUBJECT_INVALID",
|
|
28698
|
+
// machine
|
|
28699
|
+
HASHCODE_INVALID: "HASHCODE_INVALID",
|
|
28700
|
+
HASHCODE_EXPIRED: "HASHCODE_EXPIRED",
|
|
28701
|
+
LINK_REVOKED: "LINK_REVOKED",
|
|
28702
|
+
LINK_NOT_FOUND: "LINK_NOT_FOUND",
|
|
28703
|
+
WORKER_OFFLINE: "WORKER_OFFLINE",
|
|
28704
|
+
// command-0001 pick:"idle" (ADR-0171) — no idle cli in the project's pool
|
|
28705
|
+
ALL_CLIS_BUSY: "ALL_CLIS_BUSY",
|
|
28706
|
+
// worker pools (ADR-0284)
|
|
28707
|
+
WORKER_POOL_NOT_FOUND: "WORKER_POOL_NOT_FOUND",
|
|
28708
|
+
WORKER_POOL_NAME_TAKEN: "WORKER_POOL_NAME_TAKEN",
|
|
28709
|
+
WORKER_POOL_ATTACHED: "WORKER_POOL_ATTACHED",
|
|
28710
|
+
WORKER_POOL_ALREADY_ATTACHED: "WORKER_POOL_ALREADY_ATTACHED",
|
|
28711
|
+
MACHINE_ALREADY_IN_POOL: "MACHINE_ALREADY_IN_POOL",
|
|
28712
|
+
NOT_MACHINE_USER: "NOT_MACHINE_USER",
|
|
28713
|
+
// worker · physic project
|
|
28714
|
+
WORKER_NOT_FOUND: "WORKER_NOT_FOUND",
|
|
28715
|
+
PHYSIC_PROJECT_NOT_FOUND: "PHYSIC_PROJECT_NOT_FOUND",
|
|
28716
|
+
PHYSIC_PATH_OCCUPIED: "PHYSIC_PATH_OCCUPIED",
|
|
28717
|
+
AUTONOMOUS_CONFLICT: "AUTONOMOUS_CONFLICT",
|
|
28718
|
+
// A disruptive worker op (repo (re)provision / CLI update) was refused because the target
|
|
28719
|
+
// worker's autonomous mode is armed (installed && !paused) — stop it first (ADR-0317).
|
|
28720
|
+
AUTONOMOUS_ACTIVE: "AUTONOMOUS_ACTIVE",
|
|
28721
|
+
// Separation of duties (ADR-0320): a non-ADMIN tried to approve a row they wrote — the writer can't
|
|
28722
|
+
// be the approver of their own row (ADMIN may self-approve).
|
|
28723
|
+
APPROVAL_SELF: "APPROVAL_SELF",
|
|
28724
|
+
CLI_VERSION_UNSUPPORTED: "CLI_VERSION_UNSUPPORTED",
|
|
28725
|
+
// Admin cli version policy rejected (ADR-0363) — meta.reason: minAboveLatest | latestNotBlockable |
|
|
28726
|
+
// deadlinePast | invalidVersion.
|
|
28727
|
+
CLI_VERSION_POLICY_INVALID: "CLI_VERSION_POLICY_INVALID",
|
|
28728
|
+
ORCHESTRATOR_OFFLINE: "ORCHESTRATOR_OFFLINE",
|
|
28729
|
+
WS_TOKEN_INVALID: "WS_TOKEN_INVALID",
|
|
28730
|
+
// A newer cli session for the same link took over ⇒ the older one stops (ADR-0047).
|
|
28731
|
+
SESSION_REPLACED: "SESSION_REPLACED",
|
|
28732
|
+
PATH_INVALID: "PATH_INVALID",
|
|
28733
|
+
// project create/add
|
|
28734
|
+
SPEC_INVALID: "SPEC_INVALID",
|
|
28735
|
+
PATH_OCCUPIED: "PATH_OCCUPIED",
|
|
28736
|
+
GIT_ENV_MISSING: "GIT_ENV_MISSING",
|
|
28737
|
+
CLAUDE_CLI_MISSING: "CLAUDE_CLI_MISSING",
|
|
28738
|
+
SCAFFOLD_FAILED: "SCAFFOLD_FAILED",
|
|
28739
|
+
GIT_OPERATION_FAILED: "GIT_OPERATION_FAILED",
|
|
28740
|
+
AI_INIT_FAILED: "AI_INIT_FAILED",
|
|
28741
|
+
// AI spec-assist (suggest/review/compose) run failed on the worker cli (ADR-0100).
|
|
28742
|
+
AI_ASSIST_FAILED: "AI_ASSIST_FAILED",
|
|
28743
|
+
// Client-side guard (ADR-0255): the built AI prompt is longer than AI_PROMPT_MAX_LEN, so the
|
|
28744
|
+
// web blocks the dispatch and shows an actionable "spec too large" message instead of letting
|
|
28745
|
+
// the server reject it as a confusing VALIDATION_FAILED.
|
|
28746
|
+
AI_PROMPT_TOO_LARGE: "AI_PROMPT_TOO_LARGE",
|
|
28747
|
+
REPO_NOT_FOUND: "REPO_NOT_FOUND",
|
|
28748
|
+
// invitation
|
|
28749
|
+
INVITATION_TOKEN_INVALID: "INVITATION_TOKEN_INVALID",
|
|
28750
|
+
INVITATION_ALREADY_ACCEPTED: "INVITATION_ALREADY_ACCEPTED",
|
|
28751
|
+
INVITATION_ROLE_FORBIDDEN: "INVITATION_ROLE_FORBIDDEN",
|
|
28752
|
+
// notification
|
|
28753
|
+
NOTIFICATION_NOT_FOUND: "NOTIFICATION_NOT_FOUND",
|
|
28754
|
+
// sso (ADR-0024)
|
|
28755
|
+
SSO_IDENTITY_NOT_LINKED: "SSO_IDENTITY_NOT_LINKED",
|
|
28756
|
+
SSO_PROVIDER_INVALID: "SSO_PROVIDER_INVALID",
|
|
28757
|
+
// Cannot unlink the last remaining login method (SSO-only user, ADR-0161)
|
|
28758
|
+
SSO_LAST_LOGIN_METHOD: "SSO_LAST_LOGIN_METHOD",
|
|
28759
|
+
PASSWORD_NOT_SET: "PASSWORD_NOT_SET",
|
|
28760
|
+
// command · memo
|
|
28761
|
+
COMMAND_NOT_FOUND: "COMMAND_NOT_FOUND",
|
|
28762
|
+
// Stop requested for a command that already settled (done/failed/cancelled — ADR-0362).
|
|
28763
|
+
COMMAND_ALREADY_FINISHED: "COMMAND_ALREADY_FINISHED",
|
|
28764
|
+
COMMAND_DISPATCH_LIMITED: "COMMAND_DISPATCH_LIMITED",
|
|
28765
|
+
QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
|
|
28766
|
+
// A single prompt's estimated tokens exceed project `settings.tokens.perPromptTokenLimit` (ADR-0081).
|
|
28767
|
+
PROMPT_TOKEN_LIMIT_EXCEEDED: "PROMPT_TOKEN_LIMIT_EXCEEDED",
|
|
28768
|
+
// An outbound cli evaluated the input as NG (secret/path/repo/environment) — ADR-0082.
|
|
28769
|
+
OUTBOUND_REVIEW_REJECTED: "OUTBOUND_REVIEW_REJECTED",
|
|
28770
|
+
// No outbound cli available (offline/busy/out-of-token) while review is on — ADR-0082.
|
|
28771
|
+
OUTBOUND_REVIEW_UNAVAILABLE: "OUTBOUND_REVIEW_UNAVAILABLE",
|
|
28772
|
+
// A saved outbound rule regex fails to compile or is ReDoS-unsafe (ADR-0087).
|
|
28773
|
+
OUTBOUND_RULE_INVALID: "OUTBOUND_RULE_INVALID",
|
|
28774
|
+
// Console image attachment blocked because the project has `outboundReview.blockImages` (ADR-0257).
|
|
28775
|
+
IMAGE_UPLOAD_BLOCKED: "IMAGE_UPLOAD_BLOCKED",
|
|
28776
|
+
MEMO_TOO_LONG: "MEMO_TOO_LONG",
|
|
28777
|
+
// plan entitlements (ADR-0105) — resource caps + feature gates per plan tier
|
|
28778
|
+
PLAN_USER_LIMIT: "PLAN_USER_LIMIT",
|
|
28779
|
+
PLAN_PROJECT_LIMIT: "PLAN_PROJECT_LIMIT",
|
|
28780
|
+
// Plan memo-item cap reached (`maxMemoItems` — ADR-0365).
|
|
28781
|
+
PLAN_MEMO_LIMIT: "PLAN_MEMO_LIMIT",
|
|
28782
|
+
// An autonomous book reached its monthly `autonomousRequestsPerMonth` cap (ADR-0365).
|
|
28783
|
+
AUTONOMOUS_BOOK_CAP_REACHED: "AUTONOMOUS_BOOK_CAP_REACHED",
|
|
28784
|
+
PLAN_FEATURE_UNAVAILABLE: "PLAN_FEATURE_UNAVAILABLE",
|
|
28785
|
+
// deployment license ceiling (ADR-0135) — a self-host license bounds orgs/seats/projects/
|
|
28786
|
+
// storage across the whole deployment, above the per-org plan caps
|
|
28787
|
+
LICENSE_LIMIT_EXCEEDED: "LICENSE_LIMIT_EXCEEDED",
|
|
28788
|
+
// billing add-ons & rented machine-users (ADR-0123/0126) — payment-server · admin-server
|
|
28789
|
+
// buying an add-on without an active paid plan (ADR-0123)
|
|
28790
|
+
ADDON_REQUIRES_PLAN: "ADDON_REQUIRES_PLAN",
|
|
28791
|
+
// renting a machine-user with no free pool slot (ADR-0126) — the org may be waitlisted
|
|
28792
|
+
NO_HOSTED_SLOT_AVAILABLE: "NO_HOSTED_SLOT_AVAILABLE",
|
|
28793
|
+
// admin changing a rented hosted slot without forceRelease (ADR-0126)
|
|
28794
|
+
SLOT_RENTED: "SLOT_RENTED",
|
|
28795
|
+
// re-renting a machine whose rental is already `releasing` — too late to undo (ADR-0210)
|
|
28796
|
+
RENTED_MACHINE_RELEASING: "RENTED_MACHINE_RELEASING",
|
|
28797
|
+
// credit wallet & coupons (ADR-0118/0119/0120) — served by payment-server
|
|
28798
|
+
CREDIT_INSUFFICIENT: "CREDIT_INSUFFICIENT",
|
|
28799
|
+
COUPON_NOT_FOUND: "COUPON_NOT_FOUND",
|
|
28800
|
+
COUPON_EXPIRED: "COUPON_EXPIRED",
|
|
28801
|
+
COUPON_DISABLED: "COUPON_DISABLED",
|
|
28802
|
+
COUPON_EXHAUSTED: "COUPON_EXHAUSTED",
|
|
28803
|
+
COUPON_ALREADY_REDEEMED: "COUPON_ALREADY_REDEEMED",
|
|
28804
|
+
// payment-server catalog completion (ADR-0207) — credit/addon/plan errors that previously
|
|
28805
|
+
// threw raw strings or literals duplicated inside payment-server; now first-class codes.
|
|
28806
|
+
// credit mutation validation (credit.service)
|
|
28807
|
+
INVALID_AMOUNT: "INVALID_AMOUNT",
|
|
28808
|
+
INVALID_DELTA: "INVALID_DELTA",
|
|
28809
|
+
TOPUP_NOT_FOUND: "TOPUP_NOT_FOUND",
|
|
28810
|
+
// storage add-on SKU management (addon.service — admin-0031/0032)
|
|
28811
|
+
STORAGE_SKU_INVALID: "STORAGE_SKU_INVALID",
|
|
28812
|
+
STORAGE_SKU_NOT_FOUND: "STORAGE_SKU_NOT_FOUND",
|
|
28813
|
+
STORAGE_SIZE_INVALID: "STORAGE_SIZE_INVALID",
|
|
28814
|
+
ADDON_CODE_EXISTS: "ADDON_CODE_EXISTS",
|
|
28815
|
+
ADDON_PRICE_INVALID: "ADDON_PRICE_INVALID",
|
|
28816
|
+
ADDON_PRODUCT_UNKNOWN: "ADDON_PRODUCT_UNKNOWN",
|
|
28817
|
+
ADDON_QUANTITY_INVALID: "ADDON_QUANTITY_INVALID",
|
|
28818
|
+
ADDON_NOT_FOUND: "ADDON_NOT_FOUND",
|
|
28819
|
+
// plan / subscription lifecycle (billing.service — ADR-0163/0142)
|
|
28820
|
+
PLAN_CODE_REQUIRED: "PLAN_CODE_REQUIRED",
|
|
28821
|
+
PLAN_UNCHANGED: "PLAN_UNCHANGED",
|
|
28822
|
+
PLAN_CHANGE_INVALID: "PLAN_CHANGE_INVALID",
|
|
28823
|
+
NO_ACTIVE_SUBSCRIPTION: "NO_ACTIVE_SUBSCRIPTION",
|
|
28824
|
+
NO_PROVIDER_SUBSCRIPTION: "NO_PROVIDER_SUBSCRIPTION",
|
|
28825
|
+
ENTERPRISE_PLAN_NOT_ASSIGNABLE: "ENTERPRISE_PLAN_NOT_ASSIGNABLE",
|
|
28826
|
+
PAYMENT_FAILED: "PAYMENT_FAILED",
|
|
28827
|
+
// support tickets (ADR-0167) + AI help agent (ADR-0168)
|
|
28828
|
+
SUPPORT_TICKET_NOT_FOUND: "SUPPORT_TICKET_NOT_FOUND",
|
|
28829
|
+
// managed legal documents (ADR-0350): unknown/disabled document; accepting a version that is not current
|
|
28830
|
+
LEGAL_DOCUMENT_NOT_FOUND: "LEGAL_DOCUMENT_NOT_FOUND",
|
|
28831
|
+
LEGAL_VERSION_OUTDATED: "LEGAL_VERSION_OUTDATED",
|
|
28832
|
+
SUPPORT_TICKET_CLOSED: "SUPPORT_TICKET_CLOSED",
|
|
28833
|
+
// public (guest) support create (ADR-0279): the self-hosted captcha answer was wrong/expired.
|
|
28834
|
+
CAPTCHA_INVALID: "CAPTCHA_INVALID",
|
|
28835
|
+
HELP_POOL_UNAVAILABLE: "HELP_POOL_UNAVAILABLE",
|
|
28836
|
+
// AI Help daily cap reached (ADR-0286): the asker (or their org) exceeded the per-day question
|
|
28837
|
+
// cap; meta carries { scope, limit, used, resetAt }. Thrown before any dispatch (0 token cost).
|
|
28838
|
+
HELP_RATE_LIMITED: "HELP_RATE_LIMITED",
|
|
28839
|
+
// AI Help admin moderation (ADR-0237): the user/org is blocked from the channel, or the whole
|
|
28840
|
+
// AI support service is toggled off platform-wide.
|
|
28841
|
+
HELP_BLOCKED: "HELP_BLOCKED",
|
|
28842
|
+
HELP_DISABLED: "HELP_DISABLED",
|
|
28843
|
+
// Project GitHub-App git-auth (ADR-0356): method set without a credential; a rejected credential
|
|
28844
|
+
// (App ID / PEM / host / CA); an ssh repo URL under the App method; the primary repo's host differs
|
|
28845
|
+
// from the credential host; the server has no SECRETS_ENC_KEY to store/read secrets.
|
|
28846
|
+
GIT_CREDENTIAL_MISSING: "GIT_CREDENTIAL_MISSING",
|
|
28847
|
+
GIT_CREDENTIAL_INVALID: "GIT_CREDENTIAL_INVALID",
|
|
28848
|
+
GIT_AUTH_REQUIRES_HTTPS: "GIT_AUTH_REQUIRES_HTTPS",
|
|
28849
|
+
GIT_HOST_MISMATCH: "GIT_HOST_MISMATCH",
|
|
28850
|
+
SECRETS_KEY_NOT_CONFIGURED: "SECRETS_KEY_NOT_CONFIGURED",
|
|
28851
|
+
// Org announcements (ADR-0367): a target outside the sender's PM/TL scope; targets resolving to
|
|
28852
|
+
// nobody; the org's daily recipient cap (`announcementDailyRecipients`); unknown history row /
|
|
28853
|
+
// draft; the per-user draft cap.
|
|
28854
|
+
ANNOUNCEMENT_TARGET_FORBIDDEN: "ANNOUNCEMENT_TARGET_FORBIDDEN",
|
|
28855
|
+
ANNOUNCEMENT_NO_RECIPIENTS: "ANNOUNCEMENT_NO_RECIPIENTS",
|
|
28856
|
+
ANNOUNCEMENT_DAILY_LIMIT: "ANNOUNCEMENT_DAILY_LIMIT",
|
|
28857
|
+
ANNOUNCEMENT_NOT_FOUND: "ANNOUNCEMENT_NOT_FOUND",
|
|
28858
|
+
ANNOUNCEMENT_DRAFT_NOT_FOUND: "ANNOUNCEMENT_DRAFT_NOT_FOUND",
|
|
28859
|
+
ANNOUNCEMENT_DRAFT_LIMIT: "ANNOUNCEMENT_DRAFT_LIMIT",
|
|
28860
|
+
// Worker-cli operation failures surfaced to the web/admin so they localize via `errors.*`
|
|
28861
|
+
// (see CLI_ERROR_CODE_MAP below): filesystem guards, RAG, agent/content validation, support
|
|
28862
|
+
// answer, FAQ compose, git host, autonomous base push.
|
|
28863
|
+
WORKER_PATH_ESCAPES_ROOT: "WORKER_PATH_ESCAPES_ROOT",
|
|
28864
|
+
WORKER_NO_PROJECT: "WORKER_NO_PROJECT",
|
|
28865
|
+
WORKER_PATH_EXISTS: "WORKER_PATH_EXISTS",
|
|
28866
|
+
WORKER_ROOT_PROTECTED: "WORKER_ROOT_PROTECTED",
|
|
28867
|
+
WORKER_NOT_A_FILE: "WORKER_NOT_A_FILE",
|
|
28868
|
+
WORKER_INVALID_PAYLOAD: "WORKER_INVALID_PAYLOAD",
|
|
28869
|
+
WORKER_INVALID_NAME: "WORKER_INVALID_NAME",
|
|
28870
|
+
WORKER_CONTENT_REQUIRED: "WORKER_CONTENT_REQUIRED",
|
|
28871
|
+
RAG_PYTHON_MISSING: "RAG_PYTHON_MISSING",
|
|
28872
|
+
RAG_NOT_INSTALLED: "RAG_NOT_INSTALLED",
|
|
28873
|
+
RAG_NO_INDEX: "RAG_NO_INDEX",
|
|
28874
|
+
RAG_INVALID_MODEL: "RAG_INVALID_MODEL",
|
|
28875
|
+
SUPPORT_KB_EMPTY: "SUPPORT_KB_EMPTY",
|
|
28876
|
+
SUPPORT_EMPTY_ANSWER: "SUPPORT_EMPTY_ANSWER",
|
|
28877
|
+
FAQ_NO_WRITE_TOKEN: "FAQ_NO_WRITE_TOKEN",
|
|
28878
|
+
GIT_UNKNOWN_HOST: "GIT_UNKNOWN_HOST",
|
|
28879
|
+
AUTONOMOUS_BASE_PUSH_FAILED: "AUTONOMOUS_BASE_PUSH_FAILED"
|
|
28880
|
+
};
|
|
28881
|
+
var CLI_ERROR_CODE_MAP = {
|
|
28882
|
+
"path escapes the project root": { code: ErrorCode.WORKER_PATH_ESCAPES_ROOT, status: 422 },
|
|
28883
|
+
"no project served": { code: ErrorCode.WORKER_NO_PROJECT, status: 409 },
|
|
28884
|
+
"no project": { code: ErrorCode.WORKER_NO_PROJECT, status: 409 },
|
|
28885
|
+
"This worker has no project folder.": { code: ErrorCode.WORKER_NO_PROJECT, status: 409 },
|
|
28886
|
+
"already exists": { code: ErrorCode.WORKER_PATH_EXISTS, status: 409 },
|
|
28887
|
+
"destination already exists": { code: ErrorCode.WORKER_PATH_EXISTS, status: 409 },
|
|
28888
|
+
"cannot move the project root": { code: ErrorCode.WORKER_ROOT_PROTECTED, status: 422 },
|
|
28889
|
+
"cannot delete the project root": { code: ErrorCode.WORKER_ROOT_PROTECTED, status: 422 },
|
|
28890
|
+
"not a file": { code: ErrorCode.WORKER_NOT_A_FILE, status: 422 },
|
|
28891
|
+
"invalid base64 payload": { code: ErrorCode.WORKER_INVALID_PAYLOAD, status: 422 },
|
|
28892
|
+
"invalid name": { code: ErrorCode.WORKER_INVALID_NAME, status: 422 },
|
|
28893
|
+
"content is required": { code: ErrorCode.WORKER_CONTENT_REQUIRED, status: 422 },
|
|
28894
|
+
"invalid slug": { code: ErrorCode.MARKET_INVALID_PACKAGE, status: 422 },
|
|
28895
|
+
"artifact not found": { code: ErrorCode.MARKET_PACKAGE_NOT_FOUND, status: 404 },
|
|
28896
|
+
"not installed": { code: ErrorCode.MARKET_PACKAGE_NOT_FOUND, status: 404 },
|
|
28897
|
+
"python3 not found": { code: ErrorCode.RAG_PYTHON_MISSING, status: 422 },
|
|
28898
|
+
"python3 not found on the worker": { code: ErrorCode.RAG_PYTHON_MISSING, status: 422 },
|
|
28899
|
+
"RAG is not installed": { code: ErrorCode.RAG_NOT_INSTALLED, status: 409 },
|
|
28900
|
+
"no index \u2014 reindex first": { code: ErrorCode.RAG_NO_INDEX, status: 409 },
|
|
28901
|
+
"invalid model name": { code: ErrorCode.RAG_INVALID_MODEL, status: 422 },
|
|
28902
|
+
"KB repo has no documentation": { code: ErrorCode.SUPPORT_KB_EMPTY, status: 422 },
|
|
28903
|
+
"empty answer": { code: ErrorCode.SUPPORT_EMPTY_ANSWER, status: 422 },
|
|
28904
|
+
"no write token": { code: ErrorCode.FAQ_NO_WRITE_TOKEN, status: 422 },
|
|
28905
|
+
"Unknown git host \u2014 no pull request opened.": { code: ErrorCode.GIT_UNKNOWN_HOST, status: 422 },
|
|
28906
|
+
"could not push the change to the base branch (retry)": { code: ErrorCode.AUTONOMOUS_BASE_PUSH_FAILED, status: 422 },
|
|
28907
|
+
"self-approval blocked": { code: ErrorCode.APPROVAL_SELF, status: 409 }
|
|
28908
|
+
};
|
|
28909
|
+
|
|
28572
28910
|
// vendor/@4pm/constants/preferences.ts
|
|
28573
28911
|
var Locale = {
|
|
28574
28912
|
VI: "vi",
|
|
@@ -28584,8 +28922,30 @@ function normalizeLocale(value) {
|
|
|
28584
28922
|
|
|
28585
28923
|
// vendor/@4pm/constants/project-template.ts
|
|
28586
28924
|
var PROJECT_TEMPLATE = {
|
|
28587
|
-
version: "1.0
|
|
28925
|
+
version: "1.1.0",
|
|
28588
28926
|
changelog: [
|
|
28927
|
+
{
|
|
28928
|
+
version: "1.1.0",
|
|
28929
|
+
date: "2026-09-30",
|
|
28930
|
+
summary: "Multi-worker autonomous books: multi-row AI_PROGRESS claims, QA-gated task Depends, attempts sidecar (ADR-0371).",
|
|
28931
|
+
notes: [
|
|
28932
|
+
"AI_PROGRESS.md holds one row per claimed task: | Started | ID | Worker | Claim | Attempt | Task description | (was a single | Started | ID | Task description | row). Convert an existing single row into the new columns (Worker/Claim empty, Attempt 1).",
|
|
28933
|
+
"AI_TODO.md Depends may list QA-\u2026 ids (the task waits for that USER_QA answer); two or more TSK dependencies require their PRs to be merged into the base branch; Notes may carry size: S|M and from: <branch>.",
|
|
28934
|
+
"USER_QA.md notes that a question raised while implementing a task is listed in that task's Depends.",
|
|
28935
|
+
".claude/AUTONOMOUS.md documents branches & claims; .claude/.autonomous.attempts.json (per-task attempts / split-pending) is committed with the books \u2014 make sure it is NOT gitignored."
|
|
28936
|
+
],
|
|
28937
|
+
files: [
|
|
28938
|
+
"AI_PROGRESS.md",
|
|
28939
|
+
"AI_TODO.md",
|
|
28940
|
+
"USER_QA.md",
|
|
28941
|
+
".claude/AUTONOMOUS.md",
|
|
28942
|
+
".claude/templates/AI_PROGRESS.empty.md",
|
|
28943
|
+
".claude/templates/AI_PROGRESS.sample.md",
|
|
28944
|
+
".claude/templates/AI_TODO.empty.md",
|
|
28945
|
+
".claude/templates/AI_TODO.sample.md",
|
|
28946
|
+
".claude/templates/USER_QA.empty.md"
|
|
28947
|
+
]
|
|
28948
|
+
},
|
|
28589
28949
|
{
|
|
28590
28950
|
version: "1.0.2",
|
|
28591
28951
|
date: "2026-09-16",
|
|
@@ -28908,7 +29268,11 @@ var en_default = {
|
|
|
28908
29268
|
"autoRun.failed": "Autonomous cycle failed to dispatch: {note}",
|
|
28909
29269
|
"autoRun.noDaemon": 'No running daemon for profile "{profile}" \u2014 start it first: 4pm start --profile {profile}',
|
|
28910
29270
|
"autoRun.timeout": "Autonomous cycle exceeded the time limit \u2014 aborting.",
|
|
28911
|
-
"autoRun.authFailed": "Control-channel authentication failed \u2014 the daemon rejected the token."
|
|
29271
|
+
"autoRun.authFailed": "Control-channel authentication failed \u2014 the daemon rejected the token.",
|
|
29272
|
+
"autonomous.cronMissing": "Could not install the autonomous cron: this worker has no working `crontab` (cron service). Autonomous scheduling requires cron on the worker machine.",
|
|
29273
|
+
"autonomous.settingsInvalidJson": "The autonomous settings are not valid JSON.",
|
|
29274
|
+
"config.notObject": "config.json must be a JSON object.",
|
|
29275
|
+
"config.invalidJson": "Invalid JSON: {message}"
|
|
28912
29276
|
};
|
|
28913
29277
|
|
|
28914
29278
|
// src/i18n/messages/vi.json
|
|
@@ -29093,7 +29457,11 @@ var vi_default = {
|
|
|
29093
29457
|
"autoRun.failed": "Kh\xF4ng \u0111i\u1EC1u ph\u1ED1i \u0111\u01B0\u1EE3c chu k\u1EF3 t\u1EF1 \u0111\u1ED9ng: {note}",
|
|
29094
29458
|
"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}',
|
|
29095
29459
|
"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.",
|
|
29096
|
-
"autoRun.authFailed": "X\xE1c th\u1EF1c k\xEAnh \u0111i\u1EC1u khi\u1EC3n th\u1EA5t b\u1EA1i \u2014 daemon t\u1EEB ch\u1ED1i token."
|
|
29460
|
+
"autoRun.authFailed": "X\xE1c th\u1EF1c k\xEAnh \u0111i\u1EC1u khi\u1EC3n th\u1EA5t b\u1EA1i \u2014 daemon t\u1EEB ch\u1ED1i token.",
|
|
29461
|
+
"autonomous.cronMissing": "Kh\xF4ng c\xE0i \u0111\u01B0\u1EE3c cron t\u1EF1 h\xE0nh: worker n\xE0y kh\xF4ng c\xF3 `crontab` ho\u1EA1t \u0111\u1ED9ng (d\u1ECBch v\u1EE5 cron). L\u1EADp l\u1ECBch t\u1EF1 h\xE0nh c\u1EA7n cron tr\xEAn m\xE1y worker.",
|
|
29462
|
+
"autonomous.settingsInvalidJson": "C\u1EA5u h\xECnh t\u1EF1 h\xE0nh kh\xF4ng ph\u1EA3i JSON h\u1EE3p l\u1EC7.",
|
|
29463
|
+
"config.notObject": "config.json ph\u1EA3i l\xE0 m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng JSON.",
|
|
29464
|
+
"config.invalidJson": "JSON kh\xF4ng h\u1EE3p l\u1EC7: {message}"
|
|
29097
29465
|
};
|
|
29098
29466
|
|
|
29099
29467
|
// src/i18n/messages/ja.json
|
|
@@ -29278,7 +29646,11 @@ var ja_default = {
|
|
|
29278
29646
|
"autoRun.failed": "\u81EA\u5F8B\u30B5\u30A4\u30AF\u30EB\u306E\u30C7\u30A3\u30B9\u30D1\u30C3\u30C1\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {note}",
|
|
29279
29647
|
"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}',
|
|
29280
29648
|
"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",
|
|
29281
|
-
"autoRun.authFailed": "\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB\u30C1\u30E3\u30CD\u30EB\u306E\u8A8D\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F \u2014 \u30C7\u30FC\u30E2\u30F3\u304C\u30C8\u30FC\u30AF\u30F3\u3092\u62D2\u5426\u3057\u307E\u3057\u305F\u3002"
|
|
29649
|
+
"autoRun.authFailed": "\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB\u30C1\u30E3\u30CD\u30EB\u306E\u8A8D\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F \u2014 \u30C7\u30FC\u30E2\u30F3\u304C\u30C8\u30FC\u30AF\u30F3\u3092\u62D2\u5426\u3057\u307E\u3057\u305F\u3002",
|
|
29650
|
+
"autonomous.cronMissing": "\u81EA\u5F8B cron \u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F: \u3053\u306E\u30EF\u30FC\u30AB\u30FC\u306B\u306F\u52D5\u4F5C\u3059\u308B `crontab`\uFF08cron \u30B5\u30FC\u30D3\u30B9\uFF09\u304C\u3042\u308A\u307E\u305B\u3093\u3002\u81EA\u5F8B\u30B9\u30B1\u30B8\u30E5\u30FC\u30EA\u30F3\u30B0\u306B\u306F\u30EF\u30FC\u30AB\u30FC\u30DE\u30B7\u30F3\u306E cron \u304C\u5FC5\u8981\u3067\u3059\u3002",
|
|
29651
|
+
"autonomous.settingsInvalidJson": "\u81EA\u5F8B\u8A2D\u5B9A\u304C\u6709\u52B9\u306A JSON \u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
29652
|
+
"config.notObject": "config.json \u306F JSON \u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
29653
|
+
"config.invalidJson": "\u7121\u52B9\u306A JSON: {message}"
|
|
29282
29654
|
};
|
|
29283
29655
|
|
|
29284
29656
|
// src/i18n/messages/zh.json
|
|
@@ -29463,7 +29835,11 @@ var zh_default = {
|
|
|
29463
29835
|
"autoRun.failed": "\u81EA\u4E3B\u5FAA\u73AF\u8C03\u5EA6\u5931\u8D25\uFF1A{note}",
|
|
29464
29836
|
"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}",
|
|
29465
29837
|
"autoRun.timeout": "\u81EA\u4E3B\u5FAA\u73AF\u8D85\u8FC7\u65F6\u95F4\u9650\u5236 \u2014 \u6B63\u5728\u4E2D\u6B62\u3002",
|
|
29466
|
-
"autoRun.authFailed": "\u63A7\u5236\u901A\u9053\u8BA4\u8BC1\u5931\u8D25 \u2014 \u5B88\u62A4\u8FDB\u7A0B\u62D2\u7EDD\u4E86\u4EE4\u724C\u3002"
|
|
29838
|
+
"autoRun.authFailed": "\u63A7\u5236\u901A\u9053\u8BA4\u8BC1\u5931\u8D25 \u2014 \u5B88\u62A4\u8FDB\u7A0B\u62D2\u7EDD\u4E86\u4EE4\u724C\u3002",
|
|
29839
|
+
"autonomous.cronMissing": "\u65E0\u6CD5\u5B89\u88C5\u81EA\u4E3B cron\uFF1A\u6B64\u5DE5\u4F5C\u673A\u6CA1\u6709\u53EF\u7528\u7684 `crontab`\uFF08cron \u670D\u52A1\uFF09\u3002\u81EA\u4E3B\u8C03\u5EA6\u9700\u8981\u5DE5\u4F5C\u673A\u4E0A\u7684 cron\u3002",
|
|
29840
|
+
"autonomous.settingsInvalidJson": "\u81EA\u4E3B\u8BBE\u7F6E\u4E0D\u662F\u6709\u6548\u7684 JSON\u3002",
|
|
29841
|
+
"config.notObject": "config.json \u5FC5\u987B\u662F JSON \u5BF9\u8C61\u3002",
|
|
29842
|
+
"config.invalidJson": "\u65E0\u6548\u7684 JSON\uFF1A{message}"
|
|
29467
29843
|
};
|
|
29468
29844
|
|
|
29469
29845
|
// src/i18n/messages/index.ts
|
|
@@ -29550,7 +29926,7 @@ async function runLink(serverUrl, explicitProfile, token = null) {
|
|
|
29550
29926
|
|
|
29551
29927
|
// src/commands/start.ts
|
|
29552
29928
|
import { spawn as spawn11 } from "child_process";
|
|
29553
|
-
import { join as
|
|
29929
|
+
import { join as join42 } from "path";
|
|
29554
29930
|
|
|
29555
29931
|
// src/core/update.ts
|
|
29556
29932
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -32769,8 +33145,8 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
32769
33145
|
// src/core/ws-client.ts
|
|
32770
33146
|
init_wrapper();
|
|
32771
33147
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
32772
|
-
import { basename as basename7, join as
|
|
32773
|
-
import { existsSync as
|
|
33148
|
+
import { basename as basename7, join as join37 } from "path";
|
|
33149
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync9 } from "fs";
|
|
32774
33150
|
|
|
32775
33151
|
// vendor/@4pm/utils/helpers.ts
|
|
32776
33152
|
function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
|
|
@@ -37058,6 +37434,16 @@ var putGitCredentialRequestSchema = external_exports.object({
|
|
|
37058
37434
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["host"], message: "host is required for GitHub Enterprise" });
|
|
37059
37435
|
}
|
|
37060
37436
|
});
|
|
37437
|
+
function parseRepoUrl(url) {
|
|
37438
|
+
const raw = url.trim();
|
|
37439
|
+
const scp = /^[\w.-]+@([^:/\s]+):([^/\s]+)\/(.+?)(?:\.git)?\/?$/.exec(raw);
|
|
37440
|
+
if (scp) return { protocol: "ssh", host: scp[1].toLowerCase(), owner: scp[2], name: scp[3] };
|
|
37441
|
+
const std = /^(https?|ssh):\/\/(?:[^@/\s]+@)?([^/\s]+)\/([^/\s]+)\/(.+?)(?:\.git)?\/?$/.exec(raw);
|
|
37442
|
+
if (std) {
|
|
37443
|
+
return { protocol: std[1], host: std[2].toLowerCase(), owner: std[3], name: std[4] };
|
|
37444
|
+
}
|
|
37445
|
+
return null;
|
|
37446
|
+
}
|
|
37061
37447
|
function githubHostKind(host) {
|
|
37062
37448
|
const h = host.toLowerCase();
|
|
37063
37449
|
if (h === "github.com") return "github";
|
|
@@ -37492,7 +37878,30 @@ var repoSpecSchema = external_exports.object({
|
|
|
37492
37878
|
* worker clones with `-b <branch>` and the on-demand "update repos" action checks it out +
|
|
37493
37879
|
* fast-forward pulls it.
|
|
37494
37880
|
*/
|
|
37495
|
-
branch: external_exports.string().max(200).optional().default("")
|
|
37881
|
+
branch: external_exports.string().max(200).optional().default(""),
|
|
37882
|
+
/**
|
|
37883
|
+
* Where `branch` is created from when it does not exist on the remote yet (ADR-0370): the default
|
|
37884
|
+
* branch, another remote branch (`branch`, primary only), or `empty` — an orphan branch with no
|
|
37885
|
+
* history. Absent ⇒ `default`.
|
|
37886
|
+
*/
|
|
37887
|
+
base: external_exports.object({
|
|
37888
|
+
kind: external_exports.enum(["default", "branch", "empty"]),
|
|
37889
|
+
branch: external_exports.string().max(200).optional()
|
|
37890
|
+
}).optional(),
|
|
37891
|
+
/**
|
|
37892
|
+
* Primary only (ADR-0370): reset the 4PM tracking files (AI_TODO / AI_DONE / AI_PROGRESS / USER_TODO /
|
|
37893
|
+
* USER_QA) to the template's empty versions after the scaffold copy. Absent ⇒ reset only when the
|
|
37894
|
+
* source already has a 4PM scaffold.
|
|
37895
|
+
*/
|
|
37896
|
+
resetTracking: external_exports.boolean().optional()
|
|
37897
|
+
});
|
|
37898
|
+
var SCAFFOLD_TRACKING_FILES = ["AI_TODO.md", "AI_DONE.md", "AI_PROGRESS.md", "USER_TODO.md", "USER_QA.md"];
|
|
37899
|
+
var repoProbeRequestSchema = external_exports.object({
|
|
37900
|
+
url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
|
|
37901
|
+
branch: external_exports.string().max(200).optional().default(""),
|
|
37902
|
+
base: external_exports.object({ kind: external_exports.enum(["default", "branch", "empty"]), branch: external_exports.string().max(200).optional() }).optional(),
|
|
37903
|
+
/** The declared submodules to probe too — existence + protection (ADR-0371). */
|
|
37904
|
+
submodules: external_exports.array(external_exports.object({ subdir: external_exports.string().max(120), url: external_exports.string().max(500), branch: external_exports.string().max(200).optional().default("") })).max(20).optional()
|
|
37496
37905
|
});
|
|
37497
37906
|
function validateProjectRepos(repos, ctx, path = ["repos"]) {
|
|
37498
37907
|
const list = repos ?? [];
|
|
@@ -37651,7 +38060,7 @@ var userCardsQuerySchema = external_exports.object({
|
|
|
37651
38060
|
});
|
|
37652
38061
|
|
|
37653
38062
|
// src/core/scaffold.ts
|
|
37654
|
-
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
38063
|
+
import { cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
37655
38064
|
import { existsSync as existsSync4 } from "fs";
|
|
37656
38065
|
import { execFile } from "child_process";
|
|
37657
38066
|
import { tmpdir as tmpdir2 } from "os";
|
|
@@ -37670,65 +38079,82 @@ function submodulesOf(repos) {
|
|
|
37670
38079
|
const primary = singleRepo(repos);
|
|
37671
38080
|
return repos.filter((r) => r !== primary && !!r.url && !!(r.subdir ?? "").trim());
|
|
37672
38081
|
}
|
|
37673
|
-
async function
|
|
37674
|
-
if (
|
|
37675
|
-
|
|
38082
|
+
async function isRegisteredSubmodule(root, dir2) {
|
|
38083
|
+
if (existsSync4(join6(root, ".gitmodules"))) {
|
|
38084
|
+
const paths = await gitOut(root, ["config", "-f", ".gitmodules", "--get-regexp", "^submodule\\..*\\.path$"]);
|
|
38085
|
+
if (paths.split("\n").some((l) => l.split(" ").slice(1).join(" ") === dir2)) return true;
|
|
38086
|
+
}
|
|
38087
|
+
const staged = await gitOut(root, ["ls-files", "--stage", "--", dir2]);
|
|
38088
|
+
return staged.split("\n").some((l) => l.startsWith("160000 "));
|
|
38089
|
+
}
|
|
38090
|
+
async function listFiles(abs, rel = "") {
|
|
38091
|
+
const out = [];
|
|
38092
|
+
for (const e of await readdir(join6(abs, rel), { withFileTypes: true }).catch(() => [])) {
|
|
38093
|
+
if (e.name === ".git") continue;
|
|
38094
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
38095
|
+
if (e.isDirectory()) out.push(...await listFiles(abs, r));
|
|
38096
|
+
else out.push(r);
|
|
38097
|
+
}
|
|
38098
|
+
return out;
|
|
38099
|
+
}
|
|
38100
|
+
async function prepareSubmodulePath(root, dir2, emit) {
|
|
38101
|
+
const abs = join6(root, dir2);
|
|
38102
|
+
const tracked = (await gitOut(root, ["ls-files", "--", dir2])).split("\n").filter(Boolean);
|
|
38103
|
+
const onDisk = existsSync4(abs) ? await listFiles(abs) : [];
|
|
38104
|
+
const trackedRel = new Set(tracked.map((f2) => f2.slice(dir2.length + 1)));
|
|
38105
|
+
const foreign = onDisk.filter((f2) => !trackedRel.has(f2) && !f2.endsWith(".gitkeep"));
|
|
38106
|
+
if (foreign.length > 0) {
|
|
38107
|
+
throw new Error(`"${dir2}" already holds ${foreign.length} file(s) that are not part of the repo (e.g. ${foreign[0]}) \u2014 move them away, then retry.`);
|
|
38108
|
+
}
|
|
38109
|
+
if (tracked.length > 0) {
|
|
38110
|
+
emit("submodule", `Untracking the placeholder files in ${dir2}\u2026`);
|
|
38111
|
+
await run("git", ["rm", "-r", "-q", "--cached", "--", dir2], { cwd: root, timeout: 3e4 });
|
|
38112
|
+
}
|
|
38113
|
+
const modules = join6(root, ".git", "modules", dir2);
|
|
38114
|
+
if (existsSync4(modules)) {
|
|
38115
|
+
emit("submodule", `Cleaning up a half-attached ${dir2}\u2026`);
|
|
38116
|
+
await rm(modules, { recursive: true, force: true });
|
|
38117
|
+
}
|
|
38118
|
+
if (existsSync4(abs)) await rm(abs, { recursive: true, force: true });
|
|
38119
|
+
}
|
|
38120
|
+
async function attachSubmodules(root, submodules, emit, opts = {}) {
|
|
38121
|
+
const outcomes = [];
|
|
38122
|
+
if (submodules.length === 0) return outcomes;
|
|
38123
|
+
if (!existsSync4(join6(root, ".git"))) return outcomes;
|
|
37676
38124
|
let added = 0;
|
|
37677
38125
|
for (const sub of submodules) {
|
|
37678
38126
|
const dir2 = (sub.subdir ?? "").trim();
|
|
37679
38127
|
if (!dir2 || !sub.url) continue;
|
|
37680
|
-
|
|
37681
|
-
|
|
37682
|
-
|
|
38128
|
+
const b2 = (sub.branch ?? "").trim();
|
|
38129
|
+
try {
|
|
38130
|
+
if (await isRegisteredSubmodule(root, dir2)) {
|
|
38131
|
+
emit("submodule", `Submodule ${dir2} already registered \u2014 updating\u2026`);
|
|
37683
38132
|
await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37684
|
-
|
|
38133
|
+
outcomes.push({ dir: dir2, ok: true, error: null });
|
|
38134
|
+
continue;
|
|
37685
38135
|
}
|
|
37686
|
-
|
|
37687
|
-
|
|
37688
|
-
|
|
37689
|
-
|
|
37690
|
-
|
|
37691
|
-
|
|
37692
|
-
|
|
37693
|
-
|
|
37694
|
-
const b2 = (sub.branch ?? "").trim();
|
|
37695
|
-
if (b2 && createMissingBranch && !await ensureRemoteBranch(sub.url, b2, emit)) {
|
|
37696
|
-
try {
|
|
37697
|
-
await run("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 12e4 });
|
|
38136
|
+
await prepareSubmodulePath(root, dir2, emit);
|
|
38137
|
+
emit("submodule", `Adding submodule ${sub.url} \u2192 ${dir2}\u2026`);
|
|
38138
|
+
const base = { kind: opts.base?.kind === "empty" ? "empty" : "default" };
|
|
38139
|
+
const branchReady = !b2 || await remoteHasBranch(sub.url, b2) || !!opts.createMissingBranch && await ensureRemoteBranch(sub.url, b2, emit, base);
|
|
38140
|
+
if (branchReady) {
|
|
38141
|
+
await run("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 18e4 });
|
|
38142
|
+
} else {
|
|
38143
|
+
await run("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 18e4 });
|
|
37698
38144
|
await run("git", ["config", "-f", ".gitmodules", `submodule.${dir2}.branch`, b2], { cwd: root, timeout: 3e4 });
|
|
37699
|
-
|
|
37700
|
-
|
|
37701
|
-
await run("git", ["push", "-u", "origin", b2], { cwd: join6(root, dir2), timeout: 12e4 });
|
|
37702
|
-
} catch (pushErr) {
|
|
37703
|
-
const pmsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
|
|
37704
|
-
emit("git-branch-push-failed", `Submodule ${dir2}: branch "${b2}" created locally but the push failed: ${pmsg}`);
|
|
37705
|
-
}
|
|
37706
|
-
added++;
|
|
37707
|
-
continue;
|
|
37708
|
-
} catch (err) {
|
|
37709
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
37710
|
-
if (!/already exists|already registered|in the index/i.test(msg)) {
|
|
37711
|
-
throw new Error(`git submodule add ${dir2} failed: ${msg}`);
|
|
37712
|
-
}
|
|
38145
|
+
await run("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
|
|
38146
|
+
emit("git-branch-push-failed", `Submodule ${dir2}: branch "${b2}" created locally only \u2014 push it when credentials are available.`);
|
|
37713
38147
|
}
|
|
37714
|
-
}
|
|
37715
|
-
try {
|
|
37716
|
-
await run("git", ["submodule", "add", ...b2 ? ["-b", b2] : [], sub.url, dir2], { cwd: root, timeout: 12e4 });
|
|
37717
38148
|
added++;
|
|
38149
|
+
outcomes.push({ dir: dir2, ok: true, error: null });
|
|
37718
38150
|
} catch (err) {
|
|
37719
|
-
const msg =
|
|
37720
|
-
|
|
37721
|
-
|
|
37722
|
-
await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
|
|
37723
|
-
added++;
|
|
37724
|
-
} catch {
|
|
37725
|
-
}
|
|
37726
|
-
} else {
|
|
37727
|
-
throw new Error(`git submodule add ${dir2} failed: ${msg}`);
|
|
37728
|
-
}
|
|
38151
|
+
const msg = errText(err);
|
|
38152
|
+
emit("submodule-failed", `Submodule ${dir2} was not attached: ${msg}`);
|
|
38153
|
+
outcomes.push({ dir: dir2, ok: false, error: msg });
|
|
37729
38154
|
}
|
|
37730
38155
|
}
|
|
37731
|
-
if (added > 0) await commitAndPushSubmodules(root, submodules, emit);
|
|
38156
|
+
if (added > 0 && opts.commit) await commitAndPushSubmodules(root, submodules, emit);
|
|
38157
|
+
return outcomes;
|
|
37732
38158
|
}
|
|
37733
38159
|
async function commitAndPushSubmodules(root, submodules, emit) {
|
|
37734
38160
|
const dirs = submodules.map((s3) => (s3.subdir ?? "").trim()).filter(Boolean);
|
|
@@ -37749,17 +38175,11 @@ async function commitAndPushSubmodules(root, submodules, emit) {
|
|
|
37749
38175
|
}
|
|
37750
38176
|
emit("submodule", "Pushing .gitmodules to the primary remote\u2026");
|
|
37751
38177
|
try {
|
|
37752
|
-
await run("git", ["push"], { cwd: root, timeout: 12e4 });
|
|
38178
|
+
await run("git", ["push", "-u", "origin", "HEAD"], { cwd: root, timeout: 12e4 });
|
|
37753
38179
|
} catch (err) {
|
|
37754
|
-
|
|
37755
|
-
emit("submodule-push-failed", `Submodules committed locally but the push failed: ${msg}`);
|
|
38180
|
+
emit("submodule-push-failed", `Submodules committed locally but the push failed: ${errText(err)}`);
|
|
37756
38181
|
}
|
|
37757
38182
|
}
|
|
37758
|
-
function providerSlug(url) {
|
|
37759
|
-
const provider = /github\.com/i.test(url) ? "gh" : /gitlab\.com/i.test(url) ? "glab" : null;
|
|
37760
|
-
const m2 = /(?:github|gitlab)\.com[/:]+([^/]+\/.+?)(?:\.git)?\/?$/i.exec(url);
|
|
37761
|
-
return { provider, slug: m2?.[1] ?? "" };
|
|
37762
|
-
}
|
|
37763
38183
|
function errText(err) {
|
|
37764
38184
|
const e = err;
|
|
37765
38185
|
const raw = (e?.stderr || e?.message || String(err)).trim();
|
|
@@ -37772,7 +38192,7 @@ async function gitOut(cwd2, args) {
|
|
|
37772
38192
|
return "";
|
|
37773
38193
|
}
|
|
37774
38194
|
}
|
|
37775
|
-
async function
|
|
38195
|
+
async function commitAndPush(root, repo, emit) {
|
|
37776
38196
|
const result = { committed: false, pushed: false, branch: null, prUrl: null, step: null, error: null };
|
|
37777
38197
|
const fail = (step, error) => {
|
|
37778
38198
|
result.step = step;
|
|
@@ -37803,54 +38223,36 @@ async function commitAndOpenPr(root, repo, emit) {
|
|
|
37803
38223
|
const head = await gitOut(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
37804
38224
|
const branch = declared || (head && head !== "HEAD" ? head : "");
|
|
37805
38225
|
result.branch = branch || null;
|
|
37806
|
-
emit("push",
|
|
38226
|
+
emit("push", `Pushing the scaffold to ${branch || "the current branch"}\u2026`);
|
|
37807
38227
|
try {
|
|
37808
38228
|
await run("git", ["push", "-u", "origin", branch || "HEAD"], { cwd: root, timeout: 12e4 });
|
|
37809
38229
|
result.pushed = true;
|
|
38230
|
+
emit("push", `Pushed to ${branch || "the current branch"}.`);
|
|
37810
38231
|
} catch (err) {
|
|
37811
38232
|
const msg = errText(err);
|
|
37812
38233
|
emit("push-failed", `Scaffold committed locally but the push failed: ${msg} \u2014 retry from the project page when credentials are available.`);
|
|
37813
38234
|
return fail("push", msg);
|
|
37814
38235
|
}
|
|
37815
|
-
|
|
37816
|
-
if (branch && remoteHead && branch === remoteHead) {
|
|
37817
|
-
emit("pr", `Pushed to the default branch (${branch}) \u2014 no pull request needed.`);
|
|
37818
|
-
return result;
|
|
37819
|
-
}
|
|
37820
|
-
const url = (repo.url ?? "").trim() || await gitOut(root, ["remote", "get-url", "origin"]);
|
|
37821
|
-
const { provider, slug } = providerSlug(url);
|
|
37822
|
-
if (!provider) {
|
|
37823
|
-
emit("pr", "Unknown git host \u2014 skipping the pull request.");
|
|
37824
|
-
return result;
|
|
37825
|
-
}
|
|
37826
|
-
emit("pr", `Opening a pull request via ${provider}\u2026`);
|
|
37827
|
-
try {
|
|
37828
|
-
const args = provider === "glab" ? ["mr", "create", "--fill", "--yes", ...branch ? ["--source-branch", branch] : [], ...slug ? ["-R", slug] : []] : ["pr", "create", "--fill", ...branch ? ["--head", branch] : [], ...slug ? ["-R", slug] : []];
|
|
37829
|
-
const { stdout } = await run(provider, args, { cwd: root, timeout: 12e4 });
|
|
37830
|
-
const link2 = stdout.trim().split(/\s+/).find((x) => /^https?:\/\//.test(x)) ?? null;
|
|
37831
|
-
result.prUrl = link2;
|
|
37832
|
-
emit("pr", `Pull request opened${link2 ? `: ${link2}` : "."}`);
|
|
37833
|
-
return result;
|
|
37834
|
-
} catch (err) {
|
|
37835
|
-
const msg = errText(err);
|
|
37836
|
-
const existing = /already exists/i.test(msg) ? msg.match(/https?:\/\/\S+/)?.[0] ?? null : null;
|
|
37837
|
-
if (existing) {
|
|
37838
|
-
result.prUrl = existing;
|
|
37839
|
-
emit("pr", `A pull request is already open: ${existing}`);
|
|
37840
|
-
return result;
|
|
37841
|
-
}
|
|
37842
|
-
emit("pr-failed", `Could not open a pull request (${provider}): ${msg}`);
|
|
37843
|
-
return fail("pr", msg);
|
|
37844
|
-
}
|
|
38236
|
+
return result;
|
|
37845
38237
|
}
|
|
37846
38238
|
async function publishScaffold(root, emit) {
|
|
37847
|
-
let
|
|
38239
|
+
let repos = [];
|
|
37848
38240
|
try {
|
|
37849
38241
|
const spec = JSON.parse(await readFile(join6(root, "project.spec.json"), "utf8"));
|
|
37850
|
-
|
|
38242
|
+
repos = reposOf(spec);
|
|
37851
38243
|
} catch {
|
|
37852
38244
|
}
|
|
37853
|
-
|
|
38245
|
+
const repo = singleRepo(repos) ?? {};
|
|
38246
|
+
const submodules = await attachSubmodules(root, submodulesOf(repos), emit, { createMissingBranch: true, base: repo.base });
|
|
38247
|
+
return withSubmodules(await commitAndPush(root, repo, emit), submodules);
|
|
38248
|
+
}
|
|
38249
|
+
function withSubmodules(publish, submodules) {
|
|
38250
|
+
const failed = submodules.filter((x) => !x.ok);
|
|
38251
|
+
if (failed.length > 0 && !publish.step) {
|
|
38252
|
+
publish.step = "submodule";
|
|
38253
|
+
publish.error = failed.map((x) => `${x.dir}: ${x.error ?? "failed"}`).join("; ");
|
|
38254
|
+
}
|
|
38255
|
+
return { ...publish, submodules };
|
|
37854
38256
|
}
|
|
37855
38257
|
function cloneArgs(url, dest, branch) {
|
|
37856
38258
|
const b2 = (branch ?? "").trim();
|
|
@@ -37864,18 +38266,27 @@ async function remoteHasBranch(url, branch) {
|
|
|
37864
38266
|
return false;
|
|
37865
38267
|
}
|
|
37866
38268
|
}
|
|
37867
|
-
async function ensureRemoteBranch(url, branch, emit) {
|
|
38269
|
+
async function ensureRemoteBranch(url, branch, emit, base = {}) {
|
|
37868
38270
|
if (await remoteHasBranch(url, branch)) return true;
|
|
37869
38271
|
const tmp = await mkdtemp(join6(tmpdir2(), "4pm-branch-"));
|
|
37870
38272
|
try {
|
|
37871
|
-
|
|
37872
|
-
|
|
37873
|
-
|
|
38273
|
+
if (base.kind === "empty") {
|
|
38274
|
+
emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it empty (no history)\u2026`);
|
|
38275
|
+
await run("git", ["clone", "--no-checkout", "--depth", "1", url, tmp], { timeout: 12e4 });
|
|
38276
|
+
await run("git", ["checkout", "--orphan", branch], { cwd: tmp, timeout: 3e4 });
|
|
38277
|
+
await run("git", ["rm", "-r", "-q", "--cached", "--ignore-unmatch", "."], { cwd: tmp, timeout: 3e4 });
|
|
38278
|
+
await run("git", ["clean", "-fdxq"], { cwd: tmp, timeout: 6e4 });
|
|
38279
|
+
await run("git", ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", "commit", "--allow-empty", "-m", "chore: start branch (4PM)"], { cwd: tmp, timeout: 3e4 });
|
|
38280
|
+
} else {
|
|
38281
|
+
const from = base.kind === "branch" && base.branch ? base.branch : "";
|
|
38282
|
+
emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it from ${from || "the default branch"}\u2026`);
|
|
38283
|
+
await run("git", ["clone", "--depth", "1", ...from ? ["-b", from] : [], url, tmp], { timeout: 12e4 });
|
|
38284
|
+
await run("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
|
|
38285
|
+
}
|
|
37874
38286
|
await run("git", ["push", "-u", "origin", branch], { cwd: tmp, timeout: 12e4 });
|
|
37875
38287
|
return true;
|
|
37876
38288
|
} catch (err) {
|
|
37877
|
-
|
|
37878
|
-
emit("git-branch-push-failed", `Could not create branch "${branch}" on ${url}: ${msg}`);
|
|
38289
|
+
emit("git-branch-push-failed", `Could not create branch "${branch}" on ${url}: ${errText(err)}`);
|
|
37879
38290
|
return false;
|
|
37880
38291
|
} finally {
|
|
37881
38292
|
await rm(tmp, { recursive: true, force: true });
|
|
@@ -37912,15 +38323,24 @@ async function provisionRepo(target, repo, emit, opts = {}) {
|
|
|
37912
38323
|
}
|
|
37913
38324
|
const b2 = (repo.branch ?? "").trim();
|
|
37914
38325
|
if (b2 && opts.createMissingBranch && !await remoteHasBranch(repo.url, b2)) {
|
|
37915
|
-
|
|
37916
|
-
|
|
38326
|
+
const kind = repo.base?.kind ?? "default";
|
|
38327
|
+
const from = kind === "branch" ? (repo.base?.branch ?? "").trim() : "";
|
|
38328
|
+
if (kind === "empty") {
|
|
38329
|
+
emit("git", `Creating "${b2}" as an empty branch (no history) in ${repo.url}\u2026`);
|
|
38330
|
+
await run("git", ["clone", "--no-checkout", repo.url, target], { timeout: 12e4 });
|
|
38331
|
+
await run("git", ["checkout", "--orphan", b2], { cwd: target, timeout: 3e4 });
|
|
38332
|
+
await run("git", ["rm", "-r", "-q", "--cached", "--ignore-unmatch", "."], { cwd: target, timeout: 3e4 });
|
|
38333
|
+
await run("git", ["clean", "-fdxq"], { cwd: target, timeout: 6e4 });
|
|
38334
|
+
return;
|
|
38335
|
+
}
|
|
38336
|
+
emit("git", `Cloning ${repo.url} (${from || "default branch"}) to create "${b2}"\u2026`);
|
|
38337
|
+
await run("git", cloneArgs(repo.url, target, from || void 0), { timeout: 12e4 });
|
|
37917
38338
|
await run("git", ["checkout", "-b", b2], { cwd: target, timeout: 3e4 });
|
|
37918
38339
|
try {
|
|
37919
38340
|
await run("git", ["push", "-u", "origin", b2], { cwd: target, timeout: 12e4 });
|
|
37920
38341
|
emit("git", `Created and pushed branch "${b2}".`);
|
|
37921
38342
|
} catch (err) {
|
|
37922
|
-
|
|
37923
|
-
emit("git-branch-push-failed", `Branch "${b2}" created locally but the push failed: ${msg} \u2014 push it when credentials are available.`);
|
|
38343
|
+
emit("git-branch-push-failed", `Branch "${b2}" created locally but the push failed: ${errText(err)} \u2014 the publish step retries it.`);
|
|
37924
38344
|
}
|
|
37925
38345
|
return;
|
|
37926
38346
|
}
|
|
@@ -37933,7 +38353,10 @@ async function ensureReposCloned(physicRoot, repos, emit) {
|
|
|
37933
38353
|
if (!repo) return;
|
|
37934
38354
|
const step = emit ?? (() => void 0);
|
|
37935
38355
|
await provisionRepo(physicRoot, repo, step);
|
|
37936
|
-
await attachSubmodules(physicRoot, submodulesOf(list), step);
|
|
38356
|
+
await attachSubmodules(physicRoot, submodulesOf(list), step, { commit: true });
|
|
38357
|
+
}
|
|
38358
|
+
function projectSampleDir() {
|
|
38359
|
+
return sampleDir();
|
|
37937
38360
|
}
|
|
37938
38361
|
function sampleDir() {
|
|
37939
38362
|
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
@@ -37959,11 +38382,16 @@ async function scaffoldProject(payload, profileDir2, onProgress, ai2, applyGitAu
|
|
|
37959
38382
|
if (payload.gitAuth !== void 0) applyGitAuth2?.(payload.gitAuth ?? null, payload.gitAuthHost ?? null);
|
|
37960
38383
|
emit("git", "Cloning repository\u2026");
|
|
37961
38384
|
await provisionRepo(target, repo, emit, { createMissingBranch: true });
|
|
38385
|
+
const sourceHadScaffold = existsSync4(join6(target, ".4pm", ".4pm.json"));
|
|
37962
38386
|
await scaffoldRepo(target, payload.projectName, payload.spec, emit, { resetClaude: true, ai: ai2 });
|
|
37963
|
-
await
|
|
38387
|
+
if (repo.resetTracking ?? sourceHadScaffold) await resetTrackingFiles(target, emit);
|
|
38388
|
+
const submodules = await attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit, {
|
|
38389
|
+
createMissingBranch: true,
|
|
38390
|
+
base: repo.base
|
|
38391
|
+
});
|
|
37964
38392
|
emit("version", "Writing .4pm/.4pm.json\u2026");
|
|
37965
38393
|
await writeTemplateMarker(target);
|
|
37966
|
-
const publish = await
|
|
38394
|
+
const publish = withSubmodules(await commitAndPush(target, repo, emit), submodules);
|
|
37967
38395
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
37968
38396
|
return { ok: true, path: target, publish };
|
|
37969
38397
|
} catch (err) {
|
|
@@ -37988,6 +38416,17 @@ async function resetTemplateManagedClaude(dir2, label, emit) {
|
|
|
37988
38416
|
await rm(join6(claude, "settings.json"), { force: true });
|
|
37989
38417
|
await rm(join6(claude, "skills"), { recursive: true, force: true });
|
|
37990
38418
|
}
|
|
38419
|
+
async function resetTrackingFiles(target, emit) {
|
|
38420
|
+
const templates = join6(sampleDir(), ".claude", "templates");
|
|
38421
|
+
const reset = [];
|
|
38422
|
+
for (const file of SCAFFOLD_TRACKING_FILES) {
|
|
38423
|
+
const empty = join6(templates, file.replace(/\.md$/, ".empty.md"));
|
|
38424
|
+
if (!existsSync4(empty)) continue;
|
|
38425
|
+
await cp(empty, join6(target, file), { force: true });
|
|
38426
|
+
reset.push(file);
|
|
38427
|
+
}
|
|
38428
|
+
if (reset.length > 0) emit("copy", `Reset the 4PM tracking files: ${reset.join(", ")}.`);
|
|
38429
|
+
}
|
|
37991
38430
|
async function writeTemplateMarker(target) {
|
|
37992
38431
|
const dir2 = join6(target, ".4pm");
|
|
37993
38432
|
await mkdir(dir2, { recursive: true });
|
|
@@ -38088,7 +38527,9 @@ async function addProject(payload, profileDir2, onProgress, ai2) {
|
|
|
38088
38527
|
if (payload.scaffoldRepos && payload.scaffoldRepos.length > 0) {
|
|
38089
38528
|
await scaffoldRepo(target, payload.projectName, payload.spec, emit, { ai: ai2 });
|
|
38090
38529
|
}
|
|
38091
|
-
await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit);
|
|
38530
|
+
const outcomes = await attachSubmodules(target, submodulesOf(payload.repos ?? []), emit, { commit: true });
|
|
38531
|
+
const failed = outcomes.filter((x) => !x.ok);
|
|
38532
|
+
if (failed.length > 0) throw new Error(failed.map((x) => `submodule ${x.dir}: ${x.error ?? "failed"}`).join("; "));
|
|
38092
38533
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
38093
38534
|
return { ok: true, path: target };
|
|
38094
38535
|
} catch (err) {
|
|
@@ -38478,6 +38919,9 @@ var WsChannels = {
|
|
|
38478
38919
|
// server → cli (reply): retry the scaffold commit → push → PR in the project folder (ADR-0368,
|
|
38479
38920
|
// project-0074); the reply carries the new outcome, stored on `projects.scaffold_publish`.
|
|
38480
38921
|
PROJECT_PUBLISH: "project.publish",
|
|
38922
|
+
// server → cli (reply): probe a repo branch before creating a project (ADR-0370, project-0076) —
|
|
38923
|
+
// branch existence, remote branches, and whether the source ref already holds a 4PM scaffold.
|
|
38924
|
+
REPO_PROBE: "repo.probe",
|
|
38481
38925
|
// server → cli: keep the physic folder name in sync with the project (folder =
|
|
38482
38926
|
// project name — ADR-0064) when the project is renamed.
|
|
38483
38927
|
PHYSIC_SYNC: "physic.sync",
|
|
@@ -38522,6 +38966,9 @@ var WsChannels = {
|
|
|
38522
38966
|
// cli → server (reply): read / add the org's monthly autonomous-book counters — USER_TODO / USER_QA /
|
|
38523
38967
|
// AI_TODO rows vs `autonomousRequestsPerMonth` (ADR-0365)
|
|
38524
38968
|
AUTONOMOUS_BOOK_USAGE: "autonomous.bookUsage",
|
|
38969
|
+
// cli → server (one-way): an autonomous condition the project's managers must see (ADR-0371) —
|
|
38970
|
+
// protected base, lost claim, attempt limit, task split, task question. The server notifies them.
|
|
38971
|
+
AUTONOMOUS_ALERT: "autonomous.alert",
|
|
38525
38972
|
USAGE_REPORT: "usage.report",
|
|
38526
38973
|
// cli → server: Claude subscription usage snapshot (5h / weekly — ADR-0072)
|
|
38527
38974
|
MACHINE_USAGE: "machine.usage",
|
|
@@ -38630,7 +39077,7 @@ var COMMAND_CANCELLED_EXIT_CODE = 130;
|
|
|
38630
39077
|
import { spawn as spawn2 } from "child_process";
|
|
38631
39078
|
|
|
38632
39079
|
// src/common/logger/logger.ts
|
|
38633
|
-
import { appendFile, mkdir as mkdir2, readdir, unlink } from "fs/promises";
|
|
39080
|
+
import { appendFile, mkdir as mkdir2, readdir as readdir2, unlink } from "fs/promises";
|
|
38634
39081
|
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
38635
39082
|
import { join as join7 } from "path";
|
|
38636
39083
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
@@ -38690,7 +39137,7 @@ var CliLogger = class {
|
|
|
38690
39137
|
async cleanupOldLogs() {
|
|
38691
39138
|
const dir2 = this.dir;
|
|
38692
39139
|
if (!dir2) return;
|
|
38693
|
-
const files = await
|
|
39140
|
+
const files = await readdir2(dir2).catch(() => []);
|
|
38694
39141
|
const cutoff = Date.now() - this.retentionDays * DAY_MS;
|
|
38695
39142
|
for (const file of files) {
|
|
38696
39143
|
const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
|
|
@@ -39049,9 +39496,9 @@ var UpdateScheduler = class {
|
|
|
39049
39496
|
day: "2-digit",
|
|
39050
39497
|
hour: "2-digit"
|
|
39051
39498
|
}).formatToParts(/* @__PURE__ */ new Date());
|
|
39052
|
-
const
|
|
39053
|
-
const hour = Number.parseInt(
|
|
39054
|
-
const dateKey = `${
|
|
39499
|
+
const get3 = (type) => parts.find((p2) => p2.type === type)?.value ?? "";
|
|
39500
|
+
const hour = Number.parseInt(get3("hour"), 10);
|
|
39501
|
+
const dateKey = `${get3("year")}-${get3("month")}-${get3("day")}`;
|
|
39055
39502
|
if (Number.isNaN(hour)) return null;
|
|
39056
39503
|
return { hour: hour % 24, dateKey };
|
|
39057
39504
|
} catch {
|
|
@@ -40425,11 +40872,11 @@ function reportToolResult(tool, ok, message) {
|
|
|
40425
40872
|
}
|
|
40426
40873
|
|
|
40427
40874
|
// src/core/autonomous.ts
|
|
40428
|
-
import { readFile as
|
|
40875
|
+
import { readFile as readFile7, writeFile as writeFile5, chmod } from "fs/promises";
|
|
40429
40876
|
import { readFileSync as readFileSync14 } from "fs";
|
|
40430
|
-
import { execFile as
|
|
40431
|
-
import { join as
|
|
40432
|
-
import { promisify as
|
|
40877
|
+
import { execFile as execFile5, spawn as spawn5 } from "child_process";
|
|
40878
|
+
import { join as join19 } from "path";
|
|
40879
|
+
import { promisify as promisify5 } from "util";
|
|
40433
40880
|
|
|
40434
40881
|
// src/core/autonomous-config.ts
|
|
40435
40882
|
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -40444,7 +40891,11 @@ var DEFAULT_AUTONOMOUS_CONFIG = {
|
|
|
40444
40891
|
logRetentionDays: 14,
|
|
40445
40892
|
model: "",
|
|
40446
40893
|
maxSessionPct: 80,
|
|
40447
|
-
maxWeeklyPct: 90
|
|
40894
|
+
maxWeeklyPct: 90,
|
|
40895
|
+
claimTtlHours: 6,
|
|
40896
|
+
claimCheckMinutes: 10,
|
|
40897
|
+
maxTaskAttempts: 5,
|
|
40898
|
+
taskSizeHints: { sMaxFiles: 5, sMaxLines: 300, mMaxFiles: 20, mMaxLines: 1500 }
|
|
40448
40899
|
};
|
|
40449
40900
|
function autonomousConfigPath(profileDir2) {
|
|
40450
40901
|
return join15(profileDir2, "autonomous.config.json");
|
|
@@ -40463,7 +40914,20 @@ function coerce2(raw) {
|
|
|
40463
40914
|
logRetentionDays: num(o.logRetentionDays, d.logRetentionDays),
|
|
40464
40915
|
model: str(o.model, d.model),
|
|
40465
40916
|
maxSessionPct: num(o.maxSessionPct, d.maxSessionPct),
|
|
40466
|
-
maxWeeklyPct: num(o.maxWeeklyPct, d.maxWeeklyPct)
|
|
40917
|
+
maxWeeklyPct: num(o.maxWeeklyPct, d.maxWeeklyPct),
|
|
40918
|
+
claimTtlHours: Math.max(1, num(o.claimTtlHours, d.claimTtlHours)),
|
|
40919
|
+
claimCheckMinutes: Math.max(1, num(o.claimCheckMinutes, d.claimCheckMinutes)),
|
|
40920
|
+
maxTaskAttempts: Math.max(1, num(o.maxTaskAttempts, d.maxTaskAttempts)),
|
|
40921
|
+
taskSizeHints: (() => {
|
|
40922
|
+
const h = o.taskSizeHints && typeof o.taskSizeHints === "object" ? o.taskSizeHints : {};
|
|
40923
|
+
const dh = d.taskSizeHints;
|
|
40924
|
+
return {
|
|
40925
|
+
sMaxFiles: num(h.sMaxFiles, dh.sMaxFiles),
|
|
40926
|
+
sMaxLines: num(h.sMaxLines, dh.sMaxLines),
|
|
40927
|
+
mMaxFiles: num(h.mMaxFiles, dh.mMaxFiles),
|
|
40928
|
+
mMaxLines: num(h.mMaxLines, dh.mMaxLines)
|
|
40929
|
+
};
|
|
40930
|
+
})()
|
|
40467
40931
|
};
|
|
40468
40932
|
}
|
|
40469
40933
|
async function readAutonomousConfig(profileDir2) {
|
|
@@ -40539,8 +41003,481 @@ function pushRecord(h, rec) {
|
|
|
40539
41003
|
return h;
|
|
40540
41004
|
}
|
|
40541
41005
|
|
|
40542
|
-
// src/core/autonomous.ts
|
|
41006
|
+
// src/core/autonomous-books.ts
|
|
41007
|
+
import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
41008
|
+
import { join as join17 } from "path";
|
|
41009
|
+
function splitRow(line) {
|
|
41010
|
+
const body = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
41011
|
+
return body.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"));
|
|
41012
|
+
}
|
|
41013
|
+
function cell(value) {
|
|
41014
|
+
return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
|
41015
|
+
}
|
|
41016
|
+
function findTables(md) {
|
|
41017
|
+
const lines = md.split("\n");
|
|
41018
|
+
const out = [];
|
|
41019
|
+
let i = 0;
|
|
41020
|
+
while (i < lines.length) {
|
|
41021
|
+
const l = lines[i].trim();
|
|
41022
|
+
const next = lines[i + 1]?.trim() ?? "";
|
|
41023
|
+
if (l.startsWith("|") && /^\|[\s:|-]+\|?$/.test(next)) {
|
|
41024
|
+
const header = splitRow(l);
|
|
41025
|
+
let j2 = i + 2;
|
|
41026
|
+
const rows = [];
|
|
41027
|
+
while (j2 < lines.length && lines[j2].trim().startsWith("|")) {
|
|
41028
|
+
const cells = splitRow(lines[j2]);
|
|
41029
|
+
if (cells.some((c) => c.length > 0)) rows.push(cells);
|
|
41030
|
+
j2++;
|
|
41031
|
+
}
|
|
41032
|
+
out.push({ start: i, end: j2, header, rows });
|
|
41033
|
+
i = j2;
|
|
41034
|
+
} else i++;
|
|
41035
|
+
}
|
|
41036
|
+
return out;
|
|
41037
|
+
}
|
|
41038
|
+
function replaceRows(md, table, rows) {
|
|
41039
|
+
const lines = md.split("\n");
|
|
41040
|
+
const width = table.header.length;
|
|
41041
|
+
const sep9 = `|${table.header.map(() => "---").join("|")}|`;
|
|
41042
|
+
const body = (rows.length ? rows : [table.header.map(() => "")]).map(
|
|
41043
|
+
(r) => `| ${Array.from({ length: width }, (_2, k2) => cell(r[k2] ?? "")).join(" | ")} |`
|
|
41044
|
+
);
|
|
41045
|
+
const head = `| ${table.header.join(" | ")} |`;
|
|
41046
|
+
lines.splice(table.start, table.end - table.start, head, sep9, ...body);
|
|
41047
|
+
return lines.join("\n");
|
|
41048
|
+
}
|
|
41049
|
+
function col(header, name) {
|
|
41050
|
+
return header.findIndex((h) => h.toLowerCase() === name.toLowerCase());
|
|
41051
|
+
}
|
|
41052
|
+
function get(t2, row, name) {
|
|
41053
|
+
const i = col(t2.header, name);
|
|
41054
|
+
return i >= 0 ? row[i] ?? "" : "";
|
|
41055
|
+
}
|
|
41056
|
+
function build(header, values) {
|
|
41057
|
+
return header.map((h) => values[h.toLowerCase()] ?? "");
|
|
41058
|
+
}
|
|
41059
|
+
function splitIds(v2) {
|
|
41060
|
+
return v2.split(/[,\s]+/).map((x) => x.trim()).filter((x) => /^(TSK|QA)-/.test(x));
|
|
41061
|
+
}
|
|
41062
|
+
function parseTasks(md) {
|
|
41063
|
+
const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Depends") >= 0 && col(t2.header, "Priority") >= 0) ?? null;
|
|
41064
|
+
if (!table) return { table: null, tasks: [] };
|
|
41065
|
+
const tasks = table.rows.map((r) => ({
|
|
41066
|
+
id: get(table, r, "ID"),
|
|
41067
|
+
priority: get(table, r, "Priority"),
|
|
41068
|
+
tag: get(table, r, "Tag"),
|
|
41069
|
+
depends: splitIds(get(table, r, "Depends")),
|
|
41070
|
+
group: get(table, r, "Group"),
|
|
41071
|
+
desc: get(table, r, "Task description"),
|
|
41072
|
+
notes: get(table, r, "Notes")
|
|
41073
|
+
})).filter((t2) => /^TSK-/.test(t2.id));
|
|
41074
|
+
return { table, tasks };
|
|
41075
|
+
}
|
|
41076
|
+
function writeTasks(md, tasks) {
|
|
41077
|
+
const { table } = parseTasks(md);
|
|
41078
|
+
if (!table) return md;
|
|
41079
|
+
return replaceRows(
|
|
41080
|
+
md,
|
|
41081
|
+
table,
|
|
41082
|
+
tasks.map(
|
|
41083
|
+
(t2) => build(table.header, {
|
|
41084
|
+
id: t2.id,
|
|
41085
|
+
priority: t2.priority,
|
|
41086
|
+
tag: t2.tag,
|
|
41087
|
+
depends: t2.depends.join(", "),
|
|
41088
|
+
group: t2.group,
|
|
41089
|
+
"task description": t2.desc,
|
|
41090
|
+
notes: t2.notes
|
|
41091
|
+
})
|
|
41092
|
+
)
|
|
41093
|
+
);
|
|
41094
|
+
}
|
|
41095
|
+
var PROGRESS_HEADER = ["Started", "ID", "Worker", "Claim", "Attempt", "Task description"];
|
|
41096
|
+
function parseClaims(md) {
|
|
41097
|
+
const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Started") >= 0) ?? null;
|
|
41098
|
+
if (!table) return { table: null, claims: [] };
|
|
41099
|
+
const claims = table.rows.map((r) => ({
|
|
41100
|
+
started: get(table, r, "Started"),
|
|
41101
|
+
id: get(table, r, "ID"),
|
|
41102
|
+
worker: get(table, r, "Worker"),
|
|
41103
|
+
claim: get(table, r, "Claim"),
|
|
41104
|
+
attempt: Number(get(table, r, "Attempt")) || 1,
|
|
41105
|
+
desc: get(table, r, "Task description")
|
|
41106
|
+
})).filter((c) => /^TSK-/.test(c.id));
|
|
41107
|
+
return { table, claims };
|
|
41108
|
+
}
|
|
41109
|
+
function writeClaims(md, claims) {
|
|
41110
|
+
const found = parseClaims(md).table;
|
|
41111
|
+
const table = found ? { ...found, header: PROGRESS_HEADER } : { start: md.split("\n").length, end: md.split("\n").length, header: PROGRESS_HEADER, rows: [] };
|
|
41112
|
+
const src = found ? md : `${md.replace(/\n*$/, "")}
|
|
41113
|
+
|
|
41114
|
+
`;
|
|
41115
|
+
return replaceRows(
|
|
41116
|
+
src,
|
|
41117
|
+
table,
|
|
41118
|
+
claims.map((c) => [c.started, c.id, c.worker, c.claim, String(c.attempt), c.desc])
|
|
41119
|
+
);
|
|
41120
|
+
}
|
|
41121
|
+
function appendDone(md, row) {
|
|
41122
|
+
const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Timestamp") >= 0);
|
|
41123
|
+
if (!table) return md;
|
|
41124
|
+
const values = { timestamp: stamp(), id: row.id, "task description": row.desc, files: row.files, notes: row.notes };
|
|
41125
|
+
return replaceRows(md, table, [...table.rows, build(table.header, values)]);
|
|
41126
|
+
}
|
|
41127
|
+
function appendIncident(md, note) {
|
|
41128
|
+
const table = findTables(md).find((t2) => col(t2.header, "Note") >= 0 && col(t2.header, "Timestamp") >= 0 && col(t2.header, "ID") < 0);
|
|
41129
|
+
if (!table) return md;
|
|
41130
|
+
return replaceRows(md, table, [...table.rows, build(table.header, { timestamp: stamp(), note })]);
|
|
41131
|
+
}
|
|
41132
|
+
function doneRows(md) {
|
|
41133
|
+
const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Timestamp") >= 0);
|
|
41134
|
+
const out = /* @__PURE__ */ new Map();
|
|
41135
|
+
if (table) for (const r of table.rows) out.set(get(table, r, "ID"), get(table, r, "Notes"));
|
|
41136
|
+
return out;
|
|
41137
|
+
}
|
|
41138
|
+
function parseQa(md) {
|
|
41139
|
+
const table = findTables(md).find((t2) => col(t2.header, "ID") >= 0 && col(t2.header, "Answer") >= 0) ?? null;
|
|
41140
|
+
if (!table) return { table: null, rows: [] };
|
|
41141
|
+
const rows = table.rows.map((r) => ({
|
|
41142
|
+
id: get(table, r, "ID"),
|
|
41143
|
+
group: get(table, r, "Group"),
|
|
41144
|
+
depends: get(table, r, "Depends"),
|
|
41145
|
+
original: get(table, r, "Original request"),
|
|
41146
|
+
question: get(table, r, "Question / options"),
|
|
41147
|
+
answer: get(table, r, "Answer")
|
|
41148
|
+
})).filter((q2) => /^QA-/.test(q2.id));
|
|
41149
|
+
return { table, rows };
|
|
41150
|
+
}
|
|
41151
|
+
function writeQa(md, rows) {
|
|
41152
|
+
const { table } = parseQa(md);
|
|
41153
|
+
if (!table) return md;
|
|
41154
|
+
return replaceRows(
|
|
41155
|
+
md,
|
|
41156
|
+
table,
|
|
41157
|
+
rows.map(
|
|
41158
|
+
(q2) => build(table.header, {
|
|
41159
|
+
id: q2.id,
|
|
41160
|
+
group: q2.group,
|
|
41161
|
+
depends: q2.depends,
|
|
41162
|
+
"original request": q2.original,
|
|
41163
|
+
"question / options": q2.question,
|
|
41164
|
+
answer: q2.answer
|
|
41165
|
+
})
|
|
41166
|
+
)
|
|
41167
|
+
);
|
|
41168
|
+
}
|
|
41169
|
+
var ATTEMPTS_REL = ".claude/.autonomous.attempts.json";
|
|
41170
|
+
async function readAttempts(root) {
|
|
41171
|
+
try {
|
|
41172
|
+
return JSON.parse(await readFile5(join17(root, ATTEMPTS_REL), "utf8"));
|
|
41173
|
+
} catch {
|
|
41174
|
+
return {};
|
|
41175
|
+
}
|
|
41176
|
+
}
|
|
41177
|
+
async function writeAttempts(root, map) {
|
|
41178
|
+
const sorted = Object.fromEntries(Object.entries(map).sort(([a], [b2]) => a.localeCompare(b2)));
|
|
41179
|
+
await writeFile4(join17(root, ATTEMPTS_REL), `${JSON.stringify(sorted, null, 2)}
|
|
41180
|
+
`, "utf8");
|
|
41181
|
+
}
|
|
41182
|
+
function stamp(d = /* @__PURE__ */ new Date()) {
|
|
41183
|
+
return d.toISOString().slice(0, 19).replace("T", " ");
|
|
41184
|
+
}
|
|
41185
|
+
function parseStamp(v2) {
|
|
41186
|
+
const iso = v2.trim().replace(" ", "T");
|
|
41187
|
+
return Date.parse(/[zZ]|[+-]\d\d:?\d\d$/.test(iso) ? iso : `${iso}Z`);
|
|
41188
|
+
}
|
|
41189
|
+
async function readBook(root, rel) {
|
|
41190
|
+
try {
|
|
41191
|
+
return await readFile5(join17(root, rel), "utf8");
|
|
41192
|
+
} catch {
|
|
41193
|
+
return "";
|
|
41194
|
+
}
|
|
41195
|
+
}
|
|
41196
|
+
async function writeBook(root, rel, text) {
|
|
41197
|
+
await writeFile4(join17(root, rel), text, "utf8");
|
|
41198
|
+
}
|
|
41199
|
+
|
|
41200
|
+
// src/core/autonomous-git.ts
|
|
41201
|
+
import { execFile as execFile4 } from "child_process";
|
|
41202
|
+
import { existsSync as existsSync11 } from "fs";
|
|
41203
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
41204
|
+
import { join as join18 } from "path";
|
|
41205
|
+
import { promisify as promisify4 } from "util";
|
|
41206
|
+
|
|
41207
|
+
// src/core/git-host.ts
|
|
41208
|
+
import { execFile as execFile3 } from "child_process";
|
|
41209
|
+
import { promisify as promisify3 } from "util";
|
|
40543
41210
|
var run3 = promisify3(execFile3);
|
|
41211
|
+
function repoHost(url) {
|
|
41212
|
+
const p2 = parseRepoUrl(url);
|
|
41213
|
+
if (!p2) return { provider: null, host: "", slug: "" };
|
|
41214
|
+
const slug = `${p2.owner}/${p2.name}`;
|
|
41215
|
+
const provider = /gitlab/i.test(p2.host) ? "glab" : /github|ghe\.com/i.test(p2.host) ? "gh" : null;
|
|
41216
|
+
return { provider, host: p2.host, slug };
|
|
41217
|
+
}
|
|
41218
|
+
function ghHostArgs(h) {
|
|
41219
|
+
return h.host && h.host !== "github.com" ? ["--hostname", h.host] : [];
|
|
41220
|
+
}
|
|
41221
|
+
function errText2(err) {
|
|
41222
|
+
const e = err;
|
|
41223
|
+
return (e?.stderr || e?.message || String(err)).trim().split("\n").filter(Boolean).slice(-2).join(" ").slice(0, 400);
|
|
41224
|
+
}
|
|
41225
|
+
async function branchProtection(url, branch) {
|
|
41226
|
+
const h = repoHost(url);
|
|
41227
|
+
if (!h.provider || !branch) return null;
|
|
41228
|
+
try {
|
|
41229
|
+
if (h.provider === "gh") {
|
|
41230
|
+
const { stdout } = await run3("gh", ["api", ...ghHostArgs(h), `repos/${h.slug}/branches/${encodeURIComponent(branch)}`, "--jq", ".protected"], { timeout: 3e4 });
|
|
41231
|
+
return stdout.trim() === "true";
|
|
41232
|
+
}
|
|
41233
|
+
const project = encodeURIComponent(h.slug);
|
|
41234
|
+
try {
|
|
41235
|
+
await run3("glab", ["api", `projects/${project}/protected_branches/${encodeURIComponent(branch)}`], { timeout: 3e4 });
|
|
41236
|
+
return true;
|
|
41237
|
+
} catch (err) {
|
|
41238
|
+
return /404|not found/i.test(errText2(err)) ? false : null;
|
|
41239
|
+
}
|
|
41240
|
+
} catch {
|
|
41241
|
+
return null;
|
|
41242
|
+
}
|
|
41243
|
+
}
|
|
41244
|
+
async function prStateOf(url, branch) {
|
|
41245
|
+
const h = repoHost(url);
|
|
41246
|
+
if (!h.provider) return null;
|
|
41247
|
+
try {
|
|
41248
|
+
if (h.provider === "gh") {
|
|
41249
|
+
const { stdout: stdout2 } = await run3(
|
|
41250
|
+
"gh",
|
|
41251
|
+
["pr", "list", "-R", h.host && h.host !== "github.com" ? `${h.host}/${h.slug}` : h.slug, "--head", branch, "--state", "all", "--json", "state", "-L", "1"],
|
|
41252
|
+
{ timeout: 3e4 }
|
|
41253
|
+
);
|
|
41254
|
+
const s4 = JSON.parse(stdout2)[0]?.state?.toLowerCase();
|
|
41255
|
+
return s4 === "open" || s4 === "merged" || s4 === "closed" ? s4 : null;
|
|
41256
|
+
}
|
|
41257
|
+
const { stdout } = await run3("glab", ["mr", "list", "-R", h.slug, "--source-branch", branch, "--all", "-F", "json", "-P", "1"], { timeout: 3e4 });
|
|
41258
|
+
const s3 = JSON.parse(stdout)[0]?.state?.toLowerCase();
|
|
41259
|
+
return s3 === "opened" ? "open" : s3 === "merged" ? "merged" : s3 === "closed" ? "closed" : null;
|
|
41260
|
+
} catch {
|
|
41261
|
+
return null;
|
|
41262
|
+
}
|
|
41263
|
+
}
|
|
41264
|
+
async function openPullRequest(opts) {
|
|
41265
|
+
const h = repoHost(opts.url);
|
|
41266
|
+
if (!h.provider) return { url: null, error: "Unknown git host \u2014 no pull request opened." };
|
|
41267
|
+
const args = h.provider === "gh" ? ["pr", "create", "-R", h.host && h.host !== "github.com" ? `${h.host}/${h.slug}` : h.slug, "--base", opts.base, "--head", opts.head, "--title", opts.title, "--body", opts.body] : ["mr", "create", "-R", h.slug, "--target-branch", opts.base, "--source-branch", opts.head, "--title", opts.title, "--description", opts.body, "--yes"];
|
|
41268
|
+
try {
|
|
41269
|
+
const { stdout } = await run3(h.provider, args, { cwd: opts.cwd, timeout: 12e4 });
|
|
41270
|
+
return { url: stdout.trim().split(/\s+/).find((x) => /^https?:\/\//.test(x)) ?? null, error: null };
|
|
41271
|
+
} catch (err) {
|
|
41272
|
+
const msg = errText2(err);
|
|
41273
|
+
const existing = /already exists/i.test(msg) ? msg.match(/https?:\/\/\S+/)?.[0] ?? null : null;
|
|
41274
|
+
return existing ? { url: existing, error: null } : { url: null, error: msg };
|
|
41275
|
+
}
|
|
41276
|
+
}
|
|
41277
|
+
|
|
41278
|
+
// src/core/autonomous-git.ts
|
|
41279
|
+
var run4 = promisify4(execFile4);
|
|
41280
|
+
var BOOK_PATHS = [
|
|
41281
|
+
"AI_TODO.md",
|
|
41282
|
+
"AI_DONE.md",
|
|
41283
|
+
"AI_PROGRESS.md",
|
|
41284
|
+
"USER_TODO.md",
|
|
41285
|
+
"USER_QA.md",
|
|
41286
|
+
".claude/.autonomous.approvals.json",
|
|
41287
|
+
".claude/.autonomous.authors.json",
|
|
41288
|
+
ATTEMPTS_REL
|
|
41289
|
+
];
|
|
41290
|
+
async function git(cwd2, args, timeout = 12e4) {
|
|
41291
|
+
try {
|
|
41292
|
+
const { stdout } = await run4("git", args, { cwd: cwd2, timeout, maxBuffer: 32 * 1024 * 1024 });
|
|
41293
|
+
return stdout;
|
|
41294
|
+
} catch (err) {
|
|
41295
|
+
const e = err;
|
|
41296
|
+
throw new Error((e.stderr || e.message || String(err)).trim().split("\n").slice(-3).join(" "));
|
|
41297
|
+
}
|
|
41298
|
+
}
|
|
41299
|
+
async function gitQuiet(cwd2, args, timeout = 6e4) {
|
|
41300
|
+
try {
|
|
41301
|
+
return await git(cwd2, args, timeout);
|
|
41302
|
+
} catch {
|
|
41303
|
+
return "";
|
|
41304
|
+
}
|
|
41305
|
+
}
|
|
41306
|
+
async function remoteDefault(cwd2) {
|
|
41307
|
+
const ref = (await gitQuiet(cwd2, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).trim();
|
|
41308
|
+
if (ref) return ref.replace(/^origin\//, "");
|
|
41309
|
+
const out = await gitQuiet(cwd2, ["ls-remote", "--symref", "origin", "HEAD"]);
|
|
41310
|
+
return /ref: refs\/heads\/(\S+)\s+HEAD/.exec(out)?.[1] ?? "";
|
|
41311
|
+
}
|
|
41312
|
+
async function resolveBases(root) {
|
|
41313
|
+
let declared = "";
|
|
41314
|
+
let url = "";
|
|
41315
|
+
try {
|
|
41316
|
+
const spec = JSON.parse(await readFile6(join18(root, "project.spec.json"), "utf8"));
|
|
41317
|
+
const primary = spec.repos?.find((r) => r.primary) ?? spec.repos?.[0];
|
|
41318
|
+
declared = primary?.branch?.trim() ?? "";
|
|
41319
|
+
url = primary?.url?.trim() ?? "";
|
|
41320
|
+
} catch {
|
|
41321
|
+
}
|
|
41322
|
+
if (!url) url = (await gitQuiet(root, ["remote", "get-url", "origin"])).trim();
|
|
41323
|
+
const rootBase = { dir: "", url, base: declared || await remoteDefault(root) };
|
|
41324
|
+
const subs = [];
|
|
41325
|
+
if (existsSync11(join18(root, ".gitmodules"))) {
|
|
41326
|
+
const paths = (await gitQuiet(root, ["config", "-f", ".gitmodules", "--get-regexp", "^submodule\\..*\\.path$"])).split("\n").filter(Boolean);
|
|
41327
|
+
for (const line of paths) {
|
|
41328
|
+
const [key = "", ...rest] = line.split(" ");
|
|
41329
|
+
const name = key.replace(/^submodule\./, "").replace(/\.path$/, "");
|
|
41330
|
+
const dir2 = rest.join(" ");
|
|
41331
|
+
const subUrl = (await gitQuiet(root, ["config", "-f", ".gitmodules", `submodule.${name}.url`])).trim();
|
|
41332
|
+
const b2 = (await gitQuiet(root, ["config", "-f", ".gitmodules", `submodule.${name}.branch`])).trim();
|
|
41333
|
+
const abs = join18(root, dir2);
|
|
41334
|
+
subs.push({ dir: dir2, url: subUrl, base: b2 || (existsSync11(join18(abs, ".git")) ? await remoteDefault(abs) : "") });
|
|
41335
|
+
}
|
|
41336
|
+
}
|
|
41337
|
+
return { root: rootBase, subs };
|
|
41338
|
+
}
|
|
41339
|
+
async function hasRemoteBranch(cwd2, branch) {
|
|
41340
|
+
return (await gitQuiet(cwd2, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}`])).trim().length > 0;
|
|
41341
|
+
}
|
|
41342
|
+
async function currentBranch(cwd2) {
|
|
41343
|
+
const b2 = (await gitQuiet(cwd2, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
41344
|
+
return b2 === "HEAD" ? "" : b2;
|
|
41345
|
+
}
|
|
41346
|
+
async function wipCommit(cwd2, message) {
|
|
41347
|
+
await gitQuiet(cwd2, ["add", "-A"]);
|
|
41348
|
+
const staged = (await gitQuiet(cwd2, ["diff", "--cached", "--name-only"])).trim();
|
|
41349
|
+
if (!staged) return false;
|
|
41350
|
+
await git(cwd2, ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", "commit", "-q", "-m", message]).catch(() => void 0);
|
|
41351
|
+
return true;
|
|
41352
|
+
}
|
|
41353
|
+
async function syncBase(root, base) {
|
|
41354
|
+
const cur = await currentBranch(root);
|
|
41355
|
+
if ((await gitQuiet(root, ["status", "--porcelain"])).trim()) {
|
|
41356
|
+
if (cur.includes("/dev/")) await wipCommit(root, "wip: autosave before sync (4PM)");
|
|
41357
|
+
else await gitQuiet(root, ["stash", "push", "-u", "-m", "4PM autosave"]);
|
|
41358
|
+
}
|
|
41359
|
+
await git(root, ["fetch", "-q", "--prune", "origin"]);
|
|
41360
|
+
if (await hasRemoteBranch(root, base)) await git(root, ["checkout", "-q", "-B", base, `origin/${base}`]);
|
|
41361
|
+
else await git(root, ["checkout", "-q", base]);
|
|
41362
|
+
await gitQuiet(root, ["submodule", "update", "--init", "-q"], 3e5);
|
|
41363
|
+
}
|
|
41364
|
+
async function publishBooks(root, base, message, change, opts = {}) {
|
|
41365
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
41366
|
+
await git(root, ["fetch", "-q", "origin", base]).catch(() => void 0);
|
|
41367
|
+
if (await hasRemoteBranch(root, base)) {
|
|
41368
|
+
if (opts.detach) await git(root, ["checkout", "-q", "--detach", `origin/${base}`]);
|
|
41369
|
+
else await git(root, ["checkout", "-q", "-B", base, `origin/${base}`]);
|
|
41370
|
+
}
|
|
41371
|
+
if (!await change()) return "aborted";
|
|
41372
|
+
await gitQuiet(root, ["add", "--", ...BOOK_PATHS.filter((p2) => existsSync11(join18(root, p2)))]);
|
|
41373
|
+
const staged = (await gitQuiet(root, ["diff", "--cached", "--name-only"])).trim();
|
|
41374
|
+
if (!staged) return "unchanged";
|
|
41375
|
+
await git(root, ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", "commit", "-q", "-m", message]);
|
|
41376
|
+
try {
|
|
41377
|
+
await git(root, ["push", "-q", "origin", `HEAD:refs/heads/${base}`]);
|
|
41378
|
+
return "pushed";
|
|
41379
|
+
} catch (err) {
|
|
41380
|
+
const msg = String(err);
|
|
41381
|
+
if (!/non-fast-forward|fetch first|rejected|stale info/i.test(msg)) return "failed";
|
|
41382
|
+
await new Promise((r) => setTimeout(r, 500 + Math.random() * 2e3 * attempt));
|
|
41383
|
+
}
|
|
41384
|
+
}
|
|
41385
|
+
return "failed";
|
|
41386
|
+
}
|
|
41387
|
+
async function publishIntake(root, base, message) {
|
|
41388
|
+
await gitQuiet(root, ["add", "--", ...BOOK_PATHS.filter((p2) => existsSync11(join18(root, p2)))]);
|
|
41389
|
+
const staged = (await gitQuiet(root, ["diff", "--cached", "--name-only"])).trim();
|
|
41390
|
+
await gitQuiet(root, ["checkout", "--", "."]);
|
|
41391
|
+
await gitQuiet(root, ["clean", "-fdq", "--", "."]);
|
|
41392
|
+
if (!staged) return "unchanged";
|
|
41393
|
+
await git(root, ["-c", "user.name=4PM", "-c", "user.email=noreply@4pm.app", "commit", "-q", "-m", message]);
|
|
41394
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
41395
|
+
try {
|
|
41396
|
+
await git(root, ["push", "-q", "origin", `HEAD:refs/heads/${base}`]);
|
|
41397
|
+
return "pushed";
|
|
41398
|
+
} catch {
|
|
41399
|
+
await gitQuiet(root, ["fetch", "-q", "origin", base]);
|
|
41400
|
+
try {
|
|
41401
|
+
await git(root, ["rebase", "-q", `origin/${base}`]);
|
|
41402
|
+
} catch {
|
|
41403
|
+
await gitQuiet(root, ["rebase", "--abort"]);
|
|
41404
|
+
await gitQuiet(root, ["reset", "-q", "--hard", `origin/${base}`]);
|
|
41405
|
+
return "aborted";
|
|
41406
|
+
}
|
|
41407
|
+
}
|
|
41408
|
+
}
|
|
41409
|
+
return "failed";
|
|
41410
|
+
}
|
|
41411
|
+
async function remoteBook(cwd2, base, rel) {
|
|
41412
|
+
await gitQuiet(cwd2, ["fetch", "-q", "origin", base]);
|
|
41413
|
+
return gitQuiet(cwd2, ["show", `origin/${base}:${rel}`]);
|
|
41414
|
+
}
|
|
41415
|
+
async function isMerged(cwd2, url, branch, base) {
|
|
41416
|
+
const state3 = await prStateOf(url, branch);
|
|
41417
|
+
if (state3 === "merged") return true;
|
|
41418
|
+
if (state3 === "open" || state3 === "closed") return false;
|
|
41419
|
+
if (!await hasRemoteBranch(cwd2, branch)) return true;
|
|
41420
|
+
try {
|
|
41421
|
+
await git(cwd2, ["merge-base", "--is-ancestor", `origin/${branch}`, `origin/${base}`]);
|
|
41422
|
+
return true;
|
|
41423
|
+
} catch {
|
|
41424
|
+
return false;
|
|
41425
|
+
}
|
|
41426
|
+
}
|
|
41427
|
+
async function checkoutTaskBranch(cwd2, branch, base, from) {
|
|
41428
|
+
await git(cwd2, ["fetch", "-q", "--prune", "origin"]);
|
|
41429
|
+
const local = (await gitQuiet(cwd2, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`])).trim();
|
|
41430
|
+
if (await hasRemoteBranch(cwd2, branch)) {
|
|
41431
|
+
if (local && await gitQuiet(cwd2, ["merge-base", "--is-ancestor", `origin/${branch}`, branch]).then(() => true, () => false)) {
|
|
41432
|
+
await git(cwd2, ["checkout", "-q", branch]);
|
|
41433
|
+
} else await git(cwd2, ["checkout", "-q", "-B", branch, `origin/${branch}`]);
|
|
41434
|
+
return;
|
|
41435
|
+
}
|
|
41436
|
+
if (local) {
|
|
41437
|
+
await git(cwd2, ["checkout", "-q", branch]);
|
|
41438
|
+
return;
|
|
41439
|
+
}
|
|
41440
|
+
const start = from && await hasRemoteBranch(cwd2, from) ? `origin/${from}` : `origin/${base}`;
|
|
41441
|
+
await git(cwd2, ["checkout", "-q", "-B", branch, start]);
|
|
41442
|
+
}
|
|
41443
|
+
async function aheadOf(cwd2, branch, base) {
|
|
41444
|
+
return Number((await gitQuiet(cwd2, ["rev-list", "--count", `origin/${base}..${branch}`])).trim()) || 0;
|
|
41445
|
+
}
|
|
41446
|
+
async function pushBranch(cwd2, branch) {
|
|
41447
|
+
await git(cwd2, ["push", "-q", "-u", "origin", `${branch}:refs/heads/${branch}`], 18e4);
|
|
41448
|
+
}
|
|
41449
|
+
async function deliverPr(cwd2, url, branch, base, title, body) {
|
|
41450
|
+
return openPullRequest({ url, cwd: cwd2, base, head: branch, title, body });
|
|
41451
|
+
}
|
|
41452
|
+
async function mutateBooksOnBase(root, base, message, fn) {
|
|
41453
|
+
await git(root, ["fetch", "-q", "origin", base]).catch(() => void 0);
|
|
41454
|
+
if (!await hasRemoteBranch(root, base)) return "unchanged";
|
|
41455
|
+
const dir2 = join18(root, ".git", `4pm-books-${process.pid}-${Date.now()}`);
|
|
41456
|
+
await git(root, ["worktree", "add", "-q", "--detach", dir2, `origin/${base}`]);
|
|
41457
|
+
try {
|
|
41458
|
+
return await publishBooks(dir2, base, message, () => fn(dir2), { detach: true });
|
|
41459
|
+
} finally {
|
|
41460
|
+
await gitQuiet(root, ["worktree", "remove", "--force", dir2]);
|
|
41461
|
+
await gitQuiet(root, ["worktree", "prune"]);
|
|
41462
|
+
}
|
|
41463
|
+
}
|
|
41464
|
+
var lastFetch = /* @__PURE__ */ new Map();
|
|
41465
|
+
async function readBaseFile(root, base, rel) {
|
|
41466
|
+
const key = `${root}\0${base}`;
|
|
41467
|
+
if (Date.now() - (lastFetch.get(key) ?? 0) > 15e3) {
|
|
41468
|
+
await gitQuiet(root, ["fetch", "-q", "origin", base]);
|
|
41469
|
+
lastFetch.set(key, Date.now());
|
|
41470
|
+
}
|
|
41471
|
+
if (!await hasRemoteBranch(root, base)) return null;
|
|
41472
|
+
try {
|
|
41473
|
+
return await git(root, ["show", `origin/${base}:${rel}`]);
|
|
41474
|
+
} catch {
|
|
41475
|
+
return "";
|
|
41476
|
+
}
|
|
41477
|
+
}
|
|
41478
|
+
|
|
41479
|
+
// src/core/autonomous.ts
|
|
41480
|
+
var run5 = promisify5(execFile5);
|
|
40544
41481
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
40545
41482
|
var AUTHORS_REL = ".claude/.autonomous.authors.json";
|
|
40546
41483
|
var TICK_REL = ".claude/hooks/autonomous-tick.sh";
|
|
@@ -40554,7 +41491,7 @@ var BOOK_FILES = {
|
|
|
40554
41491
|
};
|
|
40555
41492
|
async function readText(path, fallback = "") {
|
|
40556
41493
|
try {
|
|
40557
|
-
return await
|
|
41494
|
+
return await readFile7(path, "utf8");
|
|
40558
41495
|
} catch {
|
|
40559
41496
|
return fallback;
|
|
40560
41497
|
}
|
|
@@ -40603,16 +41540,16 @@ var CAPPED_BOOK_FILES = { USER_TODO: "USER_TODO.md", USER_QA: "USER_QA.md", AI_T
|
|
|
40603
41540
|
async function cappedBookIds(root) {
|
|
40604
41541
|
const out = {};
|
|
40605
41542
|
for (const [book, file] of Object.entries(CAPPED_BOOK_FILES)) {
|
|
40606
|
-
out[book] = new Set(tableRowsById(await readText(
|
|
41543
|
+
out[book] = new Set(tableRowsById(await readText(join19(root, file))).keys());
|
|
40607
41544
|
}
|
|
40608
41545
|
return out;
|
|
40609
41546
|
}
|
|
40610
41547
|
function tickScript(root) {
|
|
40611
|
-
return
|
|
41548
|
+
return join19(root, TICK_REL);
|
|
40612
41549
|
}
|
|
40613
41550
|
async function crontabList() {
|
|
40614
41551
|
try {
|
|
40615
|
-
const { stdout } = await
|
|
41552
|
+
const { stdout } = await run5("crontab", ["-l"], { timeout: 1e4 });
|
|
40616
41553
|
return stdout;
|
|
40617
41554
|
} catch {
|
|
40618
41555
|
return "";
|
|
@@ -40645,13 +41582,15 @@ async function getAutonomousStatus(root, profileDir2) {
|
|
|
40645
41582
|
lastTickAt: last?.ts ?? null,
|
|
40646
41583
|
lastResult: last?.status ?? null,
|
|
40647
41584
|
consecutiveFails: hist.consecutiveFails,
|
|
40648
|
-
todayTicks: hist.ticks.day === today ? hist.ticks.count : 0
|
|
41585
|
+
todayTicks: hist.ticks.day === today ? hist.ticks.count : 0,
|
|
41586
|
+
// The last tick stopped on a protected base (ADR-0371 phase 0) — "<branch> (<repo>)".
|
|
41587
|
+
baseProtected: /^base-protected: (.+)$/.exec(last?.note ?? "")?.[1] ?? null
|
|
40649
41588
|
};
|
|
40650
41589
|
}
|
|
40651
41590
|
function isAutonomousRunning(root, profileDir2) {
|
|
40652
41591
|
try {
|
|
40653
41592
|
if (readAutonomousConfigSync(profileDir2).paused) return false;
|
|
40654
|
-
const h = JSON.parse(readFileSync14(
|
|
41593
|
+
const h = JSON.parse(readFileSync14(join19(root, ".claude/.autonomous.histories.json"), "utf8"));
|
|
40655
41594
|
const ts2 = h.records?.[h.records.length - 1]?.ts;
|
|
40656
41595
|
if (!ts2) return false;
|
|
40657
41596
|
const t2 = Date.parse(ts2.replace(" ", "T"));
|
|
@@ -40661,21 +41600,27 @@ function isAutonomousRunning(root, profileDir2) {
|
|
|
40661
41600
|
}
|
|
40662
41601
|
}
|
|
40663
41602
|
async function readAutonomous(root, profileDir2) {
|
|
40664
|
-
const
|
|
41603
|
+
const base = (await resolveBases(root).catch(() => null))?.root.base ?? "";
|
|
41604
|
+
const read = async (rel, fallback = "") => {
|
|
41605
|
+
const remote = base ? await readBaseFile(root, base, rel) : null;
|
|
41606
|
+
return remote === null ? readText(join19(root, rel), fallback) : remote || fallback;
|
|
41607
|
+
};
|
|
41608
|
+
const [settings, approvals, authors, attempts, status, ...books] = await Promise.all([
|
|
40665
41609
|
readAutonomousConfigText(profileDir2),
|
|
40666
|
-
|
|
40667
|
-
|
|
41610
|
+
read(APPROVALS_REL, "{}"),
|
|
41611
|
+
read(AUTHORS_REL, "{}"),
|
|
41612
|
+
read(ATTEMPTS_REL, "{}"),
|
|
40668
41613
|
getAutonomousStatus(root, profileDir2),
|
|
40669
|
-
...Object.values(BOOK_FILES).map((f2) =>
|
|
41614
|
+
...Object.values(BOOK_FILES).map((f2) => read(f2))
|
|
40670
41615
|
]);
|
|
40671
41616
|
const keys = Object.keys(BOOK_FILES);
|
|
40672
41617
|
const bookMap = {};
|
|
40673
41618
|
keys.forEach((k2, i) => bookMap[k2] = books[i] ?? "");
|
|
40674
|
-
return { settings, status, books: bookMap, approvals, authors };
|
|
41619
|
+
return { settings, status, books: bookMap, approvals, authors, attempts };
|
|
40675
41620
|
}
|
|
40676
41621
|
async function readAutonomousLogs(root, date) {
|
|
40677
41622
|
const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
40678
|
-
const text = await readText(
|
|
41623
|
+
const text = await readText(join19(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
|
|
40679
41624
|
const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
|
|
40680
41625
|
return { date: day, lines };
|
|
40681
41626
|
}
|
|
@@ -40685,7 +41630,7 @@ async function installCron(root, profileDir2) {
|
|
|
40685
41630
|
await chmod(script, 493).catch(() => void 0);
|
|
40686
41631
|
const kept = (await crontabList()).split("\n").filter((l) => l.trim() && !l.includes(script));
|
|
40687
41632
|
kept.push(`${cfg.cronSchedule} ${script}`);
|
|
40688
|
-
|
|
41633
|
+
return crontabSet(kept.join("\n"));
|
|
40689
41634
|
}
|
|
40690
41635
|
async function syncCron(root, profileDir2) {
|
|
40691
41636
|
if (await cronInstalled(root)) await installCron(root, profileDir2);
|
|
@@ -40697,79 +41642,92 @@ async function uninstallCron(root) {
|
|
|
40697
41642
|
const kept = cur.split("\n").filter((l) => l.trim() && !l.includes(script));
|
|
40698
41643
|
await crontabSet(kept.join("\n"));
|
|
40699
41644
|
}
|
|
41645
|
+
async function applyWebWrite(dir2, req, by) {
|
|
41646
|
+
switch (req.kind) {
|
|
41647
|
+
case "approvals": {
|
|
41648
|
+
if (req.approved && !req.byIsAdmin) {
|
|
41649
|
+
const authors = await readJsonMap(join19(dir2, AUTHORS_REL));
|
|
41650
|
+
if (entryBy(authors, req.taskId) === by) return { ok: false, code: "APPROVAL_SELF", failedId: req.taskId, error: "self-approval blocked" };
|
|
41651
|
+
}
|
|
41652
|
+
const map = parseJson(await readText(join19(dir2, APPROVALS_REL), "{}")) ?? {};
|
|
41653
|
+
if (req.approved) map[req.taskId] = { approved: true, by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
41654
|
+
else delete map[req.taskId];
|
|
41655
|
+
await writeFile5(join19(dir2, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
41656
|
+
return { ok: true };
|
|
41657
|
+
}
|
|
41658
|
+
case "approvalsBatch": {
|
|
41659
|
+
if (!req.byIsAdmin) {
|
|
41660
|
+
const authors = await readJsonMap(join19(dir2, AUTHORS_REL));
|
|
41661
|
+
const selfId = req.approve.find((id) => entryBy(authors, id) === by);
|
|
41662
|
+
if (selfId) return { ok: false, code: "APPROVAL_SELF", failedId: selfId, error: "self-approval blocked" };
|
|
41663
|
+
}
|
|
41664
|
+
const map = parseJson(await readText(join19(dir2, APPROVALS_REL), "{}")) ?? {};
|
|
41665
|
+
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
41666
|
+
const byLabel = req.byLabel ?? by;
|
|
41667
|
+
for (const taskId of req.approve) map[taskId] = { approved: true, by, byLabel, at: at2 };
|
|
41668
|
+
for (const taskId of req.unapprove) delete map[taskId];
|
|
41669
|
+
await writeFile5(join19(dir2, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
41670
|
+
return { ok: true };
|
|
41671
|
+
}
|
|
41672
|
+
case "userTodo": {
|
|
41673
|
+
const cur = await readText(join19(dir2, BOOK_FILES.userTodo));
|
|
41674
|
+
const ids = [...cur.matchAll(/REQ-(\d{4})-(\d{4})/g)];
|
|
41675
|
+
const maxGroup = ids.reduce((m2, g2) => Math.max(m2, Number(g2[1])), 0);
|
|
41676
|
+
const id = `REQ-${String(maxGroup + 1).padStart(4, "0")}-0001`;
|
|
41677
|
+
const row = `| ${id} | | | ${tableCell(req.content)} |
|
|
41678
|
+
`;
|
|
41679
|
+
await writeFile5(join19(dir2, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
|
|
41680
|
+
const authors = await readJsonMap(join19(dir2, AUTHORS_REL));
|
|
41681
|
+
authors[id] = { by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
41682
|
+
await writeFile5(join19(dir2, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
41683
|
+
return { ok: true, added: 1 };
|
|
41684
|
+
}
|
|
41685
|
+
case "bookSave": {
|
|
41686
|
+
const file = BOOK_FILES[req.book];
|
|
41687
|
+
const prev = tableRowsById(await readText(join19(dir2, file)));
|
|
41688
|
+
const next = tableRowsById(req.content);
|
|
41689
|
+
const authors = await readJsonMap(join19(dir2, AUTHORS_REL));
|
|
41690
|
+
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
41691
|
+
const byLabel = req.byLabel ?? by;
|
|
41692
|
+
let added = 0;
|
|
41693
|
+
for (const [id, cells] of next) {
|
|
41694
|
+
if (prev.get(id) !== cells) authors[id] = { by, byLabel, at: at2 };
|
|
41695
|
+
if (!prev.has(id)) added += 1;
|
|
41696
|
+
}
|
|
41697
|
+
await writeFile5(join19(dir2, file), req.content, "utf8");
|
|
41698
|
+
await writeFile5(join19(dir2, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
41699
|
+
return { ok: true, added };
|
|
41700
|
+
}
|
|
41701
|
+
default:
|
|
41702
|
+
return { ok: false, error: "not a book write" };
|
|
41703
|
+
}
|
|
41704
|
+
}
|
|
40700
41705
|
async function writeAutonomous(root, profileDir2, req, by) {
|
|
40701
41706
|
let added;
|
|
40702
41707
|
try {
|
|
40703
|
-
|
|
40704
|
-
|
|
40705
|
-
|
|
40706
|
-
|
|
40707
|
-
|
|
40708
|
-
|
|
40709
|
-
|
|
40710
|
-
|
|
40711
|
-
|
|
40712
|
-
|
|
40713
|
-
|
|
40714
|
-
|
|
40715
|
-
|
|
40716
|
-
|
|
40717
|
-
|
|
40718
|
-
|
|
40719
|
-
|
|
40720
|
-
|
|
40721
|
-
|
|
40722
|
-
|
|
40723
|
-
}
|
|
40724
|
-
|
|
40725
|
-
|
|
40726
|
-
const authors = await readJsonMap(join17(root, AUTHORS_REL));
|
|
40727
|
-
const selfId = req.approve.find((id) => entryBy(authors, id) === by);
|
|
40728
|
-
if (selfId) return { ok: false, code: "APPROVAL_SELF", failedId: selfId, error: "self-approval blocked" };
|
|
40729
|
-
}
|
|
40730
|
-
const map = parseJson(await readText(join17(root, APPROVALS_REL), "{}")) ?? {};
|
|
40731
|
-
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
40732
|
-
const byLabel = req.byLabel ?? by;
|
|
40733
|
-
for (const taskId of req.approve) map[taskId] = { approved: true, by, byLabel, at: at2 };
|
|
40734
|
-
for (const taskId of req.unapprove) delete map[taskId];
|
|
40735
|
-
await writeFile4(join17(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
40736
|
-
break;
|
|
40737
|
-
}
|
|
40738
|
-
case "userTodo": {
|
|
40739
|
-
const cur = await readText(join17(root, BOOK_FILES.userTodo));
|
|
40740
|
-
const ids = [...cur.matchAll(/REQ-(\d{4})-(\d{4})/g)];
|
|
40741
|
-
const maxGroup = ids.reduce((m2, g2) => Math.max(m2, Number(g2[1])), 0);
|
|
40742
|
-
const id = `REQ-${String(maxGroup + 1).padStart(4, "0")}-0001`;
|
|
40743
|
-
const row = `| ${id} | | | ${tableCell(req.content)} |
|
|
40744
|
-
`;
|
|
40745
|
-
await writeFile4(join17(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
|
|
40746
|
-
added = 1;
|
|
40747
|
-
const authors = await readJsonMap(join17(root, AUTHORS_REL));
|
|
40748
|
-
authors[id] = { by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
40749
|
-
await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
40750
|
-
break;
|
|
40751
|
-
}
|
|
40752
|
-
case "bookSave": {
|
|
40753
|
-
const file = BOOK_FILES[req.book];
|
|
40754
|
-
const prev = tableRowsById(await readText(join17(root, file)));
|
|
40755
|
-
const next = tableRowsById(req.content);
|
|
40756
|
-
const authors = await readJsonMap(join17(root, AUTHORS_REL));
|
|
40757
|
-
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
40758
|
-
const byLabel = req.byLabel ?? by;
|
|
40759
|
-
added = 0;
|
|
40760
|
-
for (const [id, cells] of next) {
|
|
40761
|
-
if (prev.get(id) !== cells) authors[id] = { by, byLabel, at: at2 };
|
|
40762
|
-
if (!prev.has(id)) added += 1;
|
|
40763
|
-
}
|
|
40764
|
-
await writeFile4(join17(root, file), req.content, "utf8");
|
|
40765
|
-
await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
40766
|
-
break;
|
|
40767
|
-
}
|
|
40768
|
-
case "cron": {
|
|
40769
|
-
if (req.action === "install") await installCron(root, profileDir2);
|
|
40770
|
-
else await uninstallCron(root);
|
|
40771
|
-
break;
|
|
40772
|
-
}
|
|
41708
|
+
if (req.kind === "settings") {
|
|
41709
|
+
const parsed = parseJson(req.settings);
|
|
41710
|
+
if (!parsed) return { ok: false, error: t("autonomous.settingsInvalidJson") };
|
|
41711
|
+
await writeAutonomousConfig(profileDir2, parsed);
|
|
41712
|
+
await syncCron(root, profileDir2);
|
|
41713
|
+
} else if (req.kind === "cron") {
|
|
41714
|
+
if (req.action === "install") {
|
|
41715
|
+
const wrote = await installCron(root, profileDir2);
|
|
41716
|
+
if (!wrote || !await cronInstalled(root)) {
|
|
41717
|
+
return { ok: false, error: t("autonomous.cronMissing") };
|
|
41718
|
+
}
|
|
41719
|
+
} else await uninstallCron(root);
|
|
41720
|
+
} else {
|
|
41721
|
+
const base = (await resolveBases(root).catch(() => null))?.root.base ?? "";
|
|
41722
|
+
let res;
|
|
41723
|
+
const pub = base ? await mutateBooksOnBase(root, base, `chore(web): ${req.kind} by ${req.byLabel ?? by}`, async (dir2) => {
|
|
41724
|
+
res = await applyWebWrite(dir2, req, by);
|
|
41725
|
+
return res.ok;
|
|
41726
|
+
}) : "unchanged";
|
|
41727
|
+
if (res === void 0) res = await applyWebWrite(root, req, by);
|
|
41728
|
+
else if (pub === "failed") return { ok: false, error: "could not push the change to the base branch (retry)" };
|
|
41729
|
+
if (!res.ok) return { ok: false, ...res.code ? { code: res.code } : {}, ...res.failedId ? { failedId: res.failedId } : {}, error: res.error };
|
|
41730
|
+
added = res.added;
|
|
40773
41731
|
}
|
|
40774
41732
|
return { ok: true, status: await getAutonomousStatus(root, profileDir2), ...added !== void 0 ? { added } : {} };
|
|
40775
41733
|
} catch (err) {
|
|
@@ -40778,7 +41736,7 @@ async function writeAutonomous(root, profileDir2, req, by) {
|
|
|
40778
41736
|
}
|
|
40779
41737
|
|
|
40780
41738
|
// src/core/network-probe.ts
|
|
40781
|
-
import { existsSync as
|
|
41739
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
40782
41740
|
import { connect } from "net";
|
|
40783
41741
|
var CANARY_TARGETS = [
|
|
40784
41742
|
{ host: "1.1.1.1", port: 443 },
|
|
@@ -40787,7 +41745,7 @@ var CANARY_TARGETS = [
|
|
|
40787
41745
|
var PROBE_TIMEOUT_MS = 2500;
|
|
40788
41746
|
function detectContainerized() {
|
|
40789
41747
|
try {
|
|
40790
|
-
if (
|
|
41748
|
+
if (existsSync12("/.dockerenv")) return true;
|
|
40791
41749
|
const cgroup = readFileSync15("/proc/1/cgroup", "utf8");
|
|
40792
41750
|
return /docker|containerd|kubepods|libpod/.test(cgroup);
|
|
40793
41751
|
} catch {
|
|
@@ -40855,9 +41813,9 @@ async function probeNetwork() {
|
|
|
40855
41813
|
}
|
|
40856
41814
|
|
|
40857
41815
|
// src/core/git-auth.ts
|
|
40858
|
-
import { chmodSync as chmodSync3, existsSync as
|
|
41816
|
+
import { chmodSync as chmodSync3, existsSync as existsSync13, mkdirSync as mkdirSync6, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
40859
41817
|
import { request as httpsRequest } from "https";
|
|
40860
|
-
import { delimiter, dirname as dirname4, join as
|
|
41818
|
+
import { delimiter, dirname as dirname4, join as join20, resolve as resolve3 } from "path";
|
|
40861
41819
|
var JOB_ID_ENV = "FOURPM_JOB_ID";
|
|
40862
41820
|
var PROFILE_DIR_ENV = "FOURPM_PROFILE_DIR";
|
|
40863
41821
|
var GIT_HOST_ENV = "FOURPM_GIT_HOST";
|
|
@@ -40877,8 +41835,8 @@ function helperCommand() {
|
|
|
40877
41835
|
function findRealGh(pathValue, shimDir) {
|
|
40878
41836
|
for (const dir2 of pathValue.split(delimiter)) {
|
|
40879
41837
|
if (!dir2 || dir2 === shimDir) continue;
|
|
40880
|
-
const candidate =
|
|
40881
|
-
if (
|
|
41838
|
+
const candidate = join20(dir2, process.platform === "win32" ? "gh.exe" : "gh");
|
|
41839
|
+
if (existsSync13(candidate)) return candidate;
|
|
40882
41840
|
}
|
|
40883
41841
|
return null;
|
|
40884
41842
|
}
|
|
@@ -40939,22 +41897,22 @@ function configureGitAuth(method, host, profileDir2, transport) {
|
|
|
40939
41897
|
process.env[PROFILE_DIR_ENV] = resolve3(profileDir2);
|
|
40940
41898
|
const script = process.argv[1];
|
|
40941
41899
|
const devTsconfig = script ? resolve3(dirname4(script), "..", "tsconfig.json") : "";
|
|
40942
|
-
if (process.execArgv.some((a) => a.includes("tsx")) && devTsconfig &&
|
|
41900
|
+
if (process.execArgv.some((a) => a.includes("tsx")) && devTsconfig && existsSync13(devTsconfig) && !process.env.TSX_TSCONFIG_PATH) {
|
|
40943
41901
|
process.env.TSX_TSCONFIG_PATH = devTsconfig;
|
|
40944
41902
|
}
|
|
40945
41903
|
process.env[GIT_HOST_ENV] = host;
|
|
40946
41904
|
process.env[GIT_HOST_KIND_ENV] = githubHostKind(host);
|
|
40947
41905
|
if (process.platform !== "win32") {
|
|
40948
|
-
const shimDir =
|
|
41906
|
+
const shimDir = join20(profileDir2, "bin");
|
|
40949
41907
|
const realGh = findRealGh(state.baseEnv.path, shimDir);
|
|
40950
41908
|
if (realGh) {
|
|
40951
41909
|
mkdirSync6(shimDir, { recursive: true });
|
|
40952
|
-
const shim =
|
|
41910
|
+
const shim = join20(shimDir, "gh");
|
|
40953
41911
|
writeFileSync8(shim, shimScript(realGh, cliInvocation()), { encoding: "utf8", mode: 493 });
|
|
40954
41912
|
chmodSync3(shim, 493);
|
|
40955
41913
|
process.env.PATH = `${shimDir}${delimiter}${state.baseEnv.path}`;
|
|
40956
41914
|
} else {
|
|
40957
|
-
rmSync6(
|
|
41915
|
+
rmSync6(join20(shimDir, "gh"), { force: true });
|
|
40958
41916
|
}
|
|
40959
41917
|
}
|
|
40960
41918
|
state.host = host;
|
|
@@ -41028,16 +41986,16 @@ async function revokeAll() {
|
|
|
41028
41986
|
}
|
|
41029
41987
|
|
|
41030
41988
|
// src/core/git-snapshot.ts
|
|
41031
|
-
import { execFile as
|
|
41989
|
+
import { execFile as execFile8 } from "child_process";
|
|
41032
41990
|
import { stat } from "fs/promises";
|
|
41033
|
-
import { join as
|
|
41034
|
-
import { promisify as
|
|
41991
|
+
import { join as join22, resolve as resolve5 } from "path";
|
|
41992
|
+
import { promisify as promisify8 } from "util";
|
|
41035
41993
|
|
|
41036
41994
|
// src/core/git-history.ts
|
|
41037
|
-
import { execFile as
|
|
41995
|
+
import { execFile as execFile6 } from "child_process";
|
|
41038
41996
|
import { resolve as resolve4, sep as sep2 } from "path";
|
|
41039
|
-
import { promisify as
|
|
41040
|
-
var
|
|
41997
|
+
import { promisify as promisify6 } from "util";
|
|
41998
|
+
var run6 = promisify6(execFile6);
|
|
41041
41999
|
var FS = "";
|
|
41042
42000
|
var RS = "";
|
|
41043
42001
|
function resolveRepo(root, repo) {
|
|
@@ -41046,9 +42004,9 @@ function resolveRepo(root, repo) {
|
|
|
41046
42004
|
const requested = resolve4(base, repo && repo.trim() ? repo : ".");
|
|
41047
42005
|
return requested === base || requested.startsWith(base + sep2) ? requested : base;
|
|
41048
42006
|
}
|
|
41049
|
-
async function
|
|
42007
|
+
async function git2(cwd2, args) {
|
|
41050
42008
|
try {
|
|
41051
|
-
const { stdout } = await
|
|
42009
|
+
const { stdout } = await run6("git", args, {
|
|
41052
42010
|
cwd: cwd2,
|
|
41053
42011
|
timeout: 15e3,
|
|
41054
42012
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -41061,21 +42019,21 @@ async function git(cwd2, args) {
|
|
|
41061
42019
|
async function gitRepos(root) {
|
|
41062
42020
|
if (!root) return { repos: [] };
|
|
41063
42021
|
const base = resolve4(root);
|
|
41064
|
-
const top = (await
|
|
42022
|
+
const top = (await git2(base, ["rev-parse", "--show-toplevel"])).trim();
|
|
41065
42023
|
if (!top || !(top === base || top.startsWith(base + sep2))) return { repos: [] };
|
|
41066
42024
|
const repos = [];
|
|
41067
|
-
const rootRemote = (await
|
|
41068
|
-
const rootBranch = (await
|
|
42025
|
+
const rootRemote = (await git2(base, ["remote", "get-url", "origin"])).trim();
|
|
42026
|
+
const rootBranch = (await git2(base, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
41069
42027
|
repos.push({ subdir: "", name: "", remote: rootRemote || null, branch: rootBranch || null });
|
|
41070
|
-
const subOut = await
|
|
42028
|
+
const subOut = await git2(base, ["submodule", "status"]);
|
|
41071
42029
|
for (const line of subOut.split("\n")) {
|
|
41072
42030
|
const m2 = line.trim().match(/^[-+U ]?[0-9a-f]{7,40}\s+(\S+)/);
|
|
41073
42031
|
const subPath = m2?.[1];
|
|
41074
42032
|
if (!subPath) continue;
|
|
41075
42033
|
const abs = resolve4(base, subPath);
|
|
41076
42034
|
if (!(abs === base || abs.startsWith(base + sep2))) continue;
|
|
41077
|
-
const remote = (await
|
|
41078
|
-
const branch = (await
|
|
42035
|
+
const remote = (await git2(abs, ["remote", "get-url", "origin"])).trim();
|
|
42036
|
+
const branch = (await git2(abs, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
41079
42037
|
repos.push({ subdir: subPath, name: subPath, remote: remote || null, branch: branch || null });
|
|
41080
42038
|
}
|
|
41081
42039
|
return { repos };
|
|
@@ -41085,7 +42043,7 @@ async function gitLog(root, repo, skip, limit) {
|
|
|
41085
42043
|
if (!cwd2) return { entries: [], hasMore: false };
|
|
41086
42044
|
const capped = Math.min(Math.max(limit, 1), 200);
|
|
41087
42045
|
const format = ["%H", "%h", "%an", "%aI", "%s"].join(FS) + RS;
|
|
41088
|
-
const stdout = await
|
|
42046
|
+
const stdout = await git2(cwd2, [
|
|
41089
42047
|
"log",
|
|
41090
42048
|
`--skip=${Math.max(skip, 0)}`,
|
|
41091
42049
|
`-n`,
|
|
@@ -41104,7 +42062,7 @@ async function gitCommit(root, repo, hash) {
|
|
|
41104
42062
|
const empty = { hash, author: "", date: "", subject: "", files: [] };
|
|
41105
42063
|
if (!cwd2 || !hash) return empty;
|
|
41106
42064
|
const format = ["%H", "%an", "%aI", "%s"].join(FS);
|
|
41107
|
-
const stdout = await
|
|
42065
|
+
const stdout = await git2(cwd2, [
|
|
41108
42066
|
"show",
|
|
41109
42067
|
"--name-status",
|
|
41110
42068
|
"-M",
|
|
@@ -41131,8 +42089,8 @@ async function gitCommit(root, repo, hash) {
|
|
|
41131
42089
|
async function gitCommitDiff(root, repo, hash, path) {
|
|
41132
42090
|
const cwd2 = resolveRepo(root, repo);
|
|
41133
42091
|
if (!cwd2 || !hash || !path) return { path, oldContent: "", newContent: "" };
|
|
41134
|
-
const oldContent = await
|
|
41135
|
-
const newContent = await
|
|
42092
|
+
const oldContent = await git2(cwd2, ["show", `${hash}^:${path}`]);
|
|
42093
|
+
const newContent = await git2(cwd2, ["show", `${hash}:${path}`]);
|
|
41136
42094
|
return { path, oldContent, newContent };
|
|
41137
42095
|
}
|
|
41138
42096
|
|
|
@@ -41352,7 +42310,7 @@ function summarizeAiFailure(captured, exitCode) {
|
|
|
41352
42310
|
|
|
41353
42311
|
// src/utils/time.ts
|
|
41354
42312
|
function formatTimestampInZone(date, timeZone) {
|
|
41355
|
-
const
|
|
42313
|
+
const build2 = (tz) => {
|
|
41356
42314
|
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
41357
42315
|
...tz ? { timeZone: tz } : {},
|
|
41358
42316
|
hour12: false,
|
|
@@ -41363,20 +42321,20 @@ function formatTimestampInZone(date, timeZone) {
|
|
|
41363
42321
|
minute: "2-digit",
|
|
41364
42322
|
second: "2-digit"
|
|
41365
42323
|
}).formatToParts(date);
|
|
41366
|
-
const
|
|
41367
|
-
const hour =
|
|
41368
|
-
return `${
|
|
42324
|
+
const get3 = (type) => parts.find((p2) => p2.type === type)?.value ?? "";
|
|
42325
|
+
const hour = get3("hour") === "24" ? "00" : get3("hour");
|
|
42326
|
+
return `${get3("year")}/${get3("month")}/${get3("day")} ${hour}:${get3("minute")}:${get3("second")}`;
|
|
41369
42327
|
};
|
|
41370
42328
|
try {
|
|
41371
|
-
return
|
|
42329
|
+
return build2(timeZone);
|
|
41372
42330
|
} catch {
|
|
41373
|
-
return
|
|
42331
|
+
return build2();
|
|
41374
42332
|
}
|
|
41375
42333
|
}
|
|
41376
42334
|
|
|
41377
42335
|
// src/ui/slash-commands.ts
|
|
41378
|
-
import { existsSync as
|
|
41379
|
-
import { join as
|
|
42336
|
+
import { existsSync as existsSync14 } from "fs";
|
|
42337
|
+
import { join as join21 } from "path";
|
|
41380
42338
|
var CONFIG_KEYS = [
|
|
41381
42339
|
"aiCli",
|
|
41382
42340
|
"claudeHome",
|
|
@@ -41388,7 +42346,7 @@ var CONFIG_KEYS = [
|
|
|
41388
42346
|
"aiEnv"
|
|
41389
42347
|
];
|
|
41390
42348
|
function configPath(profileDir2) {
|
|
41391
|
-
return
|
|
42349
|
+
return join21(profileDir2, "config.json");
|
|
41392
42350
|
}
|
|
41393
42351
|
function parseConfigAssignment(key, valueTokens) {
|
|
41394
42352
|
const value = valueTokens.join(" ").trim();
|
|
@@ -41438,7 +42396,7 @@ function runConfig(ctx) {
|
|
|
41438
42396
|
ctx.print(t("slash.config.wroteDefault", { verb, path }));
|
|
41439
42397
|
ctx.print(t("slash.config.addClaudeHint"));
|
|
41440
42398
|
};
|
|
41441
|
-
if (
|
|
42399
|
+
if (existsSync14(path)) {
|
|
41442
42400
|
ctx.confirm(
|
|
41443
42401
|
t("slash.config.replaceConfirm", { path }),
|
|
41444
42402
|
() => writeDefaults(t("slash.config.verbReplaced"))
|
|
@@ -41523,7 +42481,7 @@ function runOutput(ctx) {
|
|
|
41523
42481
|
}
|
|
41524
42482
|
function runLogs(ctx) {
|
|
41525
42483
|
const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
|
|
41526
|
-
const dir2 =
|
|
42484
|
+
const dir2 = join21(ctx.info.profileDir, "logs");
|
|
41527
42485
|
const lines = readRecentLogLines(dir2, n);
|
|
41528
42486
|
if (lines.length === 0) {
|
|
41529
42487
|
ctx.print(t("slash.logs.none"));
|
|
@@ -41870,13 +42828,13 @@ async function runMemoryCompaction(ai2, cwd2, input) {
|
|
|
41870
42828
|
}
|
|
41871
42829
|
|
|
41872
42830
|
// src/core/git-commit-identity.ts
|
|
41873
|
-
import { execFile as
|
|
41874
|
-
import { promisify as
|
|
41875
|
-
var
|
|
42831
|
+
import { execFile as execFile7 } from "child_process";
|
|
42832
|
+
import { promisify as promisify7 } from "util";
|
|
42833
|
+
var run7 = promisify7(execFile7);
|
|
41876
42834
|
async function setCommitAuthor(cwd2, machineUsername) {
|
|
41877
42835
|
if (!machineUsername) return;
|
|
41878
42836
|
try {
|
|
41879
|
-
await
|
|
42837
|
+
await run7("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
41880
42838
|
} catch {
|
|
41881
42839
|
}
|
|
41882
42840
|
}
|
|
@@ -41983,10 +42941,10 @@ function activeAiRunCount() {
|
|
|
41983
42941
|
return activeRuns.size;
|
|
41984
42942
|
}
|
|
41985
42943
|
function cancelActiveRun(commandId) {
|
|
41986
|
-
const
|
|
41987
|
-
if (!
|
|
41988
|
-
|
|
41989
|
-
return { ok: true, state:
|
|
42944
|
+
const run15 = activeRuns.get(commandId);
|
|
42945
|
+
if (!run15) return { ok: false, state: "unknown" };
|
|
42946
|
+
run15.controller.abort();
|
|
42947
|
+
return { ok: true, state: run15.state };
|
|
41990
42948
|
}
|
|
41991
42949
|
function handleCommandChannels(ctx, message, payload) {
|
|
41992
42950
|
if (message.channel === WsChannels.COMMAND_CANCEL) {
|
|
@@ -42082,17 +43040,18 @@ async function runLocalCommand(ctx, input) {
|
|
|
42082
43040
|
const commandId = randomCommandId();
|
|
42083
43041
|
await runAiPrompt(ctx, prompt, commandId, "local");
|
|
42084
43042
|
}
|
|
43043
|
+
var REFUSED = { exitCode: 1, cancelled: false, exhausted: false, neverStarted: true, output: "" };
|
|
42085
43044
|
async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
|
|
42086
|
-
const
|
|
42087
|
-
activeRuns.set(commandId,
|
|
43045
|
+
const run15 = { controller: new AbortController(), state: "queued" };
|
|
43046
|
+
activeRuns.set(commandId, run15);
|
|
42088
43047
|
try {
|
|
42089
|
-
await runAiPromptInner(ctx, prompt, commandId, origin,
|
|
43048
|
+
return await runAiPromptInner(ctx, prompt, commandId, origin, run15, oneShot, images, aiConfig, readOnly, bypass);
|
|
42090
43049
|
} finally {
|
|
42091
43050
|
activeRuns.delete(commandId);
|
|
42092
43051
|
if (!readOnly) requestGitSnapshot("ai-run");
|
|
42093
43052
|
}
|
|
42094
43053
|
}
|
|
42095
|
-
async function runAiPromptInner(ctx, prompt, commandId, origin,
|
|
43054
|
+
async function runAiPromptInner(ctx, prompt, commandId, origin, run15, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
|
|
42096
43055
|
const config = readProfileConfig(ctx.profileDir);
|
|
42097
43056
|
const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
|
|
42098
43057
|
const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
|
|
@@ -42103,7 +43062,7 @@ async function runAiPromptInner(ctx, prompt, commandId, origin, run12, oneShot =
|
|
|
42103
43062
|
const estimated = estimateTokens(prompt);
|
|
42104
43063
|
if (estimated > perPromptLimit) {
|
|
42105
43064
|
rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
|
|
42106
|
-
return;
|
|
43065
|
+
return REFUSED;
|
|
42107
43066
|
}
|
|
42108
43067
|
}
|
|
42109
43068
|
if (ctx.outboundReviewEnabled) {
|
|
@@ -42118,7 +43077,7 @@ async function runAiPromptInner(ctx, prompt, commandId, origin, run12, oneShot =
|
|
|
42118
43077
|
}
|
|
42119
43078
|
if (!verdict.ok) {
|
|
42120
43079
|
rejectByReview(ctx, commandId, origin, prompt, verdict.reasons);
|
|
42121
|
-
return;
|
|
43080
|
+
return REFUSED;
|
|
42122
43081
|
}
|
|
42123
43082
|
}
|
|
42124
43083
|
const cwd2 = ctx.physicRoot ?? config.physicPath ?? process.cwd();
|
|
@@ -42209,6 +43168,7 @@ ${guardedPrompt}`;
|
|
|
42209
43168
|
let resultId = null;
|
|
42210
43169
|
let resultBuf = "";
|
|
42211
43170
|
let answerText = "";
|
|
43171
|
+
const failReasons = [];
|
|
42212
43172
|
const handlers = {
|
|
42213
43173
|
onChunk: (text) => {
|
|
42214
43174
|
if (!responseHeaderShown) {
|
|
@@ -42250,6 +43210,7 @@ ${guardedPrompt}`;
|
|
|
42250
43210
|
`, log: true });
|
|
42251
43211
|
},
|
|
42252
43212
|
onAttemptFail: (label, reason) => {
|
|
43213
|
+
failReasons.push(String(reason));
|
|
42253
43214
|
const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : reason === "credits" ? `profile "${label}" is out of usage credits \u2014 trying next` : reason === "auth" ? `profile "${label}" failed to authenticate \u2014 trying next` : reason === "timeout" ? `profile "${label}" timed out \u2014 trying next` : `profile "${label}" run failed \u2014 trying next`;
|
|
42254
43215
|
ctx.bus.push({ source: origin, kind: "log", text, level: "warn" });
|
|
42255
43216
|
ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
@@ -42273,7 +43234,7 @@ ${guardedPrompt}`;
|
|
|
42273
43234
|
queue: { position, limit, running: running2 }
|
|
42274
43235
|
});
|
|
42275
43236
|
},
|
|
42276
|
-
|
|
43237
|
+
run15.controller.signal
|
|
42277
43238
|
);
|
|
42278
43239
|
if (slot.kind === "timeout") {
|
|
42279
43240
|
neverStarted = true;
|
|
@@ -42294,9 +43255,9 @@ ${guardedPrompt}`;
|
|
|
42294
43255
|
result = { exitCode: COMMAND_CANCELLED_EXIT_CODE, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "", cancelled: true };
|
|
42295
43256
|
} else {
|
|
42296
43257
|
if (slot.queued) ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", log: true, queue: null });
|
|
42297
|
-
|
|
43258
|
+
run15.state = "running";
|
|
42298
43259
|
try {
|
|
42299
|
-
result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs,
|
|
43260
|
+
result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run15.controller.signal);
|
|
42300
43261
|
} finally {
|
|
42301
43262
|
slot.handle.release();
|
|
42302
43263
|
}
|
|
@@ -42389,6 +43350,13 @@ ${guardedPrompt}`;
|
|
|
42389
43350
|
ctx.sessionIdByKey.clear();
|
|
42390
43351
|
ctx.sessionMetaByKey.clear();
|
|
42391
43352
|
}
|
|
43353
|
+
return {
|
|
43354
|
+
exitCode: result.exitCode,
|
|
43355
|
+
cancelled: !!result.cancelled,
|
|
43356
|
+
exhausted: result.exitCode !== 0 && !result.cancelled && failReasons.length > 0 && failReasons.every((r) => r === "limit" || r === "credits"),
|
|
43357
|
+
neverStarted,
|
|
43358
|
+
output: answerText
|
|
43359
|
+
};
|
|
42392
43360
|
}
|
|
42393
43361
|
function pruneResumableSessions(ctx, policy, now) {
|
|
42394
43362
|
for (const key of [...ctx.sessionIdByKey.keys()]) {
|
|
@@ -42608,14 +43576,14 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
|
|
|
42608
43576
|
}
|
|
42609
43577
|
|
|
42610
43578
|
// src/core/git-snapshot.ts
|
|
42611
|
-
var
|
|
43579
|
+
var run8 = promisify8(execFile8);
|
|
42612
43580
|
var US = "";
|
|
42613
43581
|
var RS2 = "";
|
|
42614
43582
|
var MIN_SPACING_MS = 15e3;
|
|
42615
43583
|
var DEFAULT_INTERVAL_MIN = 10;
|
|
42616
|
-
async function
|
|
43584
|
+
async function git3(cwd2, args, timeout = 2e4) {
|
|
42617
43585
|
try {
|
|
42618
|
-
const { stdout } = await
|
|
43586
|
+
const { stdout } = await run8("git", args, { cwd: cwd2, timeout, maxBuffer: 16 * 1024 * 1024 });
|
|
42619
43587
|
return stdout;
|
|
42620
43588
|
} catch {
|
|
42621
43589
|
return "";
|
|
@@ -42670,13 +43638,13 @@ async function buildRepoSnapshot(dir2, subdir) {
|
|
|
42670
43638
|
const L2 = GIT_SNAPSHOT_LIMITS;
|
|
42671
43639
|
const fmt = `--format=${RS2}%H${US}%P${US}%an${US}%aI${US}%s`;
|
|
42672
43640
|
const [abbrev, headSha, heads, remotes, remoteHead, status, gitDir] = await Promise.all([
|
|
42673
|
-
|
|
42674
|
-
|
|
42675
|
-
|
|
42676
|
-
|
|
42677
|
-
|
|
42678
|
-
|
|
42679
|
-
|
|
43641
|
+
git3(dir2, ["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
43642
|
+
git3(dir2, ["rev-parse", "--verify", "HEAD"]),
|
|
43643
|
+
git3(dir2, ["for-each-ref", "refs/heads", `--format=%(refname:short)${US}%(objectname)${US}%(upstream:short)${US}%(upstream:track,nobracket)`]),
|
|
43644
|
+
git3(dir2, ["for-each-ref", "refs/remotes/origin", `--format=%(refname:short)${US}%(objectname)${US}%(symref)`]),
|
|
43645
|
+
git3(dir2, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]),
|
|
43646
|
+
git3(dir2, ["status", "--porcelain=v2", "--untracked-files=all"]),
|
|
43647
|
+
git3(dir2, ["rev-parse", "--absolute-git-dir"])
|
|
42680
43648
|
]);
|
|
42681
43649
|
const branchName = abbrev.trim();
|
|
42682
43650
|
const detached = branchName === "HEAD";
|
|
@@ -42687,8 +43655,8 @@ async function buildRepoSnapshot(dir2, subdir) {
|
|
|
42687
43655
|
const remoteBranches = remotes.split("\n").filter(Boolean).map((l) => l.split(US)).filter(([, , symref]) => !symref).map(([name = "", sha = ""]) => ({ name: name.replace(/^origin\//, ""), sha })).filter((b2) => b2.name && b2.name !== "origin");
|
|
42688
43656
|
const { dirty, files } = parseStatus(status);
|
|
42689
43657
|
const [localOut, remoteOut] = await Promise.all([
|
|
42690
|
-
|
|
42691
|
-
|
|
43658
|
+
git3(dir2, ["log", "--branches", "--not", "--remotes", "-n", String(L2.localCommits + 1), "--name-status", fmt], 3e4),
|
|
43659
|
+
git3(dir2, ["log", "--remotes", "--date-order", "-n", String(L2.remoteCommits), fmt], 3e4)
|
|
42692
43660
|
]);
|
|
42693
43661
|
const localAll = parseLog(localOut, true);
|
|
42694
43662
|
const localCommits = localAll.slice(0, L2.localCommits).map((c) => ({
|
|
@@ -42701,10 +43669,10 @@ async function buildRepoSnapshot(dir2, subdir) {
|
|
|
42701
43669
|
const defaultBranch = remoteHead.trim().replace(/^origin\//, "") || null;
|
|
42702
43670
|
const upstream = branches.find((b2) => b2.name === branchName)?.upstream;
|
|
42703
43671
|
const baseRef = upstream ?? (defaultBranch ? `origin/${defaultBranch}` : null);
|
|
42704
|
-
const baseSha = headSha.trim() && baseRef ? (await
|
|
43672
|
+
const baseSha = headSha.trim() && baseRef ? (await git3(dir2, ["merge-base", "HEAD", baseRef])).trim() || null : null;
|
|
42705
43673
|
let lastFetchAt = null;
|
|
42706
43674
|
try {
|
|
42707
|
-
if (gitDir.trim()) lastFetchAt = (await stat(
|
|
43675
|
+
if (gitDir.trim()) lastFetchAt = (await stat(join22(gitDir.trim(), "FETCH_HEAD"))).mtime.toISOString();
|
|
42708
43676
|
} catch {
|
|
42709
43677
|
lastFetchAt = null;
|
|
42710
43678
|
}
|
|
@@ -42732,7 +43700,7 @@ async function buildGitSnapshot(root) {
|
|
|
42732
43700
|
async function fetchAll(root) {
|
|
42733
43701
|
const base = resolve5(root);
|
|
42734
43702
|
const { repos } = await gitRepos(base);
|
|
42735
|
-
for (const r of repos) await
|
|
43703
|
+
for (const r of repos) await git3(r.subdir ? resolve5(base, r.subdir) : base, ["fetch", "--prune", "--quiet", "origin"], 6e4);
|
|
42736
43704
|
}
|
|
42737
43705
|
var state2 = { host: null, lastSentAt: 0, pending: null, periodic: null, running: false };
|
|
42738
43706
|
async function reportNow(fetchFirst) {
|
|
@@ -42799,96 +43767,394 @@ function envReconnectMaxSec() {
|
|
|
42799
43767
|
|
|
42800
43768
|
// src/core/autonomous-cycle.ts
|
|
42801
43769
|
import { randomUUID } from "crypto";
|
|
42802
|
-
import { appendFile as appendFile2, mkdir as mkdir3, readFile as
|
|
42803
|
-
import { join as
|
|
42804
|
-
|
|
42805
|
-
|
|
42806
|
-
|
|
42807
|
-
|
|
42808
|
-
|
|
42809
|
-
|
|
42810
|
-
return
|
|
42811
|
-
|
|
42812
|
-
|
|
42813
|
-
|
|
42814
|
-
|
|
42815
|
-
|
|
42816
|
-
|
|
42817
|
-
|
|
43770
|
+
import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile9, readdir as readdir3, rm as rm3, stat as stat2 } from "fs/promises";
|
|
43771
|
+
import { join as join24 } from "path";
|
|
43772
|
+
|
|
43773
|
+
// src/core/autonomous-tasks.ts
|
|
43774
|
+
import { readFile as readFile8, rm as rm2, writeFile as writeFile6 } from "fs/promises";
|
|
43775
|
+
import { join as join23 } from "path";
|
|
43776
|
+
var APPROVALS_REL2 = ".claude/.autonomous.approvals.json";
|
|
43777
|
+
function claimPath(profileDir2) {
|
|
43778
|
+
return join23(profileDir2, "autonomous-claim.json");
|
|
43779
|
+
}
|
|
43780
|
+
async function readLocalClaim(profileDir2) {
|
|
43781
|
+
try {
|
|
43782
|
+
return JSON.parse(await readFile8(claimPath(profileDir2), "utf8"));
|
|
43783
|
+
} catch {
|
|
43784
|
+
return null;
|
|
43785
|
+
}
|
|
43786
|
+
}
|
|
43787
|
+
async function writeLocalClaim(profileDir2, c) {
|
|
43788
|
+
if (c) await writeFile6(claimPath(profileDir2), `${JSON.stringify(c, null, 2)}
|
|
43789
|
+
`, "utf8");
|
|
43790
|
+
else await rm2(claimPath(profileDir2), { force: true });
|
|
43791
|
+
}
|
|
43792
|
+
function groupOf(taskId) {
|
|
43793
|
+
const m2 = /^TSK-(\d+)-\d+/.exec(taskId);
|
|
43794
|
+
return m2 ? `GRP-${m2[1]}` : "GRP-0000";
|
|
43795
|
+
}
|
|
43796
|
+
function taskBranch(base, taskId) {
|
|
43797
|
+
return `dev/${base}/${groupOf(taskId)}/${taskId}`;
|
|
43798
|
+
}
|
|
43799
|
+
function fromHint(notes) {
|
|
43800
|
+
return /(?:^|;\s*)from:\s*(\S+)/.exec(notes)?.[1] ?? null;
|
|
43801
|
+
}
|
|
43802
|
+
function doneBranch(notes) {
|
|
43803
|
+
return /branch:\s*([^\s;]+)/.exec(notes)?.[1] ?? null;
|
|
43804
|
+
}
|
|
43805
|
+
async function approvedIds(root) {
|
|
43806
|
+
try {
|
|
43807
|
+
const map = JSON.parse(await readFile8(join23(root, APPROVALS_REL2), "utf8"));
|
|
43808
|
+
return new Set(Object.entries(map).filter(([, v2]) => v2?.approved === true).map(([k2]) => k2));
|
|
43809
|
+
} catch {
|
|
43810
|
+
return /* @__PURE__ */ new Set();
|
|
43811
|
+
}
|
|
43812
|
+
}
|
|
43813
|
+
function claimAgeHours(c) {
|
|
43814
|
+
const t2 = parseStamp(c.started);
|
|
43815
|
+
return Number.isFinite(t2) ? (Date.now() - t2) / 36e5 : Infinity;
|
|
43816
|
+
}
|
|
43817
|
+
async function claimState(root, mine, ttlHours) {
|
|
43818
|
+
const row = parseClaims(await readBook(root, "AI_PROGRESS.md")).claims.find((c) => c.id === mine.id);
|
|
43819
|
+
if (!row || row.claim !== mine.claim) return "lost";
|
|
43820
|
+
return claimAgeHours(row) >= ttlHours ? "expired" : "live";
|
|
43821
|
+
}
|
|
43822
|
+
async function pickTask(root, rootBase, opts) {
|
|
43823
|
+
const [todo, prog, done, qa, attempts, approved] = await Promise.all([
|
|
43824
|
+
readBook(root, "AI_TODO.md"),
|
|
43825
|
+
readBook(root, "AI_PROGRESS.md"),
|
|
43826
|
+
readBook(root, "AI_DONE.md"),
|
|
43827
|
+
readBook(root, "USER_QA.md"),
|
|
43828
|
+
readAttempts(root),
|
|
43829
|
+
approvedIds(root)
|
|
43830
|
+
]);
|
|
43831
|
+
const claims = parseClaims(prog).claims;
|
|
43832
|
+
const skip = opts.skip ?? /* @__PURE__ */ new Set();
|
|
43833
|
+
for (const c of claims) {
|
|
43834
|
+
if (skip.has(c.id) || c.worker === opts.worker || claimAgeHours(c) < opts.ttlHours) continue;
|
|
43835
|
+
const snap = attempts[c.id]?.task;
|
|
43836
|
+
const task = snap ?? { id: c.id, priority: "Medium", tag: "", depends: [], group: groupOf(c.id), desc: c.desc, notes: "" };
|
|
43837
|
+
return { task, takeover: c, from: null };
|
|
43838
|
+
}
|
|
43839
|
+
const doneMap = doneRows(done);
|
|
43840
|
+
const qaRows = new Map(parseQa(qa).rows.map((q2) => [q2.id, q2]));
|
|
43841
|
+
const claimed = new Set(claims.map((c) => c.id));
|
|
43842
|
+
const rank = (p2) => ({ high: 0, medium: 1, low: 2 })[p2.trim().toLowerCase()] ?? 1;
|
|
43843
|
+
const groupNum = (id) => Number(/^TSK-(\d+)/.exec(id)?.[1] ?? 9999);
|
|
43844
|
+
const tasks = parseTasks(todo).tasks.map((t2, i) => ({ t: t2, i })).sort((a, b2) => groupNum(a.t.id) - groupNum(b2.t.id) || rank(a.t.priority) - rank(b2.t.priority) || a.i - b2.i);
|
|
43845
|
+
for (const { t: t2 } of tasks) {
|
|
43846
|
+
if (skip.has(t2.id) || claimed.has(t2.id) || !approved.has(t2.id)) continue;
|
|
43847
|
+
const a = attempts[t2.id];
|
|
43848
|
+
if (a && (a.splitPending || a.count >= opts.maxAttempts)) continue;
|
|
43849
|
+
const qaOk = t2.depends.filter((d) => d.startsWith("QA-")).every((d) => {
|
|
43850
|
+
const q2 = qaRows.get(d);
|
|
43851
|
+
return !q2 || q2.answer.trim().length > 0 && approved.has(d);
|
|
43852
|
+
});
|
|
43853
|
+
if (!qaOk) continue;
|
|
43854
|
+
const deps = t2.depends.filter((d) => d.startsWith("TSK-"));
|
|
43855
|
+
if (deps.some((d) => !doneMap.has(d))) continue;
|
|
43856
|
+
let from = null;
|
|
43857
|
+
if (deps.length === 1) {
|
|
43858
|
+
const dep = deps[0];
|
|
43859
|
+
const branch = doneBranch(doneMap.get(dep) ?? "") ?? taskBranch(rootBase.base, dep);
|
|
43860
|
+
if (!await isMerged(root, rootBase.url, branch, rootBase.base)) from = branch;
|
|
43861
|
+
} else if (deps.length >= 2) {
|
|
43862
|
+
let allMerged = true;
|
|
43863
|
+
for (const dep of deps) {
|
|
43864
|
+
const branch = doneBranch(doneMap.get(dep) ?? "") ?? taskBranch(rootBase.base, dep);
|
|
43865
|
+
if (!await isMerged(root, rootBase.url, branch, rootBase.base)) {
|
|
43866
|
+
allMerged = false;
|
|
43867
|
+
break;
|
|
43868
|
+
}
|
|
43869
|
+
}
|
|
43870
|
+
if (!allMerged) continue;
|
|
43871
|
+
}
|
|
43872
|
+
return { task: t2, takeover: null, from: fromHint(t2.notes) ?? from };
|
|
43873
|
+
}
|
|
43874
|
+
return null;
|
|
43875
|
+
}
|
|
43876
|
+
async function applyClaim(root, cand, mine, ttlHours) {
|
|
43877
|
+
const [todo, prog, done, attempts] = await Promise.all([
|
|
43878
|
+
readBook(root, "AI_TODO.md"),
|
|
43879
|
+
readBook(root, "AI_PROGRESS.md"),
|
|
43880
|
+
readBook(root, "AI_DONE.md"),
|
|
43881
|
+
readAttempts(root)
|
|
43882
|
+
]);
|
|
43883
|
+
const claims = parseClaims(prog).claims;
|
|
43884
|
+
const existing = claims.find((c) => c.id === cand.task.id);
|
|
43885
|
+
let incident = null;
|
|
43886
|
+
if (cand.takeover) {
|
|
43887
|
+
if (!existing || existing.claim !== cand.takeover.claim || claimAgeHours(existing) < ttlHours) return false;
|
|
43888
|
+
incident = `Claim of ${cand.task.id} by ${existing.worker} expired (${ttlHours} h) \u2014 taken over by ${mine.worker}.`;
|
|
43889
|
+
attempts[cand.task.id] = { ...attempts[cand.task.id] ?? {}, count: (attempts[cand.task.id]?.count ?? 0) + 1, last: "claim expired (taken over)", at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
43890
|
+
} else {
|
|
43891
|
+
if (existing) return false;
|
|
43892
|
+
const { tasks } = parseTasks(todo);
|
|
43893
|
+
if (!tasks.some((t2) => t2.id === cand.task.id)) return false;
|
|
43894
|
+
await writeBook(root, "AI_TODO.md", writeTasks(todo, tasks.filter((t2) => t2.id !== cand.task.id)));
|
|
43895
|
+
attempts[cand.task.id] = { ...attempts[cand.task.id] ?? { count: 0, last: "", at: "" }, task: cand.task };
|
|
43896
|
+
}
|
|
43897
|
+
const row = { started: mine.started, id: cand.task.id, worker: mine.worker, claim: mine.claim, attempt: mine.attempt, desc: cand.task.desc };
|
|
43898
|
+
await writeBook(root, "AI_PROGRESS.md", writeClaims(prog, [...claims.filter((c) => c.id !== cand.task.id), row]));
|
|
43899
|
+
if (cand.takeover) attempts[cand.task.id] = { ...attempts[cand.task.id], task: attempts[cand.task.id]?.task ?? cand.task };
|
|
43900
|
+
await writeAttempts(root, attempts);
|
|
43901
|
+
if (incident) await writeBook(root, "AI_DONE.md", appendIncident(done, incident));
|
|
43902
|
+
return true;
|
|
43903
|
+
}
|
|
43904
|
+
async function dropClaim(root, mine) {
|
|
43905
|
+
const prog = await readBook(root, "AI_PROGRESS.md");
|
|
43906
|
+
const claims = parseClaims(prog).claims;
|
|
43907
|
+
const row = claims.find((c) => c.id === mine.id);
|
|
43908
|
+
if (!row || row.claim !== mine.claim) return false;
|
|
43909
|
+
await writeBook(root, "AI_PROGRESS.md", writeClaims(prog, claims.filter((c) => c.id !== mine.id)));
|
|
43910
|
+
return true;
|
|
43911
|
+
}
|
|
43912
|
+
async function applyFinish(root, mine, done) {
|
|
43913
|
+
if (!await dropClaim(root, mine)) return false;
|
|
43914
|
+
await writeBook(root, "AI_DONE.md", appendDone(await readBook(root, "AI_DONE.md"), { id: mine.id, desc: mine.task.desc, files: done.files, notes: done.notes }));
|
|
43915
|
+
const attempts = await readAttempts(root);
|
|
43916
|
+
delete attempts[mine.id];
|
|
43917
|
+
await writeAttempts(root, attempts);
|
|
43918
|
+
return true;
|
|
43919
|
+
}
|
|
43920
|
+
async function applyRelease(root, mine, reason, maxAttempts) {
|
|
43921
|
+
if (!await dropClaim(root, mine)) return null;
|
|
43922
|
+
const attempts = await readAttempts(root);
|
|
43923
|
+
const count = (attempts[mine.id]?.count ?? 0) + 1;
|
|
43924
|
+
attempts[mine.id] = { ...attempts[mine.id] ?? {}, count, last: reason.slice(0, 300), at: (/* @__PURE__ */ new Date()).toISOString(), ...count >= maxAttempts ? { splitPending: true } : {} };
|
|
43925
|
+
await writeAttempts(root, attempts);
|
|
43926
|
+
const todo = await readBook(root, "AI_TODO.md");
|
|
43927
|
+
const { tasks } = parseTasks(todo);
|
|
43928
|
+
const back = { ...mine.task, notes: appendNote(mine.task.notes, `attempt ${count} ended: ${reason.slice(0, 160)}; WIP on ${mine.branch}`) };
|
|
43929
|
+
await writeBook(root, "AI_TODO.md", writeTasks(todo, [...tasks.filter((t2) => t2.id !== mine.id), back]));
|
|
43930
|
+
await writeBook(root, "AI_DONE.md", appendIncident(await readBook(root, "AI_DONE.md"), `${mine.id} attempt ${count} by ${mine.worker}: ${reason.slice(0, 200)} \u2014 returned to AI_TODO (WIP pushed on ${mine.branch}).`));
|
|
43931
|
+
return count;
|
|
43932
|
+
}
|
|
43933
|
+
function nextId(prefix, group, texts) {
|
|
43934
|
+
const re2 = new RegExp(`${prefix}-${group}-(\\d+)`, "g");
|
|
43935
|
+
let max = 0;
|
|
43936
|
+
for (const t2 of texts) for (const m2 of t2.matchAll(re2)) max = Math.max(max, Number(m2[1]));
|
|
43937
|
+
return `${prefix}-${group}-${String(max + 1).padStart(4, "0")}`;
|
|
43938
|
+
}
|
|
43939
|
+
function appendNote(notes, add) {
|
|
43940
|
+
return notes.trim() ? `${notes.trim()}; ${add}` : add;
|
|
43941
|
+
}
|
|
43942
|
+
async function applyQuestion(root, mine, question, resetAttempts = false) {
|
|
43943
|
+
if (!await dropClaim(root, mine)) return null;
|
|
43944
|
+
if (resetAttempts) {
|
|
43945
|
+
const attempts = await readAttempts(root);
|
|
43946
|
+
delete attempts[mine.id];
|
|
43947
|
+
await writeAttempts(root, attempts);
|
|
43948
|
+
}
|
|
43949
|
+
const [todo, qa, done, attemptsText] = await Promise.all([
|
|
43950
|
+
readBook(root, "AI_TODO.md"),
|
|
43951
|
+
readBook(root, "USER_QA.md"),
|
|
43952
|
+
readBook(root, "AI_DONE.md"),
|
|
43953
|
+
readBook(root, ATTEMPTS_REL)
|
|
43954
|
+
]);
|
|
43955
|
+
const g2 = /^TSK-(\d+)/.exec(mine.id)?.[1] ?? "0000";
|
|
43956
|
+
const qaId = nextId("QA", g2, [qa, todo, done, attemptsText]);
|
|
43957
|
+
const { rows } = parseQa(qa);
|
|
43958
|
+
const qaDeps = mine.task.depends.filter((d) => d.startsWith("QA-")).join(", ");
|
|
43959
|
+
await writeBook(root, "USER_QA.md", writeQa(qa, [...rows, { id: qaId, group: mine.task.group, depends: qaDeps, original: `${mine.id}: ${mine.task.desc}`, question, answer: "" }]));
|
|
43960
|
+
const { tasks } = parseTasks(todo);
|
|
43961
|
+
const back = { ...mine.task, depends: [...mine.task.depends.filter((d) => d !== qaId), qaId], notes: appendNote(mine.task.notes, `waiting for ${qaId}; WIP on ${mine.branch}`) };
|
|
43962
|
+
await writeBook(root, "AI_TODO.md", writeTasks(todo, [...tasks.filter((t2) => t2.id !== mine.id), back]));
|
|
43963
|
+
return qaId;
|
|
43964
|
+
}
|
|
43965
|
+
async function foldTaskAnswers(root) {
|
|
43966
|
+
const [todo, qa, approved] = await Promise.all([readBook(root, "AI_TODO.md"), readBook(root, "USER_QA.md"), approvedIds(root)]);
|
|
43967
|
+
const { tasks } = parseTasks(todo);
|
|
43968
|
+
const { rows } = parseQa(qa);
|
|
43969
|
+
const answered = rows.filter((q2) => q2.answer.trim() && approved.has(q2.id) && tasks.some((t2) => t2.depends.includes(q2.id)));
|
|
43970
|
+
if (answered.length === 0) return false;
|
|
43971
|
+
const byId = new Map(answered.map((q2) => [q2.id, q2]));
|
|
43972
|
+
const next = tasks.map((t2) => {
|
|
43973
|
+
const add = t2.depends.filter((d) => byId.has(d)).map((d) => `answer ${d}: ${byId.get(d).answer}`);
|
|
43974
|
+
return add.length ? { ...t2, notes: appendNote(t2.notes, add.join("; ")) } : t2;
|
|
43975
|
+
});
|
|
43976
|
+
await writeBook(root, "AI_TODO.md", writeTasks(todo, next));
|
|
43977
|
+
await writeBook(root, "USER_QA.md", writeQa(qa, rows.filter((q2) => !byId.has(q2.id))));
|
|
43978
|
+
return true;
|
|
43979
|
+
}
|
|
43980
|
+
async function hasIntakeWork(root) {
|
|
43981
|
+
const [userTodo, todo, qa, approved] = await Promise.all([
|
|
43982
|
+
readBook(root, "USER_TODO.md"),
|
|
43983
|
+
readBook(root, "AI_TODO.md"),
|
|
43984
|
+
readBook(root, "USER_QA.md"),
|
|
43985
|
+
approvedIds(root)
|
|
43986
|
+
]);
|
|
43987
|
+
if ([...approved].some((id) => id.startsWith("REQ-") && userTodo.includes(id))) return true;
|
|
43988
|
+
const taskDeps = new Set(parseTasks(todo).tasks.flatMap((t2) => t2.depends));
|
|
43989
|
+
return parseQa(qa).rows.some((q2) => !taskDeps.has(q2.id) && q2.answer.trim() && approved.has(q2.id));
|
|
43990
|
+
}
|
|
43991
|
+
async function applySplit(root, mine, children) {
|
|
43992
|
+
if (!await dropClaim(root, mine)) return null;
|
|
43993
|
+
const [todo, done, prog] = await Promise.all([readBook(root, "AI_TODO.md"), readBook(root, "AI_DONE.md"), readBook(root, "AI_PROGRESS.md")]);
|
|
43994
|
+
const g2 = /^TSK-(\d+)/.exec(mine.id)?.[1] ?? "0000";
|
|
43995
|
+
const texts = [todo, done, prog, mine.id];
|
|
43996
|
+
const ids = [];
|
|
43997
|
+
const rows = children.map((c, i) => {
|
|
43998
|
+
const id = nextId("TSK", g2, [...texts, ...ids]);
|
|
43999
|
+
ids.push(id);
|
|
44000
|
+
const deps = i === 0 ? mine.task.depends : [ids[i - 1]];
|
|
44001
|
+
const notes = [c.size ? `size: ${c.size}` : "", c.notes ?? "", `split from ${mine.id}`, i === 0 ? `from: ${mine.branch}` : ""].filter(Boolean).join("; ");
|
|
44002
|
+
return { id, priority: c.priority || mine.task.priority || "Medium", tag: "", depends: deps, group: mine.task.group, desc: c.desc, notes };
|
|
44003
|
+
});
|
|
44004
|
+
const { tasks } = parseTasks(todo);
|
|
44005
|
+
await writeBook(root, "AI_TODO.md", writeTasks(todo, [...tasks, ...rows]));
|
|
44006
|
+
await writeBook(root, "AI_DONE.md", appendDone(done, { id: mine.id, desc: mine.task.desc, files: "", notes: `split into ${ids.join(", ")} (after repeated attempts); WIP on ${mine.branch}` }));
|
|
44007
|
+
const attempts = await readAttempts(root);
|
|
44008
|
+
delete attempts[mine.id];
|
|
44009
|
+
await writeAttempts(root, attempts);
|
|
44010
|
+
return ids;
|
|
44011
|
+
}
|
|
44012
|
+
function isSplitChild(task) {
|
|
44013
|
+
return /split from TSK-/.test(task.notes);
|
|
44014
|
+
}
|
|
44015
|
+
|
|
44016
|
+
// src/core/autonomous-cycle.ts
|
|
44017
|
+
var BOOKS_INTRO = `The five "book" files at the project root \u2014 \`USER_TODO.md\`, \`USER_QA.md\`, \`AI_TODO.md\`,
|
|
42818
44018
|
\`AI_PROGRESS.md\`, \`AI_DONE.md\` \u2014 have canonical templates in \`.claude/templates/\` (\`<NAME>.empty.md\`
|
|
42819
|
-
= EMPTY, \`<NAME>.sample.md\` = example WITH DATA).
|
|
42820
|
-
|
|
42821
|
-
|
|
42822
|
-
|
|
42823
|
-
|
|
42824
|
-
|
|
42825
|
-
|
|
42826
|
-
|
|
42827
|
-
|
|
42828
|
-
|
|
42829
|
-
|
|
42830
|
-
|
|
42831
|
-
|
|
42832
|
-
|
|
42833
|
-
|
|
42834
|
-
|
|
42835
|
-
|
|
42836
|
-
|
|
42837
|
-
|
|
42838
|
-
|
|
42839
|
-
-
|
|
42840
|
-
|
|
42841
|
-
|
|
42842
|
-
|
|
42843
|
-
|
|
42844
|
-
|
|
42845
|
-
|
|
42846
|
-
|
|
42847
|
-
|
|
42848
|
-
|
|
42849
|
-
|
|
42850
|
-
|
|
42851
|
-
|
|
42852
|
-
|
|
42853
|
-
|
|
42854
|
-
|
|
42855
|
-
|
|
42856
|
-
|
|
42857
|
-
|
|
42858
|
-
|
|
42859
|
-
|
|
42860
|
-
|
|
42861
|
-
|
|
42862
|
-
|
|
42863
|
-
|
|
42864
|
-
|
|
42865
|
-
|
|
42866
|
-
|
|
42867
|
-
|
|
42868
|
-
|
|
42869
|
-
|
|
42870
|
-
|
|
42871
|
-
|
|
42872
|
-
|
|
42873
|
-
|
|
42874
|
-
|
|
42875
|
-
|
|
42876
|
-
|
|
42877
|
-
|
|
42878
|
-
|
|
42879
|
-
|
|
42880
|
-
|
|
42881
|
-
|
|
42882
|
-
|
|
42883
|
-
|
|
42884
|
-
|
|
42885
|
-
|
|
42886
|
-
|
|
42887
|
-
|
|
42888
|
-
|
|
42889
|
-
|
|
42890
|
-
|
|
42891
|
-
|
|
44019
|
+
= EMPTY, \`<NAME>.sample.md\` = example WITH DATA). When clearing a book, overwrite it with EXACTLY the
|
|
44020
|
+
empty template. The single source of truth for approval is \`.claude/.autonomous.approvals.json\`
|
|
44021
|
+
(\`{ "<ID>": { "approved": true, \u2026 } }\`); a row is APPROVED only when its id has \`"approved": true\`.
|
|
44022
|
+
NEVER act on an unapproved row.`;
|
|
44023
|
+
function buildIntakePrompt(cfg) {
|
|
44024
|
+
const h = cfg.taskSizeHints;
|
|
44025
|
+
return `You are the 4PM autonomous INTAKE step (headless). Edit ONLY \`USER_TODO.md\`, \`USER_QA.md\` and
|
|
44026
|
+
\`AI_TODO.md\`. Do NOT run git, do NOT touch \`AI_PROGRESS.md\` / \`AI_DONE.md\` / any code \u2014 the 4PM cli
|
|
44027
|
+
commits and publishes your book edits. Write in the language the project's \`CLAUDE.md\` specifies (default English).
|
|
44028
|
+
|
|
44029
|
+
${BOOKS_INTRO}
|
|
44030
|
+
|
|
44031
|
+
## 1 \u2014 Approved user requests \u2192 tasks
|
|
44032
|
+
Read \`USER_TODO.md\` (columns \`ID | Group | Depends | Request\`). For each APPROVED \`REQ-\u2026\` whose \`Depends\`
|
|
44033
|
+
are already handled:
|
|
44034
|
+
- CLEAR request \u21D2 split it into tasks appended to \`AI_TODO.md\` (columns
|
|
44035
|
+
\`ID | Priority | Tag | Depends | Group | Task description | Notes\`), ids \`TSK-{groupid:0000}-{taskid:0000}\`
|
|
44036
|
+
(groupid UNIQUE + INCREASING across all history \u2014 the largest ever used in \`AI_TODO.md\`/\`AI_DONE.md\`/
|
|
44037
|
+
\`AI_PROGRESS.md\`/their git history, +1; taskid from 0001). \`Depends\` = the \`TSK-\u2026\` it needs.
|
|
44038
|
+
Remove the analysed request row (empty \u21D2 reset to the empty template).
|
|
44039
|
+
- UNCLEAR request \u21D2 do NOT guess: append a \`USER_QA.md\` row (\`ID | Group | Depends | Original request |
|
|
44040
|
+
Question / options | Answer\`) with a new \`QA-{groupid:0000}-{qaid:0000}\`, blank \`Answer\`; remove the request row.
|
|
44041
|
+
An unapproved request stays.
|
|
44042
|
+
|
|
44043
|
+
## 2 \u2014 Answered intake questions
|
|
44044
|
+
For each \`USER_QA.md\` row with an \`Answer\`, APPROVED, and whose id is NOT in any \`AI_TODO.md\` \`Depends\`
|
|
44045
|
+
(those are task questions \u2014 the cli handles them; leave them alone): re-analyse the original request with the
|
|
44046
|
+
answer into tasks (same rules), then remove that QA row.
|
|
44047
|
+
|
|
44048
|
+
## Task size (MANDATORY \u2014 every task must be finishable in ONE run)
|
|
44049
|
+
Size each task by SCOPE and write \`size: S\` or \`size: M\` at the start of \`Notes\`:
|
|
44050
|
+
- **S** \u2014 one small change in one module/layer (\u2248 \u2264 ${h.sMaxFiles} files, \u2248 \u2264 ${h.sMaxLines} lines), e.g. fix a bug, add a field.
|
|
44051
|
+
- **M** \u2014 one feature slice across 2\u20133 layers with ONE purpose (\u2248 \u2264 ${h.mMaxFiles} files, \u2248 \u2264 ${h.mMaxLines} lines), e.g. one CRUD screen, one API + its UI + tests.
|
|
44052
|
+
- **L** \u2014 several features, a cross-cutting change, a big migration, a new subsystem, or an architecture decision.
|
|
44053
|
+
**Never write an L task**: split it into S/M tasks chained with \`Depends\`, or ask in \`USER_QA.md\` when the split needs a decision.
|
|
44054
|
+
The counts are hints, not hard limits. Each task's \`Task description\` states the acceptance criteria and how to test it.
|
|
44055
|
+
|
|
44056
|
+
When done, reply with one short line summarising what you added.`;
|
|
44057
|
+
}
|
|
44058
|
+
function buildImplementPrompt(mine, subs, resumed) {
|
|
44059
|
+
const subLines = subs.length ? subs.map((s3) => `- submodule \`${s3.dir}\` is on branch \`${s3.branch}\` \u2014 commit changes to it INSIDE \`${s3.dir}\``).join("\n") : "- (no submodules)";
|
|
44060
|
+
return `You are the 4PM autonomous IMPLEMENT step (headless). Implement exactly ONE task, then STOP.
|
|
44061
|
+
|
|
44062
|
+
Task ${mine.id} (group ${mine.task.group}, attempt ${mine.attempt}):
|
|
44063
|
+
${mine.task.desc}
|
|
44064
|
+
Notes: ${mine.task.notes || "(none)"}
|
|
44065
|
+
${resumed ? "\nThis is a CONTINUATION: earlier work for this task is already committed on the branches below \u2014 review it (git log / git diff against the base) and continue from it; do not start over.\n" : ""}
|
|
44066
|
+
Branches (already checked out by the 4PM cli \u2014 stay on them):
|
|
44067
|
+
- project root is on \`${mine.branch}\`
|
|
44068
|
+
${subLines}
|
|
44069
|
+
|
|
44070
|
+
Rules:
|
|
44071
|
+
- Follow the project's architecture and \`CLAUDE.md\`. Add/update tests; run the project's test command and WAIT for the result.
|
|
44072
|
+
- Commit your work (\`git add\` + \`git commit\`) in the root and/or inside each submodule you changed.
|
|
44073
|
+
- Do NOT push, do NOT open pull requests, do NOT switch/create branches, do NOT edit the book files
|
|
44074
|
+
(USER_TODO/USER_QA/AI_TODO/AI_PROGRESS/AI_DONE) \u2014 the 4PM cli delivers and records everything.
|
|
44075
|
+
- If you need a human DECISION to continue (ambiguous requirement, missing information, a choice with real
|
|
44076
|
+
trade-offs), do NOT guess: commit what you have and report \`needs-input\` with the question + options.
|
|
44077
|
+
|
|
44078
|
+
Finish with ONLY one fenced \`json\` block:
|
|
44079
|
+
{"status": "done" | "needs-input" | "failed", "summary": "<what you did / why it failed>", "question": "<only for needs-input: the question and the options>"}`;
|
|
44080
|
+
}
|
|
44081
|
+
function buildSplitPrompt(mine, reasons, diff2) {
|
|
44082
|
+
return `You are the 4PM autonomous SPLIT step (read-only \u2014 do not change any file). Task ${mine.id} failed to
|
|
44083
|
+
finish after repeated attempts, so it is probably too big for one run. Split it into 2\u20136 smaller child tasks
|
|
44084
|
+
that are each finishable in ONE run (size S or M), in execution order (each depends on the previous).
|
|
44085
|
+
|
|
44086
|
+
Task: ${mine.task.desc}
|
|
44087
|
+
Notes: ${mine.task.notes || "(none)"}
|
|
44088
|
+
Why the attempts ended: ${reasons || "(unknown)"}
|
|
44089
|
+
Work already done on its WIP branch \`${mine.branch}\` (diff against the base, may be truncated):
|
|
44090
|
+
\`\`\`
|
|
44091
|
+
${diff2 || "(no changes yet)"}
|
|
44092
|
+
\`\`\`
|
|
44093
|
+
The first child continues from that WIP branch: note which parts are already done so they are not redone.
|
|
44094
|
+
Each child description must state its acceptance criteria and how to test it.
|
|
44095
|
+
|
|
44096
|
+
Reply with ONLY one fenced \`json\` block:
|
|
44097
|
+
{"children": [{"desc": "<task + acceptance criteria>", "priority": "High|Medium|Low", "size": "S|M", "notes": "<optional>"}]}`;
|
|
44098
|
+
}
|
|
44099
|
+
function parseJsonReply(text) {
|
|
44100
|
+
const fenced = [...text.matchAll(/```json\s*([\s\S]*?)```/gi)].map((m2) => m2[1].trim());
|
|
44101
|
+
const candidates = fenced.length ? fenced.reverse() : [text.slice(text.lastIndexOf("{"), text.lastIndexOf("}") + 1)];
|
|
44102
|
+
for (const c of candidates) {
|
|
44103
|
+
try {
|
|
44104
|
+
return JSON.parse(c);
|
|
44105
|
+
} catch {
|
|
44106
|
+
}
|
|
44107
|
+
}
|
|
44108
|
+
return null;
|
|
44109
|
+
}
|
|
44110
|
+
var watch = {
|
|
44111
|
+
commandId: null,
|
|
44112
|
+
claim: null,
|
|
44113
|
+
root: null,
|
|
44114
|
+
lost: false
|
|
44115
|
+
};
|
|
44116
|
+
async function verifyHeldClaim(ctx) {
|
|
44117
|
+
const { claim, root, commandId } = watch;
|
|
44118
|
+
if (!claim || !root || !commandId || watch.lost) return;
|
|
44119
|
+
const prog = await remoteBook(root, claim.base, "AI_PROGRESS.md");
|
|
44120
|
+
if (!prog) return;
|
|
44121
|
+
const row = parseClaims(prog).claims.find((c) => c.id === claim.id);
|
|
44122
|
+
if (!row || row.claim !== claim.claim) {
|
|
44123
|
+
watch.lost = true;
|
|
44124
|
+
ctx.bus.log(`autonomous: claim of ${claim.id} was taken over \u2014 stopping this run`, "warn");
|
|
44125
|
+
cancelActiveRun(commandId);
|
|
44126
|
+
}
|
|
44127
|
+
}
|
|
44128
|
+
function onReconnectVerifyClaim(ctx) {
|
|
44129
|
+
void verifyHeldClaim(ctx).catch(() => void 0);
|
|
44130
|
+
}
|
|
44131
|
+
function alert(ctx, payload) {
|
|
44132
|
+
try {
|
|
44133
|
+
ctx.send(WsChannels.AUTONOMOUS_ALERT, payload);
|
|
44134
|
+
} catch {
|
|
44135
|
+
}
|
|
44136
|
+
}
|
|
44137
|
+
function subBranches(subs, taskId, rootFrom) {
|
|
44138
|
+
const fromTask = rootFrom ? rootFrom.split("/").pop() ?? null : null;
|
|
44139
|
+
return subs.filter((s3) => s3.base).map((s3) => ({ sub: s3, branch: taskBranch(s3.base, taskId), from: fromTask ? taskBranch(s3.base, fromTask) : null }));
|
|
44140
|
+
}
|
|
44141
|
+
async function pushWip(root, mine, subs, note) {
|
|
44142
|
+
for (const s3 of subs) {
|
|
44143
|
+
const abs = join24(root, s3.sub.dir);
|
|
44144
|
+
if (await currentBranch(abs) !== s3.branch) continue;
|
|
44145
|
+
await wipCommit(abs, `wip(${mine.id}): ${note}`);
|
|
44146
|
+
if (await aheadOf(abs, s3.branch, s3.sub.base) > 0) await pushBranch(abs, s3.branch).catch(() => void 0);
|
|
44147
|
+
}
|
|
44148
|
+
if (await currentBranch(root) === mine.branch) {
|
|
44149
|
+
await wipCommit(root, `wip(${mine.id}): ${note}`);
|
|
44150
|
+
await pushBranch(root, mine.branch).catch(() => void 0);
|
|
44151
|
+
}
|
|
44152
|
+
}
|
|
44153
|
+
async function wipDiff(root, mine) {
|
|
44154
|
+
const stat6 = await gitQuiet(root, ["diff", "--stat", `origin/${mine.base}...${mine.branch}`]);
|
|
44155
|
+
const patch = await gitQuiet(root, ["diff", `origin/${mine.base}...${mine.branch}`]);
|
|
44156
|
+
return `${stat6}
|
|
44157
|
+
${patch}`.slice(0, 2e4);
|
|
42892
44158
|
}
|
|
42893
44159
|
var running = /* @__PURE__ */ new Set();
|
|
42894
44160
|
async function runAutonomousCycle(ctx) {
|
|
@@ -42908,6 +44174,7 @@ async function runAutonomousCycle(ctx) {
|
|
|
42908
44174
|
const cfg = await readAutonomousConfig(profileDir2);
|
|
42909
44175
|
await pruneLogs(root, cfg.logRetentionDays);
|
|
42910
44176
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
44177
|
+
const worker = ctx.machineUsername || "worker";
|
|
42911
44178
|
if (!ctx.isReady) {
|
|
42912
44179
|
await log("[skip] cli offline (no WS connection)");
|
|
42913
44180
|
ctx.bus.autonomousDone(false, "cli offline");
|
|
@@ -42929,13 +44196,53 @@ async function runAutonomousCycle(ctx) {
|
|
|
42929
44196
|
ctx.bus.autonomousDone(false, "max ticks reached");
|
|
42930
44197
|
return;
|
|
42931
44198
|
}
|
|
42932
|
-
|
|
42933
|
-
|
|
44199
|
+
const bases = await resolveBases(root);
|
|
44200
|
+
const rootBase = bases.root;
|
|
44201
|
+
if (!rootBase.base) {
|
|
44202
|
+
await log("[skip] base branch unknown (no spec branch, no origin/HEAD)");
|
|
44203
|
+
ctx.bus.autonomousDone(false, "base branch unknown");
|
|
44204
|
+
return;
|
|
44205
|
+
}
|
|
44206
|
+
for (const r of [rootBase, ...bases.subs]) {
|
|
44207
|
+
if (!r.base || !r.url) continue;
|
|
44208
|
+
if (await branchProtection(r.url, r.base) === true) {
|
|
44209
|
+
const repo = r.dir || "root";
|
|
44210
|
+
await log(`[stop] base branch "${r.base}" is protected in ${repo} \u2014 the books cannot be pushed`);
|
|
44211
|
+
alert(ctx, { kind: "base-protected", repo, branch: r.base, message: `The base branch "${r.base}" (${repo}) is protected; autonomous cannot record its tasks there.` });
|
|
44212
|
+
await recordRun(root, hist, "skip", `base-protected: ${r.base} (${repo})`, today);
|
|
44213
|
+
ctx.bus.autonomousDone(false, "base branch protected");
|
|
44214
|
+
return;
|
|
44215
|
+
}
|
|
44216
|
+
}
|
|
44217
|
+
await syncBase(root, rootBase.base);
|
|
44218
|
+
let mine = await readLocalClaim(profileDir2);
|
|
44219
|
+
if (mine && mine.base !== rootBase.base) mine = null;
|
|
44220
|
+
if (mine) {
|
|
44221
|
+
const state3 = await claimState(root, mine, cfg.claimTtlHours);
|
|
44222
|
+
if (state3 === "lost") {
|
|
44223
|
+
await log(`[lost] claim of ${mine.id} was taken over \u2014 dropping local work (kept on ${mine.branch} locally)`);
|
|
44224
|
+
await publishBooks(root, rootBase.base, `chore(auto): incident \u2014 ${mine.id} claim lost by ${worker}`, async () => {
|
|
44225
|
+
await writeBook(root, "AI_DONE.md", appendIncident(await readBook(root, "AI_DONE.md"), `${worker} lost its claim of ${mine.id} (taken over); its local work was not pushed.`));
|
|
44226
|
+
return true;
|
|
44227
|
+
});
|
|
44228
|
+
alert(ctx, { kind: "claim-lost", task: mine.id, message: `${worker} lost its claim of ${mine.id}.` });
|
|
44229
|
+
await writeLocalClaim(profileDir2, null);
|
|
44230
|
+
mine = null;
|
|
44231
|
+
} else if (state3 === "expired") {
|
|
44232
|
+
await log(`[release] claim of ${mine.id} reached ${cfg.claimTtlHours} h \u2014 pushing WIP and returning the task`);
|
|
44233
|
+
await checkoutTaskBranch(root, mine.branch, rootBase.base, null).catch(() => void 0);
|
|
44234
|
+
await releaseTask(ctx, root, rootBase, bases.subs, mine, `not finished within ${cfg.claimTtlHours} h`, cfg);
|
|
44235
|
+
ctx.bus.autonomousDone(false, "claim expired \u2014 task returned");
|
|
44236
|
+
return;
|
|
44237
|
+
}
|
|
44238
|
+
}
|
|
44239
|
+
if (!mine && !await hasWork(root)) {
|
|
44240
|
+
await log("[skip] no approved USER_TODO/USER_QA/AI_TODO work");
|
|
42934
44241
|
ctx.bus.autonomousDone(false, "no work");
|
|
42935
44242
|
return;
|
|
42936
44243
|
}
|
|
42937
44244
|
if (await allAiProfilesExhausted(ctx, cfg)) {
|
|
42938
|
-
await log(`[skip] all AI profiles over usage limit (\u2265 ${cfg.maxSessionPct}%/${cfg.maxWeeklyPct}%)`);
|
|
44245
|
+
await log(`[skip] all AI profiles over usage limit (\u2265 ${cfg.maxSessionPct}%/${cfg.maxWeeklyPct}%)${mine ? ` \u2014 ${mine.id} kept` : ""}`);
|
|
42939
44246
|
await recordRun(root, hist, "skip", "all AI profiles over usage limit", today);
|
|
42940
44247
|
ctx.bus.autonomousDone(false, "usage limit");
|
|
42941
44248
|
return;
|
|
@@ -42943,42 +44250,244 @@ async function runAutonomousCycle(ctx) {
|
|
|
42943
44250
|
hist.ticks = { day: today, count: todayTickCount(hist, today) + 1 };
|
|
42944
44251
|
await writeHistories(root, hist);
|
|
42945
44252
|
await log(`[run] cycle (tick ${hist.ticks.count}/${today})`);
|
|
42946
|
-
const commandId = randomUUID();
|
|
42947
44253
|
const override = cfg.model ? { model: cfg.model } : void 0;
|
|
42948
|
-
|
|
42949
|
-
|
|
42950
|
-
const
|
|
42951
|
-
|
|
42952
|
-
|
|
42953
|
-
|
|
42954
|
-
|
|
44254
|
+
let resumed = !!mine;
|
|
44255
|
+
if (!mine) {
|
|
44256
|
+
const folded = await publishBooks(root, rootBase.base, "chore(auto): fold answered task questions", () => foldTaskAnswers(root));
|
|
44257
|
+
if (folded === "pushed") await log("[fold] task-question answers folded into their tasks");
|
|
44258
|
+
const usage = await readBookUsage(ctx);
|
|
44259
|
+
const intakeBlocked = ["AI_TODO", "USER_QA"].some((b2) => {
|
|
44260
|
+
const c = usage?.books[b2];
|
|
44261
|
+
return !!c && c.limit !== null && c.used >= c.limit;
|
|
44262
|
+
});
|
|
44263
|
+
if (intakeBlocked) await log("[cap] monthly AI_TODO/USER_QA limit reached \u2014 intake skipped this cycle");
|
|
44264
|
+
else if (await hasIntakeWork(root)) {
|
|
44265
|
+
const before = await cappedBookIds(root);
|
|
44266
|
+
const out2 = await runAiPrompt(ctx, buildIntakePrompt(cfg), randomUUID(), "local", false, void 0, override, false, true);
|
|
44267
|
+
if (out2.exhausted) {
|
|
44268
|
+
await gitQuiet(root, ["checkout", "--", "."]);
|
|
44269
|
+
await log("[skip] usage limit during intake");
|
|
44270
|
+
ctx.bus.autonomousDone(false, "usage limit");
|
|
44271
|
+
return;
|
|
44272
|
+
}
|
|
44273
|
+
const res = await publishIntake(root, rootBase.base, "chore(auto): intake requests into tasks");
|
|
44274
|
+
await log(`[intake] ${res}`);
|
|
44275
|
+
if (res === "pushed") await reportNewBookRows(ctx, root, before);
|
|
44276
|
+
await syncBase(root, rootBase.base);
|
|
44277
|
+
}
|
|
44278
|
+
const skip = /* @__PURE__ */ new Set();
|
|
44279
|
+
for (let i = 0; i < 5 && !mine; i++) {
|
|
44280
|
+
const cand = await pickTask(root, rootBase, { worker, ttlHours: cfg.claimTtlHours, maxAttempts: cfg.maxTaskAttempts, skip });
|
|
44281
|
+
if (!cand) break;
|
|
44282
|
+
const attempts = await readAttempts(root);
|
|
44283
|
+
const claim = {
|
|
44284
|
+
id: cand.task.id,
|
|
44285
|
+
claim: randomUUID(),
|
|
44286
|
+
worker,
|
|
44287
|
+
started: stamp(),
|
|
44288
|
+
attempt: (attempts[cand.task.id]?.count ?? 0) + 1 + (cand.takeover ? 1 : 0),
|
|
44289
|
+
branch: taskBranch(rootBase.base, cand.task.id),
|
|
44290
|
+
base: rootBase.base,
|
|
44291
|
+
task: cand.task
|
|
44292
|
+
};
|
|
44293
|
+
const res = await publishBooks(
|
|
44294
|
+
root,
|
|
44295
|
+
rootBase.base,
|
|
44296
|
+
`chore(auto): claim ${cand.task.id} by ${worker}`,
|
|
44297
|
+
() => applyClaim(root, cand, claim, cfg.claimTtlHours)
|
|
44298
|
+
);
|
|
44299
|
+
if (res === "pushed") {
|
|
44300
|
+
mine = { ...claim, from: cand.from };
|
|
44301
|
+
await writeLocalClaim(profileDir2, mine);
|
|
44302
|
+
await log(`[claim] ${mine.id}${cand.takeover ? ` (taken over from ${cand.takeover.worker})` : ""} attempt ${mine.attempt}`);
|
|
44303
|
+
} else skip.add(cand.task.id);
|
|
44304
|
+
}
|
|
44305
|
+
if (!mine) {
|
|
44306
|
+
await log("[idle] no eligible task (waiting for approval / dependencies / answers)");
|
|
44307
|
+
hist = await readHistories(root);
|
|
44308
|
+
await recordRun(root, hist, "success", "no eligible task", today);
|
|
44309
|
+
ctx.bus.autonomousDone(true);
|
|
44310
|
+
return;
|
|
44311
|
+
}
|
|
44312
|
+
resumed = false;
|
|
44313
|
+
}
|
|
44314
|
+
const from = mine.from ?? null;
|
|
44315
|
+
await checkoutTaskBranch(root, mine.branch, rootBase.base, from);
|
|
44316
|
+
const hadWork = await aheadOf(root, mine.branch, rootBase.base) > 0;
|
|
44317
|
+
await gitQuiet(root, ["submodule", "update", "--init", "-q"], 3e5);
|
|
44318
|
+
const subs = subBranches(bases.subs, mine.id, from);
|
|
44319
|
+
for (const s3 of subs) await checkoutTaskBranch(join24(root, s3.sub.dir), s3.branch, s3.sub.base, s3.from).catch((e) => log(`[warn] submodule ${s3.sub.dir}: ${String(e)}`));
|
|
44320
|
+
const commandId = randomUUID();
|
|
44321
|
+
Object.assign(watch, { commandId, claim: mine, root, lost: false });
|
|
44322
|
+
const timer = setInterval(() => void verifyHeldClaim(ctx).catch(() => void 0), cfg.claimCheckMinutes * 6e4);
|
|
44323
|
+
timer.unref?.();
|
|
44324
|
+
let out;
|
|
42955
44325
|
try {
|
|
42956
|
-
await runAiPrompt(ctx,
|
|
42957
|
-
|
|
44326
|
+
out = await runAiPrompt(ctx, buildImplementPrompt(mine, subs.map((s3) => ({ dir: s3.sub.dir, branch: s3.branch })), resumed || hadWork), commandId, "local", false, void 0, override, false, true);
|
|
44327
|
+
} finally {
|
|
44328
|
+
clearInterval(timer);
|
|
44329
|
+
}
|
|
44330
|
+
const lost = watch.lost;
|
|
44331
|
+
Object.assign(watch, { commandId: null, claim: null, root: null, lost: false });
|
|
44332
|
+
if (await currentBranch(root) !== mine.branch) await gitQuiet(root, ["checkout", "-q", mine.branch]);
|
|
44333
|
+
if (lost) {
|
|
44334
|
+
await log(`[lost] ${mine.id} was taken over while running \u2014 stopped, nothing pushed`);
|
|
44335
|
+
alert(ctx, { kind: "claim-lost", task: mine.id, message: `${worker} lost its claim of ${mine.id} while working; its work was not pushed.` });
|
|
44336
|
+
await writeLocalClaim(profileDir2, null);
|
|
44337
|
+
await syncBase(root, rootBase.base);
|
|
44338
|
+
ctx.bus.autonomousDone(false, "claim lost");
|
|
44339
|
+
return;
|
|
44340
|
+
}
|
|
44341
|
+
if (out.exhausted) {
|
|
44342
|
+
for (const s3 of subs) await wipCommit(join24(root, s3.sub.dir), `wip(${mine.id}): out of tokens`);
|
|
44343
|
+
await wipCommit(root, `wip(${mine.id}): out of tokens`);
|
|
44344
|
+
await log(`[wait] ${mine.id}: usage limit \u2014 claim kept, resumes after the reset`);
|
|
44345
|
+
ctx.bus.autonomousDone(false, "usage limit \u2014 task kept");
|
|
44346
|
+
return;
|
|
44347
|
+
}
|
|
44348
|
+
const reply = parseJsonReply(out.output);
|
|
44349
|
+
const status = out.cancelled ? "failed" : out.exitCode !== 0 ? "failed" : reply?.status ?? "failed";
|
|
44350
|
+
if (status === "needs-input") {
|
|
44351
|
+
await pushWip(root, mine, subs, "waiting for an answer");
|
|
44352
|
+
await syncBase(root, rootBase.base);
|
|
44353
|
+
const question = reply?.question?.trim() || reply?.summary?.trim() || "The agent needs a decision to continue.";
|
|
44354
|
+
let qaId = null;
|
|
44355
|
+
await publishBooks(root, rootBase.base, `chore(auto): ${mine.id} asks a question`, async () => {
|
|
44356
|
+
qaId = await applyQuestion(root, mine, question);
|
|
44357
|
+
return qaId !== null;
|
|
44358
|
+
});
|
|
44359
|
+
await writeLocalClaim(profileDir2, null);
|
|
44360
|
+
await log(`[question] ${mine.id} parked on ${qaId ?? "?"}`);
|
|
44361
|
+
alert(ctx, { kind: "task-question", task: mine.id, message: `${mine.id} waits for an answer (${qaId ?? "QA"}): ${question.slice(0, 200)}` });
|
|
42958
44362
|
hist = await readHistories(root);
|
|
42959
|
-
hist.
|
|
42960
|
-
await recordRun(root, hist, "success", "cycle complete", today);
|
|
42961
|
-
await log("[done] cycle complete");
|
|
44363
|
+
await recordRun(root, hist, "success", `${mine.id} needs input`, today);
|
|
42962
44364
|
ctx.bus.autonomousDone(true);
|
|
42963
|
-
|
|
42964
|
-
|
|
42965
|
-
|
|
44365
|
+
return;
|
|
44366
|
+
}
|
|
44367
|
+
if (status !== "done") {
|
|
44368
|
+
const reason = reply?.summary?.trim() || (out.cancelled ? "run stopped" : `agent run failed (exit ${out.exitCode})`);
|
|
44369
|
+
await releaseTask(ctx, root, rootBase, bases.subs, mine, reason, cfg);
|
|
42966
44370
|
hist = await readHistories(root);
|
|
42967
44371
|
hist.consecutiveFails += 1;
|
|
42968
|
-
await recordRun(root, hist, "failure",
|
|
42969
|
-
await
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
|
|
42973
|
-
|
|
42974
|
-
|
|
44372
|
+
await recordRun(root, hist, "failure", `${mine.id}: ${reason}`, today);
|
|
44373
|
+
await maybeAutoPause(root, profileDir2, cfg, hist, log);
|
|
44374
|
+
ctx.bus.autonomousDone(false, reason);
|
|
44375
|
+
return;
|
|
44376
|
+
}
|
|
44377
|
+
if (await remoteClaimLost(root, mine)) {
|
|
44378
|
+
await log(`[lost] ${mine.id} was taken over before delivery \u2014 nothing pushed`);
|
|
44379
|
+
alert(ctx, { kind: "claim-lost", task: mine.id, message: `${worker} lost its claim of ${mine.id} before delivering.` });
|
|
44380
|
+
await writeLocalClaim(profileDir2, null);
|
|
44381
|
+
await syncBase(root, rootBase.base);
|
|
44382
|
+
ctx.bus.autonomousDone(false, "claim lost");
|
|
44383
|
+
return;
|
|
42975
44384
|
}
|
|
44385
|
+
let delivered;
|
|
44386
|
+
try {
|
|
44387
|
+
delivered = await deliver(root, rootBase, subs, mine, reply?.summary ?? "");
|
|
44388
|
+
} catch (err) {
|
|
44389
|
+
const reason = `delivery failed: ${String(err instanceof Error ? err.message : err)}`;
|
|
44390
|
+
await releaseTask(ctx, root, rootBase, bases.subs, mine, reason, cfg);
|
|
44391
|
+
ctx.bus.autonomousDone(false, reason);
|
|
44392
|
+
return;
|
|
44393
|
+
}
|
|
44394
|
+
await syncBase(root, rootBase.base);
|
|
44395
|
+
const fin = await publishBooks(root, rootBase.base, `chore(auto): finish ${mine.id}`, () => applyFinish(root, mine, delivered));
|
|
44396
|
+
await writeLocalClaim(profileDir2, null);
|
|
44397
|
+
await log(`[done] ${mine.id} \u2014 ${delivered.notes} (${fin})`);
|
|
44398
|
+
hist = await readHistories(root);
|
|
44399
|
+
hist.consecutiveFails = 0;
|
|
44400
|
+
await recordRun(root, hist, "success", `${mine.id} delivered`, today);
|
|
44401
|
+
ctx.bus.autonomousDone(true);
|
|
42976
44402
|
} catch (err) {
|
|
44403
|
+
await dayLog(root, `[warn] cycle error: ${err instanceof Error ? err.message : String(err)}`).catch(() => void 0);
|
|
42977
44404
|
ctx.bus.autonomousDone(false, err instanceof Error ? err.message : String(err));
|
|
42978
44405
|
} finally {
|
|
42979
44406
|
running.delete(root);
|
|
42980
44407
|
}
|
|
42981
44408
|
}
|
|
44409
|
+
async function remoteClaimLost(root, mine) {
|
|
44410
|
+
const prog = await remoteBook(root, mine.base, "AI_PROGRESS.md");
|
|
44411
|
+
if (!prog) return false;
|
|
44412
|
+
const row = parseClaims(prog).claims.find((c) => c.id === mine.id);
|
|
44413
|
+
return !row || row.claim !== mine.claim;
|
|
44414
|
+
}
|
|
44415
|
+
async function deliver(root, rootBase, subs, mine, summary) {
|
|
44416
|
+
const title = `${mine.id}: ${mine.task.desc}`.replace(/\s+/g, " ").slice(0, 100);
|
|
44417
|
+
const subPrs = [];
|
|
44418
|
+
for (const s3 of subs) {
|
|
44419
|
+
const abs = join24(root, s3.sub.dir);
|
|
44420
|
+
if (await currentBranch(abs) !== s3.branch) continue;
|
|
44421
|
+
await wipCommit(abs, `feat(${mine.id}): ${summary || mine.task.desc}`.slice(0, 200));
|
|
44422
|
+
if (await aheadOf(abs, s3.branch, s3.sub.base) === 0) continue;
|
|
44423
|
+
await pushBranch(abs, s3.branch);
|
|
44424
|
+
const pr3 = await deliverPr(abs, s3.sub.url, s3.branch, s3.sub.base, title, `${summary}
|
|
44425
|
+
|
|
44426
|
+
Task ${mine.id} (4PM autonomous).`);
|
|
44427
|
+
if (pr3.error) throw new Error(`submodule ${s3.sub.dir} PR: ${pr3.error}`);
|
|
44428
|
+
subPrs.push(`${s3.sub.dir}: ${pr3.url ?? s3.branch}`);
|
|
44429
|
+
}
|
|
44430
|
+
await wipCommit(root, `feat(${mine.id}): ${summary || mine.task.desc}`.slice(0, 200));
|
|
44431
|
+
const files = (await gitQuiet(root, ["diff", "--name-only", `origin/${rootBase.base}...${mine.branch}`])).split("\n").filter(Boolean);
|
|
44432
|
+
if (await aheadOf(root, mine.branch, rootBase.base) === 0) {
|
|
44433
|
+
return { files: "", notes: `no changes needed; branch: ${mine.branch}${subPrs.length ? `; sub PRs: ${subPrs.join(", ")}` : ""}` };
|
|
44434
|
+
}
|
|
44435
|
+
await pushBranch(root, mine.branch);
|
|
44436
|
+
const body = `${summary}
|
|
44437
|
+
|
|
44438
|
+
Task ${mine.id} (4PM autonomous).${subPrs.length ? `
|
|
44439
|
+
|
|
44440
|
+
Submodule PRs:
|
|
44441
|
+
${subPrs.map((p2) => `- ${p2}`).join("\n")}` : ""}`;
|
|
44442
|
+
const pr2 = await deliverPr(root, rootBase.url, mine.branch, rootBase.base, title, body);
|
|
44443
|
+
if (pr2.error) throw new Error(`PR: ${pr2.error}`);
|
|
44444
|
+
return {
|
|
44445
|
+
files: files.slice(0, 20).join(", ") + (files.length > 20 ? ` (+${files.length - 20})` : ""),
|
|
44446
|
+
notes: `branch: ${mine.branch}; PR: ${pr2.url ?? "opened"}${subPrs.length ? `; sub PRs: ${subPrs.join(", ")}` : ""}`
|
|
44447
|
+
};
|
|
44448
|
+
}
|
|
44449
|
+
async function releaseTask(ctx, root, rootBase, allSubs, mine, reason, cfg) {
|
|
44450
|
+
const subs = subBranches(allSubs, mine.id, null);
|
|
44451
|
+
await pushWip(root, mine, subs, reason.slice(0, 80));
|
|
44452
|
+
await syncBase(root, rootBase.base);
|
|
44453
|
+
const attempts = await readAttempts(root);
|
|
44454
|
+
const next = (attempts[mine.id]?.count ?? 0) + 1;
|
|
44455
|
+
const reasons = `${attempts[mine.id]?.last ?? ""}; ${reason}`;
|
|
44456
|
+
if (next >= cfg.maxTaskAttempts) {
|
|
44457
|
+
if (isSplitChild(mine.task)) {
|
|
44458
|
+
const q2 = `${mine.id} (a task already split from a bigger one) failed ${next} times \u2014 last: ${reason}. How should it be handled (narrow the scope, change the approach, or drop it)?`;
|
|
44459
|
+
await publishBooks(root, rootBase.base, `chore(auto): ${mine.id} needs a decision`, async () => await applyQuestion(root, mine, q2, true) !== null);
|
|
44460
|
+
alert(ctx, { kind: "task-failed-limit", task: mine.id, message: `${mine.id} failed ${next} times; a decision was requested in USER_QA.` });
|
|
44461
|
+
} else {
|
|
44462
|
+
const out = await runAiPrompt(ctx, buildSplitPrompt(mine, reasons, await wipDiff(root, mine)), randomUUID(), "local", false, void 0, cfg.model ? { model: cfg.model } : void 0, true, false);
|
|
44463
|
+
const children = parseJsonReply(out.output)?.children?.filter((c) => c?.desc?.trim()) ?? [];
|
|
44464
|
+
await syncBase(root, rootBase.base);
|
|
44465
|
+
if (children.length >= 2) {
|
|
44466
|
+
let ids = null;
|
|
44467
|
+
await publishBooks(root, rootBase.base, `chore(auto): split ${mine.id}`, async () => {
|
|
44468
|
+
ids = await applySplit(root, mine, children);
|
|
44469
|
+
return ids !== null;
|
|
44470
|
+
});
|
|
44471
|
+
alert(ctx, { kind: "task-split", task: mine.id, message: `${mine.id} was split into ${(ids ?? []).join(", ")} \u2014 approve the new tasks.` });
|
|
44472
|
+
} else {
|
|
44473
|
+
const q2 = `${mine.id} failed ${next} times and could not be split automatically \u2014 last: ${reason}. How should it be handled?`;
|
|
44474
|
+
await publishBooks(root, rootBase.base, `chore(auto): ${mine.id} needs a decision`, async () => await applyQuestion(root, mine, q2, true) !== null);
|
|
44475
|
+
alert(ctx, { kind: "task-failed-limit", task: mine.id, message: `${mine.id} failed ${next} times; a decision was requested in USER_QA.` });
|
|
44476
|
+
}
|
|
44477
|
+
}
|
|
44478
|
+
} else {
|
|
44479
|
+
await publishBooks(root, rootBase.base, `chore(auto): release ${mine.id} (attempt ${next})`, async () => await applyRelease(root, mine, reason, cfg.maxTaskAttempts) !== null);
|
|
44480
|
+
}
|
|
44481
|
+
await writeLocalClaim(ctx.profileDir, null);
|
|
44482
|
+
await dayLog(root, `[release] ${mine.id} attempt ${next}: ${reason}`);
|
|
44483
|
+
}
|
|
44484
|
+
async function maybeAutoPause(root, profileDir2, cfg, hist, log) {
|
|
44485
|
+
if (cfg.stopOnConsecutiveFailures > 0 && hist.consecutiveFails >= cfg.stopOnConsecutiveFailures) {
|
|
44486
|
+
await writeAutonomousConfig(profileDir2, { ...cfg, paused: true });
|
|
44487
|
+
await log(`[stop] ${hist.consecutiveFails} consecutive failures \u2192 paused`);
|
|
44488
|
+
}
|
|
44489
|
+
await writeHistories(root, hist);
|
|
44490
|
+
}
|
|
42982
44491
|
async function readBookUsage(ctx) {
|
|
42983
44492
|
try {
|
|
42984
44493
|
return await ctx.request(WsChannels.AUTONOMOUS_BOOK_USAGE, {});
|
|
@@ -43000,11 +44509,11 @@ async function reportNewBookRows(ctx, root, before) {
|
|
|
43000
44509
|
}
|
|
43001
44510
|
async function dayLog(root, line) {
|
|
43002
44511
|
try {
|
|
43003
|
-
const dir2 =
|
|
44512
|
+
const dir2 = join24(root, ".claude", "logs");
|
|
43004
44513
|
await mkdir3(dir2, { recursive: true });
|
|
43005
44514
|
const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
43006
44515
|
const ts2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
|
|
43007
|
-
await appendFile2(
|
|
44516
|
+
await appendFile2(join24(dir2, `autonomous-tick-${day}.log`), `${ts2} ${line}
|
|
43008
44517
|
`, "utf8");
|
|
43009
44518
|
} catch {
|
|
43010
44519
|
}
|
|
@@ -43012,12 +44521,12 @@ async function dayLog(root, line) {
|
|
|
43012
44521
|
async function pruneLogs(root, retentionDays) {
|
|
43013
44522
|
if (!(retentionDays > 0)) return;
|
|
43014
44523
|
try {
|
|
43015
|
-
const dir2 =
|
|
44524
|
+
const dir2 = join24(root, ".claude", "logs");
|
|
43016
44525
|
const cutoff = Date.now() - retentionDays * 864e5;
|
|
43017
|
-
for (const f2 of await
|
|
44526
|
+
for (const f2 of await readdir3(dir2)) {
|
|
43018
44527
|
if (!/^autonomous-tick-.*\.log$/.test(f2)) continue;
|
|
43019
|
-
const st2 = await stat2(
|
|
43020
|
-
if (st2 && st2.mtimeMs < cutoff) await
|
|
44528
|
+
const st2 = await stat2(join24(dir2, f2)).catch(() => null);
|
|
44529
|
+
if (st2 && st2.mtimeMs < cutoff) await rm3(join24(dir2, f2), { force: true }).catch(() => void 0);
|
|
43021
44530
|
}
|
|
43022
44531
|
} catch {
|
|
43023
44532
|
}
|
|
@@ -43029,15 +44538,15 @@ async function recordRun(root, hist, status, note, today) {
|
|
|
43029
44538
|
}
|
|
43030
44539
|
async function readJsonObject(path) {
|
|
43031
44540
|
try {
|
|
43032
|
-
return JSON.parse(await
|
|
44541
|
+
return JSON.parse(await readFile9(path, "utf8"));
|
|
43033
44542
|
} catch {
|
|
43034
44543
|
return {};
|
|
43035
44544
|
}
|
|
43036
44545
|
}
|
|
43037
44546
|
async function hasWork(root) {
|
|
43038
|
-
const approvals = await readJsonObject(
|
|
44547
|
+
const approvals = await readJsonObject(join24(root, ".claude/.autonomous.approvals.json"));
|
|
43039
44548
|
const approved = Object.entries(approvals).filter(([, v2]) => v2 && typeof v2 === "object" && v2.approved === true).map(([k2]) => k2);
|
|
43040
|
-
const read = (f2) =>
|
|
44549
|
+
const read = (f2) => readFile9(join24(root, f2), "utf8").catch(() => "");
|
|
43041
44550
|
const [userTodo, userQa, aiTodo, aiProgress] = await Promise.all([
|
|
43042
44551
|
read("USER_TODO.md"),
|
|
43043
44552
|
read("USER_QA.md"),
|
|
@@ -43074,7 +44583,7 @@ async function allAiProfilesExhausted(ctx, cfg) {
|
|
|
43074
44583
|
}
|
|
43075
44584
|
|
|
43076
44585
|
// src/core/fs-browse.ts
|
|
43077
|
-
import { readdir as
|
|
44586
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
43078
44587
|
import { resolve as resolve6, sep as sep3 } from "path";
|
|
43079
44588
|
async function listDir(path, root) {
|
|
43080
44589
|
if (!root) return { path: "", entries: [] };
|
|
@@ -43082,7 +44591,7 @@ async function listDir(path, root) {
|
|
|
43082
44591
|
const requested = resolve6(base, path && path.trim() ? path : ".");
|
|
43083
44592
|
const target = requested === base || requested.startsWith(base + sep3) ? requested : base;
|
|
43084
44593
|
try {
|
|
43085
|
-
const dirents = await
|
|
44594
|
+
const dirents = await readdir4(target, { withFileTypes: true });
|
|
43086
44595
|
const entries2 = dirents.map((d) => ({
|
|
43087
44596
|
name: d.name,
|
|
43088
44597
|
type: d.isDirectory() ? "dir" : "file"
|
|
@@ -43097,7 +44606,7 @@ async function listDir(path, root) {
|
|
|
43097
44606
|
}
|
|
43098
44607
|
|
|
43099
44608
|
// src/core/fs-read.ts
|
|
43100
|
-
import { readFile as
|
|
44609
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
43101
44610
|
import { resolve as resolve7, sep as sep4 } from "path";
|
|
43102
44611
|
var MAX_BYTES = 256 * 1024;
|
|
43103
44612
|
function resolveInRoot(root, relPath) {
|
|
@@ -43112,7 +44621,7 @@ async function readWorkerFile(root, path) {
|
|
|
43112
44621
|
return { path, content: "", truncated: false };
|
|
43113
44622
|
}
|
|
43114
44623
|
try {
|
|
43115
|
-
const buf = await
|
|
44624
|
+
const buf = await readFile10(target);
|
|
43116
44625
|
const truncated = buf.byteLength > MAX_BYTES;
|
|
43117
44626
|
const content = buf.subarray(0, MAX_BYTES).toString("utf8");
|
|
43118
44627
|
return { path: target, content, truncated };
|
|
@@ -43122,7 +44631,7 @@ async function readWorkerFile(root, path) {
|
|
|
43122
44631
|
}
|
|
43123
44632
|
|
|
43124
44633
|
// src/core/fs-write.ts
|
|
43125
|
-
import { mkdir as mkdir4, writeFile as
|
|
44634
|
+
import { mkdir as mkdir4, writeFile as writeFile7 } from "fs/promises";
|
|
43126
44635
|
import { dirname as dirname5, resolve as resolve8, sep as sep5 } from "path";
|
|
43127
44636
|
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
43128
44637
|
function resolveInRoot2(root, relPath) {
|
|
@@ -43142,7 +44651,7 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
43142
44651
|
}
|
|
43143
44652
|
try {
|
|
43144
44653
|
await mkdir4(dirname5(target), { recursive: true });
|
|
43145
|
-
await
|
|
44654
|
+
await writeFile7(target, content, "utf8");
|
|
43146
44655
|
return { ok: true, path: target, bytes };
|
|
43147
44656
|
} catch (err) {
|
|
43148
44657
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -43150,7 +44659,7 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
43150
44659
|
}
|
|
43151
44660
|
|
|
43152
44661
|
// src/core/fs-mutate.ts
|
|
43153
|
-
import { mkdir as mkdir5, writeFile as
|
|
44662
|
+
import { mkdir as mkdir5, writeFile as writeFile8, rename, rm as rm4, stat as stat3 } from "fs/promises";
|
|
43154
44663
|
import { basename as basename3, dirname as dirname6, resolve as resolve9, sep as sep6 } from "path";
|
|
43155
44664
|
var MAX_BYTES3 = 2 * 1024 * 1024;
|
|
43156
44665
|
function resolveInRoot3(root, rel) {
|
|
@@ -43193,7 +44702,7 @@ async function mutateFs(root, req) {
|
|
|
43193
44702
|
return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
|
|
43194
44703
|
}
|
|
43195
44704
|
await mkdir5(dirname6(target), { recursive: true });
|
|
43196
|
-
await
|
|
44705
|
+
await writeFile8(target, content, "utf8");
|
|
43197
44706
|
return { ok: true, path: target };
|
|
43198
44707
|
}
|
|
43199
44708
|
case "move": {
|
|
@@ -43212,7 +44721,7 @@ async function mutateFs(root, req) {
|
|
|
43212
44721
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
43213
44722
|
if (!target) return escaped(req.path ?? "");
|
|
43214
44723
|
if (target === resolve9(root)) return { ok: false, path: target, error: "cannot delete the project root" };
|
|
43215
|
-
await
|
|
44724
|
+
await rm4(target, { recursive: true, force: true });
|
|
43216
44725
|
return { ok: true, path: target };
|
|
43217
44726
|
}
|
|
43218
44727
|
default:
|
|
@@ -43224,7 +44733,7 @@ async function mutateFs(root, req) {
|
|
|
43224
44733
|
}
|
|
43225
44734
|
|
|
43226
44735
|
// src/core/fs-transfer.ts
|
|
43227
|
-
import { mkdir as mkdir6, readFile as
|
|
44736
|
+
import { mkdir as mkdir6, readFile as readFile11, stat as stat4, writeFile as writeFile9 } from "fs/promises";
|
|
43228
44737
|
import { basename as basename4, dirname as dirname7, extname, resolve as resolve10, sep as sep7 } from "path";
|
|
43229
44738
|
function resolveInRoot4(root, relPath) {
|
|
43230
44739
|
if (!root) return null;
|
|
@@ -43269,7 +44778,7 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
|
|
|
43269
44778
|
}
|
|
43270
44779
|
try {
|
|
43271
44780
|
await mkdir6(dirname7(target), { recursive: true });
|
|
43272
|
-
await
|
|
44781
|
+
await writeFile9(target, buf);
|
|
43273
44782
|
return { ok: true, path: target, bytes: buf.byteLength };
|
|
43274
44783
|
} catch (err) {
|
|
43275
44784
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -43284,7 +44793,7 @@ async function downloadBinaryFile(root, relPath) {
|
|
|
43284
44793
|
if (info.size > FS_TRANSFER_MAX_BYTES) {
|
|
43285
44794
|
return { error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
|
|
43286
44795
|
}
|
|
43287
|
-
const buf = await
|
|
44796
|
+
const buf = await readFile11(target);
|
|
43288
44797
|
return {
|
|
43289
44798
|
contentBase64: buf.toString("base64"),
|
|
43290
44799
|
contentType: guessMime(target),
|
|
@@ -43343,11 +44852,11 @@ function handleFsChannels(ctx, message, payload) {
|
|
|
43343
44852
|
}
|
|
43344
44853
|
|
|
43345
44854
|
// src/core/git-diff.ts
|
|
43346
|
-
import { readFile as
|
|
43347
|
-
import { execFile as
|
|
44855
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
44856
|
+
import { execFile as execFile9 } from "child_process";
|
|
43348
44857
|
import { basename as basename5, dirname as dirname8, resolve as resolve11, sep as sep8 } from "path";
|
|
43349
|
-
import { promisify as
|
|
43350
|
-
var
|
|
44858
|
+
import { promisify as promisify9 } from "util";
|
|
44859
|
+
var run9 = promisify9(execFile9);
|
|
43351
44860
|
function resolveInRoot5(root, relPath) {
|
|
43352
44861
|
if (!root) return null;
|
|
43353
44862
|
const base = resolve11(root);
|
|
@@ -43370,12 +44879,12 @@ async function gitDiff(root, path) {
|
|
|
43370
44879
|
const base = basename5(target);
|
|
43371
44880
|
let newBuf = null;
|
|
43372
44881
|
try {
|
|
43373
|
-
newBuf = await
|
|
44882
|
+
newBuf = await readFile12(target);
|
|
43374
44883
|
} catch {
|
|
43375
44884
|
}
|
|
43376
44885
|
let oldBuf = null;
|
|
43377
44886
|
try {
|
|
43378
|
-
const { stdout } = await
|
|
44887
|
+
const { stdout } = await run9("git", ["show", `HEAD:./${base}`], {
|
|
43379
44888
|
cwd: dir2,
|
|
43380
44889
|
timeout: 15e3,
|
|
43381
44890
|
maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
|
|
@@ -43405,12 +44914,12 @@ async function gitDiff(root, path) {
|
|
|
43405
44914
|
}
|
|
43406
44915
|
|
|
43407
44916
|
// src/core/git-env.ts
|
|
43408
|
-
import { execFile as
|
|
43409
|
-
import { promisify as
|
|
43410
|
-
var
|
|
44917
|
+
import { execFile as execFile10 } from "child_process";
|
|
44918
|
+
import { promisify as promisify10 } from "util";
|
|
44919
|
+
var run10 = promisify10(execFile10);
|
|
43411
44920
|
async function hasBinary(bin, args = ["--version"]) {
|
|
43412
44921
|
try {
|
|
43413
|
-
await
|
|
44922
|
+
await run10(bin, args, { timeout: 5e3 });
|
|
43414
44923
|
return true;
|
|
43415
44924
|
} catch {
|
|
43416
44925
|
return false;
|
|
@@ -43423,7 +44932,7 @@ async function checkGitEnv(provider) {
|
|
|
43423
44932
|
let account = null;
|
|
43424
44933
|
if (installed) {
|
|
43425
44934
|
try {
|
|
43426
|
-
const { stdout, stderr } = await
|
|
44935
|
+
const { stdout, stderr } = await run10(bin, ["auth", "status"], { timeout: 8e3 });
|
|
43427
44936
|
const out = `${stdout}
|
|
43428
44937
|
${stderr}`;
|
|
43429
44938
|
authenticated = /logged in|logged into|✓/i.test(out);
|
|
@@ -43436,33 +44945,33 @@ ${stderr}`;
|
|
|
43436
44945
|
}
|
|
43437
44946
|
|
|
43438
44947
|
// src/core/git-ssh-key.ts
|
|
43439
|
-
import { execFile as
|
|
43440
|
-
import { readFile as
|
|
43441
|
-
import { join as
|
|
43442
|
-
import { promisify as
|
|
43443
|
-
var
|
|
44948
|
+
import { execFile as execFile11 } from "child_process";
|
|
44949
|
+
import { readFile as readFile13, rm as rm5, mkdir as mkdir7 } from "fs/promises";
|
|
44950
|
+
import { join as join25 } from "path";
|
|
44951
|
+
import { promisify as promisify11 } from "util";
|
|
44952
|
+
var run11 = promisify11(execFile11);
|
|
43444
44953
|
function keyPaths(profileDir2) {
|
|
43445
|
-
const dir2 =
|
|
43446
|
-
const key =
|
|
44954
|
+
const dir2 = join25(profileDir2, ".ssh");
|
|
44955
|
+
const key = join25(dir2, "id_4pm");
|
|
43447
44956
|
return { dir: dir2, key, pub: `${key}.pub` };
|
|
43448
44957
|
}
|
|
43449
44958
|
async function readPublicKey(pub) {
|
|
43450
44959
|
try {
|
|
43451
|
-
return (await
|
|
44960
|
+
return (await readFile13(pub, "utf8")).trim();
|
|
43452
44961
|
} catch {
|
|
43453
44962
|
return null;
|
|
43454
44963
|
}
|
|
43455
44964
|
}
|
|
43456
44965
|
async function fingerprintOf(pub) {
|
|
43457
44966
|
try {
|
|
43458
|
-
const { stdout } = await
|
|
44967
|
+
const { stdout } = await run11("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
|
|
43459
44968
|
return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
|
|
43460
44969
|
} catch {
|
|
43461
44970
|
return null;
|
|
43462
44971
|
}
|
|
43463
44972
|
}
|
|
43464
44973
|
async function configureGit(key) {
|
|
43465
|
-
await
|
|
44974
|
+
await run11(
|
|
43466
44975
|
"git",
|
|
43467
44976
|
["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
|
|
43468
44977
|
{ timeout: 5e3 }
|
|
@@ -43471,9 +44980,9 @@ async function configureGit(key) {
|
|
|
43471
44980
|
async function generate(profileDir2) {
|
|
43472
44981
|
const { dir: dir2, key, pub } = keyPaths(profileDir2);
|
|
43473
44982
|
await mkdir7(dir2, { recursive: true });
|
|
43474
|
-
await
|
|
43475
|
-
await
|
|
43476
|
-
await
|
|
44983
|
+
await rm5(key, { force: true });
|
|
44984
|
+
await rm5(pub, { force: true });
|
|
44985
|
+
await run11("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
|
|
43477
44986
|
timeout: 15e3
|
|
43478
44987
|
});
|
|
43479
44988
|
await configureGit(key);
|
|
@@ -43481,14 +44990,14 @@ async function generate(profileDir2) {
|
|
|
43481
44990
|
}
|
|
43482
44991
|
async function remove(profileDir2) {
|
|
43483
44992
|
const { key, pub } = keyPaths(profileDir2);
|
|
43484
|
-
await
|
|
43485
|
-
await
|
|
43486
|
-
await
|
|
44993
|
+
await rm5(key, { force: true });
|
|
44994
|
+
await rm5(pub, { force: true });
|
|
44995
|
+
await run11("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
|
|
43487
44996
|
() => void 0
|
|
43488
44997
|
);
|
|
43489
44998
|
return { publicKey: null, fingerprint: null };
|
|
43490
44999
|
}
|
|
43491
|
-
async function
|
|
45000
|
+
async function get2(profileDir2) {
|
|
43492
45001
|
const { pub } = keyPaths(profileDir2);
|
|
43493
45002
|
const publicKey = await readPublicKey(pub);
|
|
43494
45003
|
return { publicKey, fingerprint: publicKey ? await fingerprintOf(pub) : null };
|
|
@@ -43497,7 +45006,7 @@ async function manageSshKey(op, profileDir2) {
|
|
|
43497
45006
|
try {
|
|
43498
45007
|
if (op === "generate") return await generate(profileDir2);
|
|
43499
45008
|
if (op === "delete") return await remove(profileDir2);
|
|
43500
|
-
return await
|
|
45009
|
+
return await get2(profileDir2);
|
|
43501
45010
|
} catch {
|
|
43502
45011
|
return { publicKey: null, fingerprint: null };
|
|
43503
45012
|
}
|
|
@@ -43555,12 +45064,12 @@ function handleGitChannels(ctx, message, payload) {
|
|
|
43555
45064
|
}
|
|
43556
45065
|
|
|
43557
45066
|
// src/core/ws-client/handlers/misc.ts
|
|
43558
|
-
import { join as
|
|
45067
|
+
import { join as join26 } from "path";
|
|
43559
45068
|
|
|
43560
45069
|
// src/core/ai-assist.ts
|
|
43561
|
-
import { execFile as
|
|
43562
|
-
import { promisify as
|
|
43563
|
-
var
|
|
45070
|
+
import { execFile as execFile12 } from "child_process";
|
|
45071
|
+
import { promisify as promisify12 } from "util";
|
|
45072
|
+
var run12 = promisify12(execFile12);
|
|
43564
45073
|
function aiCli() {
|
|
43565
45074
|
return process.env.AI_CLI || "claude";
|
|
43566
45075
|
}
|
|
@@ -43603,7 +45112,7 @@ function parseAi(cli, stdout, prompt) {
|
|
|
43603
45112
|
}
|
|
43604
45113
|
async function runAi(prompt) {
|
|
43605
45114
|
const cli = aiCli();
|
|
43606
|
-
const { stdout } = await
|
|
45115
|
+
const { stdout } = await run12(cli, aiArgs(cli, prompt), {
|
|
43607
45116
|
timeout: 12e4,
|
|
43608
45117
|
maxBuffer: 8 * 1024 * 1024
|
|
43609
45118
|
});
|
|
@@ -43705,7 +45214,7 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
43705
45214
|
}
|
|
43706
45215
|
case WsChannels.LOG_READ: {
|
|
43707
45216
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
43708
|
-
const lines = readRecentLogLines(
|
|
45217
|
+
const lines = readRecentLogLines(join26(ctx.profileDir, "logs"), limit);
|
|
43709
45218
|
ctx.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
43710
45219
|
return true;
|
|
43711
45220
|
}
|
|
@@ -43736,8 +45245,92 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
43736
45245
|
}
|
|
43737
45246
|
|
|
43738
45247
|
// src/core/ws-client/handlers/project.ts
|
|
43739
|
-
import { existsSync as
|
|
43740
|
-
import { join as
|
|
45248
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7, renameSync, rmSync as rmSync7 } from "fs";
|
|
45249
|
+
import { join as join28 } from "path";
|
|
45250
|
+
|
|
45251
|
+
// src/core/repo-probe.ts
|
|
45252
|
+
import { execFile as execFile13 } from "child_process";
|
|
45253
|
+
import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm6 } from "fs/promises";
|
|
45254
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
45255
|
+
import { join as join27 } from "path";
|
|
45256
|
+
import { promisify as promisify13 } from "util";
|
|
45257
|
+
var run13 = promisify13(execFile13);
|
|
45258
|
+
var MAX_BRANCHES = 100;
|
|
45259
|
+
var REWRITTEN = ["project.spec.json", "README.md", "CLAUDE.md", "AGENT.md", ".4pm/.4pm.json", ".claude/settings.json", ".claude/skills/", ".claude/agents/"];
|
|
45260
|
+
async function git4(args, cwd2, timeout = 6e4) {
|
|
45261
|
+
const { stdout } = await run13("git", args, { cwd: cwd2, timeout, maxBuffer: 32 * 1024 * 1024 });
|
|
45262
|
+
return stdout;
|
|
45263
|
+
}
|
|
45264
|
+
async function templateEntries() {
|
|
45265
|
+
try {
|
|
45266
|
+
return new Set((await readdir5(projectSampleDir())).filter((n) => n !== ".git"));
|
|
45267
|
+
} catch {
|
|
45268
|
+
return /* @__PURE__ */ new Set();
|
|
45269
|
+
}
|
|
45270
|
+
}
|
|
45271
|
+
async function probeSubmodule(sub) {
|
|
45272
|
+
try {
|
|
45273
|
+
const out = await git4(["ls-remote", "--symref", sub.url, "HEAD", ...sub.branch ? [`refs/heads/${sub.branch}`] : []], void 0, 3e4);
|
|
45274
|
+
const def = /ref: refs\/heads\/(\S+)\s+HEAD/.exec(out)?.[1] ?? "";
|
|
45275
|
+
const branch = sub.branch || def;
|
|
45276
|
+
const exists2 = !sub.branch || out.includes(`refs/heads/${sub.branch}`);
|
|
45277
|
+
return { subdir: sub.subdir, url: sub.url, branch, exists: exists2, protected: exists2 ? await branchProtection(sub.url, branch) : null, error: null };
|
|
45278
|
+
} catch (err) {
|
|
45279
|
+
const e = err;
|
|
45280
|
+
return { subdir: sub.subdir, url: sub.url, branch: sub.branch, exists: false, protected: null, error: (e.stderr || e.message || String(err)).trim().slice(0, 300) };
|
|
45281
|
+
}
|
|
45282
|
+
}
|
|
45283
|
+
async function probeRepo(req) {
|
|
45284
|
+
const empty = { branchExists: false, defaultBranch: null, branches: [], source: null, protected: null, submodules: [], error: null };
|
|
45285
|
+
let tmp = null;
|
|
45286
|
+
try {
|
|
45287
|
+
const [symref, heads] = await Promise.all([
|
|
45288
|
+
git4(["ls-remote", "--symref", req.url, "HEAD"], void 0, 3e4),
|
|
45289
|
+
git4(["ls-remote", "--heads", req.url], void 0, 3e4)
|
|
45290
|
+
]);
|
|
45291
|
+
const defaultBranch = /ref: refs\/heads\/(\S+)\s+HEAD/.exec(symref)?.[1] ?? null;
|
|
45292
|
+
const branches = heads.split("\n").map((l) => l.split(" ")[1] ?? "").filter((r) => r.startsWith("refs/heads/")).map((r) => r.slice("refs/heads/".length));
|
|
45293
|
+
const branch = (req.branch ?? "").trim();
|
|
45294
|
+
const branchExists = !branch || branches.includes(branch);
|
|
45295
|
+
const kind = req.base?.kind ?? "default";
|
|
45296
|
+
const ref = branchExists ? branch || defaultBranch : kind === "empty" ? null : kind === "branch" ? (req.base?.branch ?? "").trim() || null : defaultBranch;
|
|
45297
|
+
const [isProtected, submodules] = await Promise.all([
|
|
45298
|
+
branchExists ? branchProtection(req.url, branch || defaultBranch || "") : Promise.resolve(null),
|
|
45299
|
+
Promise.all((req.submodules ?? []).map(probeSubmodule))
|
|
45300
|
+
]);
|
|
45301
|
+
const result = { ...empty, branchExists, defaultBranch, branches: branches.slice(0, MAX_BRANCHES), protected: isProtected, submodules };
|
|
45302
|
+
if (!ref) return result;
|
|
45303
|
+
if (!branches.includes(ref)) return { ...result, error: `Branch "${ref}" does not exist on the remote.` };
|
|
45304
|
+
tmp = await mkdtemp2(join27(tmpdir4(), "4pm-probe-"));
|
|
45305
|
+
await git4(["init", "-q"], tmp);
|
|
45306
|
+
await git4(["remote", "add", "origin", req.url], tmp);
|
|
45307
|
+
await git4(["fetch", "-q", "--depth", "1", "--filter=blob:none", "origin", ref], tmp, 12e4);
|
|
45308
|
+
const files = (await git4(["ls-tree", "-r", "--name-only", "FETCH_HEAD"], tmp)).split("\n").filter(Boolean);
|
|
45309
|
+
const hasScaffold = files.includes(".4pm/.4pm.json");
|
|
45310
|
+
let templateVersion = null;
|
|
45311
|
+
if (hasScaffold) {
|
|
45312
|
+
try {
|
|
45313
|
+
const marker = JSON.parse(await git4(["show", "FETCH_HEAD:.4pm/.4pm.json"], tmp));
|
|
45314
|
+
templateVersion = typeof marker.templateVersion === "string" ? marker.templateVersion : null;
|
|
45315
|
+
} catch {
|
|
45316
|
+
templateVersion = null;
|
|
45317
|
+
}
|
|
45318
|
+
}
|
|
45319
|
+
const isRewritten = (f2) => REWRITTEN.some((r) => r.endsWith("/") ? f2.startsWith(r) : f2 === r);
|
|
45320
|
+
const tracking = SCAFFOLD_TRACKING_FILES.filter((f2) => files.includes(f2));
|
|
45321
|
+
const template = await templateEntries();
|
|
45322
|
+
const overwrite = files.filter((f2) => isRewritten(f2) && !f2.endsWith(".gitkeep"));
|
|
45323
|
+
const keep = files.filter(
|
|
45324
|
+
(f2) => template.has(f2.split("/")[0] ?? "") && !isRewritten(f2) && !tracking.includes(f2) && !f2.endsWith(".gitkeep")
|
|
45325
|
+
);
|
|
45326
|
+
return { ...result, source: { ref, hasScaffold, templateVersion, keep: keep.slice(0, 200), overwrite, tracking: [...tracking] } };
|
|
45327
|
+
} catch (err) {
|
|
45328
|
+
const e = err;
|
|
45329
|
+
return { ...empty, error: (e.stderr || e.message || String(err)).trim().split("\n").slice(-2).join(" ").slice(0, 400) };
|
|
45330
|
+
} finally {
|
|
45331
|
+
if (tmp) await rm6(tmp, { recursive: true, force: true }).catch(() => void 0);
|
|
45332
|
+
}
|
|
45333
|
+
}
|
|
43741
45334
|
|
|
43742
45335
|
// src/core/ai-task.ts
|
|
43743
45336
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -43809,16 +45402,16 @@ function handleProjectChannels(ctx, message, payload) {
|
|
|
43809
45402
|
case WsChannels.PHYSIC_SYNC: {
|
|
43810
45403
|
const sync = payload;
|
|
43811
45404
|
const oldName = sync.oldName;
|
|
43812
|
-
const oldPath = oldName ?
|
|
43813
|
-
const newPath =
|
|
45405
|
+
const oldPath = oldName ? join28(ctx.profileDir, oldName) : null;
|
|
45406
|
+
const newPath = join28(ctx.profileDir, sync.newName);
|
|
43814
45407
|
if (oldName) {
|
|
43815
45408
|
const oldRoot = ctx.physicFolderPath(oldName);
|
|
43816
45409
|
if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
|
|
43817
45410
|
}
|
|
43818
|
-
if (oldPath && oldName !== sync.newName &&
|
|
45411
|
+
if (oldPath && oldName !== sync.newName && existsSync15(oldPath) && !existsSync15(newPath)) {
|
|
43819
45412
|
renameSync(oldPath, newPath);
|
|
43820
45413
|
} else {
|
|
43821
|
-
if (oldPath && oldName !== sync.newName &&
|
|
45414
|
+
if (oldPath && oldName !== sync.newName && existsSync15(oldPath)) rmSync7(oldPath, { recursive: true, force: true });
|
|
43822
45415
|
mkdirSync7(newPath, { recursive: true });
|
|
43823
45416
|
}
|
|
43824
45417
|
ctx.physicRoot = ctx.physicFolderPath(sync.newName);
|
|
@@ -43833,7 +45426,7 @@ function handleProjectChannels(ctx, message, payload) {
|
|
|
43833
45426
|
if (del.name) {
|
|
43834
45427
|
const delRoot = ctx.physicFolderPath(del.name);
|
|
43835
45428
|
if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
|
|
43836
|
-
rmSync7(
|
|
45429
|
+
rmSync7(join28(ctx.profileDir, del.name), { recursive: true, force: true });
|
|
43837
45430
|
ctx.bus.log(t("project.folderDeleted", { name: del.name }));
|
|
43838
45431
|
}
|
|
43839
45432
|
void manageSshKey("delete", ctx.profileDir).catch(() => void 0);
|
|
@@ -43872,6 +45465,10 @@ function handleProjectChannels(ctx, message, payload) {
|
|
|
43872
45465
|
requestGitSnapshot("scaffold");
|
|
43873
45466
|
});
|
|
43874
45467
|
return true;
|
|
45468
|
+
case WsChannels.REPO_PROBE: {
|
|
45469
|
+
void probeRepo(payload).then((reply) => ctx.send(WsChannels.REPO_PROBE, reply, message.id));
|
|
45470
|
+
return true;
|
|
45471
|
+
}
|
|
43875
45472
|
case WsChannels.PROJECT_PUBLISH: {
|
|
43876
45473
|
const root = ctx.physicRoot;
|
|
43877
45474
|
if (!root) {
|
|
@@ -43985,13 +45582,13 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
|
|
|
43985
45582
|
}
|
|
43986
45583
|
|
|
43987
45584
|
// src/core/faq-compose.ts
|
|
43988
|
-
import { spawn as spawn9, execFile as
|
|
43989
|
-
import { existsSync as
|
|
45585
|
+
import { spawn as spawn9, execFile as execFile14 } from "child_process";
|
|
45586
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync8, rmSync as rmSync8 } from "fs";
|
|
43990
45587
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
43991
|
-
import { join as
|
|
43992
|
-
import { tmpdir as
|
|
43993
|
-
import { promisify as
|
|
43994
|
-
var execFileAsync2 =
|
|
45588
|
+
import { join as join29 } from "path";
|
|
45589
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
45590
|
+
import { promisify as promisify14 } from "util";
|
|
45591
|
+
var execFileAsync2 = promisify14(execFile14);
|
|
43995
45592
|
var NO_USAGE2 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
43996
45593
|
var COMPOSE_TIMEOUT_MS2 = 10 * 60 * 1e3;
|
|
43997
45594
|
var AGENT_DENIED_TOOLS = ["Bash", "WebFetch", "WebSearch", "NotebookEdit", "Task"];
|
|
@@ -44109,7 +45706,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
|
|
|
44109
45706
|
var MAX_OUTPUT_CHARS = 8e3;
|
|
44110
45707
|
async function runFaqCompose(req, ai2) {
|
|
44111
45708
|
if (!req.repo.token) return { prUrl: "", error: "no write token" };
|
|
44112
|
-
const workDir =
|
|
45709
|
+
const workDir = join29(tmpdir5(), "4pm-faq-sync", randomUUID3());
|
|
44113
45710
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
44114
45711
|
const auth = gitAuthArgs("x-access-token", req.repo.token);
|
|
44115
45712
|
try {
|
|
@@ -44153,7 +45750,7 @@ async function runFaqCompose(req, ai2) {
|
|
|
44153
45750
|
logger.warn("faq.compose.failed", { error: String(err) });
|
|
44154
45751
|
return { prUrl: "", error: String(err) };
|
|
44155
45752
|
} finally {
|
|
44156
|
-
if (
|
|
45753
|
+
if (existsSync16(workDir)) {
|
|
44157
45754
|
try {
|
|
44158
45755
|
rmSync8(workDir, { recursive: true, force: true });
|
|
44159
45756
|
} catch {
|
|
@@ -44337,8 +45934,8 @@ function handleToolsChannels(ctx, message, payload) {
|
|
|
44337
45934
|
}
|
|
44338
45935
|
|
|
44339
45936
|
// src/core/agents.ts
|
|
44340
|
-
import { readFile as
|
|
44341
|
-
import { join as
|
|
45937
|
+
import { readFile as readFile14, writeFile as writeFile10, mkdir as mkdir8, readdir as readdir6, rm as rm7 } from "fs/promises";
|
|
45938
|
+
import { join as join30 } from "path";
|
|
44342
45939
|
var AGENTS_REL = ".claude/agents";
|
|
44343
45940
|
var SKILLS_REL = ".claude/skills";
|
|
44344
45941
|
var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
|
|
@@ -44347,7 +45944,7 @@ function safeName(name) {
|
|
|
44347
45944
|
}
|
|
44348
45945
|
async function readText2(path, fallback = "") {
|
|
44349
45946
|
try {
|
|
44350
|
-
return await
|
|
45947
|
+
return await readFile14(path, "utf8");
|
|
44351
45948
|
} catch {
|
|
44352
45949
|
return fallback;
|
|
44353
45950
|
}
|
|
@@ -44355,24 +45952,24 @@ async function readText2(path, fallback = "") {
|
|
|
44355
45952
|
function frontmatter(text) {
|
|
44356
45953
|
const m2 = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
44357
45954
|
const body = m2?.[1] ?? "";
|
|
44358
|
-
const
|
|
44359
|
-
return { name:
|
|
45955
|
+
const get3 = (key) => body.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
45956
|
+
return { name: get3("name"), description: get3("description") };
|
|
44360
45957
|
}
|
|
44361
45958
|
async function listAgents(root) {
|
|
44362
45959
|
const subagents = [];
|
|
44363
45960
|
const skills = [];
|
|
44364
45961
|
try {
|
|
44365
|
-
for (const e of await
|
|
45962
|
+
for (const e of await readdir6(join30(root, AGENTS_REL), { withFileTypes: true })) {
|
|
44366
45963
|
if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
|
|
44367
|
-
const fm = frontmatter(await readText2(
|
|
45964
|
+
const fm = frontmatter(await readText2(join30(root, AGENTS_REL, e.name)));
|
|
44368
45965
|
subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
|
|
44369
45966
|
}
|
|
44370
45967
|
} catch {
|
|
44371
45968
|
}
|
|
44372
45969
|
try {
|
|
44373
|
-
for (const e of await
|
|
45970
|
+
for (const e of await readdir6(join30(root, SKILLS_REL), { withFileTypes: true })) {
|
|
44374
45971
|
if (!e.isDirectory()) continue;
|
|
44375
|
-
const fm = frontmatter(await readText2(
|
|
45972
|
+
const fm = frontmatter(await readText2(join30(root, SKILLS_REL, e.name, "SKILL.md")));
|
|
44376
45973
|
skills.push({ name: e.name, description: fm.description });
|
|
44377
45974
|
}
|
|
44378
45975
|
} catch {
|
|
@@ -44383,15 +45980,15 @@ async function listAgents(root) {
|
|
|
44383
45980
|
}
|
|
44384
45981
|
function itemPath(root, kind, name) {
|
|
44385
45982
|
const n = safeName(name);
|
|
44386
|
-
return kind === "subagent" ?
|
|
45983
|
+
return kind === "subagent" ? join30(root, AGENTS_REL, `${n}.md`) : join30(root, SKILLS_REL, n, "SKILL.md");
|
|
44387
45984
|
}
|
|
44388
45985
|
async function readAgent(root, kind, name) {
|
|
44389
45986
|
const content = await readText2(itemPath(root, kind, name));
|
|
44390
45987
|
if (kind !== "subagent") return { content };
|
|
44391
45988
|
const n = safeName(name);
|
|
44392
45989
|
const [short, long] = await Promise.all([
|
|
44393
|
-
readText2(
|
|
44394
|
-
readText2(
|
|
45990
|
+
readText2(join30(root, AGENTS_REL, "short-memory", `${n}.md`)),
|
|
45991
|
+
readText2(join30(root, AGENTS_REL, "long-memory", `${n}.md`))
|
|
44395
45992
|
]);
|
|
44396
45993
|
return { content, memory: { short, long } };
|
|
44397
45994
|
}
|
|
@@ -44400,14 +45997,14 @@ async function writeAgent(root, req) {
|
|
|
44400
45997
|
if (!n) return { ok: false, error: "invalid name" };
|
|
44401
45998
|
try {
|
|
44402
45999
|
if (req.action === "delete") {
|
|
44403
|
-
if (req.kind === "subagent") await
|
|
44404
|
-
else await
|
|
46000
|
+
if (req.kind === "subagent") await rm7(itemPath(root, "subagent", n), { force: true });
|
|
46001
|
+
else await rm7(join30(root, SKILLS_REL, n), { recursive: true, force: true });
|
|
44405
46002
|
return { ok: true };
|
|
44406
46003
|
}
|
|
44407
46004
|
if (req.content === void 0) return { ok: false, error: "content is required" };
|
|
44408
46005
|
const path = itemPath(root, req.kind, n);
|
|
44409
|
-
await mkdir8(
|
|
44410
|
-
await
|
|
46006
|
+
await mkdir8(join30(path, ".."), { recursive: true });
|
|
46007
|
+
await writeFile10(path, req.content, "utf8");
|
|
44411
46008
|
return { ok: true };
|
|
44412
46009
|
} catch (err) {
|
|
44413
46010
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -44415,21 +46012,21 @@ async function writeAgent(root, req) {
|
|
|
44415
46012
|
}
|
|
44416
46013
|
|
|
44417
46014
|
// src/core/secrets.ts
|
|
44418
|
-
import { readFile as
|
|
44419
|
-
import { join as
|
|
46015
|
+
import { readFile as readFile15, writeFile as writeFile11 } from "fs/promises";
|
|
46016
|
+
import { join as join31 } from "path";
|
|
44420
46017
|
var SECURITY_REL = "AI_SECURITY.md";
|
|
44421
46018
|
var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
|
|
44422
46019
|
var SECRETS_REL = "project.secrets.json";
|
|
44423
46020
|
async function readText3(path, fallback = "") {
|
|
44424
46021
|
try {
|
|
44425
|
-
return await
|
|
46022
|
+
return await readFile15(path, "utf8");
|
|
44426
46023
|
} catch {
|
|
44427
46024
|
return fallback;
|
|
44428
46025
|
}
|
|
44429
46026
|
}
|
|
44430
46027
|
async function readSecretsObject(root) {
|
|
44431
46028
|
try {
|
|
44432
|
-
const v2 = JSON.parse(await
|
|
46029
|
+
const v2 = JSON.parse(await readFile15(join31(root, SECRETS_REL), "utf8"));
|
|
44433
46030
|
return v2 && typeof v2 === "object" ? v2 : {};
|
|
44434
46031
|
} catch {
|
|
44435
46032
|
return {};
|
|
@@ -44448,8 +46045,8 @@ function placeholderKeys(text) {
|
|
|
44448
46045
|
}
|
|
44449
46046
|
async function readSecrets(root) {
|
|
44450
46047
|
const [security, placeholder, secrets] = await Promise.all([
|
|
44451
|
-
readText3(
|
|
44452
|
-
readText3(
|
|
46048
|
+
readText3(join31(root, SECURITY_REL)),
|
|
46049
|
+
readText3(join31(root, PLACEHOLDER_REL)),
|
|
44453
46050
|
readSecretsObject(root)
|
|
44454
46051
|
]);
|
|
44455
46052
|
const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
|
|
@@ -44462,14 +46059,14 @@ async function readSecrets(root) {
|
|
|
44462
46059
|
async function writeSecrets(root, req) {
|
|
44463
46060
|
try {
|
|
44464
46061
|
if (req.kind === "security") {
|
|
44465
|
-
await
|
|
46062
|
+
await writeFile11(join31(root, SECURITY_REL), req.content, "utf8");
|
|
44466
46063
|
} else if (req.kind === "placeholder") {
|
|
44467
|
-
await
|
|
46064
|
+
await writeFile11(join31(root, PLACEHOLDER_REL), req.content, "utf8");
|
|
44468
46065
|
} else {
|
|
44469
46066
|
const secrets = await readSecretsObject(root);
|
|
44470
46067
|
if (req.kind === "secret") secrets[req.key] = req.value;
|
|
44471
46068
|
else delete secrets[req.key];
|
|
44472
|
-
await
|
|
46069
|
+
await writeFile11(join31(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
|
|
44473
46070
|
}
|
|
44474
46071
|
return { ok: true };
|
|
44475
46072
|
} catch (err) {
|
|
@@ -44478,17 +46075,17 @@ async function writeSecrets(root, req) {
|
|
|
44478
46075
|
}
|
|
44479
46076
|
|
|
44480
46077
|
// src/core/agent-tools.ts
|
|
44481
|
-
import { readFile as
|
|
44482
|
-
import { dirname as dirname9, join as
|
|
46078
|
+
import { readFile as readFile16, writeFile as writeFile12, mkdir as mkdir9 } from "fs/promises";
|
|
46079
|
+
import { dirname as dirname9, join as join32 } from "path";
|
|
44483
46080
|
var SHARED_REL = ".claude/settings.json";
|
|
44484
46081
|
var LOCAL_REL = ".claude/settings.local.json";
|
|
44485
46082
|
var SECRETS_DENY = "Read(./project.secrets.json)";
|
|
44486
46083
|
function fileFor(root, scope) {
|
|
44487
|
-
return
|
|
46084
|
+
return join32(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
|
|
44488
46085
|
}
|
|
44489
46086
|
async function readSettingsObject(path) {
|
|
44490
46087
|
try {
|
|
44491
|
-
const v2 = JSON.parse(await
|
|
46088
|
+
const v2 = JSON.parse(await readFile16(path, "utf8"));
|
|
44492
46089
|
return v2 && typeof v2 === "object" ? v2 : {};
|
|
44493
46090
|
} catch {
|
|
44494
46091
|
return {};
|
|
@@ -44511,7 +46108,7 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
44511
46108
|
if (scope === "shared" && !next.deny.includes(SECRETS_DENY)) next.deny = [...next.deny, SECRETS_DENY];
|
|
44512
46109
|
obj.permissions = next;
|
|
44513
46110
|
await mkdir9(dirname9(path), { recursive: true });
|
|
44514
|
-
await
|
|
46111
|
+
await writeFile12(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
44515
46112
|
return { ok: true };
|
|
44516
46113
|
} catch (err) {
|
|
44517
46114
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -44520,8 +46117,8 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
44520
46117
|
|
|
44521
46118
|
// src/core/packages.ts
|
|
44522
46119
|
import { createHash as createHash4 } from "crypto";
|
|
44523
|
-
import { readFile as
|
|
44524
|
-
import { dirname as dirname10, join as
|
|
46120
|
+
import { readFile as readFile17, writeFile as writeFile13, mkdir as mkdir10, readdir as readdir7, rm as rm8, stat as stat5 } from "fs/promises";
|
|
46121
|
+
import { dirname as dirname10, join as join33, relative } from "path";
|
|
44525
46122
|
var AGENTS_REL2 = ".claude/agents";
|
|
44526
46123
|
var SKILLS_REL2 = ".claude/skills";
|
|
44527
46124
|
var MANIFEST_REL = ".claude/.4pm-packages.json";
|
|
@@ -44530,7 +46127,7 @@ function safeName2(name) {
|
|
|
44530
46127
|
}
|
|
44531
46128
|
async function readMaybe(path) {
|
|
44532
46129
|
try {
|
|
44533
|
-
return await
|
|
46130
|
+
return await readFile17(path);
|
|
44534
46131
|
} catch {
|
|
44535
46132
|
return null;
|
|
44536
46133
|
}
|
|
@@ -44540,12 +46137,12 @@ async function listFilesRel(dir2) {
|
|
|
44540
46137
|
async function walk2(d) {
|
|
44541
46138
|
let entries2;
|
|
44542
46139
|
try {
|
|
44543
|
-
entries2 = await
|
|
46140
|
+
entries2 = await readdir7(d, { withFileTypes: true });
|
|
44544
46141
|
} catch {
|
|
44545
46142
|
return;
|
|
44546
46143
|
}
|
|
44547
46144
|
for (const e of entries2) {
|
|
44548
|
-
const full =
|
|
46145
|
+
const full = join33(d, e.name);
|
|
44549
46146
|
if (e.isDirectory()) await walk2(full);
|
|
44550
46147
|
else if (e.isFile()) out.push(relative(dir2, full));
|
|
44551
46148
|
}
|
|
@@ -44556,16 +46153,16 @@ async function listFilesRel(dir2) {
|
|
|
44556
46153
|
async function readArtifactFiles(root, kind, slug) {
|
|
44557
46154
|
const files = [];
|
|
44558
46155
|
if (kind === "subagent") {
|
|
44559
|
-
const agent = await readMaybe(
|
|
46156
|
+
const agent = await readMaybe(join33(root, AGENTS_REL2, `${slug}.md`));
|
|
44560
46157
|
if (agent) files.push({ path: "agent.md", contentBase64: agent.toString("base64") });
|
|
44561
|
-
const short = await readMaybe(
|
|
46158
|
+
const short = await readMaybe(join33(root, AGENTS_REL2, "short-memory", `${slug}.md`));
|
|
44562
46159
|
if (short) files.push({ path: "short-memory.md", contentBase64: short.toString("base64") });
|
|
44563
|
-
const long = await readMaybe(
|
|
46160
|
+
const long = await readMaybe(join33(root, AGENTS_REL2, "long-memory", `${slug}.md`));
|
|
44564
46161
|
if (long) files.push({ path: "long-memory.md", contentBase64: long.toString("base64") });
|
|
44565
46162
|
} else {
|
|
44566
|
-
const base =
|
|
46163
|
+
const base = join33(root, SKILLS_REL2, slug);
|
|
44567
46164
|
for (const rel of await listFilesRel(base)) {
|
|
44568
|
-
const buf = await readMaybe(
|
|
46165
|
+
const buf = await readMaybe(join33(base, rel));
|
|
44569
46166
|
if (buf) files.push({ path: rel.split("\\").join("/"), contentBase64: buf.toString("base64") });
|
|
44570
46167
|
}
|
|
44571
46168
|
}
|
|
@@ -44583,7 +46180,7 @@ async function artifactSha256(root, kind, slug) {
|
|
|
44583
46180
|
return h.digest("hex");
|
|
44584
46181
|
}
|
|
44585
46182
|
async function artifactExists(root, kind, slug) {
|
|
44586
|
-
const target = kind === "subagent" ?
|
|
46183
|
+
const target = kind === "subagent" ? join33(root, AGENTS_REL2, `${slug}.md`) : join33(root, SKILLS_REL2, slug);
|
|
44587
46184
|
try {
|
|
44588
46185
|
await stat5(target);
|
|
44589
46186
|
return true;
|
|
@@ -44592,7 +46189,7 @@ async function artifactExists(root, kind, slug) {
|
|
|
44592
46189
|
}
|
|
44593
46190
|
}
|
|
44594
46191
|
async function readManifest(root) {
|
|
44595
|
-
const buf = await readMaybe(
|
|
46192
|
+
const buf = await readMaybe(join33(root, MANIFEST_REL));
|
|
44596
46193
|
if (!buf) return {};
|
|
44597
46194
|
try {
|
|
44598
46195
|
return JSON.parse(buf.toString("utf8"));
|
|
@@ -44601,35 +46198,35 @@ async function readManifest(root) {
|
|
|
44601
46198
|
}
|
|
44602
46199
|
}
|
|
44603
46200
|
async function writeManifest(root, manifest) {
|
|
44604
|
-
const path =
|
|
46201
|
+
const path = join33(root, MANIFEST_REL);
|
|
44605
46202
|
await mkdir10(dirname10(path), { recursive: true });
|
|
44606
|
-
await
|
|
46203
|
+
await writeFile13(path, JSON.stringify(manifest, null, 2), "utf8");
|
|
44607
46204
|
}
|
|
44608
46205
|
async function writeArtifactFiles(root, kind, slug, files) {
|
|
44609
46206
|
const byPath = new Map(files.map((f2) => [f2.path, Buffer.from(f2.contentBase64, "base64")]));
|
|
44610
46207
|
if (kind === "subagent") {
|
|
44611
46208
|
const agent = byPath.get("agent.md");
|
|
44612
46209
|
if (agent) {
|
|
44613
|
-
await mkdir10(
|
|
44614
|
-
await
|
|
46210
|
+
await mkdir10(join33(root, AGENTS_REL2), { recursive: true });
|
|
46211
|
+
await writeFile13(join33(root, AGENTS_REL2, `${slug}.md`), agent);
|
|
44615
46212
|
}
|
|
44616
46213
|
const short = byPath.get("short-memory.md");
|
|
44617
46214
|
if (short) {
|
|
44618
|
-
await mkdir10(
|
|
44619
|
-
await
|
|
46215
|
+
await mkdir10(join33(root, AGENTS_REL2, "short-memory"), { recursive: true });
|
|
46216
|
+
await writeFile13(join33(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
|
|
44620
46217
|
}
|
|
44621
46218
|
const long = byPath.get("long-memory.md");
|
|
44622
46219
|
if (long) {
|
|
44623
|
-
await mkdir10(
|
|
44624
|
-
await
|
|
46220
|
+
await mkdir10(join33(root, AGENTS_REL2, "long-memory"), { recursive: true });
|
|
46221
|
+
await writeFile13(join33(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
|
|
44625
46222
|
}
|
|
44626
46223
|
} else {
|
|
44627
|
-
const base =
|
|
46224
|
+
const base = join33(root, SKILLS_REL2, slug);
|
|
44628
46225
|
for (const [rel, buf] of byPath) {
|
|
44629
46226
|
if (rel.includes("..") || rel.startsWith("/")) continue;
|
|
44630
|
-
const dest =
|
|
46227
|
+
const dest = join33(base, rel);
|
|
44631
46228
|
await mkdir10(dirname10(dest), { recursive: true });
|
|
44632
|
-
await
|
|
46229
|
+
await writeFile13(dest, buf);
|
|
44633
46230
|
}
|
|
44634
46231
|
}
|
|
44635
46232
|
}
|
|
@@ -44693,11 +46290,11 @@ async function removePackage(root, slug) {
|
|
|
44693
46290
|
const entry = manifest[n];
|
|
44694
46291
|
if (!entry) return { ok: false, error: "not installed" };
|
|
44695
46292
|
if (entry.kind === "subagent") {
|
|
44696
|
-
await
|
|
44697
|
-
await
|
|
44698
|
-
await
|
|
46293
|
+
await rm8(join33(root, AGENTS_REL2, `${n}.md`), { force: true });
|
|
46294
|
+
await rm8(join33(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
|
|
46295
|
+
await rm8(join33(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
|
|
44699
46296
|
} else {
|
|
44700
|
-
await
|
|
46297
|
+
await rm8(join33(root, SKILLS_REL2, n), { recursive: true, force: true });
|
|
44701
46298
|
}
|
|
44702
46299
|
delete manifest[n];
|
|
44703
46300
|
await writeManifest(root, manifest);
|
|
@@ -44708,8 +46305,8 @@ async function removePackage(root, slug) {
|
|
|
44708
46305
|
}
|
|
44709
46306
|
|
|
44710
46307
|
// src/core/graph.ts
|
|
44711
|
-
import { readFile as
|
|
44712
|
-
import { join as
|
|
46308
|
+
import { readFile as readFile18, readdir as readdir8 } from "fs/promises";
|
|
46309
|
+
import { join as join34, relative as relative2, dirname as dirname11, resolve as resolve12, extname as extname2, basename as basename6 } from "path";
|
|
44713
46310
|
var MAX_FILES = 2e3;
|
|
44714
46311
|
var MAX_NODES = 3e3;
|
|
44715
46312
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
|
|
@@ -44721,7 +46318,7 @@ async function walk(root, accept) {
|
|
|
44721
46318
|
const dir2 = stack.pop();
|
|
44722
46319
|
let entries2;
|
|
44723
46320
|
try {
|
|
44724
|
-
entries2 = await
|
|
46321
|
+
entries2 = await readdir8(dir2, { withFileTypes: true });
|
|
44725
46322
|
} catch {
|
|
44726
46323
|
continue;
|
|
44727
46324
|
}
|
|
@@ -44729,7 +46326,7 @@ async function walk(root, accept) {
|
|
|
44729
46326
|
if (e.name.startsWith(".") && e.name !== ".claude") {
|
|
44730
46327
|
if (e.isDirectory()) continue;
|
|
44731
46328
|
}
|
|
44732
|
-
const full =
|
|
46329
|
+
const full = join34(dir2, e.name);
|
|
44733
46330
|
if (e.isDirectory()) {
|
|
44734
46331
|
if (!SKIP_DIRS.has(e.name)) stack.push(full);
|
|
44735
46332
|
} else if (accept(full)) {
|
|
@@ -44757,7 +46354,7 @@ async function buildDocs(root) {
|
|
|
44757
46354
|
const rel = relative2(root, f2).split("\\").join("/");
|
|
44758
46355
|
let text = "";
|
|
44759
46356
|
try {
|
|
44760
|
-
text = await
|
|
46357
|
+
text = await readFile18(f2, "utf8");
|
|
44761
46358
|
} catch {
|
|
44762
46359
|
continue;
|
|
44763
46360
|
}
|
|
@@ -44796,7 +46393,7 @@ async function buildCode(root) {
|
|
|
44796
46393
|
const rel = relative2(root, f2).split("\\").join("/");
|
|
44797
46394
|
let text = "";
|
|
44798
46395
|
try {
|
|
44799
|
-
text = await
|
|
46396
|
+
text = await readFile18(f2, "utf8");
|
|
44800
46397
|
} catch {
|
|
44801
46398
|
continue;
|
|
44802
46399
|
}
|
|
@@ -44848,17 +46445,17 @@ async function buildGraph(root, mode) {
|
|
|
44848
46445
|
}
|
|
44849
46446
|
|
|
44850
46447
|
// src/core/rag.ts
|
|
44851
|
-
import { readFile as
|
|
44852
|
-
import { existsSync as
|
|
44853
|
-
import { execFile as
|
|
46448
|
+
import { readFile as readFile19, writeFile as writeFile14, statfs as statfs2, mkdir as mkdir11 } from "fs/promises";
|
|
46449
|
+
import { existsSync as existsSync17 } from "fs";
|
|
46450
|
+
import { execFile as execFile15, spawn as spawn10 } from "child_process";
|
|
44854
46451
|
import { totalmem as totalmem2, cpus as cpus2 } from "os";
|
|
44855
|
-
import { join as
|
|
44856
|
-
import { promisify as
|
|
44857
|
-
var
|
|
46452
|
+
import { join as join35 } from "path";
|
|
46453
|
+
import { promisify as promisify15 } from "util";
|
|
46454
|
+
var run14 = promisify15(execFile15);
|
|
44858
46455
|
var RAG_REL = ".claude/rag";
|
|
44859
46456
|
async function tryRun(cmd, args) {
|
|
44860
46457
|
try {
|
|
44861
|
-
const { stdout } = await
|
|
46458
|
+
const { stdout } = await run14(cmd, args, { timeout: 8e3 });
|
|
44862
46459
|
return { ok: true, out: stdout.trim() };
|
|
44863
46460
|
} catch {
|
|
44864
46461
|
return { ok: false, out: "" };
|
|
@@ -44873,7 +46470,7 @@ async function detectPython() {
|
|
|
44873
46470
|
}
|
|
44874
46471
|
async function installLog(root) {
|
|
44875
46472
|
try {
|
|
44876
|
-
const text = await
|
|
46473
|
+
const text = await readFile19(join35(root, RAG_REL, "install.log"), "utf8");
|
|
44877
46474
|
return text.split("\n").filter((l) => l.length > 0).slice(-200);
|
|
44878
46475
|
} catch {
|
|
44879
46476
|
return [];
|
|
@@ -44935,14 +46532,14 @@ async function ragStatus(root) {
|
|
|
44935
46532
|
};
|
|
44936
46533
|
let modelName = "";
|
|
44937
46534
|
try {
|
|
44938
|
-
modelName = (await
|
|
46535
|
+
modelName = (await readFile19(join35(root, RAG_REL, ".installed"), "utf8")).trim();
|
|
44939
46536
|
} catch {
|
|
44940
46537
|
modelName = "";
|
|
44941
46538
|
}
|
|
44942
46539
|
const modelPresent = modelName.length > 0;
|
|
44943
46540
|
return {
|
|
44944
46541
|
installed: fastembed && sqliteVec && modelPresent,
|
|
44945
|
-
installing:
|
|
46542
|
+
installing: existsSync17(join35(root, RAG_REL, ".installing")),
|
|
44946
46543
|
python: { found: py.found, version: py.version },
|
|
44947
46544
|
deps: { pip, fastembed, sqliteVec },
|
|
44948
46545
|
model: { present: modelPresent, name: modelName },
|
|
@@ -44953,9 +46550,9 @@ async function ragStatus(root) {
|
|
|
44953
46550
|
};
|
|
44954
46551
|
}
|
|
44955
46552
|
async function readIndexMeta(root) {
|
|
44956
|
-
const indexing =
|
|
46553
|
+
const indexing = existsSync17(join35(root, RAG_REL, ".indexing"));
|
|
44957
46554
|
try {
|
|
44958
|
-
const m2 = JSON.parse(await
|
|
46555
|
+
const m2 = JSON.parse(await readFile19(join35(root, RAG_REL, ".index.json"), "utf8"));
|
|
44959
46556
|
return {
|
|
44960
46557
|
present: true,
|
|
44961
46558
|
indexing,
|
|
@@ -45022,14 +46619,14 @@ if __name__ == "__main__":
|
|
|
45022
46619
|
elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
|
|
45023
46620
|
`;
|
|
45024
46621
|
async function ensureEngine(root) {
|
|
45025
|
-
await mkdir11(
|
|
45026
|
-
await
|
|
46622
|
+
await mkdir11(join35(root, RAG_REL), { recursive: true }).catch(() => void 0);
|
|
46623
|
+
await writeFile14(join35(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
|
|
45027
46624
|
}
|
|
45028
46625
|
async function ragReindex(root) {
|
|
45029
46626
|
const py = await detectPython();
|
|
45030
46627
|
if (!py.found) return { ok: false, started: false, error: "python3 not found" };
|
|
45031
|
-
if (!
|
|
45032
|
-
if (
|
|
46628
|
+
if (!existsSync17(join35(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
|
|
46629
|
+
if (existsSync17(join35(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
|
|
45033
46630
|
await ensureEngine(root);
|
|
45034
46631
|
const script = `
|
|
45035
46632
|
set +e
|
|
@@ -45049,12 +46646,12 @@ rm -f "${RAG_REL}/.indexing"
|
|
|
45049
46646
|
async function ragQuery(root, queryText, k2 = 8) {
|
|
45050
46647
|
const py = await detectPython();
|
|
45051
46648
|
if (!py.found) return { results: [], error: "python3 not found" };
|
|
45052
|
-
if (!
|
|
46649
|
+
if (!existsSync17(join35(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
|
|
45053
46650
|
await ensureEngine(root);
|
|
45054
46651
|
try {
|
|
45055
|
-
const { stdout } = await
|
|
46652
|
+
const { stdout } = await run14(
|
|
45056
46653
|
py.bin,
|
|
45057
|
-
[
|
|
46654
|
+
[join35(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k2, 1), 20))],
|
|
45058
46655
|
{ cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
|
|
45059
46656
|
);
|
|
45060
46657
|
const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
|
|
@@ -45069,9 +46666,9 @@ async function ragInstall(root, model) {
|
|
|
45069
46666
|
}
|
|
45070
46667
|
const py = await detectPython();
|
|
45071
46668
|
if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
|
|
45072
|
-
if (
|
|
46669
|
+
if (existsSync17(join35(root, RAG_REL, ".installing"))) return { ok: true, started: true };
|
|
45073
46670
|
try {
|
|
45074
|
-
await mkdir11(
|
|
46671
|
+
await mkdir11(join35(root, RAG_REL), { recursive: true });
|
|
45075
46672
|
} catch {
|
|
45076
46673
|
}
|
|
45077
46674
|
const script = `
|
|
@@ -45098,8 +46695,8 @@ rm -f "${RAG_REL}/.installing"
|
|
|
45098
46695
|
}
|
|
45099
46696
|
|
|
45100
46697
|
// src/core/config-sync.ts
|
|
45101
|
-
import { existsSync as
|
|
45102
|
-
import { join as
|
|
46698
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
46699
|
+
import { join as join36 } from "path";
|
|
45103
46700
|
var SERVER_MANAGED_KEYS = [
|
|
45104
46701
|
"physicPath",
|
|
45105
46702
|
"sessionSwitchPct",
|
|
@@ -45116,8 +46713,8 @@ var SERVER_MANAGED_KEYS = [
|
|
|
45116
46713
|
"projectAiMemoryBudgetChars"
|
|
45117
46714
|
];
|
|
45118
46715
|
function readConfigText(profileDir2) {
|
|
45119
|
-
const path =
|
|
45120
|
-
if (
|
|
46716
|
+
const path = join36(profileDir2, "config.json");
|
|
46717
|
+
if (existsSync18(path)) {
|
|
45121
46718
|
try {
|
|
45122
46719
|
return readFileSync16(path, "utf8");
|
|
45123
46720
|
} catch {
|
|
@@ -45130,10 +46727,10 @@ function applyConfigText(profileDir2, text) {
|
|
|
45130
46727
|
try {
|
|
45131
46728
|
parsed = JSON.parse(text);
|
|
45132
46729
|
} catch (err) {
|
|
45133
|
-
return { ok: false, error:
|
|
46730
|
+
return { ok: false, error: t("config.invalidJson", { message: err.message }) };
|
|
45134
46731
|
}
|
|
45135
46732
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
45136
|
-
return { ok: false, error: "config.
|
|
46733
|
+
return { ok: false, error: t("config.notObject") };
|
|
45137
46734
|
}
|
|
45138
46735
|
const next = { ...parsed };
|
|
45139
46736
|
const current2 = readProfileConfig(profileDir2);
|
|
@@ -45839,10 +47436,11 @@ var WsClient = class {
|
|
|
45839
47436
|
this.startUsagePolling();
|
|
45840
47437
|
this.startLogUpload();
|
|
45841
47438
|
this.startKbRefresh();
|
|
45842
|
-
|
|
47439
|
+
onReconnectVerifyClaim(this.hctx);
|
|
47440
|
+
const servedRoot = () => this.physicRoot;
|
|
45843
47441
|
startGitSnapshots({
|
|
45844
47442
|
get physicRoot() {
|
|
45845
|
-
return
|
|
47443
|
+
return servedRoot();
|
|
45846
47444
|
},
|
|
45847
47445
|
profileDir: this.context.profileDir,
|
|
45848
47446
|
send: (channel, data) => this.send(channel, data, null)
|
|
@@ -46009,7 +47607,7 @@ var WsClient = class {
|
|
|
46009
47607
|
/** Create the physic folder at an absolute path if missing (best-effort). */
|
|
46010
47608
|
ensurePhysicFolderPath(folder) {
|
|
46011
47609
|
try {
|
|
46012
|
-
if (!
|
|
47610
|
+
if (!existsSync19(folder)) {
|
|
46013
47611
|
mkdirSync9(folder, { recursive: true });
|
|
46014
47612
|
this.bus.log(t("project.createdFolder", { folder }));
|
|
46015
47613
|
}
|
|
@@ -46024,7 +47622,7 @@ var WsClient = class {
|
|
|
46024
47622
|
*/
|
|
46025
47623
|
physicFolderPath(projectName) {
|
|
46026
47624
|
const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
|
|
46027
|
-
return safe ?
|
|
47625
|
+
return safe ? join37(this.context.profileDir, safe) : null;
|
|
46028
47626
|
}
|
|
46029
47627
|
/**
|
|
46030
47628
|
* Report machine.status: version + the state of the physic project being served
|
|
@@ -46039,7 +47637,7 @@ var WsClient = class {
|
|
|
46039
47637
|
projects: [],
|
|
46040
47638
|
version: CLI_VERSION,
|
|
46041
47639
|
physicPath,
|
|
46042
|
-
physicPathExists: physicPath ?
|
|
47640
|
+
physicPathExists: physicPath ? existsSync19(physicPath) : void 0,
|
|
46043
47641
|
// Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
|
|
46044
47642
|
autonomousRunning: physicPath ? isAutonomousRunning(physicPath, this.context.profileDir) : false,
|
|
46045
47643
|
// The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
|
|
@@ -46136,7 +47734,7 @@ var WsClient = class {
|
|
|
46136
47734
|
*/
|
|
46137
47735
|
uploadLog() {
|
|
46138
47736
|
try {
|
|
46139
|
-
const upload = readLogUpload(
|
|
47737
|
+
const upload = readLogUpload(join37(this.context.profileDir, "logs"));
|
|
46140
47738
|
if (!upload) return;
|
|
46141
47739
|
const sig = `${upload.fileName}:${upload.totalBytes}`;
|
|
46142
47740
|
if (sig === this.lastLogSig) return;
|
|
@@ -46299,9 +47897,9 @@ var WsClient = class {
|
|
|
46299
47897
|
|
|
46300
47898
|
// src/core/instance-lock.ts
|
|
46301
47899
|
import { readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
46302
|
-
import { join as
|
|
47900
|
+
import { join as join38 } from "path";
|
|
46303
47901
|
function lockPath(profileDir2) {
|
|
46304
|
-
return
|
|
47902
|
+
return join38(profileDir2, "start.lock");
|
|
46305
47903
|
}
|
|
46306
47904
|
function isAlive(pid) {
|
|
46307
47905
|
try {
|
|
@@ -46623,7 +48221,7 @@ var SessionBus = class {
|
|
|
46623
48221
|
// src/core/control-server.ts
|
|
46624
48222
|
import { createServer } from "net";
|
|
46625
48223
|
import { chmodSync as chmodSync5, rmSync as rmSync10 } from "fs";
|
|
46626
|
-
import { join as
|
|
48224
|
+
import { join as join40 } from "path";
|
|
46627
48225
|
|
|
46628
48226
|
// src/core/control-protocol.ts
|
|
46629
48227
|
var CONTROL_SOCKET_FILE = "control.sock";
|
|
@@ -46654,9 +48252,9 @@ function createFrameParser() {
|
|
|
46654
48252
|
// src/core/control-token.ts
|
|
46655
48253
|
import { chmodSync as chmodSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
|
|
46656
48254
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
46657
|
-
import { join as
|
|
48255
|
+
import { join as join39 } from "path";
|
|
46658
48256
|
function controlTokenPath(profileDir2) {
|
|
46659
|
-
return
|
|
48257
|
+
return join39(profileDir2, CONTROL_TOKEN_FILE);
|
|
46660
48258
|
}
|
|
46661
48259
|
function writeControlToken(profileDir2) {
|
|
46662
48260
|
const token = randomBytes3(32).toString("hex");
|
|
@@ -46685,7 +48283,7 @@ function tokenMatches(expected, given) {
|
|
|
46685
48283
|
|
|
46686
48284
|
// src/core/control-server.ts
|
|
46687
48285
|
function startControlServer(bus, profileDir2, info) {
|
|
46688
|
-
const socketPath =
|
|
48286
|
+
const socketPath = join40(profileDir2, CONTROL_SOCKET_FILE);
|
|
46689
48287
|
try {
|
|
46690
48288
|
rmSync10(socketPath, { force: true });
|
|
46691
48289
|
} catch {
|
|
@@ -47071,11 +48669,11 @@ var isOldWindows = () => {
|
|
|
47071
48669
|
}
|
|
47072
48670
|
const parts = os2.release().split(".");
|
|
47073
48671
|
const major = Number(parts[0]);
|
|
47074
|
-
const
|
|
48672
|
+
const build2 = Number(parts[2] ?? 0);
|
|
47075
48673
|
if (major < 10) {
|
|
47076
48674
|
return true;
|
|
47077
48675
|
}
|
|
47078
|
-
if (major === 10 &&
|
|
48676
|
+
if (major === 10 && build2 < 10586) {
|
|
47079
48677
|
return true;
|
|
47080
48678
|
}
|
|
47081
48679
|
return false;
|
|
@@ -52442,15 +54040,15 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
52442
54040
|
var import_react26 = __toESM(require_react(), 1);
|
|
52443
54041
|
|
|
52444
54042
|
// src/core/input-history.ts
|
|
52445
|
-
import { existsSync as
|
|
52446
|
-
import { join as
|
|
54043
|
+
import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
|
|
54044
|
+
import { join as join41 } from "path";
|
|
52447
54045
|
var MAX_HISTORY = 200;
|
|
52448
54046
|
function historyPath(profileDir2) {
|
|
52449
|
-
return
|
|
54047
|
+
return join41(profileDir2, "input-history.json");
|
|
52450
54048
|
}
|
|
52451
54049
|
function loadInputHistory(profileDir2) {
|
|
52452
54050
|
const path = historyPath(profileDir2);
|
|
52453
|
-
if (!
|
|
54051
|
+
if (!existsSync21(path)) return [];
|
|
52454
54052
|
try {
|
|
52455
54053
|
const data = JSON.parse(readFileSync20(path, "utf8"));
|
|
52456
54054
|
return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
|
|
@@ -52573,10 +54171,10 @@ function AiMarkerLine({ line }) {
|
|
|
52573
54171
|
const idx = line.text.indexOf(arrow);
|
|
52574
54172
|
if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
|
|
52575
54173
|
const before = line.text.slice(0, idx);
|
|
52576
|
-
const
|
|
52577
|
-
const name =
|
|
54174
|
+
const stamp2 = line.kind === "aireq" ? START_STAMP_RE.exec(before)?.[1] ?? "" : "";
|
|
54175
|
+
const name = stamp2 ? before.slice(stamp2.length) : before;
|
|
52578
54176
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { children: [
|
|
52579
|
-
|
|
54177
|
+
stamp2 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: stamp2 }) : null,
|
|
52580
54178
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: name }),
|
|
52581
54179
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
|
|
52582
54180
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
|
|
@@ -53060,7 +54658,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
53060
54658
|
return;
|
|
53061
54659
|
}
|
|
53062
54660
|
logger.configure({
|
|
53063
|
-
dir:
|
|
54661
|
+
dir: join42(profileDir2, "logs"),
|
|
53064
54662
|
level: resolveLogLevel(),
|
|
53065
54663
|
base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
|
|
53066
54664
|
});
|
|
@@ -53170,7 +54768,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
53170
54768
|
}
|
|
53171
54769
|
|
|
53172
54770
|
// src/commands/attach.ts
|
|
53173
|
-
import { join as
|
|
54771
|
+
import { join as join43 } from "path";
|
|
53174
54772
|
|
|
53175
54773
|
// src/core/control-client.ts
|
|
53176
54774
|
import { connect as connect2 } from "net";
|
|
@@ -53279,7 +54877,7 @@ async function runAttach(profileDir2, profileName) {
|
|
|
53279
54877
|
process.exitCode = 1;
|
|
53280
54878
|
return;
|
|
53281
54879
|
}
|
|
53282
|
-
const socketPath =
|
|
54880
|
+
const socketPath = join43(profileDir2, CONTROL_SOCKET_FILE);
|
|
53283
54881
|
const bus = new SessionBus();
|
|
53284
54882
|
let conn;
|
|
53285
54883
|
try {
|
|
@@ -53310,11 +54908,11 @@ async function runAttach(profileDir2, profileName) {
|
|
|
53310
54908
|
|
|
53311
54909
|
// src/commands/auto-run.ts
|
|
53312
54910
|
import { connect as connect3 } from "net";
|
|
53313
|
-
import { join as
|
|
54911
|
+
import { join as join44 } from "path";
|
|
53314
54912
|
var AUTO_RUN_TIMEOUT_MS = 3 * 60 * 60 * 1e3;
|
|
53315
54913
|
async function runAutoRun(profileDir2, profileName) {
|
|
53316
54914
|
initI18n(readProfileConfig(profileDir2).locale);
|
|
53317
|
-
const socketPath =
|
|
54915
|
+
const socketPath = join44(profileDir2, CONTROL_SOCKET_FILE);
|
|
53318
54916
|
const token = readControlToken(profileDir2);
|
|
53319
54917
|
const code = await new Promise((resolve13) => {
|
|
53320
54918
|
let settled = false;
|
|
@@ -53371,7 +54969,7 @@ async function runAutoRun(profileDir2, profileName) {
|
|
|
53371
54969
|
|
|
53372
54970
|
// src/commands/git-credential.ts
|
|
53373
54971
|
import { connect as connect4 } from "net";
|
|
53374
|
-
import { join as
|
|
54972
|
+
import { join as join45 } from "path";
|
|
53375
54973
|
var REQUEST_TIMEOUT_MS = 2e4;
|
|
53376
54974
|
function requestToken(host, path) {
|
|
53377
54975
|
const profileDir2 = process.env[PROFILE_DIR_ENV];
|
|
@@ -53386,7 +54984,7 @@ function requestToken(host, path) {
|
|
|
53386
54984
|
socket.destroy();
|
|
53387
54985
|
resolve13(value);
|
|
53388
54986
|
};
|
|
53389
|
-
const socket = connect4(
|
|
54987
|
+
const socket = connect4(join45(profileDir2, CONTROL_SOCKET_FILE));
|
|
53390
54988
|
socket.setEncoding("utf8");
|
|
53391
54989
|
const timer = setTimeout(() => finish(null), REQUEST_TIMEOUT_MS);
|
|
53392
54990
|
socket.on("connect", () => {
|