@4pm/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +888 -128
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1995,14 +1995,14 @@ var require_react_development = __commonJS({
1995
1995
  var thenableResult = result;
1996
1996
  var wasAwaited = false;
1997
1997
  var thenable = {
1998
- then: function(resolve5, reject) {
1998
+ then: function(resolve6, reject) {
1999
1999
  wasAwaited = true;
2000
2000
  thenableResult.then(function(returnValue2) {
2001
2001
  popActScope(prevActScopeDepth);
2002
2002
  if (actScopeDepth === 0) {
2003
- recursivelyFlushAsyncActWork(returnValue2, resolve5, reject);
2003
+ recursivelyFlushAsyncActWork(returnValue2, resolve6, reject);
2004
2004
  } else {
2005
- resolve5(returnValue2);
2005
+ resolve6(returnValue2);
2006
2006
  }
2007
2007
  }, function(error2) {
2008
2008
  popActScope(prevActScopeDepth);
@@ -2032,20 +2032,20 @@ var require_react_development = __commonJS({
2032
2032
  ReactCurrentActQueue.current = null;
2033
2033
  }
2034
2034
  var _thenable = {
2035
- then: function(resolve5, reject) {
2035
+ then: function(resolve6, reject) {
2036
2036
  if (ReactCurrentActQueue.current === null) {
2037
2037
  ReactCurrentActQueue.current = [];
2038
- recursivelyFlushAsyncActWork(returnValue, resolve5, reject);
2038
+ recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
2039
2039
  } else {
2040
- resolve5(returnValue);
2040
+ resolve6(returnValue);
2041
2041
  }
2042
2042
  }
2043
2043
  };
2044
2044
  return _thenable;
2045
2045
  } else {
2046
2046
  var _thenable2 = {
2047
- then: function(resolve5, reject) {
2048
- resolve5(returnValue);
2047
+ then: function(resolve6, reject) {
2048
+ resolve6(returnValue);
2049
2049
  }
2050
2050
  };
2051
2051
  return _thenable2;
@@ -2061,7 +2061,7 @@ var require_react_development = __commonJS({
2061
2061
  actScopeDepth = prevActScopeDepth;
2062
2062
  }
2063
2063
  }
2064
- function recursivelyFlushAsyncActWork(returnValue, resolve5, reject) {
2064
+ function recursivelyFlushAsyncActWork(returnValue, resolve6, reject) {
2065
2065
  {
2066
2066
  var queue = ReactCurrentActQueue.current;
2067
2067
  if (queue !== null) {
@@ -2070,16 +2070,16 @@ var require_react_development = __commonJS({
2070
2070
  enqueueTask(function() {
2071
2071
  if (queue.length === 0) {
2072
2072
  ReactCurrentActQueue.current = null;
2073
- resolve5(returnValue);
2073
+ resolve6(returnValue);
2074
2074
  } else {
2075
- recursivelyFlushAsyncActWork(returnValue, resolve5, reject);
2075
+ recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
2076
2076
  }
2077
2077
  });
2078
2078
  } catch (error2) {
2079
2079
  reject(error2);
2080
2080
  }
2081
2081
  } else {
2082
- resolve5(returnValue);
2082
+ resolve6(returnValue);
2083
2083
  }
2084
2084
  }
2085
2085
  }
@@ -24450,7 +24450,7 @@ function resolveProfileArg(args) {
24450
24450
  const rest = [...args.slice(0, index), ...args.slice(index + 2)];
24451
24451
  return [name, rest];
24452
24452
  }
24453
- return [process.env.FOURPM_PROFILE ?? null, args];
24453
+ return [process.env.FOURPM_PROFILE || null, args];
24454
24454
  }
24455
24455
  function listProfiles() {
24456
24456
  const base = join(homedir(), ".4pm", "profiles");
@@ -24842,6 +24842,30 @@ function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
24842
24842
  const exp = Math.min(baseMs * 2 ** attempt, maxMs);
24843
24843
  return Math.floor(exp / 2 + Math.random() * (exp / 2));
24844
24844
  }
24845
+ function looksLikeJsonOrCode(text) {
24846
+ const t = text.replace(/^\s+/, "");
24847
+ if (t.startsWith("```")) return "code";
24848
+ if (t.startsWith("{") || t.startsWith("[")) return "json";
24849
+ return null;
24850
+ }
24851
+ function prettyResult(text) {
24852
+ const trimmed = text.trim();
24853
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
24854
+ try {
24855
+ return JSON.stringify(JSON.parse(trimmed), null, 2);
24856
+ } catch {
24857
+ const start = trimmed.search(/[{[]/);
24858
+ const end = Math.max(trimmed.lastIndexOf("}"), trimmed.lastIndexOf("]"));
24859
+ if (start >= 0 && end > start) {
24860
+ try {
24861
+ return JSON.stringify(JSON.parse(trimmed.slice(start, end + 1)), null, 2);
24862
+ } catch {
24863
+ }
24864
+ }
24865
+ }
24866
+ }
24867
+ return trimmed;
24868
+ }
24845
24869
 
24846
24870
  // ../../22-packages/11-shared/11-constants/src/roles.ts
24847
24871
  var Role = {
@@ -24880,6 +24904,8 @@ var PermissionCode = {
24880
24904
  // ADMIN resets a sub-account's password directly (user-0010)
24881
24905
  USER_SET_PASSWORD: "user.set_password",
24882
24906
  USER_DELETE: "user.delete",
24907
+ // org-level pause/resume of a user (ADR-0093)
24908
+ USER_PAUSE: "user.pause",
24883
24909
  // invite users by email (ADR-0021)
24884
24910
  USER_INVITE: "user.invite",
24885
24911
  // team
@@ -24904,6 +24930,11 @@ var PermissionCode = {
24904
24930
  PROJECT_DETACH_USER: "project.detach_user",
24905
24931
  PROJECT_ATTACH_MANAGER: "project.attach_manager",
24906
24932
  PROJECT_DETACH_MANAGER: "project.detach_manager",
24933
+ // project lifecycle (ADR-0092): pause+resume / close+reopen
24934
+ PROJECT_PAUSE: "project.pause",
24935
+ PROJECT_CLOSE: "project.close",
24936
+ // project-membership pause+resume of a direct member (ADR-0093)
24937
+ PROJECT_PAUSE_MEMBER: "project.pause_member",
24907
24938
  PROJECT_AI_ASSIST: "project.ai_assist",
24908
24939
  PROJECT_MEMO_READ: "project.memo_read",
24909
24940
  PROJECT_MEMO_WRITE: "project.memo_write",
@@ -24971,6 +25002,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
24971
25002
  P.USER_READ,
24972
25003
  P.USER_UPDATE,
24973
25004
  P.USER_DELETE,
25005
+ P.USER_PAUSE,
24974
25006
  P.USER_INVITE,
24975
25007
  P.QUOTA_READ,
24976
25008
  P.QUOTA_MANAGE,
@@ -24994,6 +25026,9 @@ var ROLE_DEFAULT_PERMISSIONS = {
24994
25026
  P.PROJECT_DETACH_USER,
24995
25027
  P.PROJECT_ATTACH_MANAGER,
24996
25028
  P.PROJECT_DETACH_MANAGER,
25029
+ P.PROJECT_PAUSE,
25030
+ P.PROJECT_CLOSE,
25031
+ P.PROJECT_PAUSE_MEMBER,
24997
25032
  P.PROJECT_AI_ASSIST,
24998
25033
  P.PROJECT_MEMO_READ,
24999
25034
  P.PROJECT_MEMO_WRITE,
@@ -25060,7 +25095,9 @@ var ROLE_DEFAULT_PERMISSIONS = {
25060
25095
  P.INTEGRATION_PAGE_WRITE
25061
25096
  ]
25062
25097
  };
25063
- var TL_DEFAULT_PERMISSIONS = ROLE_DEFAULT_PERMISSIONS[Role.PM].filter((code) => code !== P.USER_DELETE);
25098
+ var TL_DEFAULT_PERMISSIONS = ROLE_DEFAULT_PERMISSIONS[Role.PM].filter(
25099
+ (code) => code !== P.USER_DELETE && code !== P.USER_PAUSE
25100
+ );
25064
25101
  var PROJECT_SCOPE_CHECKED_CODES = [
25065
25102
  P.PROJECT_UPDATE,
25066
25103
  P.PROJECT_DELETE,
@@ -25070,6 +25107,9 @@ var PROJECT_SCOPE_CHECKED_CODES = [
25070
25107
  P.PROJECT_DETACH_USER,
25071
25108
  P.PROJECT_ATTACH_MANAGER,
25072
25109
  P.PROJECT_DETACH_MANAGER,
25110
+ P.PROJECT_PAUSE,
25111
+ P.PROJECT_CLOSE,
25112
+ P.PROJECT_PAUSE_MEMBER,
25073
25113
  P.PROJECT_AI_ASSIST
25074
25114
  ];
25075
25115
  var SELF_SERVICE_CODES = [
@@ -25104,6 +25144,9 @@ var TEAM_GRANTABLE_CODES = [
25104
25144
  P.PROJECT_DETACH_USER,
25105
25145
  P.PROJECT_ATTACH_MANAGER,
25106
25146
  P.PROJECT_DETACH_MANAGER,
25147
+ P.PROJECT_PAUSE,
25148
+ P.PROJECT_CLOSE,
25149
+ P.PROJECT_PAUSE_MEMBER,
25107
25150
  P.PROJECT_AI_ASSIST,
25108
25151
  P.PROJECT_MEMO_READ,
25109
25152
  P.PROJECT_MEMO_WRITE,
@@ -25122,6 +25165,9 @@ var PROJECT_GRANTABLE_CODES = [
25122
25165
  P.PROJECT_DETACH_USER,
25123
25166
  P.PROJECT_ATTACH_MANAGER,
25124
25167
  P.PROJECT_DETACH_MANAGER,
25168
+ P.PROJECT_PAUSE,
25169
+ P.PROJECT_CLOSE,
25170
+ P.PROJECT_PAUSE_MEMBER,
25125
25171
  P.PROJECT_AI_ASSIST,
25126
25172
  P.PROJECT_MEMO_READ,
25127
25173
  P.PROJECT_MEMO_WRITE,
@@ -25178,6 +25224,12 @@ var QuotaOnExceed = {
25178
25224
  WARN: "warn"
25179
25225
  };
25180
25226
 
25227
+ // ../../22-packages/11-shared/11-constants/src/billing.ts
25228
+ var PaymentProvider = {
25229
+ STRIPE: "stripe",
25230
+ PAYPAL: "paypal"
25231
+ };
25232
+
25181
25233
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
25182
25234
  var external_exports = {};
25183
25235
  __export(external_exports, {
@@ -29228,6 +29280,7 @@ var baseRequestSchema = external_exports.object({
29228
29280
  order: external_exports.enum(["asc", "desc"]).default("desc")
29229
29281
  });
29230
29282
  var pinnedFilterSchema = external_exports.enum(["true", "false"]).transform((v) => v === "true").optional();
29283
+ var deletedFilterSchema = external_exports.enum(["true", "false"]).transform((v) => v === "true").optional();
29231
29284
 
29232
29285
  // ../../22-packages/11-shared/41-validation/src/schemas.ts
29233
29286
  var passwordSchema = external_exports.string().min(8, "password: at least 8 characters").max(72, "password: at most 72 characters").regex(/[a-zA-Z]/, "password: needs at least 1 letter").regex(/[0-9]/, "password: needs at least 1 digit");
@@ -29243,12 +29296,18 @@ var ipAllowlistSchema = external_exports.array(ipCidrSchema).max(50).default([])
29243
29296
  var hexTokenSchema = external_exports.string().regex(/^[0-9a-f]{64}$/, "token: invalid format");
29244
29297
 
29245
29298
  // ../../22-packages/11-shared/31-dto/src/auth.ts
29299
+ var paymentProviderSchema = external_exports.enum([
29300
+ PaymentProvider.STRIPE,
29301
+ PaymentProvider.PAYPAL
29302
+ ]);
29246
29303
  var registerRequestSchema = external_exports.object({
29247
29304
  username: usernameSchema,
29248
29305
  email: emailSchema,
29249
29306
  password: passwordSchema,
29250
29307
  phone: phoneSchema,
29251
- ipAllowlist: ipAllowlistSchema.optional()
29308
+ ipAllowlist: ipAllowlistSchema.optional(),
29309
+ planCode: external_exports.string().min(1).max(50).optional(),
29310
+ provider: paymentProviderSchema.optional()
29252
29311
  });
29253
29312
  var mailTokenRequestSchema = external_exports.object({
29254
29313
  token: hexTokenSchema
@@ -29306,7 +29365,9 @@ var rolesSchema = external_exports.array(external_exports.enum(ALL_ROLES)).min(1
29306
29365
  message: "ADMIN/MACHINE cannot be combined with other roles"
29307
29366
  });
29308
29367
  var listUsersQuerySchema = baseRequestSchema.extend({
29309
- role: external_exports.enum(ALL_ROLES).optional()
29368
+ role: external_exports.enum(ALL_ROLES).optional(),
29369
+ /** ADMIN-only Trash: list soft-deleted users (ADR-0109). */
29370
+ deleted: deletedFilterSchema
29310
29371
  });
29311
29372
  var createUserRequestSchema = external_exports.object({
29312
29373
  username: usernameSchema,
@@ -29342,7 +29403,9 @@ var contactChangeConfirmSchema = external_exports.object({
29342
29403
 
29343
29404
  // ../../22-packages/11-shared/31-dto/src/team.ts
29344
29405
  var listTeamsQuerySchema = baseRequestSchema.extend({
29345
- pinned: pinnedFilterSchema
29406
+ pinned: pinnedFilterSchema,
29407
+ /** ADMIN-only Trash: list soft-deleted teams (ADR-0109). */
29408
+ deleted: deletedFilterSchema
29346
29409
  });
29347
29410
  var createTeamRequestSchema = external_exports.object({
29348
29411
  name: external_exports.string().min(1).max(100),
@@ -29360,7 +29423,9 @@ var attachTeamUserRequestSchema = external_exports.object({
29360
29423
 
29361
29424
  // ../../22-packages/11-shared/31-dto/src/project.ts
29362
29425
  var listProjectsQuerySchema = baseRequestSchema.extend({
29363
- pinned: pinnedFilterSchema
29426
+ pinned: pinnedFilterSchema,
29427
+ /** ADMIN-only Trash: list soft-deleted projects (ADR-0109). */
29428
+ deleted: deletedFilterSchema
29364
29429
  });
29365
29430
  var recentProjectsQuerySchema = external_exports.object({
29366
29431
  limit: external_exports.coerce.number().int().min(1).max(20).default(5)
@@ -29370,11 +29435,37 @@ var createProjectRequestSchema = external_exports.object({
29370
29435
  description: external_exports.string().max(2e3).optional(),
29371
29436
  machineUserId: external_exports.string().uuid().optional()
29372
29437
  });
29438
+ var DEFAULT_OUTBOUND_RULES = {
29439
+ secret: [
29440
+ String.raw`/\b(api[_-]?key|secret|password|passwd|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i`,
29441
+ String.raw`/AKIA[0-9A-Z]{16}/`,
29442
+ String.raw`/\b(gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|sk-[A-Za-z0-9]{20,})\b/`,
29443
+ String.raw`/-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/`,
29444
+ String.raw`/\b[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)\s*=\s*\S{6,}/`
29445
+ ],
29446
+ environment: [
29447
+ String.raw`/\b(?:ssh|scp|rsync)\s+[\w.-]+@/i`,
29448
+ String.raw`/\bsudo\b/i`,
29449
+ String.raw`/\bkubectl\b/i`,
29450
+ String.raw`/\bdocker\s+(?:push|-H|--host)\b/i`,
29451
+ String.raw`/\b(?:aws|gcloud|az)\s+\w+/i`,
29452
+ String.raw`/\bcurl\b[^\n]*\bhttps?:\/\/(?!localhost|127\.0\.0\.1)/i`
29453
+ ]
29454
+ };
29455
+ function compileRulePattern(entry) {
29456
+ const literal = /^\/(.+)\/([gimsuy]*)$/s.exec(entry.trim());
29457
+ if (literal) return new RegExp(literal[1] ?? "", literal[2] ?? "");
29458
+ return new RegExp(entry.trim());
29459
+ }
29373
29460
  var updateProjectRequestSchema = external_exports.object({
29374
29461
  name: external_exports.string().min(1).max(100).optional(),
29375
29462
  description: external_exports.string().max(2e3).optional(),
29376
29463
  // Mid-tier IP allowlist (ADR-0050) — PM(manage)/ADMIN only; must fall within org.
29377
- ipAllowlist: ipAllowlistSchema.optional()
29464
+ ipAllowlist: ipAllowlistSchema.optional(),
29465
+ // Messenger retention in days (0 = keep forever) — manager only (ADR-0078).
29466
+ messengerRetentionDays: external_exports.number().int().min(0).max(3650).optional(),
29467
+ // Project settings (ADR-0081) — key-level shallow-merge; `tokens` clamped on read.
29468
+ settings: external_exports.record(external_exports.unknown()).optional()
29378
29469
  });
29379
29470
  var MEMO_MAX_LENGTH = 65536;
29380
29471
  var putMemoRequestSchema = external_exports.object({
@@ -29390,9 +29481,15 @@ var updateMemoItemSchema = external_exports.object({
29390
29481
  }).refine((v) => v.content !== void 0 || v.done !== void 0, {
29391
29482
  message: "content or done required"
29392
29483
  });
29484
+ var putSpecDraftRequestSchema = external_exports.object({
29485
+ spec: external_exports.record(external_exports.unknown()),
29486
+ specVersion: external_exports.number().int().positive().optional()
29487
+ });
29393
29488
  var repoSpecSchema = external_exports.object({
29394
29489
  /** Role label (e.g. "docs" / "web" / "server") — descriptive, not constrained. */
29395
29490
  role: external_exports.string().max(60).optional().default(""),
29491
+ /** Free-form description of what this repo holds (optional). */
29492
+ desc: external_exports.string().max(500).optional().default(""),
29396
29493
  /** Exactly one repo of the project is the primary. */
29397
29494
  primary: external_exports.boolean().optional().default(false),
29398
29495
  /** `create` a new repo (provider) or use an `existing` one (url). */
@@ -29404,14 +29501,44 @@ var repoSpecSchema = external_exports.object({
29404
29501
  /** Existing repo url to clone/link (op=existing). */
29405
29502
  url: external_exports.string().max(500).optional().default(""),
29406
29503
  /** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
29407
- subdir: external_exports.string().max(120).optional().default("")
29504
+ subdir: external_exports.string().max(120).optional().default(""),
29505
+ /** Default branch for this repo (empty ⇒ provider default, e.g. `main`). */
29506
+ defaultBranch: external_exports.string().max(100).optional().default(""),
29507
+ /** `.gitignore` template name for this repo (empty ⇒ none). */
29508
+ gitignore: external_exports.string().max(40).optional().default(""),
29509
+ /** Commit-message convention for this repo (empty ⇒ unspecified). */
29510
+ commitConvention: external_exports.string().max(40).optional().default("")
29511
+ });
29512
+ var specFieldEnvelopeSchema = external_exports.object({
29513
+ id: external_exports.string().min(1),
29514
+ name: external_exports.string().optional().default(""),
29515
+ description: external_exports.string().optional().default(""),
29516
+ type: external_exports.string().optional().default("text"),
29517
+ options: external_exports.array(external_exports.string()).optional(),
29518
+ value: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional().default("")
29519
+ });
29520
+ var subagentSpecSchema = external_exports.object({
29521
+ name: external_exports.string().max(120).optional().default(""),
29522
+ description: external_exports.string().max(2e3).optional().default("")
29408
29523
  });
29409
29524
  var projectSpecSchema = external_exports.object({
29410
29525
  specVersion: external_exports.number().int().positive(),
29411
- id: external_exports.string().min(1),
29412
- name: external_exports.string().min(1),
29413
- repos: external_exports.array(repoSpecSchema).optional()
29414
- }).passthrough().superRefine((v, ctx) => {
29526
+ fields: external_exports.array(specFieldEnvelopeSchema),
29527
+ repos: external_exports.array(repoSpecSchema).optional().default([]),
29528
+ subagents: external_exports.array(subagentSpecSchema).optional().default([]),
29529
+ meta: external_exports.record(external_exports.unknown()).optional()
29530
+ }).superRefine((v, ctx) => {
29531
+ const byId = new Map(v.fields.map((f) => [f.id, f.value]));
29532
+ for (const key of ["id", "name"]) {
29533
+ const val = byId.get(key);
29534
+ if (typeof val !== "string" || val.trim() === "") {
29535
+ ctx.addIssue({
29536
+ code: external_exports.ZodIssueCode.custom,
29537
+ path: ["fields"],
29538
+ message: `the "${key}" field is required`
29539
+ });
29540
+ }
29541
+ }
29415
29542
  if (v.repos && v.repos.length > 0) {
29416
29543
  const primaries = v.repos.filter((r) => r.primary).length;
29417
29544
  if (primaries !== 1) {
@@ -29425,8 +29552,6 @@ var projectSpecSchema = external_exports.object({
29425
29552
  });
29426
29553
  var createFromSpecRequestSchema = external_exports.object({
29427
29554
  projectId: external_exports.string().uuid(),
29428
- /** Target directory on the worker (from FsPicker — machine-0007). */
29429
- path: external_exports.string().min(1).max(500),
29430
29555
  spec: projectSpecSchema
29431
29556
  });
29432
29557
  var addRepoRequestSchema = external_exports.object({
@@ -29436,17 +29561,14 @@ var addRepoRequestSchema = external_exports.object({
29436
29561
  repoUrl: external_exports.string().url().max(500).optional(),
29437
29562
  provider: external_exports.enum(["gh", "glab"]).optional()
29438
29563
  });
29439
- var specSuggestRequestSchema = external_exports.object({
29440
- projectId: external_exports.string().uuid(),
29441
- machineLinkId: external_exports.string().uuid(),
29442
- field: external_exports.string().min(1).max(100),
29443
- /** The spec-so-far (may be incomplete) for context. */
29444
- spec: external_exports.record(external_exports.unknown())
29564
+
29565
+ // ../../22-packages/11-shared/31-dto/src/activity.ts
29566
+ var listActivityQuerySchema = baseRequestSchema.extend({
29567
+ /** Filter by action prefix, e.g. "project." */
29568
+ action: external_exports.string().max(50).optional()
29445
29569
  });
29446
- var specReviewRequestSchema = external_exports.object({
29447
- projectId: external_exports.string().uuid(),
29448
- machineLinkId: external_exports.string().uuid(),
29449
- spec: external_exports.record(external_exports.unknown())
29570
+ var lifecycleActionRequestSchema = external_exports.object({
29571
+ note: external_exports.string().max(500).optional()
29450
29572
  });
29451
29573
 
29452
29574
  // ../../22-packages/11-shared/31-dto/src/permission.ts
@@ -29505,17 +29627,39 @@ var updateMachineLinkRequestSchema = external_exports.object({
29505
29627
  });
29506
29628
 
29507
29629
  // ../../22-packages/11-shared/31-dto/src/command.ts
29630
+ var COMMAND_MAX_LEN = 8e3;
29631
+ var AI_PROMPT_MAX_LEN = 2e5;
29508
29632
  var dispatchCommandRequestSchema = external_exports.object({
29509
29633
  projectId: external_exports.string().uuid(),
29510
29634
  machineLinkId: external_exports.string().uuid(),
29511
- // In AI mode `command` carries the raw prompt (can be long), else an executable name.
29512
- command: external_exports.string().min(1).max(8e3),
29635
+ // In AI mode `command` carries the raw prompt (can be long — the spec-assist prompts
29636
+ // embed the whole self-describing spec envelope, ADR-0106), else an executable name.
29637
+ command: external_exports.string().min(1).max(AI_PROMPT_MAX_LEN),
29513
29638
  args: external_exports.array(external_exports.string().max(1e3)).max(50).optional(),
29514
29639
  /**
29515
29640
  * AI prompt mode: run `command` as a prompt through the cli's AI CLI (profile
29516
29641
  * failover) instead of spawning it verbatim (mirrors a locally-typed prompt).
29517
29642
  */
29518
- ai: external_exports.boolean().optional()
29643
+ ai: external_exports.boolean().optional(),
29644
+ /**
29645
+ * Short human label shown to OTHER tabs watching this cli's activity feed (ADR-0101):
29646
+ * e.g. "AI review" so the Console shows a friendly line instead of the raw prompt.
29647
+ */
29648
+ label: external_exports.string().max(120).optional()
29649
+ }).superRefine((v, ctx) => {
29650
+ if (!v.ai && v.command.length > COMMAND_MAX_LEN) {
29651
+ ctx.addIssue({
29652
+ code: external_exports.ZodIssueCode.too_big,
29653
+ maximum: COMMAND_MAX_LEN,
29654
+ type: "string",
29655
+ inclusive: true,
29656
+ path: ["command"],
29657
+ message: `command too long (max ${COMMAND_MAX_LEN})`
29658
+ });
29659
+ }
29660
+ });
29661
+ var listCommandsQuerySchema = baseRequestSchema.extend({
29662
+ projectId: external_exports.string().uuid()
29519
29663
  });
29520
29664
 
29521
29665
  // ../../22-packages/11-shared/31-dto/src/invitation.ts
@@ -29603,10 +29747,27 @@ var planLimitSchema = external_exports.object({
29603
29747
  period: external_exports.enum([QuotaPeriod.DAY, QuotaPeriod.MONTH]),
29604
29748
  onExceed: external_exports.enum([QuotaOnExceed.BLOCK, QuotaOnExceed.WARN]).default(QuotaOnExceed.BLOCK)
29605
29749
  });
29750
+ var planEntitlementsSchema = external_exports.object({
29751
+ /** Max human seats (MACHINE excluded — ADR-0041); null = unlimited. */
29752
+ maxUsers: external_exports.number().int().min(1).nullable().default(null),
29753
+ /** Max projects; null = unlimited. */
29754
+ maxProjects: external_exports.number().int().min(1).nullable().default(null),
29755
+ /** Allowed to create teams. */
29756
+ teams: external_exports.boolean().default(false),
29757
+ /** Allowed to use Community/4rum. */
29758
+ community: external_exports.boolean().default(false),
29759
+ /** Retention cap (days) for log/command/chat; null = unlimited. */
29760
+ retentionDays: external_exports.number().int().min(1).nullable().default(null)
29761
+ });
29762
+ var checkoutRequestSchema = external_exports.object({
29763
+ planCode: external_exports.string().min(1).max(50),
29764
+ provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
29765
+ });
29606
29766
  var upsertPlanRequestSchema = external_exports.object({
29607
29767
  name: external_exports.string().min(1).max(100),
29608
29768
  price: external_exports.number().int().min(0).default(0),
29609
- limits: external_exports.array(planLimitSchema).default([])
29769
+ limits: external_exports.array(planLimitSchema).default([]),
29770
+ entitlements: planEntitlementsSchema.optional()
29610
29771
  });
29611
29772
  var assignPlanRequestSchema = external_exports.object({
29612
29773
  planCode: external_exports.string().min(1).max(50),
@@ -29670,6 +29831,12 @@ var WsChannels = {
29670
29831
  FS_READ: "fs.read",
29671
29832
  GIT_DIFF: "git.diff",
29672
29833
  GIT_ENV: "git.env",
29834
+ // git history browse (read-through cli — ADR-0089): repos on the physic project,
29835
+ // commit log, changed files of a commit, parent↔commit diff of one file.
29836
+ GIT_REPOS: "git.repos",
29837
+ GIT_LOG: "git.log",
29838
+ GIT_COMMIT: "git.commit",
29839
+ GIT_COMMIT_DIFF: "git.commit-diff",
29673
29840
  PROJECT_CREATE: "project.create",
29674
29841
  PROJECT_ADD: "project.add",
29675
29842
  PROJECT_PROGRESS: "project.progress",
@@ -29681,8 +29848,8 @@ var WsChannels = {
29681
29848
  PHYSIC_DELETE: "physic.delete",
29682
29849
  // command lifecycle
29683
29850
  COMMAND_ANNOUNCE: "command.announce",
29684
- AI_SUGGEST: "ai.suggest",
29685
- AI_REVIEW: "ai.review",
29851
+ // AI spec-assist (suggest/review/compose) moved off dedicated ai.* channels to
29852
+ // command.dispatch({ ai:true }) → the profile-failover AI path (ADR-0100).
29686
29853
  // quota & usage (ADR-0020)
29687
29854
  QUOTA_CHECK: "quota.check",
29688
29855
  USAGE_REPORT: "usage.report",
@@ -29691,7 +29858,11 @@ var WsChannels = {
29691
29858
  // cli → server: durable per-command history record on finish (ADR-0072)
29692
29859
  COMMAND_HISTORY: "command.history",
29693
29860
  // server → cli (reply): tail the cli's own JSONL logs on demand (ADR-0072)
29694
- LOG_READ: "log.read"
29861
+ LOG_READ: "log.read",
29862
+ // outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
29863
+ REVIEW_REQUEST: "review.request",
29864
+ REVIEW_EVALUATE: "review.evaluate",
29865
+ REVIEW_RESULT: "review.result"
29695
29866
  };
29696
29867
 
29697
29868
  // ../../22-packages/21-protocol/21-websocket/src/crypto.ts
@@ -30026,7 +30197,7 @@ async function runCommand(dispatch, emit) {
30026
30197
  });
30027
30198
  child.stdout?.on("data", (data) => batcher.push(data.toString("utf8")));
30028
30199
  child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
30029
- await new Promise((resolve5) => {
30200
+ await new Promise((resolve6) => {
30030
30201
  child.on("close", (exitCode) => {
30031
30202
  batcher.flush();
30032
30203
  emit({
@@ -30036,7 +30207,7 @@ async function runCommand(dispatch, emit) {
30036
30207
  done: true,
30037
30208
  exitCode: exitCode ?? -1
30038
30209
  });
30039
- resolve5();
30210
+ resolve6();
30040
30211
  });
30041
30212
  child.on("error", (err) => {
30042
30213
  emit({
@@ -30047,7 +30218,7 @@ async function runCommand(dispatch, emit) {
30047
30218
  done: true,
30048
30219
  exitCode: -1
30049
30220
  });
30050
- resolve5();
30221
+ resolve6();
30051
30222
  });
30052
30223
  });
30053
30224
  }
@@ -30127,10 +30298,10 @@ function isSessionLimit(text) {
30127
30298
  );
30128
30299
  }
30129
30300
  function runAttempt(commandId, cmd, args, cwd2, env3, onChunk) {
30130
- return new Promise((resolve5) => {
30301
+ return new Promise((resolve6) => {
30131
30302
  void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
30132
30303
  if (out.chunk) onChunk(out.chunk);
30133
- if (out.done) resolve5(out.exitCode ?? -1);
30304
+ if (out.done) resolve6(out.exitCode ?? -1);
30134
30305
  });
30135
30306
  });
30136
30307
  }
@@ -30291,16 +30462,16 @@ async function fetchUsage(token) {
30291
30462
  }
30292
30463
  }
30293
30464
  function runUsageCli(dir2, aiCli2) {
30294
- return new Promise((resolve5) => {
30465
+ return new Promise((resolve6) => {
30295
30466
  let out = "";
30296
30467
  let child = null;
30297
30468
  const finish = () => {
30298
30469
  clearTimeout(timer);
30299
- resolve5(out);
30470
+ resolve6(out);
30300
30471
  };
30301
30472
  const timer = setTimeout(() => {
30302
30473
  child?.kill();
30303
- resolve5(out);
30474
+ resolve6(out);
30304
30475
  }, 3e4);
30305
30476
  timer.unref?.();
30306
30477
  try {
@@ -30423,26 +30594,95 @@ async function runAi(prompt) {
30423
30594
  });
30424
30595
  return parseAi(cli, stdout, prompt);
30425
30596
  }
30426
- async function aiSuggest(req) {
30597
+ async function aiGenerate(prompt) {
30598
+ const { text } = await runAi(prompt);
30599
+ return text;
30600
+ }
30601
+ async function aiOutboundReview(input, allowedRepos, projectPath) {
30602
+ const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
30603
+ ${input}`;
30604
+ const { text, tokens } = await runAi(prompt);
30605
+ const start = text.indexOf("{");
30606
+ const end = text.lastIndexOf("}");
30607
+ if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
30427
30608
  try {
30428
- const prompt = `You are helping fill a software project specification. Suggest a concise value for the field "${req.field}". Reply with ONLY the suggested text, no preamble. Context (spec so far, JSON):
30429
- ${JSON.stringify(req.spec)}`;
30430
- const { text, tokens } = await runAi(prompt);
30431
- return { value: text, tokens };
30432
- } catch (err) {
30433
- return { value: "", error: err instanceof Error ? err.message : String(err) };
30609
+ const j = JSON.parse(text.slice(start, end + 1));
30610
+ const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
30611
+ return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
30612
+ } catch {
30613
+ return { ok: false, reasons: ["ai_parse"], tokens };
30434
30614
  }
30435
30615
  }
30436
- async function aiReview(req) {
30437
- try {
30438
- const prompt = `Review this software project specification for inconsistencies, gaps or contradictions. Reply with a short list, one issue per line, no preamble. If it looks consistent, reply exactly "OK". Spec (JSON):
30439
- ${JSON.stringify(req.spec)}`;
30440
- const { text: out, tokens } = await runAi(prompt);
30441
- const issues = out === "OK" ? [] : out.split("\n").map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
30442
- return { issues, tokens };
30443
- } catch (err) {
30444
- return { issues: [], error: err instanceof Error ? err.message : String(err) };
30616
+
30617
+ // src/core/outbound-review.ts
30618
+ function compilePatterns(sources, fallback) {
30619
+ const list = sources ?? fallback;
30620
+ const out = [];
30621
+ for (const src of list) {
30622
+ try {
30623
+ out.push(compileRulePattern(src));
30624
+ } catch (err) {
30625
+ logger.warn("outbound-review.bad-pattern", {
30626
+ pattern: src,
30627
+ error: err instanceof Error ? err.message : String(err)
30628
+ });
30629
+ }
30445
30630
  }
30631
+ return out;
30632
+ }
30633
+ function referencedRepos(text) {
30634
+ const repos = /* @__PURE__ */ new Set();
30635
+ const re = /(?:https?:\/\/|git@)([\w.-]+)[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?(?=[\s"')]|$)/gi;
30636
+ let m;
30637
+ while (m = re.exec(text)) repos.add(`${m[1]}/${m[2]}`.toLowerCase());
30638
+ return [...repos];
30639
+ }
30640
+ function ruleReview(input, allowedRepos, projectPath, secretPatterns, envPatterns) {
30641
+ const reasons = /* @__PURE__ */ new Set();
30642
+ const secretRe = compilePatterns(secretPatterns, DEFAULT_OUTBOUND_RULES.secret);
30643
+ const envRe = compilePatterns(envPatterns, DEFAULT_OUTBOUND_RULES.environment);
30644
+ if (secretRe.some((re) => re.test(input))) reasons.add("secret");
30645
+ if (envRe.some((re) => re.test(input))) reasons.add("environment");
30646
+ if (/(?:^|[\s"'(])(?:\.\.[/\\])+/.test(input)) reasons.add("path");
30647
+ const absSystem = /(?:^|[\s"'(=])(\/(?:etc|root|var|usr|bin|home|private|proc|sys)\b|[A-Za-z]:\\\\)/;
30648
+ if (absSystem.test(input) && projectPath && !input.includes(projectPath)) reasons.add("path");
30649
+ if (allowedRepos.length) {
30650
+ const allow = allowedRepos.map((r) => r.toLowerCase());
30651
+ const refs = referencedRepos(input);
30652
+ if (refs.some((r) => !allow.some((a) => a.includes(r) || r.includes(a)))) reasons.add("repo");
30653
+ }
30654
+ return [...reasons];
30655
+ }
30656
+ async function evaluateReview(payload) {
30657
+ const reasons = /* @__PURE__ */ new Set();
30658
+ let tokens = 0;
30659
+ if (payload.ruleCheck) {
30660
+ const violations = ruleReview(
30661
+ payload.prompt,
30662
+ payload.allowedRepos,
30663
+ payload.projectPath ?? "",
30664
+ payload.secretPatterns,
30665
+ payload.envPatterns
30666
+ );
30667
+ for (const r of violations) reasons.add(r);
30668
+ }
30669
+ if (payload.aiReview) {
30670
+ try {
30671
+ const ai = await aiOutboundReview(
30672
+ payload.prompt,
30673
+ payload.allowedRepos,
30674
+ payload.projectPath ?? ""
30675
+ );
30676
+ tokens = ai.tokens;
30677
+ if (!ai.ok) for (const r of ai.reasons) reasons.add(r);
30678
+ } catch (err) {
30679
+ logger.warn("outbound-review.ai-error", {
30680
+ commandId: payload.commandId,
30681
+ error: err instanceof Error ? err.message : String(err)
30682
+ });
30683
+ }
30684
+ }
30685
+ return { commandId: payload.commandId, ok: reasons.size === 0, reasons: [...reasons], tokens };
30446
30686
  }
30447
30687
 
30448
30688
  // src/core/command-history.ts
@@ -30703,13 +30943,142 @@ ${stderr}`;
30703
30943
  return { installed, authenticated, account, claudeCli };
30704
30944
  }
30705
30945
 
30706
- // src/core/scaffold.ts
30707
- import { cp, mkdir as mkdir2, writeFile } from "fs/promises";
30946
+ // src/core/git-history.ts
30947
+ import { readdir as readdir3 } from "fs/promises";
30708
30948
  import { execFile as execFile4 } from "child_process";
30709
- import { dirname as dirname4, join as join12, resolve as resolve4 } from "path";
30710
- import { fileURLToPath as fileURLToPath3 } from "url";
30949
+ import { resolve as resolve4, sep as sep3 } from "path";
30711
30950
  import { promisify as promisify4 } from "util";
30712
30951
  var run4 = promisify4(execFile4);
30952
+ var FS = "";
30953
+ var RS = "";
30954
+ function resolveRepo(root, repo) {
30955
+ if (!root) return null;
30956
+ const base = resolve4(root);
30957
+ const requested = resolve4(base, repo && repo.trim() ? repo : ".");
30958
+ return requested === base || requested.startsWith(base + sep3) ? requested : base;
30959
+ }
30960
+ async function git(cwd2, args) {
30961
+ try {
30962
+ const { stdout } = await run4("git", args, {
30963
+ cwd: cwd2,
30964
+ timeout: 15e3,
30965
+ maxBuffer: 8 * 1024 * 1024
30966
+ });
30967
+ return stdout;
30968
+ } catch {
30969
+ return "";
30970
+ }
30971
+ }
30972
+ async function gitRepos(root) {
30973
+ if (!root) return { repos: [] };
30974
+ const base = resolve4(root);
30975
+ const found = /* @__PURE__ */ new Set();
30976
+ const repos = [];
30977
+ const add = async (dir2) => {
30978
+ const inside = await git(dir2, ["rev-parse", "--show-toplevel"]);
30979
+ const top = inside.trim();
30980
+ if (!top || found.has(top) || !(top === base || top.startsWith(base + sep3))) return;
30981
+ found.add(top);
30982
+ const subdir = top === base ? "" : top.slice(base.length + 1);
30983
+ const remote = (await git(top, ["remote", "get-url", "origin"])).trim();
30984
+ repos.push({ subdir, name: subdir || "", remote: remote || null });
30985
+ };
30986
+ await add(base);
30987
+ try {
30988
+ const lvl1 = await readdir3(base, { withFileTypes: true });
30989
+ for (const d of lvl1) {
30990
+ if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
30991
+ const p1 = resolve4(base, d.name);
30992
+ await add(p1);
30993
+ try {
30994
+ const lvl2 = await readdir3(p1, { withFileTypes: true });
30995
+ for (const d2 of lvl2) {
30996
+ if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
30997
+ await add(resolve4(p1, d2.name));
30998
+ }
30999
+ } catch {
31000
+ }
31001
+ }
31002
+ } catch {
31003
+ }
31004
+ repos.sort((a, b) => a.subdir.localeCompare(b.subdir));
31005
+ return { repos };
31006
+ }
31007
+ async function gitLog(root, repo, skip, limit) {
31008
+ const cwd2 = resolveRepo(root, repo);
31009
+ if (!cwd2) return { entries: [], hasMore: false };
31010
+ const capped = Math.min(Math.max(limit, 1), 200);
31011
+ const format = ["%H", "%h", "%an", "%aI", "%s"].join(FS) + RS;
31012
+ const stdout = await git(cwd2, [
31013
+ "log",
31014
+ `--skip=${Math.max(skip, 0)}`,
31015
+ `-n`,
31016
+ String(capped + 1),
31017
+ `--pretty=format:${format}`
31018
+ ]);
31019
+ const rows = stdout.split(RS).map((r) => r.replace(/^\n/, "")).filter((r) => r.length > 0);
31020
+ const entries2 = rows.slice(0, capped).map((row) => {
31021
+ const [hash = "", shortHash = "", author = "", date = "", subject = ""] = row.split(FS);
31022
+ return { hash, shortHash, author, date, subject };
31023
+ });
31024
+ return { entries: entries2, hasMore: rows.length > capped };
31025
+ }
31026
+ async function gitCommit(root, repo, hash) {
31027
+ const cwd2 = resolveRepo(root, repo);
31028
+ const empty = { hash, author: "", date: "", subject: "", files: [] };
31029
+ if (!cwd2 || !hash) return empty;
31030
+ const format = ["%H", "%an", "%aI", "%s"].join(FS);
31031
+ const stdout = await git(cwd2, [
31032
+ "show",
31033
+ "--name-status",
31034
+ "-M",
31035
+ `--pretty=format:${format}`,
31036
+ hash
31037
+ ]);
31038
+ if (!stdout) return empty;
31039
+ const lines = stdout.split("\n");
31040
+ const [full = hash, author = "", date = "", subject = ""] = (lines[0] ?? "").split(FS);
31041
+ const files = [];
31042
+ for (const line of lines.slice(1)) {
31043
+ if (!line.trim()) continue;
31044
+ const parts = line.split(" ");
31045
+ const status = (parts[0] ?? "").charAt(0);
31046
+ if (!status) continue;
31047
+ if (status === "R" || status === "C") {
31048
+ files.push({ path: parts[2] ?? "", status, oldPath: parts[1] ?? void 0 });
31049
+ } else {
31050
+ files.push({ path: parts[1] ?? "", status });
31051
+ }
31052
+ }
31053
+ return { hash: full, author, date, subject, files };
31054
+ }
31055
+ async function gitCommitDiff(root, repo, hash, path) {
31056
+ const cwd2 = resolveRepo(root, repo);
31057
+ if (!cwd2 || !hash || !path) return { path, oldContent: "", newContent: "" };
31058
+ const oldContent = await git(cwd2, ["show", `${hash}^:${path}`]);
31059
+ const newContent = await git(cwd2, ["show", `${hash}:${path}`]);
31060
+ return { path, oldContent, newContent };
31061
+ }
31062
+
31063
+ // src/core/git-commit-identity.ts
31064
+ import { execFile as execFile5 } from "child_process";
31065
+ import { promisify as promisify5 } from "util";
31066
+ var run5 = promisify5(execFile5);
31067
+ async function setCommitAuthor(cwd2, machineUsername) {
31068
+ if (!machineUsername) return;
31069
+ try {
31070
+ await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
31071
+ } catch {
31072
+ }
31073
+ }
31074
+
31075
+ // src/core/scaffold.ts
31076
+ import { cp, mkdir as mkdir2, writeFile } from "fs/promises";
31077
+ import { execFile as execFile6 } from "child_process";
31078
+ import { dirname as dirname4, join as join12, resolve as resolve5 } from "path";
31079
+ import { fileURLToPath as fileURLToPath3 } from "url";
31080
+ import { promisify as promisify6 } from "util";
31081
+ var run6 = promisify6(execFile6);
30713
31082
  function reposOf(spec) {
30714
31083
  const repos = spec?.repos;
30715
31084
  return Array.isArray(repos) ? repos : [];
@@ -30722,9 +31091,9 @@ async function provisionRepos(target, repos, emit) {
30722
31091
  for (const repo of repos) {
30723
31092
  if (repo.primary) {
30724
31093
  emit("git", "Initializing the primary repo\u2026");
30725
- await run4("git", ["init"], { cwd: target, timeout: 2e4 });
31094
+ await run6("git", ["init"], { cwd: target, timeout: 2e4 });
30726
31095
  if (repo.op === "existing" && repo.url) {
30727
- await run4("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
31096
+ await run6("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
30728
31097
  () => void 0
30729
31098
  );
30730
31099
  }
@@ -30733,23 +31102,24 @@ async function provisionRepos(target, repos, emit) {
30733
31102
  const dir2 = join12(target, subDirName(repo));
30734
31103
  if (repo.op === "existing" && repo.url) {
30735
31104
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
30736
- await run4("git", ["clone", repo.url, dir2], { timeout: 12e4 });
31105
+ await run6("git", ["clone", repo.url, dir2], { timeout: 12e4 });
30737
31106
  } else {
30738
31107
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
30739
31108
  await mkdir2(dir2, { recursive: true });
30740
- await run4("git", ["init"], { cwd: dir2, timeout: 2e4 });
31109
+ await run6("git", ["init"], { cwd: dir2, timeout: 2e4 });
30741
31110
  }
30742
31111
  }
30743
31112
  }
30744
31113
  function sampleDir() {
30745
31114
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
30746
31115
  const here = dirname4(fileURLToPath3(import.meta.url));
30747
- return resolve4(here, "../../project-sample");
31116
+ return resolve5(here, "../../project-sample");
30748
31117
  }
30749
- async function scaffoldProject(payload, onProgress) {
31118
+ async function scaffoldProject(payload, profileDir2, onProgress) {
30750
31119
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
30751
31120
  try {
30752
- const target = resolve4(payload.path);
31121
+ const root = process.env.SCAFFOLD_ROOT ? resolve5(process.env.SCAFFOLD_ROOT) : resolve5(profileDir2);
31122
+ const target = join12(root, payload.projectName);
30753
31123
  await mkdir2(target, { recursive: true });
30754
31124
  emit("copy", "Copying the sample template\u2026");
30755
31125
  await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
@@ -30763,23 +31133,60 @@ async function scaffoldProject(payload, onProgress) {
30763
31133
  }
30764
31134
  const repos = reposOf(payload.spec);
30765
31135
  if (repos.length > 0) await provisionRepos(target, repos, emit);
31136
+ if (payload.spec) await aiInit(target, payload.spec, emit);
30766
31137
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
30767
31138
  return { ok: true, path: target };
30768
31139
  } catch (err) {
30769
31140
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
30770
31141
  }
30771
31142
  }
31143
+ async function aiInit(target, spec, emit) {
31144
+ emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
31145
+ const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
31146
+ if (subagents.length > 0) {
31147
+ await mkdir2(join12(target, ".claude", "agents"), { recursive: true });
31148
+ for (const sa of subagents) {
31149
+ const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
31150
+ if (!name) continue;
31151
+ const body = `---
31152
+ name: ${name}
31153
+ ---
31154
+
31155
+ ${(sa.description || "").trim()}
31156
+ `;
31157
+ await writeFile(join12(target, ".claude", "agents", `${name}.md`), body, "utf8");
31158
+ }
31159
+ }
31160
+ const specJson = JSON.stringify(spec);
31161
+ await generateFile(
31162
+ join12(target, "README.md"),
31163
+ `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
31164
+ ${specJson}`
31165
+ );
31166
+ await generateFile(
31167
+ join12(target, "CLAUDE.md"),
31168
+ `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
31169
+ ${specJson}`
31170
+ );
31171
+ }
31172
+ async function generateFile(path, prompt) {
31173
+ try {
31174
+ const text = await aiGenerate(prompt);
31175
+ if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
31176
+ } catch {
31177
+ }
31178
+ }
30772
31179
  async function addProject(payload, onProgress) {
30773
31180
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
30774
31181
  try {
30775
- const target = resolve4(payload.path);
31182
+ const target = resolve5(payload.path);
30776
31183
  await mkdir2(target, { recursive: true });
30777
31184
  if (payload.repoUrl) {
30778
31185
  emit("clone", `Cloning ${payload.repoUrl}\u2026`);
30779
- await run4("git", ["clone", payload.repoUrl, target], { timeout: 12e4 });
31186
+ await run6("git", ["clone", payload.repoUrl, target], { timeout: 12e4 });
30780
31187
  } else {
30781
31188
  emit("init", "git init\u2026");
30782
- await run4("git", ["init"], { cwd: target, timeout: 2e4 });
31189
+ await run6("git", ["init"], { cwd: target, timeout: 2e4 });
30783
31190
  }
30784
31191
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Repo ready.", done: true });
30785
31192
  return { ok: true, path: target };
@@ -30823,6 +31230,8 @@ var WsClient = class {
30823
31230
  heartbeat = null;
30824
31231
  usageTimer = null;
30825
31232
  usageSnapshot = null;
31233
+ /** Whether the serving project requires outbound review before each AI spawn (ADR-0082). */
31234
+ outboundReviewEnabled = false;
30826
31235
  stopped = false;
30827
31236
  /** Resolver to wake the backoff sleep early (set while waiting) — for /reconnect. */
30828
31237
  wakeReconnect = null;
@@ -30836,6 +31245,9 @@ var WsClient = class {
30836
31245
  /** The physic project folder root this cli serves (null = no project) — fs.list is
30837
31246
  * scoped to it so the web FsPicker can only browse inward, never out. */
30838
31247
  physicRoot = null;
31248
+ /** This machine-user's username — the git commit author for this project (ADR-0097).
31249
+ * Push uses the account already logged in with gh/glab on the worker. */
31250
+ machineUsername = "";
30839
31251
  /** Waiters resolved when the session next becomes connected (readiness gate before
30840
31252
  * running a local prompt). */
30841
31253
  connectWaiters = [];
@@ -30850,6 +31262,11 @@ var WsClient = class {
30850
31262
  get bus() {
30851
31263
  return this.context.bus;
30852
31264
  }
31265
+ /** True once the session stopped for good (logout / replaced / version) — the
31266
+ * resilient run wrapper uses this to tell a clean stop from a crash to recover from. */
31267
+ get isStopped() {
31268
+ return this.stopped;
31269
+ }
30853
31270
  /**
30854
31271
  * Force an immediate reconnect: wake the backoff sleep if waiting, else drop the live
30855
31272
  * socket so the run() loop reconnects (the next backoff is skipped). No-op if stopped.
@@ -30870,14 +31287,14 @@ var WsClient = class {
30870
31287
  */
30871
31288
  awaitConnected(timeoutMs) {
30872
31289
  if (this.sessionKey && this.bus.status === "connected") return Promise.resolve(true);
30873
- return new Promise((resolve5) => {
31290
+ return new Promise((resolve6) => {
30874
31291
  const waiter = () => {
30875
31292
  clearTimeout(timer);
30876
- resolve5(true);
31293
+ resolve6(true);
30877
31294
  };
30878
31295
  const timer = setTimeout(() => {
30879
31296
  this.connectWaiters = this.connectWaiters.filter((w) => w !== waiter);
30880
- resolve5(false);
31297
+ resolve6(false);
30881
31298
  }, timeoutMs);
30882
31299
  this.connectWaiters.push(waiter);
30883
31300
  });
@@ -30896,17 +31313,16 @@ var WsClient = class {
30896
31313
  this.forceReconnect = false;
30897
31314
  return Promise.resolve();
30898
31315
  }
30899
- return new Promise((resolve5) => {
31316
+ return new Promise((resolve6) => {
30900
31317
  const timer = setTimeout(() => {
30901
31318
  this.wakeReconnect = null;
30902
- resolve5();
31319
+ resolve6();
30903
31320
  }, ms);
30904
- timer.unref?.();
30905
31321
  this.wakeReconnect = () => {
30906
31322
  clearTimeout(timer);
30907
31323
  this.wakeReconnect = null;
30908
31324
  this.forceReconnect = false;
30909
- resolve5();
31325
+ resolve6();
30910
31326
  };
30911
31327
  });
30912
31328
  }
@@ -30935,6 +31351,8 @@ var WsClient = class {
30935
31351
  `Token request rejected (HASHCODE_INVALID) \u2014 retrying (${hashcodeInvalidStreak}/${MAX_HASHCODE_INVALID_RETRIES}).`,
30936
31352
  "warn"
30937
31353
  );
31354
+ } else if (err.errorCode === "USER_PAUSED") {
31355
+ this.bus.log("This account is paused \u2014 retrying until it is resumed.", "warn");
30938
31356
  } else {
30939
31357
  this.bus.log(`Connection error: ${err}`, "error");
30940
31358
  }
@@ -30958,7 +31376,7 @@ var WsClient = class {
30958
31376
  * One WS session: token → connect → handshake → handle messages until close.
30959
31377
  */
30960
31378
  connectOnce() {
30961
- return new Promise((resolve5, reject) => {
31379
+ return new Promise((resolve6, reject) => {
30962
31380
  void (async () => {
30963
31381
  this.bus.setStatus("connecting");
30964
31382
  const token = await requestWsToken(this.credential.serverUrl, this.credential.hashcode3);
@@ -30973,6 +31391,12 @@ var WsClient = class {
30973
31391
  timezone: token.timezone
30974
31392
  });
30975
31393
  }
31394
+ writeProfileConfig(this.context.profileDir, {
31395
+ sessionSwitchPct: token.projectTokens?.sessionSwitchPct ?? 0,
31396
+ perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0
31397
+ });
31398
+ this.outboundReviewEnabled = token.outboundReview?.enabled === true;
31399
+ this.machineUsername = token.machineUsername ?? "";
30976
31400
  if (token.scope && token.scope !== this.credential.scope) {
30977
31401
  this.credential.scope = token.scope;
30978
31402
  writeCredential(this.context.profileDir, this.credential);
@@ -31010,7 +31434,7 @@ var WsClient = class {
31010
31434
  if (code === CLOSE_DRAINING) {
31011
31435
  this.bus.log("Server draining \u2014 will reconnect (backoff + jitter).");
31012
31436
  }
31013
- resolve5();
31437
+ resolve6();
31014
31438
  });
31015
31439
  socket.on("error", (err) => {
31016
31440
  this.clearHeartbeat();
@@ -31070,6 +31494,11 @@ var WsClient = class {
31070
31494
  this.socket?.close();
31071
31495
  return;
31072
31496
  }
31497
+ if (message.errorCode === "USER_PAUSED") {
31498
+ this.bus.log("This account is paused \u2014 retrying until it is resumed.", "warn");
31499
+ this.socket?.close();
31500
+ return;
31501
+ }
31073
31502
  this.bus.log(`Server reported an error: ${message.errorCode}`, "warn");
31074
31503
  this.socket?.close();
31075
31504
  return;
@@ -31174,6 +31603,32 @@ var WsClient = class {
31174
31603
  (reply) => this.send(WsChannels.GIT_ENV, reply, message.id)
31175
31604
  );
31176
31605
  break;
31606
+ case WsChannels.GIT_REPOS:
31607
+ void gitRepos(this.physicRoot).then(
31608
+ (reply) => this.send(WsChannels.GIT_REPOS, reply, message.id)
31609
+ );
31610
+ break;
31611
+ case WsChannels.GIT_LOG: {
31612
+ const req = payload;
31613
+ void gitLog(this.physicRoot, req.repo ?? "", req.skip ?? 0, req.limit ?? 50).then(
31614
+ (reply) => this.send(WsChannels.GIT_LOG, reply, message.id)
31615
+ );
31616
+ break;
31617
+ }
31618
+ case WsChannels.GIT_COMMIT: {
31619
+ const req = payload;
31620
+ void gitCommit(this.physicRoot, req.repo ?? "", req.hash).then(
31621
+ (reply) => this.send(WsChannels.GIT_COMMIT, reply, message.id)
31622
+ );
31623
+ break;
31624
+ }
31625
+ case WsChannels.GIT_COMMIT_DIFF: {
31626
+ const req = payload;
31627
+ void gitCommitDiff(this.physicRoot, req.repo ?? "", req.hash, req.path).then(
31628
+ (reply) => this.send(WsChannels.GIT_COMMIT_DIFF, reply, message.id)
31629
+ );
31630
+ break;
31631
+ }
31177
31632
  case WsChannels.LOG_READ: {
31178
31633
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
31179
31634
  const lines = readRecentLogLines(join13(this.context.profileDir, "logs"), limit);
@@ -31183,6 +31638,7 @@ var WsClient = class {
31183
31638
  case WsChannels.PROJECT_CREATE:
31184
31639
  void scaffoldProject(
31185
31640
  payload,
31641
+ this.context.profileDir,
31186
31642
  (p) => this.send(WsChannels.PROJECT_PROGRESS, p)
31187
31643
  ).then((reply) => this.send(WsChannels.PROJECT_CREATE, reply, message.id));
31188
31644
  break;
@@ -31192,14 +31648,11 @@ var WsClient = class {
31192
31648
  (p) => this.send(WsChannels.PROJECT_PROGRESS, p)
31193
31649
  ).then((reply) => this.send(WsChannels.PROJECT_ADD, reply, message.id));
31194
31650
  break;
31195
- case WsChannels.AI_SUGGEST:
31196
- void aiSuggest(payload).then(
31197
- (reply) => this.send(WsChannels.AI_SUGGEST, reply, message.id)
31198
- );
31199
- break;
31200
- case WsChannels.AI_REVIEW:
31201
- void aiReview(payload).then(
31202
- (reply) => this.send(WsChannels.AI_REVIEW, reply, message.id)
31651
+ // AI spec-assist (project-0012/0013/0033) now runs via command.dispatch({ ai:true })
31652
+ // → runAiPrompt (profile-failover), not dedicated ai.* channels (ADR-0100).
31653
+ case WsChannels.REVIEW_EVALUATE:
31654
+ void evaluateReview(payload).then(
31655
+ (reply) => this.send(WsChannels.REVIEW_EVALUATE, reply, message.id)
31203
31656
  );
31204
31657
  break;
31205
31658
  default:
@@ -31242,16 +31695,48 @@ var WsClient = class {
31242
31695
  */
31243
31696
  async runAiPrompt(prompt, commandId, origin) {
31244
31697
  const config = readProfileConfig(this.context.profileDir);
31698
+ const perPromptLimit = config.perPromptTokenLimit ?? 0;
31699
+ if (perPromptLimit > 0) {
31700
+ const estimated = estimateTokens(prompt);
31701
+ if (estimated > perPromptLimit) {
31702
+ this.rejectPromptOverLimit(commandId, origin, estimated, perPromptLimit);
31703
+ return;
31704
+ }
31705
+ }
31706
+ if (this.outboundReviewEnabled) {
31707
+ let verdict;
31708
+ try {
31709
+ verdict = await this.request(WsChannels.REVIEW_REQUEST, {
31710
+ commandId,
31711
+ prompt
31712
+ });
31713
+ } catch {
31714
+ verdict = { commandId, ok: false, reasons: ["unavailable"] };
31715
+ }
31716
+ if (!verdict.ok) {
31717
+ this.rejectByReview(commandId, origin, verdict.reasons);
31718
+ return;
31719
+ }
31720
+ }
31245
31721
  const cwd2 = this.physicRoot ?? config.physicPath ?? process.cwd();
31246
31722
  if (this.physicRoot) {
31247
31723
  this.ensurePhysicFolderPath(this.physicRoot);
31724
+ await setCommitAuthor(cwd2, this.machineUsername);
31248
31725
  } else {
31249
31726
  this.bus.log("No project attached \u2014 running in the current directory.", "warn");
31250
31727
  }
31251
31728
  const cmd = config.aiCli ?? "claude";
31252
31729
  const workingDir = getWorkingProfile(this.context.profileDir, cmd);
31253
- const plan = planAiRun(prompt, config, workingDir);
31254
- this.bus.push({ source: origin, kind: "cmd", text: `${plan.cmd} \u2039 ${prompt}` });
31730
+ const startDir = this.profileUnderSessionPressure(config, cmd, workingDir);
31731
+ if (startDir && startDir !== workingDir) {
31732
+ this.bus.push({
31733
+ source: origin,
31734
+ kind: "log",
31735
+ text: `session ${this.usageSnapshot?.session.utilizationPct ?? 0}% \u2265 ${config.sessionSwitchPct}% \u2014 switching to Claude profile "${profileDisplayLabel(startDir)}"`
31736
+ });
31737
+ }
31738
+ const plan = planAiRun(prompt, config, startDir ?? workingDir);
31739
+ this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
31255
31740
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
31256
31741
  if (origin === "local") {
31257
31742
  this.send(WsChannels.COMMAND_ANNOUNCE, {
@@ -31265,11 +31750,37 @@ var WsClient = class {
31265
31750
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
31266
31751
  this.bus.startBusy(plan.cmd);
31267
31752
  let seq = 0;
31753
+ let responseHeaderShown = false;
31754
+ let displayMode = "pending";
31755
+ let resultId = null;
31756
+ let resultBuf = "";
31268
31757
  const result = await runAiFailover(plan, commandId, cwd2, {
31269
31758
  onChunk: (text) => {
31270
- this.bus.push({ source: origin, kind: "out", text });
31759
+ if (!responseHeaderShown) {
31760
+ this.bus.push({ source: origin, kind: "aires", text: `${plan.cmd} \u203A` });
31761
+ responseHeaderShown = true;
31762
+ }
31271
31763
  appendCommandOutput(commandId, text);
31272
31764
  this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: text });
31765
+ if (displayMode === "result") {
31766
+ resultBuf += text;
31767
+ if (resultId) this.bus.updateEntry(resultId, resultBuf);
31768
+ return;
31769
+ }
31770
+ if (displayMode === "prose") {
31771
+ this.bus.push({ source: origin, kind: "out", text });
31772
+ return;
31773
+ }
31774
+ resultBuf += text;
31775
+ if (resultBuf.replace(/^\s+/, "").length === 0) return;
31776
+ const kind = looksLikeJsonOrCode(resultBuf);
31777
+ if (kind) {
31778
+ displayMode = "result";
31779
+ resultId = this.bus.push({ source: origin, kind: "result", text: resultBuf, resultKind: kind });
31780
+ } else {
31781
+ displayMode = "prose";
31782
+ this.bus.push({ source: origin, kind: "out", text: resultBuf });
31783
+ }
31273
31784
  },
31274
31785
  onAttemptStart: (label, index, total) => this.bus.push({
31275
31786
  source: origin,
@@ -31324,6 +31835,79 @@ var WsClient = class {
31324
31835
  level: result.exitCode === 0 ? "info" : "error"
31325
31836
  });
31326
31837
  }
31838
+ /**
31839
+ * Pick the Claude profile to try first when under session pressure (ADR-0081). Returns
31840
+ * the next candidate dir (cyclically after the current working one) when the knob is set
31841
+ * and the live session utilization is at/over it AND there is more than one candidate;
31842
+ * otherwise null (keep the normal working-first ordering). Claude-only (session % is a
31843
+ * Claude subscription metric).
31844
+ */
31845
+ profileUnderSessionPressure(config, cmd, workingDir) {
31846
+ const threshold = config.sessionSwitchPct ?? 0;
31847
+ if (threshold <= 0 || cmd !== "claude") return null;
31848
+ const util2 = this.usageSnapshot?.session.utilizationPct ?? 0;
31849
+ if (util2 < threshold) return null;
31850
+ const dirs = claudeConfigDirs(config);
31851
+ if (dirs.length < 2) return null;
31852
+ const currentIndex = workingDir ? dirs.indexOf(workingDir) : -1;
31853
+ const next = dirs[(currentIndex + 1) % dirs.length];
31854
+ return next ?? null;
31855
+ }
31856
+ /**
31857
+ * Reject a prompt whose estimated tokens exceed the project's per-prompt limit (ADR-0081)
31858
+ * without spawning: surface it in the transcript + close the command on the server with a
31859
+ * PROMPT_TOKEN_LIMIT_EXCEEDED note (exit 1). No usage is metered (nothing ran).
31860
+ */
31861
+ rejectPromptOverLimit(commandId, origin, estimated, limit) {
31862
+ const message = `[PROMPT_TOKEN_LIMIT_EXCEEDED] Prompt (~${estimated} tokens) exceeds the project per-prompt limit of ${limit} tokens.`;
31863
+ logger.warn("command.ai.prompt-limit", { commandId, origin, estimated, limit });
31864
+ this.bus.push({ source: origin, kind: "log", text: message, level: "error" });
31865
+ if (origin === "local") {
31866
+ this.send(WsChannels.COMMAND_ANNOUNCE, {
31867
+ commandId,
31868
+ cmd: "claude",
31869
+ args: [],
31870
+ origin: "local"
31871
+ });
31872
+ }
31873
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 0, chunk: message });
31874
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 1, chunk: "", done: true, exitCode: 1 });
31875
+ this.bus.push({
31876
+ source: origin,
31877
+ kind: "exit",
31878
+ text: "\u2717 prompt rejected \u2014 over the project per-prompt token limit",
31879
+ level: "error"
31880
+ });
31881
+ }
31882
+ /**
31883
+ * Reject a prompt an outbound reviewer blocked (ADR-0082) without spawning: surface the
31884
+ * verdict + close the command on the server. `reasons` contains only violation categories
31885
+ * (never secret values); a "unavailable" reason maps to no-outbound-available.
31886
+ */
31887
+ rejectByReview(commandId, origin, reasons) {
31888
+ const unavailable = reasons.includes("unavailable");
31889
+ const code = unavailable ? "OUTBOUND_REVIEW_UNAVAILABLE" : "OUTBOUND_REVIEW_REJECTED";
31890
+ const detail = reasons.filter((r) => r !== "unavailable").join(", ");
31891
+ const message = unavailable ? `[${code}] No outbound reviewer is available \u2014 input blocked.` : `[${code}] Outbound review rejected the input${detail ? ` (${detail})` : ""}.`;
31892
+ logger.warn("command.ai.outbound-review", { commandId, origin, reasons });
31893
+ this.bus.push({ source: origin, kind: "log", text: message, level: "error" });
31894
+ if (origin === "local") {
31895
+ this.send(WsChannels.COMMAND_ANNOUNCE, {
31896
+ commandId,
31897
+ cmd: "claude",
31898
+ args: [],
31899
+ origin: "local"
31900
+ });
31901
+ }
31902
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 0, chunk: message });
31903
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 1, chunk: "", done: true, exitCode: 1 });
31904
+ this.bus.push({
31905
+ source: origin,
31906
+ kind: "exit",
31907
+ text: "\u2717 input blocked by outbound review",
31908
+ level: "error"
31909
+ });
31910
+ }
31327
31911
  /**
31328
31912
  * Send one envelope (encrypted payload + per-message wsToken — arch 0004).
31329
31913
  * When disconnected: the running command's output is held in a buffer (512KB cap,
@@ -31350,7 +31934,7 @@ var WsClient = class {
31350
31934
  * Used for quota.check before spawning an AI cli (ADR-0020).
31351
31935
  */
31352
31936
  request(channel, data) {
31353
- return new Promise((resolve5, reject) => {
31937
+ return new Promise((resolve6, reject) => {
31354
31938
  if (!this.socket || !this.sessionKey) {
31355
31939
  reject(new Error("Not connected to the server."));
31356
31940
  return;
@@ -31370,7 +31954,7 @@ var WsClient = class {
31370
31954
  }, 15e3);
31371
31955
  timer.unref?.();
31372
31956
  this.pending.set(envelope.id, {
31373
- resolve: (value) => resolve5(value),
31957
+ resolve: (value) => resolve6(value),
31374
31958
  timer
31375
31959
  });
31376
31960
  this.socket.send(JSON.stringify(envelope));
@@ -31584,7 +32168,7 @@ var SessionBus = class {
31584
32168
  get status() {
31585
32169
  return this.currentStatus;
31586
32170
  }
31587
- /** Producer → consumers: push one transcript entry. */
32171
+ /** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
31588
32172
  push(input) {
31589
32173
  const entry = {
31590
32174
  id: randomUUID2(),
@@ -31595,6 +32179,23 @@ var SessionBus = class {
31595
32179
  this.history.push(entry);
31596
32180
  if (this.history.length > REPLAY_CAP) this.history.shift();
31597
32181
  this.emitter.emit("transcript", entry);
32182
+ return entry.id;
32183
+ }
32184
+ /**
32185
+ * Producer → consumers: replace the text of an already-pushed entry (ADR-0108) — used to
32186
+ * grow a streaming `result` block in place without pushing a new line per chunk. No-op if the
32187
+ * entry has aged out of the replay window.
32188
+ */
32189
+ updateEntry(id, text) {
32190
+ const entry = this.history.find((e) => e.id === id);
32191
+ if (!entry) return;
32192
+ entry.text = text;
32193
+ this.emitter.emit("transcript-update", entry);
32194
+ }
32195
+ /** Subscribe to in-place entry updates (a `result` block growing — ADR-0108). */
32196
+ onTranscriptUpdate(fn) {
32197
+ this.emitter.on("transcript-update", fn);
32198
+ return () => this.emitter.off("transcript-update", fn);
31598
32199
  }
31599
32200
  /** Shortcut for a lifecycle log line (source = system). */
31600
32201
  log(text, level = "info") {
@@ -33699,7 +34300,7 @@ var src_default = Yoga;
33699
34300
 
33700
34301
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
33701
34302
  var import_react_reconciler = __toESM(require_react_reconciler(), 1);
33702
- var import_constants8 = __toESM(require_constants(), 1);
34303
+ var import_constants10 = __toESM(require_constants(), 1);
33703
34304
  import process3 from "process";
33704
34305
 
33705
34306
  // ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
@@ -34941,7 +35542,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
34941
35542
  scheduleTimeout: setTimeout,
34942
35543
  cancelTimeout: clearTimeout,
34943
35544
  noTimeout: -1,
34944
- getCurrentEventPriority: () => import_constants8.DefaultEventPriority,
35545
+ getCurrentEventPriority: () => import_constants10.DefaultEventPriority,
34945
35546
  beforeActiveInstanceBlur() {
34946
35547
  },
34947
35548
  afterActiveInstanceBlur() {
@@ -37015,8 +37616,8 @@ var Ink = class {
37015
37616
  }
37016
37617
  }
37017
37618
  async waitUntilExit() {
37018
- this.exitPromise ||= new Promise((resolve5, reject) => {
37019
- this.resolveExitPromise = resolve5;
37619
+ this.exitPromise ||= new Promise((resolve6, reject) => {
37620
+ this.resolveExitPromise = resolve6;
37020
37621
  this.rejectExitPromise = reject;
37021
37622
  });
37022
37623
  return this.exitPromise;
@@ -37513,6 +38114,16 @@ function textColor(line) {
37513
38114
  if (line.kind === "exit") return "green";
37514
38115
  return void 0;
37515
38116
  }
38117
+ function AiMarkerLine({ line }) {
38118
+ const arrow = line.kind === "aireq" ? "\u2039" : "\u203A";
38119
+ const idx = line.text.indexOf(arrow);
38120
+ if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
38121
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { children: [
38122
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: line.text.slice(0, idx) }),
38123
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
38124
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
38125
+ ] });
38126
+ }
37516
38127
  function wrapToWidth(text, width) {
37517
38128
  if (width <= 0 || text.length <= width) return [text];
37518
38129
  const rows = [];
@@ -37538,20 +38149,94 @@ function wrapToWidth(text, width) {
37538
38149
  if (line) rows.push(line.trimEnd());
37539
38150
  return rows.length > 0 ? rows : [""];
37540
38151
  }
37541
- function flattenEntries(entries2, textWidth) {
38152
+ var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
38153
+ function toSegments(raw) {
38154
+ if (!raw.includes("<detail")) {
38155
+ return raw.split("\n").map((text) => ({ text, collapsed: false }));
38156
+ }
38157
+ const out = [];
38158
+ const re = new RegExp(DETAIL_RE);
38159
+ let last = 0;
38160
+ let m;
38161
+ while ((m = re.exec(raw)) !== null) {
38162
+ const before = raw.slice(last, m.index).replace(/\n$/, "");
38163
+ if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
38164
+ out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
38165
+ last = m.index + m[0].length;
38166
+ if (raw[last] === "\n") last += 1;
38167
+ }
38168
+ const rest = raw.slice(last);
38169
+ if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
38170
+ if (out.length === 0) out.push({ text: "", collapsed: false });
38171
+ return out;
38172
+ }
38173
+ function pushCollapsible(lines, entry, opts) {
38174
+ const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
38175
+ const base = { source: entry.source, level: entry.level, kind: entry.kind };
38176
+ lines.push({
38177
+ ...base,
38178
+ key: `${keyBase}:m`,
38179
+ text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
38180
+ showTag: firstRow,
38181
+ collapsed: true,
38182
+ spacer: false,
38183
+ blockIndex: blockNo
38184
+ });
38185
+ if (!expanded) return;
38186
+ prettyResult(body).split("\n").forEach(
38187
+ (seg, i) => wrapToWidth(seg, textWidth).forEach(
38188
+ (row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
38189
+ )
38190
+ );
38191
+ }
38192
+ function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
37542
38193
  const lines = [];
38194
+ let prevKey = null;
38195
+ let blockNo = 0;
37543
38196
  for (const entry of entries2) {
38197
+ const blockKey = `${entry.source}|${entry.kind}`;
38198
+ const newBlock = blockKey !== prevKey;
38199
+ if (newBlock && lines.length > 0) {
38200
+ lines.push({
38201
+ key: `sp:${entry.id}`,
38202
+ source: entry.source,
38203
+ level: entry.level,
38204
+ kind: entry.kind,
38205
+ text: "",
38206
+ showTag: false,
38207
+ collapsed: false,
38208
+ spacer: true
38209
+ });
38210
+ }
38211
+ prevKey = blockKey;
38212
+ let firstRow = newBlock;
38213
+ if (entry.kind === "result") {
38214
+ blockNo += 1;
38215
+ const count = prettyResult(entry.text).split("\n").length;
38216
+ const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
38217
+ pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
38218
+ continue;
38219
+ }
37544
38220
  const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
37545
- const parts = raw.split("\n");
37546
- parts.forEach((text, i) => {
37547
- wrapToWidth(text, textWidth).forEach((row, j) => {
38221
+ toSegments(raw).forEach((seg, i) => {
38222
+ if (seg.collapsed) {
38223
+ blockNo += 1;
38224
+ pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
38225
+ firstRow = false;
38226
+ return;
38227
+ }
38228
+ wrapToWidth(seg.text, textWidth).forEach((row, j) => {
37548
38229
  lines.push({
37549
38230
  key: `${entry.id}:${i}:${j}`,
37550
38231
  source: entry.source,
37551
38232
  level: entry.level,
37552
38233
  kind: entry.kind,
37553
- text: row
38234
+ text: row,
38235
+ showTag: firstRow,
38236
+ collapsed: false,
38237
+ spacer: false
37554
38238
  });
38239
+ firstRow = false;
37555
38240
  });
37556
38241
  });
37557
38242
  }
@@ -37560,8 +38245,8 @@ function flattenEntries(entries2, textWidth) {
37560
38245
  function TranscriptLine({ line }) {
37561
38246
  const tag = sourceTag(line.source);
37562
38247
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "row", children: [
37563
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { width: 7, marginRight: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: tag.color, dimColor: line.source === "system", children: tag.text }) }),
37564
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { flexGrow: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: textColor(line), wrap: "truncate-end", children: line.text || " " }) })
38248
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { width: 7, marginRight: 1, children: line.showTag ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: tag.color, dimColor: line.source === "system", children: tag.text }) : null }),
38249
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { flexGrow: 1, children: line.spacer ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }) : line.collapsed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, wrap: "truncate-end", children: line.text || " " }) : line.kind === "aireq" || line.kind === "aires" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AiMarkerLine, { line }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: textColor(line), wrap: "truncate-end", children: line.text || " " }) })
37565
38250
  ] });
37566
38251
  }
37567
38252
 
@@ -37758,6 +38443,19 @@ function runWhoami(ctx) {
37758
38443
  ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
37759
38444
  });
37760
38445
  }
38446
+ function runFold(ctx, action) {
38447
+ if (ctx.maxBlock === 0) {
38448
+ ctx.print("No collapsible blocks on screen.", "warn");
38449
+ return;
38450
+ }
38451
+ const n = ctx.args[0] != null ? Number(ctx.args[0]) : ctx.maxBlock;
38452
+ if (!Number.isInteger(n) || n < 1 || n > ctx.maxBlock) {
38453
+ ctx.print(`usage: /${action} [1-${ctx.maxBlock}] (no number \u21D2 the newest block)`, "error");
38454
+ return;
38455
+ }
38456
+ if (action === "expand") ctx.expand(n);
38457
+ else ctx.collapse(n);
38458
+ }
37761
38459
  var COMMANDS = [
37762
38460
  {
37763
38461
  name: "help",
@@ -37810,6 +38508,18 @@ var COMMANDS = [
37810
38508
  description: "Tail the profile's structured log",
37811
38509
  run: runLogs
37812
38510
  },
38511
+ {
38512
+ name: "expand",
38513
+ usage: "/expand [N]",
38514
+ description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
38515
+ run: (ctx) => runFold(ctx, "expand")
38516
+ },
38517
+ {
38518
+ name: "collapse",
38519
+ usage: "/collapse [N]",
38520
+ description: "Collapse an expanded \u25B8[N] block (default: newest)",
38521
+ run: (ctx) => runFold(ctx, "collapse")
38522
+ },
37813
38523
  {
37814
38524
  name: "reconnect",
37815
38525
  usage: "/reconnect",
@@ -38016,13 +38726,18 @@ function App2({
38016
38726
  const [usage, setUsage] = (0, import_react26.useState)(bus.usage);
38017
38727
  const [sessionTokens, setSessionTokens] = (0, import_react26.useState)(bus.sessionTokens);
38018
38728
  const [scrollBack, setScrollBack] = (0, import_react26.useState)(0);
38729
+ const [expanded, setExpanded] = (0, import_react26.useState)(() => /* @__PURE__ */ new Set());
38019
38730
  const [history, setHistory] = (0, import_react26.useState)(() => loadInputHistory(info.profileDir));
38020
38731
  const [startedAt] = (0, import_react26.useState)(() => Date.now());
38021
38732
  (0, import_react26.useEffect)(() => {
38022
- const offTranscript = bus.onTranscript(
38023
- (entry) => setEntries(
38733
+ const offTranscript = bus.onTranscript((entry) => {
38734
+ setEntries(
38024
38735
  (prev) => prev.length >= ENTRY_CAP ? [...prev.slice(prev.length - ENTRY_CAP + 1), entry] : [...prev, entry]
38025
- )
38736
+ );
38737
+ if (entry.kind === "aireq" || entry.kind === "aires") setExpanded(/* @__PURE__ */ new Set());
38738
+ });
38739
+ const offUpdate = bus.onTranscriptUpdate(
38740
+ (updated) => setEntries((prev) => prev.map((e) => e.id === updated.id ? { ...e, text: updated.text } : e))
38026
38741
  );
38027
38742
  const offStatus = bus.onStatus(setStatus);
38028
38743
  const offBusy = bus.onBusy(setBusy);
@@ -38034,6 +38749,7 @@ function App2({
38034
38749
  const offTokens = bus.onTokens(setSessionTokens);
38035
38750
  return () => {
38036
38751
  offTranscript();
38752
+ offUpdate();
38037
38753
  offStatus();
38038
38754
  offBusy();
38039
38755
  offProfile();
@@ -38044,6 +38760,29 @@ function App2({
38044
38760
  offTokens();
38045
38761
  };
38046
38762
  }, [bus]);
38763
+ const bodyHeight = Math.max(3, rows - CHROME_ROWS);
38764
+ const textWidth = Math.max(10, cols - 1 - 8);
38765
+ const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth, expanded), [entries2, textWidth, expanded]);
38766
+ const maxBlock = (0, import_react26.useMemo)(() => lines.reduce((m, l) => Math.max(m, l.blockIndex ?? 0), 0), [lines]);
38767
+ const expandFold = (n) => {
38768
+ const target = n ?? maxBlock;
38769
+ if (target < 1 || target > maxBlock) return;
38770
+ setExpanded((prev) => {
38771
+ const next = new Set(prev);
38772
+ if (next.has(target)) next.delete(target);
38773
+ else next.add(target);
38774
+ return next;
38775
+ });
38776
+ };
38777
+ const collapseFold = (n) => {
38778
+ const target = n ?? maxBlock;
38779
+ setExpanded((prev) => {
38780
+ if (!prev.has(target)) return prev;
38781
+ const next = new Set(prev);
38782
+ next.delete(target);
38783
+ return next;
38784
+ });
38785
+ };
38047
38786
  const handleSubmit = (input) => {
38048
38787
  setScrollBack(0);
38049
38788
  setHistory((prev) => appendInputHistory(info.profileDir, input, prev));
@@ -38061,15 +38800,15 @@ function App2({
38061
38800
  quit: () => exit(),
38062
38801
  submitAi: (aiInput) => bus.submitLocal(aiInput),
38063
38802
  reconnect: () => bus.requestReconnect(),
38803
+ expand: expandFold,
38804
+ collapse: collapseFold,
38805
+ maxBlock,
38064
38806
  live: { status, scope, worker, project, activeProfile, startedAt }
38065
38807
  });
38066
38808
  return;
38067
38809
  }
38068
38810
  bus.submitLocal(input);
38069
38811
  };
38070
- const bodyHeight = Math.max(3, rows - CHROME_ROWS);
38071
- const textWidth = Math.max(10, cols - 1 - 8);
38072
- const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth), [entries2, textWidth]);
38073
38812
  const maxScroll = Math.max(0, lines.length - bodyHeight);
38074
38813
  const clampedBack = Math.min(scrollBack, maxScroll);
38075
38814
  const lineRows = clampedBack > 0 ? bodyHeight - 1 : bodyHeight;
@@ -38172,6 +38911,20 @@ async function runStart(profileDir2, profileName) {
38172
38911
  profileDir: profileDir2,
38173
38912
  bus
38174
38913
  });
38914
+ const runResilient = async () => {
38915
+ for (; ; ) {
38916
+ try {
38917
+ await client.run();
38918
+ return;
38919
+ } catch (err) {
38920
+ const e = err instanceof Error ? err : new Error(String(err));
38921
+ logger.error("ws.run.crash", { message: e.message, stack: e.stack });
38922
+ if (client.isStopped) return;
38923
+ bus.log(`Recovered from an internal error \u2014 reconnecting: ${e.message}`, "error");
38924
+ await new Promise((resolve6) => setTimeout(resolve6, 1e3));
38925
+ }
38926
+ }
38927
+ };
38175
38928
  const useTui = Boolean(process.stdout.isTTY) && process.env.FOURPM_HEADLESS !== "1";
38176
38929
  if (useTui) {
38177
38930
  const info = {
@@ -38190,12 +38943,12 @@ async function runStart(profileDir2, profileName) {
38190
38943
  bus.setActiveProfile(workingDir ? profileDisplayLabel(workingDir) : labels[0] ?? null);
38191
38944
  const tui = runTui(bus, info);
38192
38945
  bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
38193
- void client.run();
38946
+ void runResilient();
38194
38947
  await tui;
38195
38948
  } else {
38196
38949
  attachConsoleSink(bus);
38197
38950
  bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
38198
- await client.run();
38951
+ await runResilient();
38199
38952
  }
38200
38953
  } finally {
38201
38954
  release();
@@ -38225,7 +38978,7 @@ async function runUnlink(profileDir2, profileName) {
38225
38978
 
38226
38979
  // src/commands/update.ts
38227
38980
  async function runUpdate(profileDir2, profileName, serverUrlFlag) {
38228
- const serverUrl = (serverUrlFlag ?? readCredential(profileDir2, profileName)?.serverUrl ?? process.env.FOURPM_SERVER ?? "http://localhost:3001").replace(/\/$/, "");
38981
+ const serverUrl = (serverUrlFlag || readCredential(profileDir2, profileName)?.serverUrl || process.env.FOURPM_SERVER || "http://localhost:42001").replace(/\/$/, "");
38229
38982
  let result;
38230
38983
  try {
38231
38984
  result = await updateToLatest(serverUrl);
@@ -38295,14 +39048,21 @@ function Picker({
38295
39048
  ] });
38296
39049
  }
38297
39050
  function selectFromList(items, title) {
38298
- return new Promise((resolve5) => {
39051
+ return new Promise((resolve6) => {
38299
39052
  let chosen = null;
38300
39053
  const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v) => chosen = v }));
38301
- void app.waitUntilExit().then(() => resolve5(chosen));
39054
+ void app.waitUntilExit().then(() => resolve6(chosen));
38302
39055
  });
38303
39056
  }
38304
39057
 
38305
39058
  // src/index.ts
39059
+ process.on("unhandledRejection", (reason) => {
39060
+ const err = reason instanceof Error ? reason : new Error(String(reason));
39061
+ logger.error("process.unhandledRejection", { message: err.message, stack: err.stack });
39062
+ });
39063
+ process.on("uncaughtException", (err) => {
39064
+ logger.error("process.uncaughtException", { message: err.message, stack: err.stack });
39065
+ });
38306
39066
  function flagValue(args, name) {
38307
39067
  const index = args.indexOf(`--${name}`);
38308
39068
  return index >= 0 ? args[index + 1] : void 0;
@@ -38331,7 +39091,7 @@ async function main() {
38331
39091
  const [command, ...rest] = args;
38332
39092
  switch (command) {
38333
39093
  case "link": {
38334
- const serverUrl = flagValue(rest, "server") ?? process.env.FOURPM_SERVER ?? "http://localhost:3001";
39094
+ const serverUrl = flagValue(rest, "server") || process.env.FOURPM_SERVER || "http://localhost:42001";
38335
39095
  await runLink(serverUrl.replace(/\/$/, ""), explicitProfile);
38336
39096
  break;
38337
39097
  }
@@ -38380,7 +39140,7 @@ async function main() {
38380
39140
  "",
38381
39141
  "Profiles: named after the paired account (e.g. mcacc1); when several exist,",
38382
39142
  "start/unlink show a picker (\u2191\u2193 + Enter) \u2014 no --profile needed.",
38383
- "Server URL: from FOURPM_SERVER (default http://localhost:3001)."
39143
+ "Server URL: from FOURPM_SERVER (default http://localhost:42001)."
38384
39144
  ].join("\n")
38385
39145
  );
38386
39146
  }