@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 CHANGED
@@ -11606,7 +11606,7 @@ var require_react_reconciler_development = __commonJS({
11606
11606
  }
11607
11607
  }
11608
11608
  var assign = Object.assign;
11609
- function get2(key) {
11609
+ function get3(key) {
11610
11610
  return key._reactInternals;
11611
11611
  }
11612
11612
  function set(key, value) {
@@ -11961,7 +11961,7 @@ var require_react_reconciler_development = __commonJS({
11961
11961
  instance._warnedAboutRefsInRender = true;
11962
11962
  }
11963
11963
  }
11964
- var fiber = get2(component);
11964
+ var fiber = get3(component);
11965
11965
  if (!fiber) {
11966
11966
  return false;
11967
11967
  }
@@ -18301,7 +18301,7 @@ var require_react_reconciler_development = __commonJS({
18301
18301
  var classComponentUpdater = {
18302
18302
  isMounted,
18303
18303
  enqueueSetState: function(inst, payload, callback) {
18304
- var fiber = get2(inst);
18304
+ var fiber = get3(inst);
18305
18305
  var eventTime = requestEventTime();
18306
18306
  var lane = requestUpdateLane(fiber);
18307
18307
  var update = createUpdate(eventTime, lane);
@@ -18322,7 +18322,7 @@ var require_react_reconciler_development = __commonJS({
18322
18322
  }
18323
18323
  },
18324
18324
  enqueueReplaceState: function(inst, payload, callback) {
18325
- var fiber = get2(inst);
18325
+ var fiber = get3(inst);
18326
18326
  var eventTime = requestEventTime();
18327
18327
  var lane = requestUpdateLane(fiber);
18328
18328
  var update = createUpdate(eventTime, lane);
@@ -18344,7 +18344,7 @@ var require_react_reconciler_development = __commonJS({
18344
18344
  }
18345
18345
  },
18346
18346
  enqueueForceUpdate: function(inst, callback) {
18347
- var fiber = get2(inst);
18347
+ var fiber = get3(inst);
18348
18348
  var eventTime = requestEventTime();
18349
18349
  var lane = requestUpdateLane(fiber);
18350
18350
  var update = createUpdate(eventTime, lane);
@@ -26201,7 +26201,7 @@ var require_react_reconciler_development = __commonJS({
26201
26201
  if (!parentComponent) {
26202
26202
  return emptyContextObject;
26203
26203
  }
26204
- var fiber = get2(parentComponent);
26204
+ var fiber = get3(parentComponent);
26205
26205
  var parentContext = findCurrentUnmaskedContext(fiber);
26206
26206
  if (fiber.tag === ClassComponent) {
26207
26207
  var Component = fiber.type;
@@ -26212,7 +26212,7 @@ var require_react_reconciler_development = __commonJS({
26212
26212
  return parentContext;
26213
26213
  }
26214
26214
  function findHostInstance(component) {
26215
- var fiber = get2(component);
26215
+ var fiber = get3(component);
26216
26216
  if (fiber === void 0) {
26217
26217
  if (typeof component.render === "function") {
26218
26218
  throw new Error("Unable to find node on an unmounted component.");
@@ -26229,7 +26229,7 @@ var require_react_reconciler_development = __commonJS({
26229
26229
  }
26230
26230
  function findHostInstanceWithWarning(component, methodName) {
26231
26231
  {
26232
- var fiber = get2(component);
26232
+ var fiber = get3(component);
26233
26233
  if (fiber === void 0) {
26234
26234
  if (typeof component.render === "function") {
26235
26235
  throw new Error("Unable to find node on an unmounted component.");
@@ -27177,7 +27177,7 @@ var require_stack_utils = __commonJS({
27177
27177
  const evalCol = Number(match[6]);
27178
27178
  let file = match[7];
27179
27179
  const lnum = match[8];
27180
- const col = match[9];
27180
+ const col2 = match[9];
27181
27181
  const native = match[10] === "native";
27182
27182
  const closeParen = match[11] === ")";
27183
27183
  let method;
@@ -27185,8 +27185,8 @@ var require_stack_utils = __commonJS({
27185
27185
  if (lnum) {
27186
27186
  res.line = Number(lnum);
27187
27187
  }
27188
- if (col) {
27189
- res.column = Number(col);
27188
+ if (col2) {
27189
+ res.column = Number(col2);
27190
27190
  }
27191
27191
  if (closeParen && file) {
27192
27192
  let closes = 0;
@@ -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.2",
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 join38 } from "path";
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 join33 } from "path";
32773
- import { existsSync as existsSync18, mkdirSync as mkdirSync9 } from "fs";
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 attachSubmodules(root, submodules, emit, createMissingBranch = false) {
37674
- if (submodules.length === 0) return;
37675
- if (!existsSync4(join6(root, ".git"))) return;
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
- if (existsSync4(join6(root, dir2, ".git")) || existsSync4(join6(root, ".git", "modules", dir2))) {
37681
- emit("submodule", `Submodule ${dir2} already present \u2014 updating\u2026`);
37682
- try {
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
- } catch {
38133
+ outcomes.push({ dir: dir2, ok: true, error: null });
38134
+ continue;
37685
38135
  }
37686
- continue;
37687
- }
37688
- const abs = join6(root, dir2);
37689
- if (existsSync4(abs)) {
37690
- emit("submodule", `Removing placeholder ${dir2} before attaching the submodule\u2026`);
37691
- await rm(abs, { recursive: true, force: true });
37692
- }
37693
- emit("submodule", `Adding submodule ${sub.url} \u2192 ${dir2}\u2026`);
37694
- const b2 = (sub.branch ?? "").trim();
37695
- if (b2 && createMissingBranch && !await ensureRemoteBranch(sub.url, b2, emit)) {
37696
- try {
37697
- await run("git", ["submodule", "add", sub.url, dir2], { cwd: root, timeout: 12e4 });
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
- try {
37700
- await run("git", ["checkout", "-b", b2], { cwd: join6(root, dir2), timeout: 3e4 });
37701
- await run("git", ["push", "-u", "origin", b2], { cwd: join6(root, dir2), timeout: 12e4 });
37702
- } catch (pushErr) {
37703
- const pmsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
37704
- emit("git-branch-push-failed", `Submodule ${dir2}: branch "${b2}" created locally but the push failed: ${pmsg}`);
37705
- }
37706
- added++;
37707
- continue;
37708
- } catch (err) {
37709
- const msg = err instanceof Error ? err.message : String(err);
37710
- if (!/already exists|already registered|in the index/i.test(msg)) {
37711
- throw new Error(`git submodule add ${dir2} failed: ${msg}`);
37712
- }
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 = err instanceof Error ? err.message : String(err);
37720
- if (/already exists|already registered|in the index/i.test(msg)) {
37721
- try {
37722
- await run("git", ["submodule", "update", "--init", "--", dir2], { cwd: root, timeout: 12e4 });
37723
- added++;
37724
- } catch {
37725
- }
37726
- } else {
37727
- throw new Error(`git submodule add ${dir2} failed: ${msg}`);
37728
- }
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
- const msg = err instanceof Error ? err.message : String(err);
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 commitAndOpenPr(root, repo, emit) {
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", "Pushing the scaffold branch\u2026");
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
- const remoteHead = (await gitOut(root, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).replace(/^origin\//, "");
37816
- if (branch && remoteHead && branch === remoteHead) {
37817
- emit("pr", `Pushed to the default branch (${branch}) \u2014 no pull request needed.`);
37818
- return result;
37819
- }
37820
- const url = (repo.url ?? "").trim() || await gitOut(root, ["remote", "get-url", "origin"]);
37821
- const { provider, slug } = providerSlug(url);
37822
- if (!provider) {
37823
- emit("pr", "Unknown git host \u2014 skipping the pull request.");
37824
- return result;
37825
- }
37826
- emit("pr", `Opening a pull request via ${provider}\u2026`);
37827
- try {
37828
- const args = provider === "glab" ? ["mr", "create", "--fill", "--yes", ...branch ? ["--source-branch", branch] : [], ...slug ? ["-R", slug] : []] : ["pr", "create", "--fill", ...branch ? ["--head", branch] : [], ...slug ? ["-R", slug] : []];
37829
- const { stdout } = await run(provider, args, { cwd: root, timeout: 12e4 });
37830
- const link2 = stdout.trim().split(/\s+/).find((x) => /^https?:\/\//.test(x)) ?? null;
37831
- result.prUrl = link2;
37832
- emit("pr", `Pull request opened${link2 ? `: ${link2}` : "."}`);
37833
- return result;
37834
- } catch (err) {
37835
- const msg = errText(err);
37836
- const existing = /already exists/i.test(msg) ? msg.match(/https?:\/\/\S+/)?.[0] ?? null : null;
37837
- if (existing) {
37838
- result.prUrl = existing;
37839
- emit("pr", `A pull request is already open: ${existing}`);
37840
- return result;
37841
- }
37842
- emit("pr-failed", `Could not open a pull request (${provider}): ${msg}`);
37843
- return fail("pr", msg);
37844
- }
38236
+ return result;
37845
38237
  }
37846
38238
  async function publishScaffold(root, emit) {
37847
- let repo = {};
38239
+ let repos = [];
37848
38240
  try {
37849
38241
  const spec = JSON.parse(await readFile(join6(root, "project.spec.json"), "utf8"));
37850
- repo = singleRepo(reposOf(spec)) ?? {};
38242
+ repos = reposOf(spec);
37851
38243
  } catch {
37852
38244
  }
37853
- return commitAndOpenPr(root, repo, emit);
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
- emit("git", `Branch "${branch}" not found on ${url} \u2014 creating it from the default branch\u2026`);
37872
- await run("git", ["clone", "--depth", "1", url, tmp], { timeout: 12e4 });
37873
- await run("git", ["checkout", "-b", branch], { cwd: tmp, timeout: 3e4 });
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
- const msg = err instanceof Error ? err.message : String(err);
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
- emit("git", `Cloning ${repo.url} (default branch) to create "${b2}"\u2026`);
37916
- await run("git", ["clone", repo.url, target], { timeout: 12e4 });
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
- const msg = err instanceof Error ? err.message : String(err);
37923
- emit("git-branch-push-failed", `Branch "${b2}" created locally but the push failed: ${msg} \u2014 push it when credentials are available.`);
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 attachSubmodules(target, submodulesOf(reposOf(payload.spec)), emit, true);
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 commitAndOpenPr(target, repo, emit);
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 readdir(dir2).catch(() => []);
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 get2 = (type) => parts.find((p2) => p2.type === type)?.value ?? "";
39053
- const hour = Number.parseInt(get2("hour"), 10);
39054
- const dateKey = `${get2("year")}-${get2("month")}-${get2("day")}`;
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 readFile5, writeFile as writeFile4, chmod } from "fs/promises";
40875
+ import { readFile as readFile7, writeFile as writeFile5, chmod } from "fs/promises";
40429
40876
  import { readFileSync as readFileSync14 } from "fs";
40430
- import { execFile as execFile3, spawn as spawn5 } from "child_process";
40431
- import { join as join17 } from "path";
40432
- import { promisify as promisify3 } from "util";
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 readFile5(path, "utf8");
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(join17(root, file))).keys());
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 join17(root, TICK_REL);
41548
+ return join19(root, TICK_REL);
40612
41549
  }
40613
41550
  async function crontabList() {
40614
41551
  try {
40615
- const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
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(join17(root, ".claude/.autonomous.histories.json"), "utf8"));
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 [settings, approvals, authors, status, ...books] = await Promise.all([
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
- readText(join17(root, APPROVALS_REL), "{}"),
40667
- readText(join17(root, AUTHORS_REL), "{}"),
41610
+ read(APPROVALS_REL, "{}"),
41611
+ read(AUTHORS_REL, "{}"),
41612
+ read(ATTEMPTS_REL, "{}"),
40668
41613
  getAutonomousStatus(root, profileDir2),
40669
- ...Object.values(BOOK_FILES).map((f2) => readText(join17(root, 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(join17(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
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
- await crontabSet(kept.join("\n"));
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
- switch (req.kind) {
40704
- case "settings": {
40705
- const parsed = parseJson(req.settings);
40706
- if (!parsed) return { ok: false, error: "settings is not valid JSON" };
40707
- await writeAutonomousConfig(profileDir2, parsed);
40708
- await syncCron(root, profileDir2);
40709
- break;
40710
- }
40711
- case "approvals": {
40712
- if (req.approved && !req.byIsAdmin) {
40713
- const authors = await readJsonMap(join17(root, AUTHORS_REL));
40714
- if (entryBy(authors, req.taskId) === by) {
40715
- return { ok: false, code: "APPROVAL_SELF", failedId: req.taskId, error: "self-approval blocked" };
40716
- }
40717
- }
40718
- const map = parseJson(await readText(join17(root, APPROVALS_REL), "{}")) ?? {};
40719
- if (req.approved) map[req.taskId] = { approved: true, by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
40720
- else delete map[req.taskId];
40721
- await writeFile4(join17(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
40722
- break;
40723
- }
40724
- case "approvalsBatch": {
40725
- if (!req.byIsAdmin) {
40726
- const authors = await readJsonMap(join17(root, AUTHORS_REL));
40727
- const selfId = req.approve.find((id) => entryBy(authors, id) === by);
40728
- if (selfId) return { ok: false, code: "APPROVAL_SELF", failedId: selfId, error: "self-approval blocked" };
40729
- }
40730
- const map = parseJson(await readText(join17(root, APPROVALS_REL), "{}")) ?? {};
40731
- const at2 = (/* @__PURE__ */ new Date()).toISOString();
40732
- const byLabel = req.byLabel ?? by;
40733
- for (const taskId of req.approve) map[taskId] = { approved: true, by, byLabel, at: at2 };
40734
- for (const taskId of req.unapprove) delete map[taskId];
40735
- await writeFile4(join17(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
40736
- break;
40737
- }
40738
- case "userTodo": {
40739
- const cur = await readText(join17(root, BOOK_FILES.userTodo));
40740
- const ids = [...cur.matchAll(/REQ-(\d{4})-(\d{4})/g)];
40741
- const maxGroup = ids.reduce((m2, g2) => Math.max(m2, Number(g2[1])), 0);
40742
- const id = `REQ-${String(maxGroup + 1).padStart(4, "0")}-0001`;
40743
- const row = `| ${id} | | | ${tableCell(req.content)} |
40744
- `;
40745
- await writeFile4(join17(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
40746
- added = 1;
40747
- const authors = await readJsonMap(join17(root, AUTHORS_REL));
40748
- authors[id] = { by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
40749
- await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
40750
- break;
40751
- }
40752
- case "bookSave": {
40753
- const file = BOOK_FILES[req.book];
40754
- const prev = tableRowsById(await readText(join17(root, file)));
40755
- const next = tableRowsById(req.content);
40756
- const authors = await readJsonMap(join17(root, AUTHORS_REL));
40757
- const at2 = (/* @__PURE__ */ new Date()).toISOString();
40758
- const byLabel = req.byLabel ?? by;
40759
- added = 0;
40760
- for (const [id, cells] of next) {
40761
- if (prev.get(id) !== cells) authors[id] = { by, byLabel, at: at2 };
40762
- if (!prev.has(id)) added += 1;
40763
- }
40764
- await writeFile4(join17(root, file), req.content, "utf8");
40765
- await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
40766
- break;
40767
- }
40768
- case "cron": {
40769
- if (req.action === "install") await installCron(root, profileDir2);
40770
- else await uninstallCron(root);
40771
- break;
40772
- }
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 existsSync11, readFileSync as readFileSync15 } from "fs";
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 (existsSync11("/.dockerenv")) return true;
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 existsSync12, mkdirSync as mkdirSync6, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
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 join18, resolve as resolve3 } from "path";
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 = join18(dir2, process.platform === "win32" ? "gh.exe" : "gh");
40881
- if (existsSync12(candidate)) return candidate;
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 && existsSync12(devTsconfig) && !process.env.TSX_TSCONFIG_PATH) {
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 = join18(profileDir2, "bin");
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 = join18(shimDir, "gh");
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(join18(shimDir, "gh"), { force: true });
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 execFile6 } from "child_process";
41989
+ import { execFile as execFile8 } from "child_process";
41032
41990
  import { stat } from "fs/promises";
41033
- import { join as join20, resolve as resolve5 } from "path";
41034
- import { promisify as promisify6 } from "util";
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 execFile4 } from "child_process";
41995
+ import { execFile as execFile6 } from "child_process";
41038
41996
  import { resolve as resolve4, sep as sep2 } from "path";
41039
- import { promisify as promisify4 } from "util";
41040
- var run4 = promisify4(execFile4);
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 git(cwd2, args) {
42007
+ async function git2(cwd2, args) {
41050
42008
  try {
41051
- const { stdout } = await run4("git", args, {
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 git(base, ["rev-parse", "--show-toplevel"])).trim();
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 git(base, ["remote", "get-url", "origin"])).trim();
41068
- const rootBranch = (await git(base, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
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 git(base, ["submodule", "status"]);
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 git(abs, ["remote", "get-url", "origin"])).trim();
41078
- const branch = (await git(abs, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
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 git(cwd2, [
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 git(cwd2, [
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 git(cwd2, ["show", `${hash}^:${path}`]);
41135
- const newContent = await git(cwd2, ["show", `${hash}:${path}`]);
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 build = (tz) => {
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 get2 = (type) => parts.find((p2) => p2.type === type)?.value ?? "";
41367
- const hour = get2("hour") === "24" ? "00" : get2("hour");
41368
- return `${get2("year")}/${get2("month")}/${get2("day")} ${hour}:${get2("minute")}:${get2("second")}`;
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 build(timeZone);
42329
+ return build2(timeZone);
41372
42330
  } catch {
41373
- return build();
42331
+ return build2();
41374
42332
  }
41375
42333
  }
41376
42334
 
41377
42335
  // src/ui/slash-commands.ts
41378
- import { existsSync as existsSync13 } from "fs";
41379
- import { join as join19 } from "path";
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 join19(profileDir2, "config.json");
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 (existsSync13(path)) {
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 = join19(ctx.info.profileDir, "logs");
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 execFile5 } from "child_process";
41874
- import { promisify as promisify5 } from "util";
41875
- var run5 = promisify5(execFile5);
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 run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
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 run12 = activeRuns.get(commandId);
41987
- if (!run12) return { ok: false, state: "unknown" };
41988
- run12.controller.abort();
41989
- return { ok: true, state: run12.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 run12 = { controller: new AbortController(), state: "queued" };
42087
- activeRuns.set(commandId, run12);
43045
+ const run15 = { controller: new AbortController(), state: "queued" };
43046
+ activeRuns.set(commandId, run15);
42088
43047
  try {
42089
- await runAiPromptInner(ctx, prompt, commandId, origin, run12, oneShot, images, aiConfig, readOnly, bypass);
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, run12, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
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
- run12.controller.signal
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
- run12.state = "running";
43258
+ run15.state = "running";
42298
43259
  try {
42299
- result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run12.controller.signal);
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 run6 = promisify6(execFile6);
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 git2(cwd2, args, timeout = 2e4) {
43584
+ async function git3(cwd2, args, timeout = 2e4) {
42617
43585
  try {
42618
- const { stdout } = await run6("git", args, { cwd: cwd2, timeout, maxBuffer: 16 * 1024 * 1024 });
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
- git2(dir2, ["rev-parse", "--abbrev-ref", "HEAD"]),
42674
- git2(dir2, ["rev-parse", "--verify", "HEAD"]),
42675
- git2(dir2, ["for-each-ref", "refs/heads", `--format=%(refname:short)${US}%(objectname)${US}%(upstream:short)${US}%(upstream:track,nobracket)`]),
42676
- git2(dir2, ["for-each-ref", "refs/remotes/origin", `--format=%(refname:short)${US}%(objectname)${US}%(symref)`]),
42677
- git2(dir2, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]),
42678
- git2(dir2, ["status", "--porcelain=v2", "--untracked-files=all"]),
42679
- git2(dir2, ["rev-parse", "--absolute-git-dir"])
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
- git2(dir2, ["log", "--branches", "--not", "--remotes", "-n", String(L2.localCommits + 1), "--name-status", fmt], 3e4),
42691
- git2(dir2, ["log", "--remotes", "--date-order", "-n", String(L2.remoteCommits), fmt], 3e4)
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 git2(dir2, ["merge-base", "HEAD", baseRef])).trim() || null : null;
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(join20(gitDir.trim(), "FETCH_HEAD"))).mtime.toISOString();
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 git2(r.subdir ? resolve5(base, r.subdir) : base, ["fetch", "--prune", "--quiet", "origin"], 6e4);
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 readFile6, readdir as readdir2, rm as rm2, stat as stat2 } from "fs/promises";
42803
- import { join as join21 } from "path";
42804
- function buildAutonomousCyclePrompt(opts = {}) {
42805
- const capNote = opts.intakeBlocked ? `
42806
-
42807
- **Step 2 is DISABLED this cycle** \u2014 the organization reached its monthly plan limit for new \`AI_TODO\` /
42808
- \`USER_QA\` rows. Do NOT add any row to \`AI_TODO.md\` or \`USER_QA.md\`; skip Step 2 entirely and continue
42809
- with the already-approved work.` : "";
42810
- return `You are running UNATTENDED as the 4PM autonomous cycle (headless, one tick). Do EXACTLY ONE small
42811
- work cycle with the steps below, then STOP. Keep each cycle to ONE small task so you never run out of
42812
- tokens mid-way. If a step fails, append a row to the **Incidents** table of \`AI_DONE.md\` and STOP \u2014
42813
- do not push on. Write all books/docs/code in the language the project's \`CLAUDE.md\` specifies (default
42814
- English).${capNote}
42815
-
42816
- ## Books & approval (MUST read the approvals file)
42817
- The five "book" files at the project root \u2014 \`USER_TODO.md\`, \`USER_QA.md\`, \`AI_TODO.md\`,
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). To tell empty/has-work, COMPARE a book to its
42820
- \`<NAME>.empty.md\` (equal after trimming \u21D2 empty). When clearing a book, overwrite it with EXACTLY the
42821
- empty template (\`cp .claude/templates/<NAME>.empty.md <NAME>\`).
42822
-
42823
- The single source of truth for approval is \`.claude/.autonomous.approvals.json\` \u2014 a map
42824
- \`{ "<ID>": { "approved": true, "by": "...", "at": "..." } }\` (the user ticks rows in the web grids). A
42825
- row is APPROVED only when its ID has \`"approved": true\`. Row IDs: \`USER_TODO\` = \`REQ-\u2026\`, \`USER_QA\`
42826
- = \`QA-\u2026\`, \`AI_TODO\` = \`TSK-\u2026\`. NEVER act on an unapproved row.
42827
-
42828
- ## Step 1 \u2014 Base branch (from the spec, ADR-0292)
42829
- The project root IS the primary repo (ADR-0314). Its currently checked-out branch is the base branch
42830
- (4PM cloned it with \`-b <branch>\`): \`BASE=$(git rev-parse --abbrev-ref HEAD)\`. Sync it:
42831
- \`git fetch origin\` then \`git pull --ff-only origin "$BASE"\` (skip the pull if the remote has no such
42832
- branch yet). Do NOT invent a separate integration/dev branch.
42833
-
42834
- ## Step 2 \u2014 Intake APPROVED user requests \u2192 tasks
42835
- Read \`USER_TODO.md\` (content-only columns \`ID | Group | Depends | Request\`; who/when wrote or approved a
42836
- row lives in \`.claude/.autonomous.authors.json\` / \`.autonomous.approvals.json\`, ADR-0320 \u2014 never add
42837
- provenance columns to the table). For each row that is APPROVED (its \`REQ-\u2026\` in the approvals file) AND
42838
- whose \`Depends\` \`REQ-\u2026\` are already handled:
42839
- - If the request is CLEAR: split it into small tasks and append them to \`AI_TODO.md\` (7 columns
42840
- \`ID | Priority | Tag | Depends | Group | Task description | Notes\`), each with an ID
42841
- \`TSK-{groupid:0000}-{taskid:0000}\`. **groupid** must be UNIQUE + INCREASING across all history \u2014 find
42842
- the largest ever used in git history of \`AI_TODO.md\`/\`AI_DONE.md\`/\`AI_PROGRESS.md\`, then use max+1;
42843
- **taskid** starts at 0001 within the group. Leave \`Tag\` blank unless a catalog tag applies; leave
42844
- \`Priority\` = High/Medium/Low (default Medium). Then REMOVE the analysed request row from
42845
- \`USER_TODO.md\` (if it becomes empty, reset it to the empty template).
42846
- - If the request is UNCLEAR (ambiguous/missing info/contradictory): do NOT guess. Append a row to
42847
- \`USER_QA.md\` (content-only columns \`ID | Group | Depends | Original request | Question / options |
42848
- Answer\`) with a new \`QA-{groupid:0000}-{qaid:0000}\` id, the original request and the question +
42849
- options; leave \`Answer\` blank for the user. Remove the request row from \`USER_TODO.md\`. (Do NOT
42850
- generate tasks for it.)
42851
- An unapproved request row is LEFT in place (wait for approval).
42852
-
42853
- ## Step 3 \u2014 Fold APPROVED answers back (USER_QA)
42854
- For each \`USER_QA.md\` row that has an \`Answer\` AND is APPROVED (its \`QA-\u2026\` in the approvals file):
42855
- re-analyse using the answer \u2014 append the resulting tasks to \`AI_TODO.md\` (same ID rules as Step 2) \u2014
42856
- then clear that QA row. An unanswered or unapproved QA row is left in place.
42857
-
42858
- ## Step 4 \u2014 Pick ONE approved task and start it
42859
- Read \`AI_TODO.md\` + the approvals file. Filter tasks that are BOTH approved (\`TSK-\u2026\` in approvals) AND
42860
- have every \`Depends\` \`TSK-\u2026\` already in \`AI_DONE.md\`. Run group by group (smallest eligible group
42861
- first), within a group High \u2192 Medium \u2192 Low, then line order. If NO eligible task: append a row to the
42862
- \`AI_DONE.md\` Incidents table (e.g. "only generated tasks / waiting for approval / waiting for deps")
42863
- and STOP. Otherwise move the chosen task into \`AI_PROGRESS.md\` (with a start timestamp), remove it from
42864
- \`AI_TODO.md\`, and commit on the base branch: \`git commit -am "chore(auto): start TSK-\u2026"\`.
42865
-
42866
- ## Step 5 \u2014 Implement on a task branch + test
42867
- \`git checkout -b task/TSK-\u2026\`. Implement following the project's architecture + \`CLAUDE.md\`; add/update
42868
- tests. Run the project's test command (per \`CLAUDE.md\` / the project's scripts); WAIT for it to finish
42869
- and check the result. Keep any produced report/evidence. Commit: \`git commit -am "feat(TSK-\u2026): <desc>"\`.
42870
-
42871
- ## Step 6 \u2014 Pull request into the base branch (NO direct merge \u2014 ADR-0319/0271)
42872
- 1. Rebase onto the freshest base to minimise conflicts: \`git fetch origin\` then
42873
- \`git rebase origin/"$BASE"\` (on conflict you can't resolve confidently, \`git rebase --abort\`, write a
42874
- \`USER_QA.md\` row, and STOP).
42875
- 2. Push the task branch: \`git push -u origin task/TSK-\u2026\`.
42876
- 3. Open a PR into the base branch with \`gh\` (GitHub) or \`glab\` (GitLab) \u2014 e.g.
42877
- \`gh pr create --base "$BASE" --head task/TSK-\u2026 --title "TSK-\u2026: <desc>" --body "<summary>"\`. Do NOT
42878
- merge it and do NOT fast-forward the base yourself \u2014 a human reviews the PR. If no remote / no
42879
- \`gh\`/\`glab\` auth is available, keep the local task-branch commit and append an Incidents row saying
42880
- the PR could not be opened.
42881
-
42882
- ## Step 7 \u2014 Update the books
42883
- On the base branch, append a row to the \`AI_DONE.md\` Done table (Timestamp, ID, Task description, Files,
42884
- Notes \u2014 include the PR link if opened) and REMOVE the task from \`AI_PROGRESS.md\` (reset it to the empty
42885
- template if nothing is in progress). Commit + push the base branch:
42886
- \`git commit -am "chore(auto): finish TSK-\u2026 (PR opened)" && git push origin "$BASE"\` (a push rejected as
42887
- non-fast-forward \u21D2 \`git pull --rebase origin "$BASE"\` then push again).
42888
-
42889
- ## Step 8 \u2014 Stop
42890
- Print a one-line summary (task id + PR link or incident). STOP \u2014 the next cron tick runs the next cycle.
42891
- The 4PM cli serializes cycles (one at a time); you do NOT manage any run lock.`;
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
- if (!await hasWork(root)) {
42933
- await log("[skip] no approved USER_TODO/USER_QA/AI_TODO work, AI_PROGRESS empty");
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
- const usage = await readBookUsage(ctx);
42949
- const intakeBlocked = ["AI_TODO", "USER_QA"].some((b2) => {
42950
- const c = usage?.books[b2];
42951
- return !!c && c.limit !== null && c.used >= c.limit;
42952
- });
42953
- if (intakeBlocked) await log("[cap] monthly AI_TODO/USER_QA limit reached \u2014 intake skipped this cycle");
42954
- const booksBefore = await cappedBookIds(root);
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, buildAutonomousCyclePrompt({ intakeBlocked }), commandId, "local", false, void 0, override, false, true);
42957
- await reportNewBookRows(ctx, root, booksBefore);
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.consecutiveFails = 0;
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
- } catch (err) {
42964
- const msg = err instanceof Error ? err.message : String(err);
42965
- await reportNewBookRows(ctx, root, booksBefore);
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", msg, today);
42969
- await log(`[warn] cycle failed: ${msg}`);
42970
- if (cfg.stopOnConsecutiveFailures > 0 && hist.consecutiveFails >= cfg.stopOnConsecutiveFailures) {
42971
- await writeAutonomousConfig(profileDir2, { ...cfg, paused: true });
42972
- await log(`[stop] ${hist.consecutiveFails} consecutive failures \u2192 paused`);
42973
- }
42974
- ctx.bus.autonomousDone(false, msg);
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 = join21(root, ".claude", "logs");
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(join21(dir2, `autonomous-tick-${day}.log`), `${ts2} ${line}
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 = join21(root, ".claude", "logs");
44524
+ const dir2 = join24(root, ".claude", "logs");
43016
44525
  const cutoff = Date.now() - retentionDays * 864e5;
43017
- for (const f2 of await readdir2(dir2)) {
44526
+ for (const f2 of await readdir3(dir2)) {
43018
44527
  if (!/^autonomous-tick-.*\.log$/.test(f2)) continue;
43019
- const st2 = await stat2(join21(dir2, f2)).catch(() => null);
43020
- if (st2 && st2.mtimeMs < cutoff) await rm2(join21(dir2, f2), { force: true }).catch(() => void 0);
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 readFile6(path, "utf8"));
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(join21(root, ".claude/.autonomous.approvals.json"));
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) => readFile6(join21(root, f2), "utf8").catch(() => "");
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 readdir3 } from "fs/promises";
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 readdir3(target, { withFileTypes: true });
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 readFile7 } from "fs/promises";
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 readFile7(target);
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 writeFile5 } from "fs/promises";
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 writeFile5(target, content, "utf8");
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 writeFile6, rename, rm as rm3, stat as stat3 } from "fs/promises";
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 writeFile6(target, content, "utf8");
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 rm3(target, { recursive: true, force: true });
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 readFile8, stat as stat4, writeFile as writeFile7 } from "fs/promises";
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 writeFile7(target, buf);
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 readFile8(target);
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 readFile9 } from "fs/promises";
43347
- import { execFile as execFile7 } from "child_process";
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 promisify7 } from "util";
43350
- var run7 = promisify7(execFile7);
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 readFile9(target);
44882
+ newBuf = await readFile12(target);
43374
44883
  } catch {
43375
44884
  }
43376
44885
  let oldBuf = null;
43377
44886
  try {
43378
- const { stdout } = await run7("git", ["show", `HEAD:./${base}`], {
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 execFile8 } from "child_process";
43409
- import { promisify as promisify8 } from "util";
43410
- var run8 = promisify8(execFile8);
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 run8(bin, args, { timeout: 5e3 });
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 run8(bin, ["auth", "status"], { timeout: 8e3 });
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 execFile9 } from "child_process";
43440
- import { readFile as readFile10, rm as rm4, mkdir as mkdir7 } from "fs/promises";
43441
- import { join as join22 } from "path";
43442
- import { promisify as promisify9 } from "util";
43443
- var run9 = promisify9(execFile9);
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 = join22(profileDir2, ".ssh");
43446
- const key = join22(dir2, "id_4pm");
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 readFile10(pub, "utf8")).trim();
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 run9("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
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 run9(
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 rm4(key, { force: true });
43475
- await rm4(pub, { force: true });
43476
- await run9("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
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 rm4(key, { force: true });
43485
- await rm4(pub, { force: true });
43486
- await run9("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
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 get(profileDir2) {
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 get(profileDir2);
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 join23 } from "path";
45067
+ import { join as join26 } from "path";
43559
45068
 
43560
45069
  // src/core/ai-assist.ts
43561
- import { execFile as execFile10 } from "child_process";
43562
- import { promisify as promisify10 } from "util";
43563
- var run10 = promisify10(execFile10);
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 run10(cli, aiArgs(cli, prompt), {
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(join23(ctx.profileDir, "logs"), limit);
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 existsSync14, mkdirSync as mkdirSync7, renameSync, rmSync as rmSync7 } from "fs";
43740
- import { join as join24 } from "path";
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 ? join24(ctx.profileDir, oldName) : null;
43813
- const newPath = join24(ctx.profileDir, sync.newName);
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 && existsSync14(oldPath) && !existsSync14(newPath)) {
45411
+ if (oldPath && oldName !== sync.newName && existsSync15(oldPath) && !existsSync15(newPath)) {
43819
45412
  renameSync(oldPath, newPath);
43820
45413
  } else {
43821
- if (oldPath && oldName !== sync.newName && existsSync14(oldPath)) rmSync7(oldPath, { recursive: true, force: true });
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(join24(ctx.profileDir, del.name), { recursive: true, force: true });
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 execFile11 } from "child_process";
43989
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, rmSync as rmSync8 } from "fs";
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 join25 } from "path";
43992
- import { tmpdir as tmpdir4 } from "os";
43993
- import { promisify as promisify11 } from "util";
43994
- var execFileAsync2 = promisify11(execFile11);
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 = join25(tmpdir4(), "4pm-faq-sync", randomUUID3());
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 (existsSync15(workDir)) {
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 readFile11, writeFile as writeFile8, mkdir as mkdir8, readdir as readdir4, rm as rm5 } from "fs/promises";
44341
- import { join as join26 } from "path";
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 readFile11(path, "utf8");
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 get2 = (key) => body.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
44359
- return { name: get2("name"), description: get2("description") };
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 readdir4(join26(root, AGENTS_REL), { withFileTypes: true })) {
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(join26(root, AGENTS_REL, e.name)));
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 readdir4(join26(root, SKILLS_REL), { withFileTypes: true })) {
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(join26(root, SKILLS_REL, e.name, "SKILL.md")));
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" ? join26(root, AGENTS_REL, `${n}.md`) : join26(root, SKILLS_REL, n, "SKILL.md");
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(join26(root, AGENTS_REL, "short-memory", `${n}.md`)),
44394
- readText2(join26(root, AGENTS_REL, "long-memory", `${n}.md`))
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 rm5(itemPath(root, "subagent", n), { force: true });
44404
- else await rm5(join26(root, SKILLS_REL, n), { recursive: true, force: true });
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(join26(path, ".."), { recursive: true });
44410
- await writeFile8(path, req.content, "utf8");
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 readFile12, writeFile as writeFile9 } from "fs/promises";
44419
- import { join as join27 } from "path";
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 readFile12(path, "utf8");
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 readFile12(join27(root, SECRETS_REL), "utf8"));
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(join27(root, SECURITY_REL)),
44452
- readText3(join27(root, PLACEHOLDER_REL)),
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 writeFile9(join27(root, SECURITY_REL), req.content, "utf8");
46062
+ await writeFile11(join31(root, SECURITY_REL), req.content, "utf8");
44466
46063
  } else if (req.kind === "placeholder") {
44467
- await writeFile9(join27(root, PLACEHOLDER_REL), req.content, "utf8");
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 writeFile9(join27(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
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 readFile13, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
44482
- import { dirname as dirname9, join as join28 } from "path";
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 join28(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
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 readFile13(path, "utf8"));
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 writeFile10(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
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 readFile14, writeFile as writeFile11, mkdir as mkdir10, readdir as readdir5, rm as rm6, stat as stat5 } from "fs/promises";
44524
- import { dirname as dirname10, join as join29, relative } from "path";
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 readFile14(path);
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 readdir5(d, { withFileTypes: true });
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 = join29(d, e.name);
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(join29(root, AGENTS_REL2, `${slug}.md`));
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(join29(root, AGENTS_REL2, "short-memory", `${slug}.md`));
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(join29(root, AGENTS_REL2, "long-memory", `${slug}.md`));
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 = join29(root, SKILLS_REL2, slug);
46163
+ const base = join33(root, SKILLS_REL2, slug);
44567
46164
  for (const rel of await listFilesRel(base)) {
44568
- const buf = await readMaybe(join29(base, rel));
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" ? join29(root, AGENTS_REL2, `${slug}.md`) : join29(root, SKILLS_REL2, slug);
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(join29(root, MANIFEST_REL));
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 = join29(root, MANIFEST_REL);
46201
+ const path = join33(root, MANIFEST_REL);
44605
46202
  await mkdir10(dirname10(path), { recursive: true });
44606
- await writeFile11(path, JSON.stringify(manifest, null, 2), "utf8");
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(join29(root, AGENTS_REL2), { recursive: true });
44614
- await writeFile11(join29(root, AGENTS_REL2, `${slug}.md`), agent);
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(join29(root, AGENTS_REL2, "short-memory"), { recursive: true });
44619
- await writeFile11(join29(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
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(join29(root, AGENTS_REL2, "long-memory"), { recursive: true });
44624
- await writeFile11(join29(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
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 = join29(root, SKILLS_REL2, slug);
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 = join29(base, rel);
46227
+ const dest = join33(base, rel);
44631
46228
  await mkdir10(dirname10(dest), { recursive: true });
44632
- await writeFile11(dest, buf);
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 rm6(join29(root, AGENTS_REL2, `${n}.md`), { force: true });
44697
- await rm6(join29(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
44698
- await rm6(join29(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
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 rm6(join29(root, SKILLS_REL2, n), { recursive: true, force: true });
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 readFile15, readdir as readdir6 } from "fs/promises";
44712
- import { join as join30, relative as relative2, dirname as dirname11, resolve as resolve12, extname as extname2, basename as basename6 } from "path";
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 readdir6(dir2, { withFileTypes: true });
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 = join30(dir2, e.name);
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 readFile15(f2, "utf8");
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 readFile15(f2, "utf8");
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 readFile16, writeFile as writeFile12, statfs as statfs2, mkdir as mkdir11 } from "fs/promises";
44852
- import { existsSync as existsSync16 } from "fs";
44853
- import { execFile as execFile12, spawn as spawn10 } from "child_process";
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 join31 } from "path";
44856
- import { promisify as promisify12 } from "util";
44857
- var run11 = promisify12(execFile12);
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 run11(cmd, args, { timeout: 8e3 });
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 readFile16(join31(root, RAG_REL, "install.log"), "utf8");
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 readFile16(join31(root, RAG_REL, ".installed"), "utf8")).trim();
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: existsSync16(join31(root, RAG_REL, ".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 = existsSync16(join31(root, RAG_REL, ".indexing"));
46553
+ const indexing = existsSync17(join35(root, RAG_REL, ".indexing"));
44957
46554
  try {
44958
- const m2 = JSON.parse(await readFile16(join31(root, RAG_REL, ".index.json"), "utf8"));
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(join31(root, RAG_REL), { recursive: true }).catch(() => void 0);
45026
- await writeFile12(join31(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
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 (!existsSync16(join31(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
45032
- if (existsSync16(join31(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
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 (!existsSync16(join31(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
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 run11(
46652
+ const { stdout } = await run14(
45056
46653
  py.bin,
45057
- [join31(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k2, 1), 20))],
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 (existsSync16(join31(root, RAG_REL, ".installing"))) return { ok: true, started: true };
46669
+ if (existsSync17(join35(root, RAG_REL, ".installing"))) return { ok: true, started: true };
45073
46670
  try {
45074
- await mkdir11(join31(root, RAG_REL), { recursive: true });
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 existsSync17, readFileSync as readFileSync16 } from "fs";
45102
- import { join as join32 } from "path";
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 = join32(profileDir2, "config.json");
45120
- if (existsSync17(path)) {
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: `Invalid JSON: ${err.message}` };
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.json must be a JSON object." };
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
- const client = this;
47439
+ onReconnectVerifyClaim(this.hctx);
47440
+ const servedRoot = () => this.physicRoot;
45843
47441
  startGitSnapshots({
45844
47442
  get physicRoot() {
45845
- return client.physicRoot;
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 (!existsSync18(folder)) {
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 ? join33(this.context.profileDir, safe) : null;
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 ? existsSync18(physicPath) : void 0,
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(join33(this.context.profileDir, "logs"));
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 join34 } from "path";
47900
+ import { join as join38 } from "path";
46303
47901
  function lockPath(profileDir2) {
46304
- return join34(profileDir2, "start.lock");
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 join36 } from "path";
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 join35 } from "path";
48255
+ import { join as join39 } from "path";
46658
48256
  function controlTokenPath(profileDir2) {
46659
- return join35(profileDir2, CONTROL_TOKEN_FILE);
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 = join36(profileDir2, CONTROL_SOCKET_FILE);
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 build = Number(parts[2] ?? 0);
48672
+ const build2 = Number(parts[2] ?? 0);
47075
48673
  if (major < 10) {
47076
48674
  return true;
47077
48675
  }
47078
- if (major === 10 && build < 10586) {
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 existsSync20, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
52446
- import { join as join37 } from "path";
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 join37(profileDir2, "input-history.json");
54047
+ return join41(profileDir2, "input-history.json");
52450
54048
  }
52451
54049
  function loadInputHistory(profileDir2) {
52452
54050
  const path = historyPath(profileDir2);
52453
- if (!existsSync20(path)) return [];
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 stamp = line.kind === "aireq" ? START_STAMP_RE.exec(before)?.[1] ?? "" : "";
52577
- const name = stamp ? before.slice(stamp.length) : before;
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
- stamp ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: stamp }) : null,
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: join38(profileDir2, "logs"),
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 join39 } from "path";
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 = join39(profileDir2, CONTROL_SOCKET_FILE);
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 join40 } from "path";
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 = join40(profileDir2, CONTROL_SOCKET_FILE);
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 join41 } from "path";
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(join41(profileDir2, CONTROL_SOCKET_FILE));
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", () => {