@modootoday/pmcp 0.14.1 → 0.16.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.
@@ -64,6 +64,11 @@ var tuiOptions = [
64
64
  },
65
65
  { name: "rows", describe: "Terminal rows (12\u20131000)", placeholder: "<count>" },
66
66
  { name: "index", describe: "Worker dock index", placeholder: "<count>" },
67
+ {
68
+ name: "memory-mb",
69
+ describe: "Worker memory reservation",
70
+ placeholder: "<count>"
71
+ },
67
72
  {
68
73
  name: "confirm-session",
69
74
  describe: "Exact target ID for stop",
@@ -71,9 +76,24 @@ var tuiOptions = [
71
76
  },
72
77
  {
73
78
  name: "config",
74
- describe: "Optional mailbox pmcp.toml",
79
+ describe: "Project pmcp.toml (or mailbox config for create)",
75
80
  placeholder: "<file>"
76
81
  },
82
+ {
83
+ name: "runtime",
84
+ describe: "Native runtime ID for main launch or worker start",
85
+ placeholder: "<runtime>"
86
+ },
87
+ {
88
+ name: "yes",
89
+ describe: "Confirm the first native runtime start",
90
+ boolean: true
91
+ },
92
+ {
93
+ name: "new",
94
+ describe: "Create a workspace after the previous group is stopped",
95
+ boolean: true
96
+ },
77
97
  {
78
98
  name: "actor-file",
79
99
  describe: "Optional private mailbox observer credential",
@@ -81,6 +101,10 @@ var tuiOptions = [
81
101
  }
82
102
  ];
83
103
  var allowed = {
104
+ launch: ["config", "runtime", "yes", "new"],
105
+ plan: ["config", "runtime"],
106
+ recovery: ["config"],
107
+ "start-worker": ["config", "view", "runtime", "memory-mb"],
84
108
  doctor: [],
85
109
  create: ["group-file", "columns", "rows", "config", "actor-file"],
86
110
  inspect: ["view"],
@@ -98,10 +122,12 @@ var allowed = {
98
122
  close: ["view"],
99
123
  dismiss: ["view"],
100
124
  management: ["view"],
125
+ "worker-menu": ["view"],
101
126
  check: ["view"]
102
127
  };
103
128
  function parseTui(args) {
104
- const [action, ...extra] = args.positional;
129
+ let [action, ...extra] = args.positional;
130
+ if (action === void 0) action = one(args, "view") ? "open" : "launch";
105
131
  const options = action ? allowed[action] : void 0;
106
132
  if (!action || !options || extra.length)
107
133
  throw new ArgumentError("Provide one supported TUI operation");
@@ -121,6 +147,33 @@ function parseTui(args) {
121
147
  return Number(value);
122
148
  };
123
149
  if (action === "doctor") return { action, input: {} };
150
+ if (action === "start-worker") {
151
+ const config = one(args, "config");
152
+ const viewFile = one(args, "view");
153
+ if (config && viewFile)
154
+ throw new ArgumentError("Choose --config or --view for worker start");
155
+ return {
156
+ action,
157
+ viewFile,
158
+ input: {},
159
+ startup: {
160
+ config,
161
+ runtime: required("runtime"),
162
+ memoryMb: number("memory-mb")
163
+ }
164
+ };
165
+ }
166
+ if (action === "launch" || action === "plan" || action === "recovery")
167
+ return {
168
+ action,
169
+ input: {},
170
+ startup: {
171
+ config: one(args, "config"),
172
+ runtime: one(args, "runtime"),
173
+ fresh: args.flags.has("new"),
174
+ confirmed: args.flags.has("yes")
175
+ }
176
+ };
124
177
  if (action === "create") {
125
178
  const config = one(args, "config");
126
179
  const actorFile = one(args, "actor-file");
@@ -677,9 +730,9 @@ async function attachNative(spec, renew) {
677
730
  }, 2e4) : void 0;
678
731
  let exitCode;
679
732
  try {
680
- exitCode = await new Promise((resolve4, reject) => {
733
+ exitCode = await new Promise((resolve5, reject) => {
681
734
  client.once("error", reject);
682
- client.once("exit", (code) => resolve4(code ?? 1));
735
+ client.once("exit", (code) => resolve5(code ?? 1));
683
736
  });
684
737
  } finally {
685
738
  if (timer) clearInterval(timer);
@@ -858,7 +911,6 @@ var Connection = class {
858
911
  constructor(records2) {
859
912
  this.records = records2;
860
913
  }
861
- records;
862
914
  tmux(args, options = {}) {
863
915
  return command(
864
916
  "tmux",
@@ -1095,8 +1147,6 @@ var LaunchJournal = class {
1095
1147
  this.id = id2;
1096
1148
  runtimeUnit(id2);
1097
1149
  }
1098
- records;
1099
- id;
1100
1150
  path(kind) {
1101
1151
  if (!["intent", "proof", "grant", "committed", "aborted"].includes(kind))
1102
1152
  throw new Error("invalid_launch_artifact");
@@ -1594,7 +1644,7 @@ var DockerEngine = class {
1594
1644
  if (!/^1\.\d+$/.test(version))
1595
1645
  throw new Error("invalid_docker_api_version");
1596
1646
  const payload = JSON.stringify(configuration);
1597
- return new Promise((resolve4, reject) => {
1647
+ return new Promise((resolve5, reject) => {
1598
1648
  const req = request(
1599
1649
  {
1600
1650
  socketPath: new URL(endpoint).pathname,
@@ -1629,7 +1679,7 @@ var DockerEngine = class {
1629
1679
  );
1630
1680
  if (!/^[a-f0-9]{64}$/.test(id2))
1631
1681
  throw new Error("invalid_container_id");
1632
- resolve4(id2);
1682
+ resolve5(id2);
1633
1683
  } catch (error) {
1634
1684
  reject(error);
1635
1685
  }
@@ -2225,7 +2275,6 @@ var Backend = class {
2225
2275
  this.runDirectory = recordStore.directory;
2226
2276
  this.metadataPath = recordStore.metadataPath;
2227
2277
  }
2228
- recordStore;
2229
2278
  connection;
2230
2279
  owner;
2231
2280
  socket;
@@ -2709,7 +2758,7 @@ async function watchSession(operation) {
2709
2758
  )
2710
2759
  });
2711
2760
  if (index + 1 < count)
2712
- await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
2761
+ await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
2713
2762
  }
2714
2763
  return {
2715
2764
  schemaVersion: 1,
@@ -3364,7 +3413,6 @@ var CommonHarness = class {
3364
3413
  constructor(view) {
3365
3414
  this.view = view;
3366
3415
  }
3367
- view;
3368
3416
  async call(family, action, input = {}) {
3369
3417
  const group = new GroupStore(this.view.groupFile).state.group;
3370
3418
  if (group.owner !== this.view.groupOwner) throw new Error("foreign_group");
@@ -3398,6 +3446,19 @@ var CommonHarness = class {
3398
3446
  // src/tui/application/feedback.ts
3399
3447
  import { join as join20 } from "path";
3400
3448
  var hints = {
3449
+ startup_requires_recovery: "Run pmcp tui recovery for saved ownership and exact inspection commands. No runtime was restarted.",
3450
+ startup_group_paused: "The saved group is paused. Resume it explicitly through the harness CLI before reopening.",
3451
+ startup_main_unavailable: "Inspect the saved main and recovery state. Stop the old group explicitly before using pmcp tui --new.",
3452
+ startup_busy: "Another workspace launch is running. Wait and retry.",
3453
+ startup_runtime_conflict: "The saved main uses another runtime. Stop the old group before creating a new workspace.",
3454
+ workspace_already_running: "Reopen the existing workspace, or stop its exact owned group before using --new.",
3455
+ runtime_executable_unavailable: "Install the selected native CLI on PATH, or choose an installed runtime with --runtime.",
3456
+ view_client_unavailable: "Open the view before using the worker menu, or run pmcp tui start-worker with --view and --runtime.",
3457
+ runtime_not_granted: "Choose a runtime granted by this owned group. Inspect the group before creating another workspace.",
3458
+ runtime_capacity_exceeded: "Stop an exact owned worker before adding another, or use the main session alone.",
3459
+ aggregate_memory_budget_exceeded: "Use fewer sessions or start a worker with an explicit smaller --memory-mb reservation.",
3460
+ group_not_ready: "Inspect the group and resume a paused group explicitly before adding a worker.",
3461
+ startup_group_not_ready: "Inspect the group and resume a paused group explicitly before adding a worker.",
3401
3462
  control_connection_lost: "The native writer or lease disappeared. Inspect and release control before reconnecting.",
3402
3463
  control_busy: "Another controller holds the lease. Release it explicitly before retrying.",
3403
3464
  native_writer_active: "A native writer is still attached. Detach that writer before retrying.",
@@ -3453,6 +3514,14 @@ function phaseLabel(phase2) {
3453
3514
  return labels[phase2];
3454
3515
  }
3455
3516
 
3517
+ // src/tui/presentation/target.ts
3518
+ function targetStatus(view) {
3519
+ const role2 = view.target === view.mainId ? "MAIN" : "WORKER";
3520
+ const phase2 = view.phase === "read-only" ? "OBSERVE" : phaseLabel(view.phase);
3521
+ const input = view.phase === "controlled" ? "INPUT ON" : "INPUT OFF";
3522
+ return `${role2} ${view.target.slice(0, 8)} | ${phase2} | ${input}`;
3523
+ }
3524
+
3456
3525
  // src/tui/adapters/tmux/connection.ts
3457
3526
  import { spawn as spawn2 } from "child_process";
3458
3527
  import { join as join21 } from "path";
@@ -3501,9 +3570,6 @@ var ViewConnection = class {
3501
3570
  this.identity = identity;
3502
3571
  this.socket = join21(directory, "s");
3503
3572
  }
3504
- directory;
3505
- owner;
3506
- identity;
3507
3573
  socket;
3508
3574
  raw(args) {
3509
3575
  const env = { ...process.env };
@@ -3573,9 +3639,9 @@ var ViewConnection = class {
3573
3639
  ["-S", this.socket, "attach-session", "-E", "-t", view.session],
3574
3640
  { stdio: "inherit", env }
3575
3641
  );
3576
- return new Promise((resolve4, reject) => {
3642
+ return new Promise((resolve5, reject) => {
3577
3643
  client.once("error", reject);
3578
- client.once("exit", (code) => resolve4(code ?? 1));
3644
+ client.once("exit", (code) => resolve5(code ?? 1));
3579
3645
  });
3580
3646
  }
3581
3647
  };
@@ -3606,7 +3672,8 @@ function bindKeys(connection, view, file) {
3606
3672
  ["b", "mailbox"],
3607
3673
  ["d", "detach"],
3608
3674
  ["!", "dismiss"],
3609
- ["g", "management"]
3675
+ ["g", "management"],
3676
+ ["n", "worker-menu"]
3610
3677
  ])
3611
3678
  connection.command([
3612
3679
  "bind-key",
@@ -3627,7 +3694,7 @@ function bindKeys(connection, view, file) {
3627
3694
  "bind-key",
3628
3695
  "?",
3629
3696
  "display-message",
3630
- "m main; w/1-4 worker; c control; r release; h history; b mailbox; d detach; ! dismiss error; double Ctrl-g forwards native Ctrl-g"
3697
+ "m main; n add worker; w/1-4 worker; c control; r release; h history; b mailbox; d detach; ! dismiss error; double Ctrl-g forwards native Ctrl-g"
3631
3698
  ]);
3632
3699
  connection.command([
3633
3700
  "set-hook",
@@ -3654,8 +3721,6 @@ var TmuxView = class {
3654
3721
  view.serverIdentity
3655
3722
  );
3656
3723
  }
3657
- view;
3658
- stateFile;
3659
3724
  connection;
3660
3725
  shell(argv2) {
3661
3726
  return `exec ${argv2.map(quote).join(" ")}`;
@@ -3827,7 +3892,7 @@ var TmuxView = class {
3827
3892
  "set-option",
3828
3893
  "-g",
3829
3894
  "status-right",
3830
- `${view.target === view.mainId ? "main" : "worker"} | ${phaseLabel(view.phase)}${failure2} | Ctrl-g ?`
3895
+ `${targetStatus(view)}${failure2} | Ctrl-g ?`
3831
3896
  ]);
3832
3897
  }
3833
3898
  input(pane, enabled) {
@@ -3850,6 +3915,32 @@ var TmuxView = class {
3850
3915
  clients() {
3851
3916
  return this.connection.command(["list-clients", "-F", "#{client_pid}"]);
3852
3917
  }
3918
+ workerMenu(view) {
3919
+ const pane = view.workerPane ?? view.mainPane;
3920
+ this.connection.assertOwned(pane);
3921
+ const popup = [
3922
+ "tmux",
3923
+ "-S",
3924
+ this.connection.socket,
3925
+ "display-popup",
3926
+ "-E",
3927
+ "-t",
3928
+ pane,
3929
+ "-w",
3930
+ "90%",
3931
+ "-h",
3932
+ "80%",
3933
+ this.shell([
3934
+ view.node,
3935
+ view.worker,
3936
+ "worker-menu",
3937
+ "--view",
3938
+ this.stateFile,
3939
+ "--callback"
3940
+ ])
3941
+ ];
3942
+ this.connection.command(["run-shell", "-b", popup.map(quote).join(" ")]);
3943
+ }
3853
3944
  copy(pane) {
3854
3945
  this.connection.assertOwned(pane);
3855
3946
  this.connection.command(["copy-mode", "-t", pane]);
@@ -3880,7 +3971,7 @@ async function waitFor(predicate, timeoutMs) {
3880
3971
  const deadline = Date.now() + timeoutMs;
3881
3972
  while (Date.now() < deadline) {
3882
3973
  if (await predicate()) return;
3883
- await new Promise((resolve4) => setTimeout(resolve4, 50));
3974
+ await new Promise((resolve5) => setTimeout(resolve5, 50));
3884
3975
  }
3885
3976
  throw new Error("writer_not_ready");
3886
3977
  }
@@ -3916,6 +4007,7 @@ function dockLines(snapshot) {
3916
4007
  ),
3917
4008
  "",
3918
4009
  "Ctrl-g w: observe worker",
4010
+ "Ctrl-g n: add worker",
3919
4011
  "Ctrl-g m: return to main",
3920
4012
  "Ctrl-g c: acquire control",
3921
4013
  "Ctrl-g r: release control",
@@ -3927,7 +4019,7 @@ function dockLines(snapshot) {
3927
4019
  ];
3928
4020
  }
3929
4021
  function coordinationLines(snapshot, view) {
3930
- const target = `TARGET ${view.target === view.mainId ? "main" : "worker"} ${safe(view.target.slice(0, 8))} / ${phaseLabel(view.phase)}`;
4022
+ const target = `TARGET ${safe(targetStatus(view))}`;
3931
4023
  if (view.failure)
3932
4024
  return [
3933
4025
  target,
@@ -4076,6 +4168,7 @@ function resolveEncoder(model = DEFAULT_MODEL_ID, options = {}) {
4076
4168
  }
4077
4169
 
4078
4170
  // src/config.ts
4171
+ var CONFIG_FILE = "pmcp.toml";
4079
4172
  function embeddingSection(path, dir, value) {
4080
4173
  if (value === void 0) return void 0;
4081
4174
  if (value === null || typeof value !== "object" || Array.isArray(value))
@@ -4166,6 +4259,17 @@ var CATALOG_KEYS = /* @__PURE__ */ new Set([
4166
4259
  "scopes",
4167
4260
  "builtin"
4168
4261
  ]);
4262
+ function findConfig(start2) {
4263
+ let dir = resolve2(start2);
4264
+ for (; ; ) {
4265
+ const candidate = join23(dir, CONFIG_FILE);
4266
+ if (existsSync10(candidate)) return candidate;
4267
+ if (existsSync10(join23(dir, ".git"))) return null;
4268
+ const parent = dirname5(dir);
4269
+ if (parent === dir) return null;
4270
+ dir = parent;
4271
+ }
4272
+ }
4169
4273
  function stringList(path, key, value) {
4170
4274
  if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
4171
4275
  throw new ConfigError(path, `[catalog] ${key} must be a list of strings`);
@@ -4304,8 +4408,6 @@ var MailboxError = class extends Error {
4304
4408
  this.retryable = retryable;
4305
4409
  this.name = "MailboxError";
4306
4410
  }
4307
- code;
4308
- retryable;
4309
4411
  };
4310
4412
  function failure(error) {
4311
4413
  if (error instanceof MailboxError) {
@@ -5551,7 +5653,7 @@ ${content}
5551
5653
  stopped = true;
5552
5654
  });
5553
5655
  while (!stopped) {
5554
- await new Promise((resolve4) => setTimeout(resolve4, 1500));
5656
+ await new Promise((resolve5) => setTimeout(resolve5, 1500));
5555
5657
  await draw();
5556
5658
  }
5557
5659
  }
@@ -5669,6 +5771,11 @@ var handlers = {
5669
5771
  close: closeView,
5670
5772
  check: checkControl,
5671
5773
  dismiss: ({ view, file }) => dismissFailure(file, view),
5774
+ "worker-menu": async (context) => {
5775
+ if (!context.renderer.clients()) throw new Error("view_client_unavailable");
5776
+ await releaseControl(context);
5777
+ context.renderer.workerMenu(context.view);
5778
+ },
5672
5779
  management: async (context) => {
5673
5780
  await releaseControl(context);
5674
5781
  if (context.view.managementWindow) {
@@ -5802,6 +5909,70 @@ function runtime() {
5802
5909
  return { node: realpathSync10(available.node), worker: bundled };
5803
5910
  }
5804
5911
 
5912
+ // src/tui/startup/diagnostics.ts
5913
+ function remediation(tools, userManagerAvailable) {
5914
+ const steps = [];
5915
+ if (tools.platform !== "linux")
5916
+ steps.push(
5917
+ "Use a qualified Linux host for the terminal workspace. Other PMCP CLI features do not require this host profile."
5918
+ );
5919
+ const missing = ["tmux", "flock", "systemctl", "systemdRun"].filter(
5920
+ (name) => !tools[name]
5921
+ );
5922
+ if (missing.length)
5923
+ steps.push(
5924
+ `Install the missing tmux, util-linux or systemd tools for your distribution and add them to PATH: ${missing.join(", ")}.`
5925
+ );
5926
+ if (!tools.nodeSupported)
5927
+ steps.push(
5928
+ "Select Node.js 22 or later through your existing version manager and check node --version in this terminal."
5929
+ );
5930
+ if (tools.supported && !userManagerAvailable)
5931
+ steps.push(
5932
+ "Check systemctl --user status in your login session and restore access to its user manager before launching. Doctor does not start or repair services."
5933
+ );
5934
+ return steps;
5935
+ }
5936
+ function inspectTerminalHost() {
5937
+ const tools = dependencies();
5938
+ let userManagerAvailable = false;
5939
+ let reason;
5940
+ try {
5941
+ if (!tools.supported) throw new Error("tui_dependencies_unavailable");
5942
+ assertNativeHost();
5943
+ userManagerAvailable = true;
5944
+ } catch (error) {
5945
+ reason = error instanceof Error ? error.message : "native_host_unavailable";
5946
+ }
5947
+ const runtimes = Object.entries(runtimeExecutables).map(
5948
+ ([id2, executable]) => {
5949
+ const path = locateExecutable(executable);
5950
+ return {
5951
+ id: id2,
5952
+ executable,
5953
+ installed: Boolean(path),
5954
+ ...path ? { path } : {},
5955
+ nativeAcceptance: "not-assessed",
5956
+ authentication: "not-inspected"
5957
+ };
5958
+ }
5959
+ );
5960
+ return {
5961
+ schemaVersion: 1,
5962
+ ok: true,
5963
+ ...tools,
5964
+ supported: tools.supported && userManagerAvailable,
5965
+ userManagerAvailable,
5966
+ ...reason ? { reason } : {},
5967
+ runtimes,
5968
+ qualification: "Prerequisite availability only; native behavior and cgroup delegation require separate qualification.",
5969
+ remediation: remediation(tools, userManagerAvailable),
5970
+ nextCommands: ["pmcp tui plan", "pmcp tui recovery"],
5971
+ providerInvoked: false,
5972
+ authenticationInspected: false
5973
+ };
5974
+ }
5975
+
5805
5976
  // src/tui/application/create.ts
5806
5977
  import { join as join29 } from "path";
5807
5978
  import { realpathSync as realpathSync11 } from "fs";
@@ -5880,8 +6051,9 @@ async function createView(input) {
5880
6051
 
5881
6052
  // src/tui/application/invoke.ts
5882
6053
  async function invokeTui(invocation) {
5883
- if (invocation.action === "doctor")
5884
- return { schemaVersion: 1, ok: true, ...dependencies() };
6054
+ if (["launch", "plan", "recovery", "start-worker"].includes(invocation.action))
6055
+ throw new Error("startup_requires_cli_context");
6056
+ if (invocation.action === "doctor") return inspectTerminalHost();
5885
6057
  if (invocation.action === "create") return createView(invocation.create);
5886
6058
  if (invocation.action === "inspect")
5887
6059
  return operate("inspect", invocation.viewFile);
@@ -5917,11 +6089,379 @@ function failedInvocation(action, error) {
5917
6089
  };
5918
6090
  }
5919
6091
 
6092
+ // src/tui/startup/workspace.ts
6093
+ import { existsSync as existsSync16 } from "fs";
6094
+
6095
+ // src/tui/startup/config.ts
6096
+ import { realpathSync as realpathSync12 } from "fs";
6097
+ import { resolve as resolve4 } from "path";
6098
+ var preference = [
6099
+ "codex-cli",
6100
+ "gemini-cli",
6101
+ "antigravity",
6102
+ "grok-cli",
6103
+ "claude-code"
6104
+ ];
6105
+ function startupSettings(request2) {
6106
+ const path = request2.config ? resolve4(request2.cwd, request2.config) : findConfig(request2.cwd);
6107
+ const config = path ? readConfig(path) : void 0;
6108
+ const source2 = config?.raw.tui;
6109
+ const fail = (message) => {
6110
+ throw new ConfigError(path ?? "pmcp.toml", `[tui] ${message}`);
6111
+ };
6112
+ if (source2 !== void 0 && (source2 === null || typeof source2 !== "object" || Array.isArray(source2)))
6113
+ fail("must be a table");
6114
+ const values = source2 ?? {};
6115
+ const allowed2 = /* @__PURE__ */ new Set([
6116
+ "runtime",
6117
+ "memory_mb",
6118
+ "session_memory_mb",
6119
+ "max_active",
6120
+ "group_file"
6121
+ ]);
6122
+ for (const key of Object.keys(values)) {
6123
+ if (!allowed2.has(key)) fail(`unknown key ${key}`);
6124
+ }
6125
+ const string = (key) => {
6126
+ const value = values[key];
6127
+ if (value === void 0) return void 0;
6128
+ if (typeof value !== "string" || !value.trim())
6129
+ fail(`${key} must be a non-empty string`);
6130
+ return value;
6131
+ };
6132
+ const integer2 = (key, fallback, min, max) => {
6133
+ const value = values[key] ?? fallback;
6134
+ if (typeof value !== "number" || !Number.isSafeInteger(value))
6135
+ fail(`${key} must be an integer`);
6136
+ if (Number(value) < min || Number(value) > max)
6137
+ fail(`${key} must be between ${min} and ${max}`);
6138
+ return Number(value);
6139
+ };
6140
+ const configuredRuntime = string("runtime");
6141
+ if (configuredRuntime !== void 0 && configuredRuntime !== "auto" && !Object.hasOwn(runtimeExecutables, configuredRuntime))
6142
+ fail("runtime must be auto or a supported runtime ID");
6143
+ const available = preference.filter(
6144
+ (runtime3) => locateExecutable(runtimeExecutables[runtime3], request2.cwd)
6145
+ );
6146
+ let runtime2 = request2.runtime ?? configuredRuntime ?? "auto";
6147
+ if (runtime2 === "auto") runtime2 = available[0] ?? "codex-cli";
6148
+ if (!Object.hasOwn(runtimeExecutables, runtime2))
6149
+ fail("runtime must be a supported runtime ID");
6150
+ const memoryMb = integer2("memory_mb", 2048, 128, 4096);
6151
+ const sessionMemoryMb = integer2("session_memory_mb", 768, 32, 1536);
6152
+ if (sessionMemoryMb > memoryMb)
6153
+ fail("session memory exceeds the group budget");
6154
+ const groupFile = string("group_file");
6155
+ return {
6156
+ projectRoot: realpathSync12(config?.dir ?? request2.cwd),
6157
+ ...config ? { configFile: config.path } : {},
6158
+ runtime: runtime2,
6159
+ allowedRuntimes: [.../* @__PURE__ */ new Set([...available, runtime2])],
6160
+ memoryMb,
6161
+ sessionMemoryMb,
6162
+ maxActive: integer2("max_active", 2, 1, 5),
6163
+ ...groupFile ? { groupFile: resolve4(config?.dir ?? request2.cwd, groupFile) } : {}
6164
+ };
6165
+ }
6166
+
6167
+ // src/tui/startup/state.ts
6168
+ import { createHash as createHash8 } from "crypto";
6169
+ import { existsSync as existsSync15, lstatSync as lstatSync8, mkdirSync as mkdirSync6 } from "fs";
6170
+ import { homedir as homedir6 } from "os";
6171
+ import { dirname as dirname9, join as join30 } from "path";
6172
+ function startupFile(projectRoot) {
6173
+ const root = join30(
6174
+ process.env.XDG_STATE_HOME ?? join30(homedir6(), ".local/state"),
6175
+ "pmcp/launcher"
6176
+ );
6177
+ const key = createHash8("sha256").update(projectRoot).digest("hex");
6178
+ return join30(root, `${key}.json`);
6179
+ }
6180
+ function readStartup(file, projectRoot) {
6181
+ if (!existsSync15(file)) return null;
6182
+ const value = object(readPrivateJson(file));
6183
+ if (value.schemaVersion !== 1 || value.projectRoot !== projectRoot)
6184
+ throw new Error("foreign_startup_state");
6185
+ if (!["creating", "starting", "view-pending", "ready"].includes(
6186
+ String(value.phase)
6187
+ ))
6188
+ throw new Error("invalid_startup_state");
6189
+ text(value.runtime, "runtime");
6190
+ if (value.phase !== "creating") {
6191
+ text(value.groupFile, "group_file");
6192
+ text(value.generation, "generation");
6193
+ }
6194
+ if (value.phase === "ready") text(value.viewFile, "view_file");
6195
+ return value;
6196
+ }
6197
+ function saveStartup(file, state) {
6198
+ privateJson(file, state);
6199
+ }
6200
+
6201
+ // src/tui/startup/workspace.ts
6202
+ function succeeded(receipt) {
6203
+ if (receipt.ok !== true)
6204
+ throw new Error(String(receipt.error ?? "startup_operation_failed"));
6205
+ return receipt;
6206
+ }
6207
+ async function prepareWorkspace(request2) {
6208
+ const settings = startupSettings(request2);
6209
+ const file = startupFile(settings.projectRoot);
6210
+ let state = readStartup(file, settings.projectRoot);
6211
+ if (state && !["ready", "view-pending"].includes(state.phase)) {
6212
+ if (!request2.fresh || state.phase === "creating")
6213
+ throw new Error("startup_requires_recovery");
6214
+ }
6215
+ if (state && settings.groupFile && state.groupFile !== settings.groupFile)
6216
+ throw new Error("startup_group_conflict");
6217
+ if (state) {
6218
+ const group = new GroupStore(state.groupFile).state.group;
6219
+ assertGroup(group, settings.projectRoot, state.generation);
6220
+ if (group.state === "paused") throw new Error("startup_group_paused");
6221
+ if (request2.fresh) {
6222
+ if (group.state !== "stopped")
6223
+ throw new Error("workspace_already_running");
6224
+ state = null;
6225
+ } else {
6226
+ if (group.state !== "ready") throw new Error("startup_requires_recovery");
6227
+ if (request2.runtime && request2.runtime !== state.runtime)
6228
+ throw new Error("startup_runtime_conflict");
6229
+ }
6230
+ }
6231
+ if (!state && settings.groupFile) {
6232
+ const store = new GroupStore(settings.groupFile);
6233
+ const group = store.state.group;
6234
+ assertGroup(group, settings.projectRoot);
6235
+ if (group.state !== "ready") throw new Error("startup_group_not_ready");
6236
+ state = {
6237
+ schemaVersion: 1,
6238
+ projectRoot: settings.projectRoot,
6239
+ phase: "view-pending",
6240
+ groupFile: store.path,
6241
+ generation: group.generation,
6242
+ runtime: settings.runtime
6243
+ };
6244
+ saveStartup(file, state);
6245
+ }
6246
+ if (!state) {
6247
+ state = {
6248
+ schemaVersion: 1,
6249
+ projectRoot: settings.projectRoot,
6250
+ phase: "creating",
6251
+ runtime: settings.runtime
6252
+ };
6253
+ saveStartup(file, state);
6254
+ const created = succeeded(
6255
+ await invoke({
6256
+ family: "group",
6257
+ action: "create",
6258
+ input: {
6259
+ projectRoot: settings.projectRoot,
6260
+ allowedRuntimes: [...settings.allowedRuntimes],
6261
+ memoryMb: settings.memoryMb,
6262
+ maxActive: settings.maxActive
6263
+ }
6264
+ })
6265
+ );
6266
+ state.groupFile = String(created.groupFile);
6267
+ state.generation = created.group.generation;
6268
+ state.phase = "starting";
6269
+ saveStartup(file, state);
6270
+ succeeded(
6271
+ await invoke({
6272
+ family: "session",
6273
+ action: "start",
6274
+ groupFile: state.groupFile,
6275
+ generation: state.generation,
6276
+ input: {
6277
+ runtime: settings.runtime,
6278
+ role: "main",
6279
+ memoryMb: settings.sessionMemoryMb
6280
+ }
6281
+ })
6282
+ );
6283
+ state.phase = "view-pending";
6284
+ saveStartup(file, state);
6285
+ }
6286
+ const observed = succeeded(
6287
+ await invoke({
6288
+ family: "session",
6289
+ action: "list",
6290
+ groupFile: state.groupFile
6291
+ })
6292
+ );
6293
+ const sessions = observed.sessions;
6294
+ const main = sessions.find(
6295
+ (session) => session.role === "main" && session.alive
6296
+ );
6297
+ if (!main) throw new Error("startup_main_unavailable");
6298
+ state.runtime = main.runtime;
6299
+ const existing = state.viewFile && existsSync16(state.viewFile) ? readView(state.viewFile).view : void 0;
6300
+ if (existing) {
6301
+ if (existing.groupFile !== state.groupFile || existing.generation !== state.generation)
6302
+ throw new Error("foreign_startup_view");
6303
+ }
6304
+ if (!existing || existing.state === "closed") {
6305
+ const created = await createView({
6306
+ groupFile: state.groupFile,
6307
+ columns: process.stdout.columns ?? Number(process.env.PMCP_TUI_COLUMNS),
6308
+ rows: process.stdout.rows ?? Number(process.env.PMCP_TUI_ROWS)
6309
+ });
6310
+ state.viewFile = created.viewFile;
6311
+ }
6312
+ state.phase = "ready";
6313
+ saveStartup(file, state);
6314
+ return {
6315
+ schemaVersion: 1,
6316
+ ok: true,
6317
+ viewFile: state.viewFile,
6318
+ groupFile: state.groupFile,
6319
+ generation: state.generation,
6320
+ runtime: state.runtime,
6321
+ readOnly: true
6322
+ };
6323
+ }
6324
+ function assertGroup(group, projectRoot, generation) {
6325
+ if (group.projectRoot !== projectRoot)
6326
+ throw new Error("startup_project_mismatch");
6327
+ if (generation && group.generation !== generation)
6328
+ throw new Error("stale_group_generation");
6329
+ }
6330
+
6331
+ // src/tui/application/worker-start.ts
6332
+ async function startViewWorker(file, runtime2, memoryMb) {
6333
+ const { view, common } = openView(file);
6334
+ if (view.state !== "open") throw new Error("view_closed");
6335
+ const observed = await common.snapshot();
6336
+ if (observed.group.state !== "ready") throw new Error("group_not_ready");
6337
+ const main = observed.sessions.find(
6338
+ (session) => session.id === view.mainId && session.role === "main"
6339
+ );
6340
+ if (!main?.alive) throw new Error("startup_main_unavailable");
6341
+ return common.call("session", "start", {
6342
+ input: { role: "worker", runtime: runtime2, memoryMb: memoryMb ?? main.memoryMb }
6343
+ });
6344
+ }
6345
+
6346
+ // src/cli/entry.ts
6347
+ function interactiveEntry(terminal) {
6348
+ if (!terminal.stdin || !terminal.stdout) return false;
6349
+ if (!terminal.term || terminal.term === "dumb") return false;
6350
+ return !terminal.ci || terminal.ci === "false" || terminal.ci === "0";
6351
+ }
6352
+
6353
+ // src/tui/startup/host.ts
6354
+ function terminalSupport() {
6355
+ try {
6356
+ if (!dependencies().supported)
6357
+ throw new Error("tui_dependencies_unavailable");
6358
+ assertNativeHost();
6359
+ planLayout(process.stdout.columns ?? 0, process.stdout.rows ?? 0);
6360
+ return { supported: true };
6361
+ } catch (error) {
6362
+ return {
6363
+ supported: false,
6364
+ reason: error instanceof Error ? error.message : "terminal_unavailable"
6365
+ };
6366
+ }
6367
+ }
6368
+ function requireTerminal() {
6369
+ if (!interactiveEntry({
6370
+ stdin: process.stdin.isTTY === true,
6371
+ stdout: process.stdout.isTTY === true,
6372
+ term: process.env.TERM,
6373
+ ci: process.env.CI
6374
+ }))
6375
+ throw new Error("terminal_required");
6376
+ const support = terminalSupport();
6377
+ if (!support.supported) throw new Error(support.reason);
6378
+ }
6379
+
6380
+ // src/tui/presentation/runtime-choice.ts
6381
+ import { createInterface } from "readline/promises";
6382
+ async function chooseRuntime(prompt, choices, preferred) {
6383
+ if (!choices.length) throw new Error("runtime_executable_unavailable");
6384
+ const fallback = Math.max(choices.indexOf(preferred), 0);
6385
+ process.stderr.write(
6386
+ `${choices.map((runtime2, index) => `${index + 1}. ${runtime2}`).join("\n")}
6387
+ `
6388
+ );
6389
+ while (true) {
6390
+ const answer = (await prompt.question(
6391
+ `Runtime [1-${choices.length}, Enter=${fallback + 1}, q=cancel] `
6392
+ )).trim();
6393
+ if (answer === "q") return void 0;
6394
+ if (answer === "") return choices[fallback];
6395
+ if (!/^\d+$/.test(answer)) continue;
6396
+ const index = Number(answer) - 1;
6397
+ if (Number.isSafeInteger(index) && index >= 0 && index < choices.length)
6398
+ return choices[index];
6399
+ }
6400
+ }
6401
+ async function confirmRuntimeStart(prompt, runtime2) {
6402
+ const answer = await prompt.question(
6403
+ `Start ${runtime2}? [Enter=start, q=cancel] `
6404
+ );
6405
+ return answer.trim() === "";
6406
+ }
6407
+ function terminalPrompt() {
6408
+ return createInterface({ input: process.stdin, output: process.stderr });
6409
+ }
6410
+
6411
+ // src/tui/presentation/worker-menu.ts
6412
+ async function runWorkerMenu(file) {
6413
+ requireTerminal();
6414
+ const { view, common } = openView(file);
6415
+ if (view.state !== "open") throw new Error("view_closed");
6416
+ const observed = await common.snapshot();
6417
+ const group = observed.group;
6418
+ if (group.state !== "ready") throw new Error("startup_group_not_ready");
6419
+ const choices = group.allowedRuntimes.filter((runtime2) => {
6420
+ const executable = runtimeExecutables[runtime2];
6421
+ if (!executable) return false;
6422
+ if (group.profile === "docker")
6423
+ return Boolean(group.dockerPolicy?.commands[runtime2]);
6424
+ return Boolean(locateExecutable(executable, group.projectRoot));
6425
+ });
6426
+ const main = observed.sessions.find((session) => session.id === view.mainId);
6427
+ if (!main?.alive) throw new Error("startup_main_unavailable");
6428
+ process.stderr.write(`PMCP WORKER | ${group.projectRoot}
6429
+ `);
6430
+ const prompt = terminalPrompt();
6431
+ try {
6432
+ const runtime2 = await chooseRuntime(prompt, choices, main.runtime);
6433
+ if (!runtime2 || !await confirmRuntimeStart(prompt, runtime2)) return;
6434
+ await startViewWorker(file, runtime2);
6435
+ } finally {
6436
+ prompt.close();
6437
+ }
6438
+ }
6439
+
5920
6440
  // src/tui/application/worker.ts
5921
6441
  var [role, ...argv] = process.argv.slice(2);
5922
6442
  var callback2 = argv.includes("--callback");
5923
6443
  try {
5924
- if (role === "attach") {
6444
+ if (role === "startup") {
6445
+ const [encoded, ...extra] = argv;
6446
+ if (!encoded || extra.length || encoded.length > 16384 || !/^[A-Za-z0-9_-]+$/.test(encoded))
6447
+ throw new Error("invalid_startup_arguments");
6448
+ const request2 = JSON.parse(
6449
+ Buffer.from(encoded, "base64url").toString("utf8")
6450
+ );
6451
+ if (typeof request2.cwd !== "string" || !request2.cwd)
6452
+ throw new Error("invalid_startup_arguments");
6453
+ const result = await prepareWorkspace(request2);
6454
+ process.stdout.write(`${JSON.stringify(result)}
6455
+ `);
6456
+ } else if (role === "worker-menu") {
6457
+ const invocation = parseTui(
6458
+ parseArgs(
6459
+ ["worker-menu", ...argv.filter((arg) => arg !== "--callback")],
6460
+ tuiOptions
6461
+ )
6462
+ );
6463
+ await runWorkerMenu(invocation.viewFile);
6464
+ } else if (role === "attach") {
5925
6465
  const [file, sessionId, leaseFile] = argv;
5926
6466
  if (!file || !sessionId) throw new Error("invalid_attachment_arguments");
5927
6467
  await waitFor(() => {
@@ -5953,7 +6493,14 @@ try {
5953
6493
  tuiOptions
5954
6494
  )
5955
6495
  );
5956
- if (invocation.action === "create" || invocation.action === "doctor")
6496
+ if ([
6497
+ "create",
6498
+ "doctor",
6499
+ "launch",
6500
+ "plan",
6501
+ "recovery",
6502
+ "start-worker"
6503
+ ].includes(invocation.action))
5957
6504
  throw new Error("unsupported_worker_operation");
5958
6505
  const result = role === "locked" ? await operate(
5959
6506
  invocation.action,
@@ -5972,7 +6519,8 @@ try {
5972
6519
  const file = argv[index + 1];
5973
6520
  if (index >= 0 && file) {
5974
6521
  try {
5975
- recordCallbackFailure(file, argv[0] ?? "callback", error);
6522
+ const action = role === "worker-menu" ? role : argv[0] ?? "callback";
6523
+ recordCallbackFailure(file, action, error);
5976
6524
  const { view } = readView(file);
5977
6525
  if (view.state === "open") new TmuxView(view, file).status(view);
5978
6526
  } catch {