@cleocode/cleo 2026.5.131 → 2026.5.132

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -47108,24 +47108,143 @@ var init_server = __esm({
47108
47108
  }
47109
47109
  });
47110
47110
 
47111
- // packages/cleo/src/cli/commands/docs-viewer.ts
47112
- import { spawn } from "node:child_process";
47113
- import { open as fsOpen } from "node:fs/promises";
47111
+ // packages/cleo/src/cli/docs-viewer-subsystem.ts
47112
+ import { mkdir as mkdir2 } from "node:fs/promises";
47114
47113
  import { join as join20 } from "node:path";
47115
- import { fileURLToPath as fileURLToPath5 } from "node:url";
47116
- import { getCleoHome as getCleoHome3, getProjectRoot as getProjectRoot37 } from "@cleocode/core";
47117
- function getCleoBinPath() {
47118
- const thisFile = fileURLToPath5(import.meta.url);
47119
- return join20(thisFile, "..", "..", "index.js");
47114
+ import { getCleoHome as getCleoHome3 } from "@cleocode/core";
47115
+ import { defineSubsystem } from "@cleocode/runtime/daemon";
47116
+ function getViewerPaths() {
47117
+ const cleoHome = getCleoHome3();
47118
+ return {
47119
+ pidFile: viewerPidFilePath(),
47120
+ logFile: join20(cleoHome, "viewer.log"),
47121
+ logDir: cleoHome
47122
+ };
47123
+ }
47124
+ function isViewerProcessRunning(pid) {
47125
+ return isProcessAlive(pid);
47120
47126
  }
47121
- async function waitForExit(pid, timeoutMs, intervalMs = 100) {
47122
- const deadline = Date.now() + timeoutMs;
47123
- while (Date.now() < deadline) {
47124
- if (!isProcessAlive(pid)) return true;
47125
- await new Promise((r) => setTimeout(r, intervalMs));
47127
+ async function getViewerStatus() {
47128
+ const record = await readViewerPidFile();
47129
+ if (!record) return { running: false, pid: null, port: null, host: null, url: null };
47130
+ if (!isProcessAlive(record.pid)) {
47131
+ await removeViewerPidFile();
47132
+ return { running: false, pid: null, port: null, host: null, url: null };
47126
47133
  }
47127
- return !isProcessAlive(pid);
47134
+ return {
47135
+ running: true,
47136
+ pid: record.pid,
47137
+ port: record.port,
47138
+ host: record.host,
47139
+ url: `http://${record.host}:${record.port}`
47140
+ };
47128
47141
  }
47142
+ function createDocsViewerSubsystem(opts = {}) {
47143
+ const startPort = opts.startPort ?? VIEWER_DEFAULT_PORT;
47144
+ const endPort = opts.endPort ?? VIEWER_DEFAULT_END_PORT;
47145
+ const host = opts.host ?? VIEWER_DEFAULT_HOST;
47146
+ const autoIncrement = !(opts.noAutoPort ?? false);
47147
+ let live;
47148
+ return defineSubsystem({
47149
+ name: VIEWER_SUBSYSTEM_NAME,
47150
+ async start() {
47151
+ const { pidFile, logDir } = getViewerPaths();
47152
+ const existing = await getViewerStatus();
47153
+ if (existing.running && existing.pid !== null) {
47154
+ const ctx2 = {
47155
+ pid: existing.pid,
47156
+ pidFile,
47157
+ port: existing.port ?? startPort,
47158
+ host: existing.host ?? host
47159
+ };
47160
+ live = ctx2;
47161
+ return ctx2;
47162
+ }
47163
+ await mkdir2(logDir, { recursive: true });
47164
+ const handle = await startViewer({
47165
+ startPort,
47166
+ endPort,
47167
+ host,
47168
+ autoIncrement
47169
+ });
47170
+ await writeViewerPidFile({
47171
+ pid: process.pid,
47172
+ port: handle.port,
47173
+ host: handle.host,
47174
+ projectRoot: process.cwd(),
47175
+ startedAt: Date.now()
47176
+ });
47177
+ const ctx = {
47178
+ pid: process.pid,
47179
+ pidFile,
47180
+ port: handle.port,
47181
+ host: handle.host
47182
+ };
47183
+ live = ctx;
47184
+ return ctx;
47185
+ },
47186
+ healthProbe() {
47187
+ if (live === void 0) {
47188
+ const stopped = "stopped";
47189
+ return {
47190
+ child_id: VIEWER_SUBSYSTEM_NAME,
47191
+ pid: 0,
47192
+ state: stopped,
47193
+ restart_count: 0,
47194
+ detail: "not started"
47195
+ };
47196
+ }
47197
+ const alive = isViewerProcessRunning(live.pid);
47198
+ const state = alive ? "running" : "stopped";
47199
+ return {
47200
+ child_id: VIEWER_SUBSYSTEM_NAME,
47201
+ pid: alive ? live.pid : 0,
47202
+ state,
47203
+ restart_count: 0,
47204
+ detail: alive ? `url=http://${live.host}:${live.port} pid=${live.pid}` : `pid=${live.pid} exited`
47205
+ };
47206
+ },
47207
+ async shutdown(context) {
47208
+ if (!isViewerProcessRunning(context.pid)) {
47209
+ await removeViewerPidFile();
47210
+ live = void 0;
47211
+ return;
47212
+ }
47213
+ try {
47214
+ process.kill(context.pid, "SIGTERM");
47215
+ } catch {
47216
+ }
47217
+ for (let i = 0; i < SIGTERM_GRACE_ITERATIONS; i++) {
47218
+ if (!isViewerProcessRunning(context.pid)) break;
47219
+ await new Promise((resolve11) => setTimeout(resolve11, 500));
47220
+ }
47221
+ if (isViewerProcessRunning(context.pid)) {
47222
+ try {
47223
+ process.kill(context.pid, "SIGKILL");
47224
+ } catch {
47225
+ }
47226
+ }
47227
+ await removeViewerPidFile();
47228
+ live = void 0;
47229
+ }
47230
+ });
47231
+ }
47232
+ var VIEWER_DEFAULT_PORT, VIEWER_DEFAULT_END_PORT, VIEWER_DEFAULT_HOST, VIEWER_SUBSYSTEM_NAME, SIGTERM_GRACE_ITERATIONS;
47233
+ var init_docs_viewer_subsystem = __esm({
47234
+ "packages/cleo/src/cli/docs-viewer-subsystem.ts"() {
47235
+ "use strict";
47236
+ init_pidfile();
47237
+ init_server();
47238
+ VIEWER_DEFAULT_PORT = 7777;
47239
+ VIEWER_DEFAULT_END_PORT = 7800;
47240
+ VIEWER_DEFAULT_HOST = "127.0.0.1";
47241
+ VIEWER_SUBSYSTEM_NAME = "cleo-docs-viewer";
47242
+ SIGTERM_GRACE_ITERATIONS = 20;
47243
+ }
47244
+ });
47245
+
47246
+ // packages/cleo/src/cli/commands/docs-viewer.ts
47247
+ import { spawn } from "node:child_process";
47129
47248
  function openInBrowser(url) {
47130
47249
  try {
47131
47250
  let cmd;
@@ -47144,72 +47263,40 @@ function openInBrowser(url) {
47144
47263
  } catch {
47145
47264
  }
47146
47265
  }
47147
- async function spawnDetachedServer(opts) {
47148
- const logFile = join20(getCleoHome3(), "viewer.log");
47149
- const handle = await fsOpen(logFile, "a").catch(() => null);
47150
- const stdio = handle ? ["ignore", handle.fd, handle.fd] : ["ignore", "ignore", "ignore"];
47151
- const args = [
47152
- getCleoBinPath(),
47153
- "docs",
47154
- "serve",
47155
- "--port",
47156
- String(opts.startPort),
47157
- "--end-port",
47158
- String(opts.endPort),
47159
- "--host",
47160
- opts.host
47161
- ];
47162
- if (opts.noAutoPort) args.push("--no-auto-port");
47163
- const child = spawn(process.execPath, args, {
47164
- detached: true,
47165
- stdio,
47166
- env: { ...process.env, [DETACHED_CHILD_ENV]: "1" },
47167
- cwd: getProjectRoot37()
47168
- });
47169
- child.unref();
47170
- if (handle) await handle.close();
47171
- return child;
47172
- }
47173
- var DETACHED_CHILD_ENV, serveCommand, openCommand, stopCommand4, viewerStatusCommand, runViewerStatus, viewerCommand, docsViewerSubcommands;
47266
+ var serveCommand, openCommand, stopCommand4, viewerStatusCommand, runViewerStatus, viewerCommand, docsViewerSubcommands;
47174
47267
  var init_docs_viewer = __esm({
47175
47268
  "packages/cleo/src/cli/commands/docs-viewer.ts"() {
47176
47269
  "use strict";
47177
47270
  init_src2();
47178
47271
  init_dist();
47179
47272
  init_pidfile();
47180
- init_server();
47273
+ init_docs_viewer_subsystem();
47181
47274
  init_renderers();
47182
- DETACHED_CHILD_ENV = "CLEO_VIEWER_DETACHED_CHILD";
47183
47275
  serveCommand = defineCommand({
47184
47276
  meta: {
47185
47277
  name: "serve",
47186
- description: "[legacy] Run docs viewer \u2014 prefer `cleo docs viewer start`"
47278
+ description: "Run docs viewer \u2014 prefer `cleo docs viewer start`"
47187
47279
  },
47188
47280
  args: {
47189
47281
  port: {
47190
47282
  type: "string",
47191
47283
  description: "Starting port (default 7777)",
47192
- default: "7777"
47284
+ default: String(VIEWER_DEFAULT_PORT)
47193
47285
  },
47194
47286
  "end-port": {
47195
47287
  type: "string",
47196
47288
  description: "Last port to try when auto-incrementing (default 7800)",
47197
- default: "7800"
47289
+ default: String(VIEWER_DEFAULT_END_PORT)
47198
47290
  },
47199
47291
  host: {
47200
47292
  type: "string",
47201
47293
  description: "Bind host (default 127.0.0.1)",
47202
- default: "127.0.0.1"
47294
+ default: VIEWER_DEFAULT_HOST
47203
47295
  },
47204
47296
  "no-auto-port": {
47205
47297
  type: "boolean",
47206
47298
  description: "Disable auto-increment when start port is busy",
47207
47299
  default: false
47208
- },
47209
- detach: {
47210
- type: "boolean",
47211
- description: "Run in background; write pid to viewer.pid; exit immediately",
47212
- default: false
47213
47300
  }
47214
47301
  },
47215
47302
  async run({ args }) {
@@ -47217,8 +47304,6 @@ var init_docs_viewer = __esm({
47217
47304
  const endPort = Number.parseInt(String(args["end-port"]), 10);
47218
47305
  const host = String(args.host);
47219
47306
  const noAutoPort = Boolean(args["no-auto-port"]);
47220
- const detach = Boolean(args.detach);
47221
- const isDetachedChild = process.env[DETACHED_CHILD_ENV] === "1";
47222
47307
  if (!Number.isFinite(startPort) || startPort < 1 || startPort > 65535) {
47223
47308
  cliError(`invalid --port: ${args.port}`, 6 /* VALIDATION_ERROR */, { name: "E_VALIDATION" });
47224
47309
  return;
@@ -47229,79 +47314,25 @@ var init_docs_viewer = __esm({
47229
47314
  });
47230
47315
  return;
47231
47316
  }
47232
- if (detach && !isDetachedChild) {
47233
- const existing = await readViewerPidFile();
47234
- if (existing && isProcessAlive(existing.pid)) {
47235
- cliOutput(
47236
- {
47237
- running: true,
47238
- pid: existing.pid,
47239
- port: existing.port,
47240
- host: existing.host,
47241
- url: `http://${existing.host}:${existing.port}`,
47242
- pidFile: viewerPidFilePath()
47243
- },
47244
- {
47245
- command: "docs serve",
47246
- operation: "docs.serve",
47247
- message: `viewer already running on http://${existing.host}:${existing.port}`
47248
- }
47249
- );
47250
- return;
47251
- }
47252
- const child = await spawnDetachedServer({ startPort, endPort, host, noAutoPort });
47253
- if (!child.pid) {
47254
- cliError("failed to spawn detached viewer process", 1 /* GENERAL_ERROR */, {
47255
- name: "E_SPAWN_FAILED"
47256
- });
47257
- return;
47258
- }
47259
- let record2 = null;
47260
- const deadline = Date.now() + 1e4;
47261
- while (Date.now() < deadline) {
47262
- await new Promise((r) => setTimeout(r, 150));
47263
- record2 = await readViewerPidFile();
47264
- if (record2 && record2.pid === child.pid) break;
47265
- if (!isProcessAlive(child.pid)) {
47266
- cliError(
47267
- "detached viewer exited before binding (check ~/.local/share/cleo/viewer.log)",
47268
- 1 /* GENERAL_ERROR */,
47269
- { name: "E_VIEWER_EXITED" }
47270
- );
47271
- return;
47272
- }
47273
- }
47274
- if (!record2 || record2.pid !== child.pid) {
47275
- cliError("timed out waiting for detached viewer to bind", 1 /* GENERAL_ERROR */, {
47276
- name: "E_VIEWER_TIMEOUT"
47277
- });
47278
- return;
47279
- }
47317
+ try {
47318
+ const subsystem = createDocsViewerSubsystem({ startPort, endPort, host, noAutoPort });
47319
+ const ctx = await subsystem.start();
47320
+ const { pidFile } = getViewerPaths();
47280
47321
  cliOutput(
47281
47322
  {
47282
47323
  running: true,
47283
- pid: record2.pid,
47284
- port: record2.port,
47285
- host: record2.host,
47286
- url: `http://${record2.host}:${record2.port}`,
47287
- pidFile: viewerPidFilePath()
47324
+ pid: ctx.pid,
47325
+ port: ctx.port,
47326
+ host: ctx.host,
47327
+ url: `http://${ctx.host}:${ctx.port}`,
47328
+ pidFile
47288
47329
  },
47289
47330
  {
47290
47331
  command: "docs serve",
47291
47332
  operation: "docs.serve",
47292
- message: `viewer started on http://${record2.host}:${record2.port}`
47333
+ message: `viewer started on http://${ctx.host}:${ctx.port}`
47293
47334
  }
47294
47335
  );
47295
- return;
47296
- }
47297
- let handle;
47298
- try {
47299
- handle = await startViewer({
47300
- startPort,
47301
- endPort,
47302
- host,
47303
- autoIncrement: !noAutoPort
47304
- });
47305
47336
  } catch (err) {
47306
47337
  const e = err;
47307
47338
  if (e.code === "E_NO_PORT" || e.code === "EADDRINUSE") {
@@ -47312,49 +47343,12 @@ var init_docs_viewer = __esm({
47312
47343
  }
47313
47344
  throw err;
47314
47345
  }
47315
- const record = {
47316
- pid: process.pid,
47317
- port: handle.port,
47318
- host: handle.host,
47319
- projectRoot: getProjectRoot37(),
47320
- startedAt: Date.now()
47321
- };
47322
- await writeViewerPidFile(record);
47323
- const url = `http://${handle.host}:${handle.port}`;
47324
- if (isDetachedChild) {
47325
- } else {
47326
- humanInfo(`viewer listening on ${url} (Ctrl+C to stop)`);
47327
- cliOutput(
47328
- {
47329
- running: true,
47330
- pid: record.pid,
47331
- port: record.port,
47332
- host: record.host,
47333
- url,
47334
- pidFile: viewerPidFilePath()
47335
- },
47336
- {
47337
- command: "docs serve",
47338
- operation: "docs.serve",
47339
- message: `viewer running on ${url}`
47340
- }
47341
- );
47342
- }
47343
- const shutdown = async (signal) => {
47344
- handle.server.close();
47345
- await removeViewerPidFile();
47346
- process.exit(signal === "SIGINT" ? 130 : 143);
47347
- };
47348
- process.on("SIGINT", () => void shutdown("SIGINT"));
47349
- process.on("SIGTERM", () => void shutdown("SIGTERM"));
47350
- await new Promise((res) => handle.server.on("close", res));
47351
- await removeViewerPidFile();
47352
47346
  }
47353
47347
  });
47354
47348
  openCommand = defineCommand({
47355
47349
  meta: {
47356
47350
  name: "open",
47357
- description: "[legacy] Open docs viewer in browser \u2014 prefer `cleo docs viewer open`"
47351
+ description: "Open docs viewer in browser \u2014 prefer `cleo docs viewer open`"
47358
47352
  },
47359
47353
  args: {
47360
47354
  slug: {
@@ -47365,17 +47359,17 @@ var init_docs_viewer = __esm({
47365
47359
  port: {
47366
47360
  type: "string",
47367
47361
  description: "Starting port for the viewer (default 7777)",
47368
- default: "7777"
47362
+ default: String(VIEWER_DEFAULT_PORT)
47369
47363
  },
47370
47364
  "end-port": {
47371
47365
  type: "string",
47372
47366
  description: "Last port to try (default 7800)",
47373
- default: "7800"
47367
+ default: String(VIEWER_DEFAULT_END_PORT)
47374
47368
  },
47375
47369
  host: {
47376
47370
  type: "string",
47377
47371
  description: "Bind host (default 127.0.0.1)",
47378
- default: "127.0.0.1"
47372
+ default: VIEWER_DEFAULT_HOST
47379
47373
  },
47380
47374
  "no-launch": {
47381
47375
  type: "boolean",
@@ -47385,49 +47379,30 @@ var init_docs_viewer = __esm({
47385
47379
  },
47386
47380
  async run({ args }) {
47387
47381
  const slug = args.slug ? String(args.slug) : void 0;
47388
- let record = await readViewerPidFile();
47389
- if (record && !isProcessAlive(record.pid)) {
47390
- await removeViewerPidFile();
47391
- record = null;
47392
- }
47393
- if (!record) {
47382
+ let status = await getViewerStatus();
47383
+ if (!status.running) {
47394
47384
  const startPort = Number.parseInt(String(args.port), 10);
47395
47385
  const endPort = Number.parseInt(String(args["end-port"]), 10);
47396
47386
  const host = String(args.host);
47397
- const child = await spawnDetachedServer({
47398
- startPort,
47399
- endPort,
47400
- host,
47401
- noAutoPort: false
47402
- });
47403
- if (!child.pid) {
47404
- cliError("failed to spawn detached viewer process", 1 /* GENERAL_ERROR */, {
47405
- name: "E_SPAWN_FAILED"
47406
- });
47407
- return;
47408
- }
47409
- const deadline = Date.now() + 1e4;
47410
- while (Date.now() < deadline) {
47411
- await new Promise((r) => setTimeout(r, 150));
47412
- record = await readViewerPidFile();
47413
- if (record && record.pid === child.pid) break;
47414
- if (!isProcessAlive(child.pid)) {
47415
- cliError(
47416
- "detached viewer exited before binding (check ~/.local/share/cleo/viewer.log)",
47417
- 1 /* GENERAL_ERROR */,
47418
- { name: "E_VIEWER_EXITED" }
47419
- );
47420
- return;
47421
- }
47422
- }
47423
- if (!record) {
47424
- cliError("timed out waiting for viewer to bind", 1 /* GENERAL_ERROR */, {
47425
- name: "E_VIEWER_TIMEOUT"
47387
+ try {
47388
+ const subsystem = createDocsViewerSubsystem({ startPort, endPort, host });
47389
+ await subsystem.start();
47390
+ status = await getViewerStatus();
47391
+ } catch (err) {
47392
+ const e = err;
47393
+ cliError(e.message ?? "failed to start docs viewer", 1 /* GENERAL_ERROR */, {
47394
+ name: "E_VIEWER_START_FAILED"
47426
47395
  });
47427
47396
  return;
47428
47397
  }
47429
47398
  }
47430
- const url = slug ? `http://${record.host}:${record.port}/docs/${encodeURIComponent(slug)}` : `http://${record.host}:${record.port}/`;
47399
+ if (!status.running || status.pid === null || status.port === null || status.host === null) {
47400
+ cliError("timed out waiting for viewer to bind", 1 /* GENERAL_ERROR */, {
47401
+ name: "E_VIEWER_TIMEOUT"
47402
+ });
47403
+ return;
47404
+ }
47405
+ const url = slug ? `http://${status.host}:${status.port}/docs/${encodeURIComponent(slug)}` : `http://${status.host}:${status.port}/`;
47431
47406
  if (!args["no-launch"]) {
47432
47407
  openInBrowser(url);
47433
47408
  }
@@ -47435,9 +47410,9 @@ var init_docs_viewer = __esm({
47435
47410
  {
47436
47411
  opened: true,
47437
47412
  slug: slug ?? null,
47438
- pid: record.pid,
47439
- port: record.port,
47440
- host: record.host,
47413
+ pid: status.pid,
47414
+ port: status.port,
47415
+ host: status.host,
47441
47416
  url
47442
47417
  },
47443
47418
  {
@@ -47451,7 +47426,7 @@ var init_docs_viewer = __esm({
47451
47426
  stopCommand4 = defineCommand({
47452
47427
  meta: {
47453
47428
  name: "stop",
47454
- description: "[legacy] Stop the docs viewer \u2014 prefer `cleo docs viewer stop`"
47429
+ description: "Stop the docs viewer \u2014 prefer `cleo docs viewer stop`"
47455
47430
  },
47456
47431
  args: {
47457
47432
  timeout: {
@@ -47485,38 +47460,29 @@ var init_docs_viewer = __esm({
47485
47460
  );
47486
47461
  return;
47487
47462
  }
47488
- try {
47489
- process.kill(record.pid, "SIGTERM");
47490
- } catch (err) {
47491
- const e = err;
47492
- if (e.code !== "ESRCH") {
47493
- cliError(
47494
- `failed to signal viewer pid ${record.pid}: ${e.message ?? e.code}`,
47495
- 1 /* GENERAL_ERROR */,
47496
- { name: "E_SIGNAL_FAILED" }
47497
- );
47498
- return;
47499
- }
47500
- }
47463
+ const { pidFile } = getViewerPaths();
47464
+ const subsystem = createDocsViewerSubsystem({
47465
+ startPort: record.port,
47466
+ host: record.host
47467
+ });
47468
+ await subsystem.shutdown({
47469
+ pid: record.pid,
47470
+ pidFile,
47471
+ port: record.port,
47472
+ host: record.host
47473
+ });
47501
47474
  const timeoutSec = Math.max(1, Number.parseInt(String(args.timeout), 10) || 10);
47502
- const exited = await waitForExit(record.pid, timeoutSec * 1e3);
47503
- if (!exited) {
47504
- try {
47505
- process.kill(record.pid, "SIGKILL");
47506
- } catch {
47507
- }
47508
- }
47509
- await removeViewerPidFile();
47510
47475
  cliOutput(
47511
47476
  {
47512
47477
  stopped: true,
47513
47478
  pid: record.pid,
47514
- graceful: exited
47479
+ graceful: true,
47480
+ timeoutSec
47515
47481
  },
47516
47482
  {
47517
47483
  command: "docs stop",
47518
47484
  operation: "docs.stop",
47519
- message: exited ? `viewer (pid ${record.pid}) stopped gracefully` : `viewer (pid ${record.pid}) force-killed after ${timeoutSec}s`
47485
+ message: `viewer (pid ${record.pid}) stopped`
47520
47486
  }
47521
47487
  );
47522
47488
  }
@@ -47524,15 +47490,15 @@ var init_docs_viewer = __esm({
47524
47490
  viewerStatusCommand = defineCommand({
47525
47491
  meta: {
47526
47492
  name: "viewer-status",
47527
- description: "[legacy] Report viewer state \u2014 prefer `cleo docs viewer status`"
47493
+ description: "Report viewer state \u2014 prefer `cleo docs viewer status`"
47528
47494
  },
47529
47495
  async run() {
47530
47496
  await runViewerStatus();
47531
47497
  }
47532
47498
  });
47533
47499
  runViewerStatus = async () => {
47534
- const record = await readViewerPidFile();
47535
- if (!record) {
47500
+ const status = await getViewerStatus();
47501
+ if (!status.running || status.pid === null) {
47536
47502
  cliOutput(
47537
47503
  { running: false, pidFile: viewerPidFilePath() },
47538
47504
  {
@@ -47543,40 +47509,19 @@ var init_docs_viewer = __esm({
47543
47509
  );
47544
47510
  return;
47545
47511
  }
47546
- const alive = isProcessAlive(record.pid);
47547
- if (!alive) {
47548
- await removeViewerPidFile();
47549
- cliOutput(
47550
- {
47551
- running: false,
47552
- reason: "stale pidfile",
47553
- pid: record.pid,
47554
- pidFile: viewerPidFilePath()
47555
- },
47556
- {
47557
- command: "docs viewer-status",
47558
- operation: "docs.viewer-status",
47559
- message: `stale pidfile removed (pid ${record.pid} not alive)`
47560
- }
47561
- );
47562
- return;
47563
- }
47564
47512
  cliOutput(
47565
47513
  {
47566
47514
  running: true,
47567
- pid: record.pid,
47568
- port: record.port,
47569
- host: record.host,
47570
- projectRoot: record.projectRoot,
47571
- startedAt: record.startedAt,
47572
- uptimeMs: Date.now() - record.startedAt,
47573
- url: `http://${record.host}:${record.port}`,
47515
+ pid: status.pid,
47516
+ port: status.port,
47517
+ host: status.host,
47518
+ url: status.url,
47574
47519
  pidFile: viewerPidFilePath()
47575
47520
  },
47576
47521
  {
47577
47522
  command: "docs viewer-status",
47578
47523
  operation: "docs.viewer-status",
47579
- message: `viewer running (pid ${record.pid})`
47524
+ message: `viewer running (pid ${status.pid})`
47580
47525
  }
47581
47526
  );
47582
47527
  };
@@ -47613,7 +47558,7 @@ __export(docs_exports, {
47613
47558
  _llmOutputCommand: () => _llmOutputCommand,
47614
47559
  docsCommand: () => docsCommand
47615
47560
  });
47616
- import { appendFile, mkdir as mkdir2, readdir, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
47561
+ import { appendFile, mkdir as mkdir3, readdir, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
47617
47562
  import { dirname as dirname8, isAbsolute as isAbsolute2, join as join21, resolve as resolve5 } from "node:path";
47618
47563
  import { pushWarning as pushWarning3 } from "@cleocode/core";
47619
47564
  import {
@@ -47624,7 +47569,7 @@ import {
47624
47569
  DEFAULT_SIMILARITY_THRESHOLD,
47625
47570
  detectStrayCleoDb as detectStrayCleoDb2,
47626
47571
  getAgentOutputsAbsolute,
47627
- getProjectRoot as getProjectRoot38,
47572
+ getProjectRoot as getProjectRoot37,
47628
47573
  readJson,
47629
47574
  resolveWorktreeFilePath,
47630
47575
  resolveWorktreeRouting as resolveWorktreeRouting4
@@ -47896,7 +47841,7 @@ var init_docs3 = __esm({
47896
47841
  resolvedFile = resolveWorktreeFilePath(String(fileArg), routing);
47897
47842
  }
47898
47843
  if (args.slug && args.type) {
47899
- const projectRoot = await getProjectRoot38();
47844
+ const projectRoot = await getProjectRoot37();
47900
47845
  let warnThreshold = DEFAULT_SIMILARITY_THRESHOLD;
47901
47846
  let mode = DEFAULT_SIMILARITY_MODE;
47902
47847
  try {
@@ -47955,7 +47900,7 @@ var init_docs3 = __esm({
47955
47900
  })}
47956
47901
  `;
47957
47902
  try {
47958
- await mkdir2(join21(projectRoot, ".cleo", "audit"), { recursive: true });
47903
+ await mkdir3(join21(projectRoot, ".cleo", "audit"), { recursive: true });
47959
47904
  await appendFile(
47960
47905
  join21(projectRoot, ".cleo", "audit", "similar-bypass.jsonl"),
47961
47906
  auditLine,
@@ -48276,7 +48221,7 @@ var init_docs3 = __esm({
48276
48221
  const taskId = String(args.task);
48277
48222
  const includeAttachments = args["include-attachments"] !== false;
48278
48223
  const includeMemoryRefs = args["include-memory-refs"] === true;
48279
- const projectRoot = getProjectRoot38();
48224
+ const projectRoot = getProjectRoot37();
48280
48225
  try {
48281
48226
  const result = await dispatchDocsRaw("query", "llm-output", {
48282
48227
  mode: "task-export",
@@ -48287,7 +48232,7 @@ var init_docs3 = __esm({
48287
48232
  let writtenPath;
48288
48233
  if (typeof args.out === "string" && args.out.length > 0) {
48289
48234
  const outPath = isAbsolute2(args.out) ? args.out : resolve5(projectRoot, args.out);
48290
- await mkdir2(dirname8(outPath), { recursive: true });
48235
+ await mkdir3(dirname8(outPath), { recursive: true });
48291
48236
  await writeFile2(outPath, result.content, "utf8");
48292
48237
  writtenPath = outPath;
48293
48238
  }
@@ -48347,7 +48292,7 @@ var init_docs3 = __esm({
48347
48292
  },
48348
48293
  async run({ args }) {
48349
48294
  const forId = String(args.for);
48350
- const projectRoot = getProjectRoot38();
48295
+ const projectRoot = getProjectRoot37();
48351
48296
  try {
48352
48297
  const result = await dispatchDocsRaw("query", "llm-output", {
48353
48298
  for: forId,
@@ -48359,7 +48304,7 @@ var init_docs3 = __esm({
48359
48304
  let writtenPath;
48360
48305
  if (typeof args.out === "string" && args.out.length > 0) {
48361
48306
  const outPath = isAbsolute2(args.out) ? args.out : resolve5(projectRoot, args.out);
48362
- await mkdir2(dirname8(outPath), { recursive: true });
48307
+ await mkdir3(dirname8(outPath), { recursive: true });
48363
48308
  await writeFile2(outPath, result.content, "utf8");
48364
48309
  writtenPath = outPath;
48365
48310
  }
@@ -48424,7 +48369,7 @@ var init_docs3 = __esm({
48424
48369
  },
48425
48370
  async run({ args }) {
48426
48371
  const forId = String(args.for);
48427
- const projectRoot = getProjectRoot38();
48372
+ const projectRoot = getProjectRoot37();
48428
48373
  try {
48429
48374
  const result = await dispatchDocsRaw("query", "llm-output", {
48430
48375
  for: forId,
@@ -48436,7 +48381,7 @@ var init_docs3 = __esm({
48436
48381
  let writtenPath;
48437
48382
  if (typeof args.out === "string" && args.out.length > 0) {
48438
48383
  const outPath = isAbsolute2(args.out) ? args.out : resolve5(projectRoot, args.out);
48439
- await mkdir2(dirname8(outPath), { recursive: true });
48384
+ await mkdir3(dirname8(outPath), { recursive: true });
48440
48385
  await writeFile2(outPath, result.content, "utf8");
48441
48386
  writtenPath = outPath;
48442
48387
  }
@@ -48650,7 +48595,7 @@ var init_docs3 = __esm({
48650
48595
  }
48651
48596
  },
48652
48597
  async run({ args }) {
48653
- const projectRoot = getProjectRoot38();
48598
+ const projectRoot = getProjectRoot37();
48654
48599
  const rawStrategy = args.strategy ?? "three-way";
48655
48600
  const strategy = rawStrategy === "cherry-pick" || rawStrategy === "multi-diff" ? rawStrategy : "three-way";
48656
48601
  try {
@@ -48662,7 +48607,7 @@ var init_docs3 = __esm({
48662
48607
  });
48663
48608
  if (typeof args.out === "string" && args.out.length > 0) {
48664
48609
  const outPath = isAbsolute2(args.out) ? args.out : resolve5(projectRoot, args.out);
48665
- await mkdir2(dirname8(outPath), { recursive: true });
48610
+ await mkdir3(dirname8(outPath), { recursive: true });
48666
48611
  await writeFile2(outPath, result.merged, "utf8");
48667
48612
  humanInfo(`Wrote merged content to ${outPath}`);
48668
48613
  }
@@ -49420,7 +49365,7 @@ var init_docs3 = __esm({
49420
49365
  }
49421
49366
  },
49422
49367
  async run({ args }) {
49423
- const projectRoot = getProjectRoot38();
49368
+ const projectRoot = getProjectRoot37();
49424
49369
  const dirArg = String(args.dir);
49425
49370
  const scanRoot = isAbsolute2(dirArg) ? dirArg : resolve5(projectRoot, dirArg);
49426
49371
  const dryRun = args["dry-run"] === true;
@@ -49434,7 +49379,7 @@ var init_docs3 = __esm({
49434
49379
  })}
49435
49380
  `;
49436
49381
  try {
49437
- await mkdir2(join21(projectRoot, ".cleo", "audit"), { recursive: true });
49382
+ await mkdir3(join21(projectRoot, ".cleo", "audit"), { recursive: true });
49438
49383
  await appendFile(
49439
49384
  join21(projectRoot, ".cleo", "audit", "import-force-bypass.jsonl"),
49440
49385
  auditLine,
@@ -49482,7 +49427,7 @@ var init_docs3 = __esm({
49482
49427
  }
49483
49428
  },
49484
49429
  async run({ args }) {
49485
- const projectRoot = getProjectRoot38();
49430
+ const projectRoot = getProjectRoot37();
49486
49431
  const { registry, configError } = loadCliRegistry(projectRoot);
49487
49432
  const kinds = registry.list().map(toWireKind);
49488
49433
  const extensionsCount = kinds.filter((k) => k.isExtension).length;
@@ -49537,7 +49482,7 @@ var init_docs3 = __esm({
49537
49482
  deprecated: "docs list-types",
49538
49483
  replacement: "docs schema"
49539
49484
  });
49540
- const projectRoot = getProjectRoot38();
49485
+ const projectRoot = getProjectRoot37();
49541
49486
  const { registry, configError } = loadCliRegistry(projectRoot);
49542
49487
  const kinds = registry.list().map(toWireKind);
49543
49488
  const extensionsCount = kinds.filter((k) => k.isExtension).length;
@@ -49631,7 +49576,7 @@ var doctor_db_substrate_exports = {};
49631
49576
  __export(doctor_db_substrate_exports, {
49632
49577
  doctorDbSubstrateCommand: () => doctorDbSubstrateCommand
49633
49578
  });
49634
- import { getProjectRoot as getProjectRoot39, pushWarning as pushWarning4 } from "@cleocode/core";
49579
+ import { getProjectRoot as getProjectRoot38, pushWarning as pushWarning4 } from "@cleocode/core";
49635
49580
  import { surveyDbSubstrate, surveyFleetDbSubstrate } from "@cleocode/core/doctor/db-substrate.js";
49636
49581
  function pushSubstrateWarnings(result) {
49637
49582
  for (const warning of result.warnings) {
@@ -49764,7 +49709,7 @@ var init_doctor_db_substrate = __esm({
49764
49709
  const result = isFleet ? surveyFleetDbSubstrate(
49765
49710
  typeof args["fleet-root"] === "string" && args["fleet-root"].length > 0 ? args["fleet-root"] : DEFAULT_FLEET_ROOT,
49766
49711
  options
49767
- ) : surveyDbSubstrate(getProjectRoot39(), options);
49712
+ ) : surveyDbSubstrate(getProjectRoot38(), options);
49768
49713
  pushSubstrateWarnings(result);
49769
49714
  pushPerDbWarnings(result);
49770
49715
  cliOutput(result, {
@@ -49784,7 +49729,7 @@ var doctor_legacy_backups_exports = {};
49784
49729
  __export(doctor_legacy_backups_exports, {
49785
49730
  doctorLegacyBackupsCommand: () => doctorLegacyBackupsCommand
49786
49731
  });
49787
- import { getProjectRoot as getProjectRoot40 } from "@cleocode/core";
49732
+ import { getProjectRoot as getProjectRoot39 } from "@cleocode/core";
49788
49733
  import { pruneLegacyBackups, scanLegacyBackups } from "@cleocode/core/doctor/legacy-backups.js";
49789
49734
  function parsePositiveInt(raw, fallback) {
49790
49735
  if (raw === void 0 || raw === null) return fallback;
@@ -49828,7 +49773,7 @@ var init_doctor_legacy_backups = __esm({
49828
49773
  async run({ args }) {
49829
49774
  const softRetentionDays = parsePositiveInt(args["soft-retention-days"], 30);
49830
49775
  const hardRetentionDays = parsePositiveInt(args["hard-retention-days"], 90);
49831
- const projectRoot = getProjectRoot40();
49776
+ const projectRoot = getProjectRoot39();
49832
49777
  let result;
49833
49778
  if (args.prune === true) {
49834
49779
  const dryRun = args["dry-run"] !== false;
@@ -50395,7 +50340,7 @@ __export(migrate_agents_v2_exports, {
50395
50340
  import { createHash as createHash2 } from "node:crypto";
50396
50341
  import { appendFileSync as appendFileSync2, existsSync as existsSync14, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync13 } from "node:fs";
50397
50342
  import { join as join23 } from "node:path";
50398
- import { getProjectRoot as getProjectRoot41, installAgentFromCant } from "@cleocode/core/internal";
50343
+ import { getProjectRoot as getProjectRoot40, installAgentFromCant } from "@cleocode/core/internal";
50399
50344
  import { openCleoDb as openCleoDb2 } from "@cleocode/core/store/open-cleo-db";
50400
50345
  function sha256Hex(bytes) {
50401
50346
  return createHash2("sha256").update(bytes).digest("hex");
@@ -50582,7 +50527,7 @@ var init_migrate_agents_v2 = __esm({
50582
50527
  }
50583
50528
  },
50584
50529
  async run({ args }) {
50585
- const projectRoot = getProjectRoot41();
50530
+ const projectRoot = getProjectRoot40();
50586
50531
  const verbose = args.quiet !== true;
50587
50532
  if (verbose) {
50588
50533
  humanInfo("Scanning .cleo/cant/agents/ and .cleo/agents/ for unregistered agents...");
@@ -50626,7 +50571,7 @@ __export(doctor_exports, {
50626
50571
  });
50627
50572
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
50628
50573
  import { join as join24 } from "node:path";
50629
- import { getProjectRoot as getProjectRoot42, pushWarning as pushWarning5 } from "@cleocode/core";
50574
+ import { getProjectRoot as getProjectRoot41, pushWarning as pushWarning5 } from "@cleocode/core";
50630
50575
  import { renderInvariantAuditLines } from "@cleocode/core/doctor/invariant-audit-render.js";
50631
50576
  import {
50632
50577
  quarantineRogueCleoDir,
@@ -50899,9 +50844,18 @@ var init_doctor = __esm({
50899
50844
  type: "boolean",
50900
50845
  description: "Migrate <root>/.cleo/worktree-include (legacy) \u2192 <root>/.worktreeinclude (canonical, T9983). Combine with --dry-run to preview."
50901
50846
  },
50847
+ /**
50848
+ * T11489 — DHQ-037/019: detect and auto-heal a leaked `core.worktree` key
50849
+ * in the shared `.git/config`. Reports E_WT_CONFIG_LEAK when found; heals
50850
+ * automatically unless `--dry-run` is passed.
50851
+ */
50852
+ "check-worktree-config": {
50853
+ type: "boolean",
50854
+ description: "Detect and auto-heal a leaked core.worktree key in .git/config (T11489 / DHQ-037). Combine with --dry-run to report without healing."
50855
+ },
50902
50856
  "dry-run": {
50903
50857
  type: "boolean",
50904
- description: "With --quarantine-rogue-cleo-dirs or --scan-stray-nexus-dbs: print what would be done without acting"
50858
+ description: "With --quarantine-rogue-cleo-dirs, --scan-stray-nexus-dbs, or --check-worktree-config: print what would be done without acting"
50905
50859
  },
50906
50860
  /**
50907
50861
  * Show brain.db health dashboard (T1908 / BBTT-W2-4).
@@ -50951,9 +50905,64 @@ var init_doctor = __esm({
50951
50905
  const progress = createDoctorProgress(isHuman);
50952
50906
  progress.start();
50953
50907
  try {
50908
+ if (args["check-worktree-config"]) {
50909
+ const { execFileSync: execFile2 } = await import("node:child_process");
50910
+ const { join: pathJoin } = await import("node:path");
50911
+ const { existsSync: existsSync21 } = await import("node:fs");
50912
+ const { detectAndHealCoreWorktreeLeak } = await import("@cleocode/worktree");
50913
+ const projectRoot = getProjectRoot41();
50914
+ let gitRoot = projectRoot;
50915
+ try {
50916
+ gitRoot = execFile2("git", ["rev-parse", "--show-toplevel"], {
50917
+ cwd: projectRoot,
50918
+ encoding: "utf-8",
50919
+ stdio: ["pipe", "pipe", "pipe"]
50920
+ }).trim();
50921
+ } catch {
50922
+ }
50923
+ const isDryRun = args["dry-run"] === true;
50924
+ progress.step(0, "Checking for leaked core.worktree in .git/config");
50925
+ if (isDryRun) {
50926
+ const gitConfigPath = pathJoin(gitRoot, ".git", "config");
50927
+ let leakedValue;
50928
+ if (existsSync21(gitConfigPath)) {
50929
+ try {
50930
+ leakedValue = execFile2("git", ["config", "--file", gitConfigPath, "--get", "core.worktree"], {
50931
+ encoding: "utf-8",
50932
+ stdio: ["pipe", "pipe", "pipe"]
50933
+ }).trim() || void 0;
50934
+ } catch {
50935
+ }
50936
+ }
50937
+ const report = {
50938
+ gitRoot,
50939
+ gitConfigPath,
50940
+ leakDetected: !!leakedValue,
50941
+ leakedValue,
50942
+ healed: false,
50943
+ dryRun: true,
50944
+ message: leakedValue ? `DRY-RUN: core.worktree="${leakedValue}" found \u2014 would unset` : "No core.worktree leak detected"
50945
+ };
50946
+ progress.complete(report.message);
50947
+ cliOutput(report, { command: "doctor", operation: "doctor.check-worktree-config" });
50948
+ if (leakedValue) process.exitCode = 2;
50949
+ } else {
50950
+ const result = detectAndHealCoreWorktreeLeak(gitRoot);
50951
+ const report = {
50952
+ gitRoot,
50953
+ ...result,
50954
+ dryRun: false,
50955
+ message: result.leakDetected ? result.healed ? `E_WT_CONFIG_LEAK healed: removed core.worktree="${result.leakedValue}"` : `E_WT_CONFIG_LEAK detected but heal FAILED: ${result.healError}` : "No core.worktree leak detected"
50956
+ };
50957
+ progress.complete(report.message);
50958
+ cliOutput(report, { command: "doctor", operation: "doctor.check-worktree-config" });
50959
+ if (result.leakDetected && !result.healed) process.exitCode = 1;
50960
+ }
50961
+ return;
50962
+ }
50954
50963
  if (args.brain) {
50955
50964
  const { computeBrainHealthDashboard } = await import("@cleocode/core/memory/brain-health-dashboard.js");
50956
- const projectRoot = getProjectRoot42();
50965
+ const projectRoot = getProjectRoot41();
50957
50966
  const dashboard = await computeBrainHealthDashboard(projectRoot);
50958
50967
  cliOutput(dashboard, { command: "doctor", operation: "doctor.brain" });
50959
50968
  if (dashboard.hasP0Failure) {
@@ -50962,7 +50971,7 @@ var init_doctor = __esm({
50962
50971
  return;
50963
50972
  }
50964
50973
  if (args["scan-test-fixtures-in-prod"]) {
50965
- const projectRoot = getProjectRoot42();
50974
+ const projectRoot = getProjectRoot41();
50966
50975
  const matches = await scanTestFixturesInProd(projectRoot);
50967
50976
  const dryRun = args["dry-run"] !== false && args.quarantine !== true;
50968
50977
  const quarantined = !dryRun && matches.length > 0 ? await quarantineTestFixtures(projectRoot, matches) : void 0;
@@ -51050,7 +51059,7 @@ var init_doctor = __esm({
51050
51059
  progress.complete("Comprehensive diagnostics complete");
51051
51060
  } else if (args["scan-rogue-cleo-dirs"]) {
51052
51061
  progress.step(0, "Scanning for rogue .cleo/ directories");
51053
- const projectRoot = getProjectRoot42();
51062
+ const projectRoot = getProjectRoot41();
51054
51063
  const reports = scanRogueCleoDirs(projectRoot);
51055
51064
  progress.complete(
51056
51065
  `Found ${reports.length} rogue .cleo/ director${reports.length === 1 ? "y" : "ies"}`
@@ -51059,7 +51068,7 @@ var init_doctor = __esm({
51059
51068
  } else if (args["quarantine-rogue-cleo-dirs"]) {
51060
51069
  const isDryRun = args["dry-run"] === true;
51061
51070
  progress.step(0, `${isDryRun ? "[DRY RUN] " : ""}Scanning for rogue .cleo/ directories`);
51062
- const projectRoot = getProjectRoot42();
51071
+ const projectRoot = getProjectRoot41();
51063
51072
  const reports = scanRogueCleoDirs(projectRoot);
51064
51073
  if (reports.length === 0) {
51065
51074
  progress.complete("No rogue .cleo/ directories found \u2014 nothing to quarantine");
@@ -51113,7 +51122,7 @@ var init_doctor = __esm({
51113
51122
  const { detectAndRemoveLegacyGlobalFiles, detectAndRemoveStrayProjectNexus } = await import("@cleocode/core/store/cleanup-legacy.js");
51114
51123
  const { getCleoHome: getCleoHome6 } = await import("@cleocode/core");
51115
51124
  const cleoHome = getCleoHome6();
51116
- const projectRoot = getProjectRoot42();
51125
+ const projectRoot = getProjectRoot41();
51117
51126
  const legacyResult = detectAndRemoveLegacyGlobalFiles(cleoHome);
51118
51127
  const strayResult = detectAndRemoveStrayProjectNexus(projectRoot);
51119
51128
  const isDryRun = args["dry-run"] === true;
@@ -51146,7 +51155,7 @@ var init_doctor = __esm({
51146
51155
  } else if (args["audit-worktree-orphans"]) {
51147
51156
  progress.step(0, "Comprehensive worktree anomaly audit (T9808 / council D009)");
51148
51157
  const { auditWorktreeOrphansComprehensive, scanWorktreeOrphansBudgeted } = await import("@cleocode/core/doctor/worktree-orphans.js");
51149
- const projectRoot = getProjectRoot42();
51158
+ const projectRoot = getProjectRoot41();
51150
51159
  const timeoutSecs = args["timeout"] !== void 0 ? Number.parseInt(String(args["timeout"]), 10) : 30;
51151
51160
  const timeoutMs = Number.isFinite(timeoutSecs) && timeoutSecs > 0 ? timeoutSecs * 1e3 : 3e4;
51152
51161
  const maxEntriesPerLevel = args["max-entries-per-level"] !== void 0 ? Number.parseInt(String(args["max-entries-per-level"]), 10) : 500;
@@ -51203,7 +51212,7 @@ var init_doctor = __esm({
51203
51212
  `${isDryRun ? "[DRY RUN] " : ""}Scanning + pruning worktree-orphan .cleo/ directories`
51204
51213
  );
51205
51214
  const { pruneWorktreeOrphans, scanWorktreeOrphansBudgeted } = await import("@cleocode/core/doctor/worktree-orphans.js");
51206
- const projectRoot = getProjectRoot42();
51215
+ const projectRoot = getProjectRoot41();
51207
51216
  const timeoutSecs = args["timeout"] !== void 0 ? Number.parseInt(String(args["timeout"]), 10) : 30;
51208
51217
  const timeoutMs = Number.isFinite(timeoutSecs) && timeoutSecs > 0 ? timeoutSecs * 1e3 : 3e4;
51209
51218
  const maxEntriesPerLevel = args["max-entries-per-level"] !== void 0 ? Number.parseInt(String(args["max-entries-per-level"]), 10) : 500;
@@ -51282,7 +51291,7 @@ var init_doctor = __esm({
51282
51291
  `${isDryRun ? "[DRY RUN] " : ""}Migrating .cleo/worktree-include \u2192 .worktreeinclude`
51283
51292
  );
51284
51293
  const { migrateWorktreeIncludeFile } = await import("@cleocode/core");
51285
- const projectRoot = getProjectRoot42();
51294
+ const projectRoot = getProjectRoot41();
51286
51295
  const result = await migrateWorktreeIncludeFile(projectRoot, { dryRun: isDryRun });
51287
51296
  progress.complete(`Migration ${result.action}`);
51288
51297
  cliOutput(result, { command: "doctor", operation: "doctor.migrate-worktree-include" });
@@ -51301,7 +51310,7 @@ var init_doctor = __esm({
51301
51310
  const stepLabel = isFocusedAlias ? "Auditing Saga hierarchy for ADR-073 invariants" : "Walking central INVARIANTS_REGISTRY";
51302
51311
  progress.step(0, stepLabel);
51303
51312
  const { auditInvariantRegistry } = await import("@cleocode/core/doctor/invariant-audit.js");
51304
- const projectRoot = getProjectRoot42();
51313
+ const projectRoot = getProjectRoot41();
51305
51314
  const result = await auditInvariantRegistry(projectRoot, { adrFilter });
51306
51315
  const operation = isFocusedAlias ? "doctor.audit-sagas" : "doctor.audit-invariants";
51307
51316
  const summary = `Invariant audit complete \u2014 ${result.totalCount} entries walked, ${result.errorCount} error / ${result.warningCount} warning / ${result.infoCount} info violation(s), ${result.notApplicableCount} not-applicable, ${result.documentedCount} documented`;
@@ -51323,7 +51332,7 @@ var init_doctor = __esm({
51323
51332
  { command: "doctor", operation: "admin.health" }
51324
51333
  );
51325
51334
  try {
51326
- const projectRoot = getProjectRoot42();
51335
+ const projectRoot = getProjectRoot41();
51327
51336
  const conflicts = readMigrationConflicts(projectRoot);
51328
51337
  if (conflicts.length > 0) {
51329
51338
  progress.complete(
@@ -51354,7 +51363,7 @@ var init_doctor = __esm({
51354
51363
  progress.complete("Health check complete");
51355
51364
  }
51356
51365
  try {
51357
- const projectRoot = getProjectRoot42();
51366
+ const projectRoot = getProjectRoot41();
51358
51367
  const { auditSagaHierarchy } = await import("@cleocode/core/doctor/saga-audit.js");
51359
51368
  const sagaAudit = await auditSagaHierarchy(projectRoot);
51360
51369
  if (sagaAudit.sagas.length === 0) {
@@ -52522,6 +52531,48 @@ var init_gc = __esm({
52522
52531
  }
52523
52532
  });
52524
52533
 
52534
+ // packages/cleo/src/cli/commands/go.ts
52535
+ var go_exports = {};
52536
+ __export(go_exports, {
52537
+ goCommand: () => goCommand
52538
+ });
52539
+ import { getProjectRoot as getProjectRoot42, go } from "@cleocode/core";
52540
+ var goCommand;
52541
+ var init_go = __esm({
52542
+ "packages/cleo/src/cli/commands/go.ts"() {
52543
+ "use strict";
52544
+ init_define_cli_command();
52545
+ init_renderers();
52546
+ goCommand = defineCommand({
52547
+ meta: {
52548
+ name: "go",
52549
+ description: "SG-AUTOPILOT: run one turn of briefing\u2192sagaNext\u2192ready\u2192stage-branch\u2192ivtr loop"
52550
+ },
52551
+ args: {
52552
+ saga: {
52553
+ type: "string",
52554
+ description: "Optional saga task ID to scope the autopilot run (default: auto-select)",
52555
+ required: false
52556
+ },
52557
+ headless: {
52558
+ type: "boolean",
52559
+ description: "Suppress interactive annotations; suitable for daemon / unattended use",
52560
+ required: false
52561
+ }
52562
+ },
52563
+ async run({ args }) {
52564
+ const projectRoot = getProjectRoot42();
52565
+ const result = await go.cleoGo({
52566
+ sagaId: typeof args.saga === "string" && args.saga.length > 0 ? args.saga : void 0,
52567
+ headless: args.headless === true,
52568
+ projectRoot
52569
+ });
52570
+ cliOutput(result.success ? result.data : result, { command: "go", operation: "go.run" });
52571
+ }
52572
+ });
52573
+ }
52574
+ });
52575
+
52525
52576
  // packages/cleo/src/cli/commands/goal.ts
52526
52577
  var goal_exports = {};
52527
52578
  __export(goal_exports, {
@@ -52541,7 +52592,7 @@ function resolveTurnBudget(turns) {
52541
52592
  const n = typeof turns === "string" ? Number.parseInt(turns, 10) : Number(turns);
52542
52593
  return Number.isInteger(n) && n > 0 ? n : DEFAULT_TURN_BUDGET;
52543
52594
  }
52544
- var DEFAULT_TURN_BUDGET, setCommand, statusCommand9, subgoalCommand, appendCommand, goalCommand;
52595
+ var DEFAULT_TURN_BUDGET, setCommand, statusCommand9, subgoalCommand, advanceCommand2, appendCommand, goalCommand;
52545
52596
  var init_goal2 = __esm({
52546
52597
  "packages/cleo/src/cli/commands/goal.ts"() {
52547
52598
  "use strict";
@@ -52636,6 +52687,34 @@ var init_goal2 = __esm({
52636
52687
  cliOutput(record, { command: "goal subgoal", operation: "goal.subgoal" });
52637
52688
  }
52638
52689
  });
52690
+ advanceCommand2 = defineCommand({
52691
+ meta: {
52692
+ name: "advance",
52693
+ description: "Advance a goal one turn (load \u2192 judge \u2192 persist \u2192 continuation). Entry point for the Stop-hook loop."
52694
+ },
52695
+ args: {
52696
+ goalId: {
52697
+ type: "positional",
52698
+ description: "The goal id (idempotency key) to advance"
52699
+ }
52700
+ },
52701
+ async run({ args }) {
52702
+ const goalId = String(args.goalId ?? "").trim();
52703
+ if (goalId.length === 0) {
52704
+ cliError("goal advance requires a <goalId>", 6 /* VALIDATION_ERROR */, {
52705
+ name: "E_VALIDATION"
52706
+ });
52707
+ return;
52708
+ }
52709
+ const cwd = getProjectRoot43();
52710
+ const result = await goal.advanceGoalWithPersist(goalId, { cwd });
52711
+ if (!result) {
52712
+ cliError(`Goal '${goalId}' not found.`, 4 /* NOT_FOUND */, { name: "E_NOT_FOUND" });
52713
+ return;
52714
+ }
52715
+ cliOutput(result, { command: "goal advance", operation: "goal.advance" });
52716
+ }
52717
+ });
52639
52718
  appendCommand = defineCommand({
52640
52719
  meta: {
52641
52720
  name: "append",
@@ -52671,11 +52750,12 @@ var init_goal2 = __esm({
52671
52750
  goalCommand = defineCommand({
52672
52751
  meta: {
52673
52752
  name: "goal",
52674
- description: "DB-persisted, per-agent, evidence-gate-aware goal loop (set/status/subgoal/append)."
52753
+ description: "DB-persisted, per-agent, evidence-gate-aware goal loop (set/status/advance/subgoal/append)."
52675
52754
  },
52676
52755
  subCommands: {
52677
52756
  set: setCommand,
52678
52757
  status: statusCommand9,
52758
+ advance: advanceCommand2,
52679
52759
  subgoal: subgoalCommand,
52680
52760
  append: appendCommand
52681
52761
  },
@@ -53842,7 +53922,7 @@ __export(init_exports, {
53842
53922
  });
53843
53923
  import { existsSync as existsSync15, readFileSync as readFileSync14 } from "node:fs";
53844
53924
  import { join as join27 } from "node:path";
53845
- import { fileURLToPath as fileURLToPath6 } from "node:url";
53925
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
53846
53926
  import {
53847
53927
  CleoError as CleoError5,
53848
53928
  getWorkflowTemplatesDir as getCoreWorkflowTemplatesDir,
@@ -53853,7 +53933,7 @@ import {
53853
53933
  import { getTemplatesByKind } from "@cleocode/core/templates/registry";
53854
53934
  function getGitignoreTemplate() {
53855
53935
  try {
53856
- const thisFile = fileURLToPath6(import.meta.url);
53936
+ const thisFile = fileURLToPath5(import.meta.url);
53857
53937
  const packageRoot = join27(thisFile, "..", "..", "..", "..");
53858
53938
  const localTemplatePath = join27(packageRoot, "templates", "cleo-gitignore");
53859
53939
  const monorepoTemplatePath = join27(packageRoot, "..", "..", "templates", "cleo-gitignore");
@@ -58481,7 +58561,7 @@ var nexus_exports = {};
58481
58561
  __export(nexus_exports, {
58482
58562
  nexusCommand: () => nexusCommand
58483
58563
  });
58484
- import { appendFile as appendFile2, mkdir as mkdir3 } from "node:fs/promises";
58564
+ import { appendFile as appendFile2, mkdir as mkdir4 } from "node:fs/promises";
58485
58565
  import { homedir as homedir5 } from "node:os";
58486
58566
  import path4 from "node:path";
58487
58567
  import { getProjectRoot as getProjectRoot48 } from "@cleocode/core";
@@ -58493,7 +58573,7 @@ async function appendDeprecationTelemetry(op, replacement) {
58493
58573
  try {
58494
58574
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
58495
58575
  const dir = path4.join(homedir5(), ".local", "state", "cleo", "nexus-deprecation");
58496
- await mkdir3(dir, { recursive: true });
58576
+ await mkdir4(dir, { recursive: true });
58497
58577
  const record = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), op, replacement }) + "\n";
58498
58578
  await appendFile2(path4.join(dir, `${dateStr}.jsonl`), record, "utf8");
58499
58579
  } catch {
@@ -62168,7 +62248,7 @@ var phase_exports = {};
62168
62248
  __export(phase_exports, {
62169
62249
  phaseCommand: () => phaseCommand
62170
62250
  });
62171
- var showCommand10, listCommand15, setCommand2, startCommand6, completeCommand3, advanceCommand2, renameCommand, deleteCommand2, phaseCommand;
62251
+ var showCommand10, listCommand15, setCommand2, startCommand6, completeCommand3, advanceCommand3, renameCommand, deleteCommand2, phaseCommand;
62172
62252
  var init_phase = __esm({
62173
62253
  "packages/cleo/src/cli/commands/phase.ts"() {
62174
62254
  "use strict";
@@ -62268,7 +62348,7 @@ var init_phase = __esm({
62268
62348
  );
62269
62349
  }
62270
62350
  });
62271
- advanceCommand2 = defineCommand({
62351
+ advanceCommand3 = defineCommand({
62272
62352
  meta: { name: "advance", description: "Complete current phase and start next" },
62273
62353
  args: {
62274
62354
  force: {
@@ -62350,7 +62430,7 @@ var init_phase = __esm({
62350
62430
  set: setCommand2,
62351
62431
  start: startCommand6,
62352
62432
  complete: completeCommand3,
62353
- advance: advanceCommand2,
62433
+ advance: advanceCommand3,
62354
62434
  rename: renameCommand,
62355
62435
  delete: deleteCommand2
62356
62436
  },
@@ -65609,7 +65689,7 @@ __export(saga_exports, {
65609
65689
  sagaCommand: () => sagaCommand
65610
65690
  });
65611
65691
  import { parseAcceptanceCriteria } from "@cleocode/core";
65612
- var createCommand3, addCommand12, detachCommand2, listCommand22, membersCommand, repairCommand, reconcileCommand5, rollupCommand2, sagaCommand;
65692
+ var createCommand3, addCommand12, detachCommand2, listCommand22, membersCommand, repairCommand, reconcileCommand5, rollupCommand2, nextCommand3, sagaCommand;
65613
65693
  var init_saga = __esm({
65614
65694
  "packages/cleo/src/cli/commands/saga.ts"() {
65615
65695
  "use strict";
@@ -65820,6 +65900,26 @@ var init_saga = __esm({
65820
65900
  cliOutput(response.data ?? {}, { command: "saga", operation: "tasks.saga.rollup" });
65821
65901
  }
65822
65902
  });
65903
+ nextCommand3 = defineCommand({
65904
+ meta: {
65905
+ name: "next",
65906
+ description: "Return the next actionable Saga and its ready-frontier task IDs"
65907
+ },
65908
+ args: {
65909
+ sagaId: {
65910
+ type: "positional",
65911
+ description: "Optional saga task ID. Omit to auto-select from canonical order.",
65912
+ required: false
65913
+ }
65914
+ },
65915
+ async run({ args }) {
65916
+ const { sagas: sagas2, getProjectRoot: getProjectRoot57 } = await import("@cleocode/core");
65917
+ const projectRoot = getProjectRoot57();
65918
+ const sagaId = typeof args.sagaId === "string" && args.sagaId.length > 0 ? args.sagaId : void 0;
65919
+ const result = await sagas2.sagaNext(projectRoot, { sagaId });
65920
+ cliOutput(result.success ? result.data : result, { command: "saga", operation: "saga.next" });
65921
+ }
65922
+ });
65823
65923
  sagaCommand = defineCommand({
65824
65924
  meta: {
65825
65925
  name: "saga",
@@ -65833,7 +65933,8 @@ var init_saga = __esm({
65833
65933
  members: membersCommand,
65834
65934
  rollup: rollupCommand2,
65835
65935
  repair: repairCommand,
65836
- reconcile: reconcileCommand5
65936
+ reconcile: reconcileCommand5,
65937
+ next: nextCommand3
65837
65938
  },
65838
65939
  async run({ cmd, rawArgs }) {
65839
65940
  const firstArg = rawArgs?.find((a) => !a.startsWith("-"));
@@ -66075,7 +66176,7 @@ async function writeRuntimeVersionMetadata(mode, source, version) {
66075
66176
  `installed=${(/* @__PURE__ */ new Date()).toISOString()}`
66076
66177
  ];
66077
66178
  await import("node:fs/promises").then(
66078
- ({ writeFile: writeFile4, mkdir: mkdir5 }) => mkdir5(cleoHome, { recursive: true }).then(
66179
+ ({ writeFile: writeFile4, mkdir: mkdir6 }) => mkdir6(cleoHome, { recursive: true }).then(
66079
66180
  () => writeFile4(join29(cleoHome, "VERSION"), `${lines.join("\n")}
66080
66181
  `, "utf-8")
66081
66182
  )
@@ -72919,15 +73020,12 @@ var init_verify = __esm({
72919
73020
  }
72920
73021
  });
72921
73022
 
72922
- // packages/cleo/src/cli/commands/web.ts
72923
- var web_exports = {};
72924
- __export(web_exports, {
72925
- webCommand: () => webCommand
72926
- });
73023
+ // packages/cleo/src/cli/web-subsystem.ts
72927
73024
  import { execFileSync as execFileSync4, spawn as spawn3 } from "node:child_process";
72928
- import { mkdir as mkdir4, open, readFile as readFile8, rm, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
73025
+ import { mkdir as mkdir5, open, readFile as readFile8, rm, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
72929
73026
  import { join as join35 } from "node:path";
72930
- import { CleoError as CleoError12, formatError as formatError5, getCleoHome as getCleoHome5 } from "@cleocode/core";
73027
+ import { getCleoHome as getCleoHome5 } from "@cleocode/core";
73028
+ import { defineSubsystem as defineSubsystem2 } from "@cleocode/runtime/daemon";
72931
73029
  function getWebPaths() {
72932
73030
  const cleoHome = getCleoHome5();
72933
73031
  return {
@@ -72937,7 +73035,7 @@ function getWebPaths() {
72937
73035
  logFile: join35(cleoHome, "logs", "web-server.log")
72938
73036
  };
72939
73037
  }
72940
- function isProcessRunning(pid) {
73038
+ function isWebProcessRunning(pid) {
72941
73039
  try {
72942
73040
  process.kill(pid, 0);
72943
73041
  return true;
@@ -72945,20 +73043,20 @@ function isProcessRunning(pid) {
72945
73043
  return false;
72946
73044
  }
72947
73045
  }
72948
- async function getStatus() {
73046
+ async function getWebStatus() {
72949
73047
  const { pidFile, configFile } = getWebPaths();
72950
73048
  try {
72951
73049
  const pidStr = (await readFile8(pidFile, "utf-8")).trim();
72952
73050
  const pid = Number.parseInt(pidStr, 10);
72953
- if (Number.isNaN(pid) || !isProcessRunning(pid)) {
73051
+ if (Number.isNaN(pid) || !isWebProcessRunning(pid)) {
72954
73052
  return { running: false, pid: null, port: null, host: null, url: null };
72955
73053
  }
72956
- let port = DEFAULT_PORT;
72957
- let host = DEFAULT_HOST;
73054
+ let port = WEB_DEFAULT_PORT;
73055
+ let host = WEB_DEFAULT_HOST;
72958
73056
  try {
72959
73057
  const config = JSON.parse(await readFile8(configFile, "utf-8"));
72960
- port = config.port ?? DEFAULT_PORT;
72961
- host = config.host ?? DEFAULT_HOST;
73058
+ port = config.port ?? WEB_DEFAULT_PORT;
73059
+ host = config.host ?? WEB_DEFAULT_HOST;
72962
73060
  } catch {
72963
73061
  }
72964
73062
  return { running: true, pid, port, host, url: `http://${host}:${port}` };
@@ -72966,118 +73064,212 @@ async function getStatus() {
72966
73064
  return { running: false, pid: null, port: null, host: null, url: null };
72967
73065
  }
72968
73066
  }
72969
- async function startWebServer(port, host) {
72970
- const { pidFile, configFile, logFile, logDir } = getWebPaths();
72971
- const status = await getStatus();
72972
- if (status.running) {
72973
- throw new CleoError12(1 /* GENERAL_ERROR */, `Server already running (PID: ${status.pid})`);
72974
- }
72975
- const projectRoot = process.env["CLEO_ROOT"] ?? process.cwd();
72976
- const studioDir = process.env["CLEO_STUDIO_DIR"] ?? join35(projectRoot, "packages", "studio", "build");
72977
- await mkdir4(logDir, { recursive: true });
72978
- await writeFile3(
72979
- configFile,
72980
- JSON.stringify({
72981
- port,
72982
- host,
72983
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
72984
- })
72985
- );
72986
- const webIndexPath = join35(studioDir, "index.js");
72987
- try {
72988
- await stat2(webIndexPath);
72989
- } catch {
72990
- try {
72991
- execFileSync4("pnpm", ["--filter", "@cleocode/studio", "run", "build"], {
72992
- cwd: projectRoot,
72993
- stdio: "ignore"
72994
- });
72995
- } catch {
72996
- throw new CleoError12(
72997
- 1 /* GENERAL_ERROR */,
72998
- `Studio build failed. Run: pnpm --filter @cleocode/studio run build
72999
- Logs: ${logFile}`
73067
+ function createWebSubsystem(opts = {}) {
73068
+ const port = opts.port ?? WEB_DEFAULT_PORT;
73069
+ const host = opts.host ?? WEB_DEFAULT_HOST;
73070
+ let live;
73071
+ return defineSubsystem2({
73072
+ name: WEB_SUBSYSTEM_NAME,
73073
+ async start() {
73074
+ const { pidFile, configFile, logFile, logDir } = getWebPaths();
73075
+ const existing = await getWebStatus();
73076
+ if (existing.running && existing.pid !== null) {
73077
+ const ctx2 = {
73078
+ pid: existing.pid,
73079
+ pidFile,
73080
+ logFile,
73081
+ port: existing.port ?? port,
73082
+ host: existing.host ?? host
73083
+ };
73084
+ live = ctx2;
73085
+ return ctx2;
73086
+ }
73087
+ const projectRoot = process.env["CLEO_ROOT"] ?? process.cwd();
73088
+ const studioDir = process.env["CLEO_STUDIO_DIR"] ?? join35(projectRoot, "packages", "studio", "build");
73089
+ const webIndexPath = join35(studioDir, "index.js");
73090
+ await mkdir5(logDir, { recursive: true });
73091
+ await writeFile3(
73092
+ configFile,
73093
+ JSON.stringify({ port, host, startedAt: (/* @__PURE__ */ new Date()).toISOString() })
73000
73094
  );
73001
- }
73002
- }
73003
- const logFileHandle = await open(logFile, "a");
73004
- const serverProcess = spawn3("node", [webIndexPath], {
73005
- cwd: studioDir,
73006
- env: {
73007
- ...process.env,
73008
- HOST: host,
73009
- PORT: String(port),
73010
- // Pass CLEO paths through to the studio server
73011
- CLEO_ROOT: projectRoot
73095
+ try {
73096
+ await stat2(webIndexPath);
73097
+ } catch {
73098
+ try {
73099
+ execFileSync4("pnpm", ["--filter", "@cleocode/studio", "run", "build"], {
73100
+ cwd: projectRoot,
73101
+ stdio: "ignore"
73102
+ });
73103
+ } catch {
73104
+ throw new Error(
73105
+ `Studio build failed. Run: pnpm --filter @cleocode/studio run build
73106
+ Logs: ${logFile}`
73107
+ );
73108
+ }
73109
+ }
73110
+ const logFileHandle = await open(logFile, "a");
73111
+ const serverProcess = spawn3("node", [webIndexPath], {
73112
+ cwd: studioDir,
73113
+ env: {
73114
+ ...process.env,
73115
+ HOST: host,
73116
+ PORT: String(port),
73117
+ CLEO_ROOT: projectRoot
73118
+ },
73119
+ detached: true,
73120
+ stdio: ["ignore", logFileHandle.fd, logFileHandle.fd]
73121
+ });
73122
+ serverProcess.unref();
73123
+ const pidFileTmp = `${pidFile}.tmp`;
73124
+ await writeFile3(pidFileTmp, String(serverProcess.pid));
73125
+ await rm(pidFile, { force: true });
73126
+ await writeFile3(pidFile, String(serverProcess.pid));
73127
+ await rm(pidFileTmp, { force: true });
73128
+ await logFileHandle.close();
73129
+ let started = false;
73130
+ for (let i = 0; i < STARTUP_POLL_ITERATIONS; i++) {
73131
+ try {
73132
+ const response = await fetch(`http://${host}:${port}/api/health`);
73133
+ if (response.ok) {
73134
+ started = true;
73135
+ break;
73136
+ }
73137
+ } catch {
73138
+ }
73139
+ await new Promise((resolve11) => setTimeout(resolve11, 500));
73140
+ }
73141
+ if (!started) {
73142
+ try {
73143
+ process.kill(serverProcess.pid, "SIGTERM");
73144
+ } catch {
73145
+ }
73146
+ await rm(pidFile, { force: true });
73147
+ throw new Error("Studio web server failed to start within 15 seconds");
73148
+ }
73149
+ const ctx = {
73150
+ pid: serverProcess.pid,
73151
+ pidFile,
73152
+ logFile,
73153
+ port,
73154
+ host
73155
+ };
73156
+ live = ctx;
73157
+ return ctx;
73012
73158
  },
73013
- detached: true,
73014
- stdio: ["ignore", logFileHandle.fd, logFileHandle.fd]
73015
- });
73016
- serverProcess.unref();
73017
- const pidFileTmp = `${pidFile}.tmp`;
73018
- await writeFile3(pidFileTmp, String(serverProcess.pid));
73019
- await rm(pidFile, { force: true });
73020
- await writeFile3(pidFile, String(serverProcess.pid));
73021
- await rm(pidFileTmp, { force: true });
73022
- await logFileHandle.close();
73023
- const maxAttempts = 30;
73024
- let started = false;
73025
- for (let i = 0; i < maxAttempts; i++) {
73026
- try {
73027
- const response = await fetch(`http://${host}:${port}/api/health`);
73028
- if (response.ok) {
73029
- started = true;
73030
- break;
73159
+ healthProbe() {
73160
+ if (live === void 0) {
73161
+ const stopped = "stopped";
73162
+ return {
73163
+ child_id: WEB_SUBSYSTEM_NAME,
73164
+ pid: 0,
73165
+ state: stopped,
73166
+ restart_count: 0,
73167
+ detail: "not started"
73168
+ };
73031
73169
  }
73032
- } catch {
73033
- }
73034
- await new Promise((resolve11) => setTimeout(resolve11, 500));
73035
- }
73036
- if (!started) {
73037
- try {
73038
- process.kill(serverProcess.pid);
73039
- } catch {
73040
- }
73041
- await rm(pidFile, { force: true });
73042
- throw new CleoError12(1 /* GENERAL_ERROR */, "Server failed to start within 15 seconds");
73043
- }
73044
- cliOutput(
73045
- {
73046
- pid: serverProcess.pid,
73047
- port,
73048
- host,
73049
- url: `http://${host}:${port}`,
73050
- logFile
73170
+ const alive = isWebProcessRunning(live.pid);
73171
+ const state = alive ? "running" : "stopped";
73172
+ return {
73173
+ child_id: WEB_SUBSYSTEM_NAME,
73174
+ pid: alive ? live.pid : 0,
73175
+ state,
73176
+ restart_count: 0,
73177
+ detail: alive ? `url=http://${live.host}:${live.port} pid=${live.pid}` : `pid=${live.pid} exited`
73178
+ };
73051
73179
  },
73052
- { command: "web", message: `CLEO Web UI running on port ${port}` }
73053
- );
73180
+ async shutdown(context) {
73181
+ const { pidFile } = context;
73182
+ if (!isWebProcessRunning(context.pid)) {
73183
+ await rm(pidFile, { force: true });
73184
+ live = void 0;
73185
+ return;
73186
+ }
73187
+ try {
73188
+ if (process.platform === "win32") {
73189
+ spawn3("taskkill", ["/PID", String(context.pid), "/T"], { stdio: "ignore" });
73190
+ } else {
73191
+ process.kill(context.pid, "SIGTERM");
73192
+ }
73193
+ } catch {
73194
+ }
73195
+ for (let i = 0; i < SIGTERM_GRACE_ITERATIONS2; i++) {
73196
+ if (!isWebProcessRunning(context.pid)) break;
73197
+ await new Promise((resolve11) => setTimeout(resolve11, 500));
73198
+ }
73199
+ if (isWebProcessRunning(context.pid)) {
73200
+ try {
73201
+ if (process.platform === "win32") {
73202
+ spawn3("taskkill", ["/PID", String(context.pid), "/F", "/T"], { stdio: "ignore" });
73203
+ } else {
73204
+ process.kill(context.pid, "SIGKILL");
73205
+ }
73206
+ } catch {
73207
+ }
73208
+ }
73209
+ await rm(pidFile, { force: true });
73210
+ live = void 0;
73211
+ }
73212
+ });
73054
73213
  }
73055
- var DEFAULT_PORT, DEFAULT_HOST, startCommand9, stopCommand6, restartCommand, statusCommand18, openCommand3, webCommand;
73214
+ var WEB_DEFAULT_PORT, WEB_DEFAULT_HOST, WEB_SUBSYSTEM_NAME, SIGTERM_GRACE_ITERATIONS2, STARTUP_POLL_ITERATIONS;
73215
+ var init_web_subsystem = __esm({
73216
+ "packages/cleo/src/cli/web-subsystem.ts"() {
73217
+ "use strict";
73218
+ WEB_DEFAULT_PORT = 3456;
73219
+ WEB_DEFAULT_HOST = "127.0.0.1";
73220
+ WEB_SUBSYSTEM_NAME = "cleo-web";
73221
+ SIGTERM_GRACE_ITERATIONS2 = 60;
73222
+ STARTUP_POLL_ITERATIONS = 30;
73223
+ }
73224
+ });
73225
+
73226
+ // packages/cleo/src/cli/commands/web.ts
73227
+ var web_exports = {};
73228
+ __export(web_exports, {
73229
+ webCommand: () => webCommand
73230
+ });
73231
+ import { spawn as spawn4 } from "node:child_process";
73232
+ import { rm as rm2 } from "node:fs/promises";
73233
+ import { CleoError as CleoError12, formatError as formatError5 } from "@cleocode/core";
73234
+ var startCommand9, stopCommand6, restartCommand, statusCommand18, openCommand3, webCommand;
73056
73235
  var init_web = __esm({
73057
73236
  "packages/cleo/src/cli/commands/web.ts"() {
73058
73237
  "use strict";
73059
73238
  init_src2();
73060
73239
  init_dist();
73061
73240
  init_renderers();
73062
- DEFAULT_PORT = 3456;
73063
- DEFAULT_HOST = "127.0.0.1";
73241
+ init_web_subsystem();
73064
73242
  startCommand9 = defineCommand({
73065
73243
  meta: { name: "start", description: "Start the web server" },
73066
73244
  args: {
73067
73245
  port: {
73068
73246
  type: "string",
73069
73247
  description: "Server port",
73070
- default: String(DEFAULT_PORT)
73248
+ default: String(WEB_DEFAULT_PORT)
73071
73249
  },
73072
73250
  host: {
73073
73251
  type: "string",
73074
73252
  description: "Server host",
73075
- default: DEFAULT_HOST
73253
+ default: WEB_DEFAULT_HOST
73076
73254
  }
73077
73255
  },
73078
73256
  async run({ args }) {
73079
73257
  try {
73080
- await startWebServer(Number.parseInt(args.port, 10), args.host);
73258
+ const port = Number.parseInt(args.port, 10);
73259
+ const host = args.host;
73260
+ const subsystem = createWebSubsystem({ port, host });
73261
+ const ctx = await subsystem.start();
73262
+ const { logFile } = getWebPaths();
73263
+ cliOutput(
73264
+ {
73265
+ pid: ctx.pid,
73266
+ port: ctx.port,
73267
+ host: ctx.host,
73268
+ url: `http://${ctx.host}:${ctx.port}`,
73269
+ logFile
73270
+ },
73271
+ { command: "web", message: `CLEO Web UI running on port ${ctx.port}` }
73272
+ );
73081
73273
  } catch (err) {
73082
73274
  if (err instanceof CleoError12) {
73083
73275
  console.error(formatError5(err));
@@ -73091,36 +73283,24 @@ var init_web = __esm({
73091
73283
  meta: { name: "stop", description: "Stop the web server" },
73092
73284
  async run() {
73093
73285
  try {
73094
- const { pidFile } = getWebPaths();
73095
- const status = await getStatus();
73096
- if (!status.running || !status.pid) {
73097
- await rm(pidFile, { force: true });
73286
+ const { pidFile, logFile } = getWebPaths();
73287
+ const status = await getWebStatus();
73288
+ if (!status.running || status.pid === null) {
73289
+ await rm2(pidFile, { force: true });
73098
73290
  cliOutput({ running: false }, { command: "web", message: "Server is not running" });
73099
73291
  return;
73100
73292
  }
73101
- try {
73102
- if (process.platform === "win32") {
73103
- spawn3("taskkill", ["/PID", String(status.pid), "/T"], { stdio: "ignore" });
73104
- } else {
73105
- process.kill(status.pid, "SIGTERM");
73106
- }
73107
- } catch {
73108
- }
73109
- for (let i = 0; i < 60; i++) {
73110
- if (!isProcessRunning(status.pid)) break;
73111
- await new Promise((resolve11) => setTimeout(resolve11, 500));
73112
- }
73113
- if (isProcessRunning(status.pid)) {
73114
- try {
73115
- if (process.platform === "win32") {
73116
- spawn3("taskkill", ["/PID", String(status.pid), "/F", "/T"], { stdio: "ignore" });
73117
- } else {
73118
- process.kill(status.pid, "SIGKILL");
73119
- }
73120
- } catch {
73121
- }
73122
- }
73123
- await rm(pidFile, { force: true });
73293
+ const subsystem = createWebSubsystem({
73294
+ port: status.port ?? WEB_DEFAULT_PORT,
73295
+ host: status.host ?? WEB_DEFAULT_HOST
73296
+ });
73297
+ await subsystem.shutdown({
73298
+ pid: status.pid,
73299
+ pidFile,
73300
+ logFile,
73301
+ port: status.port ?? WEB_DEFAULT_PORT,
73302
+ host: status.host ?? WEB_DEFAULT_HOST
73303
+ });
73124
73304
  cliOutput({ stopped: true }, { command: "web", message: "CLEO Web UI stopped" });
73125
73305
  } catch (err) {
73126
73306
  if (err instanceof CleoError12) {
@@ -73137,44 +73317,45 @@ var init_web = __esm({
73137
73317
  port: {
73138
73318
  type: "string",
73139
73319
  description: "Server port",
73140
- default: String(DEFAULT_PORT)
73320
+ default: String(WEB_DEFAULT_PORT)
73141
73321
  },
73142
73322
  host: {
73143
73323
  type: "string",
73144
73324
  description: "Server host",
73145
- default: DEFAULT_HOST
73325
+ default: WEB_DEFAULT_HOST
73146
73326
  }
73147
73327
  },
73148
73328
  async run({ args }) {
73149
73329
  try {
73150
- const { pidFile } = getWebPaths();
73151
- const status = await getStatus();
73152
- if (status.running && status.pid) {
73153
- try {
73154
- if (process.platform === "win32") {
73155
- spawn3("taskkill", ["/PID", String(status.pid), "/T"], { stdio: "ignore" });
73156
- } else {
73157
- process.kill(status.pid, "SIGTERM");
73158
- }
73159
- } catch {
73160
- }
73161
- for (let i = 0; i < 60; i++) {
73162
- if (!isProcessRunning(status.pid)) break;
73163
- await new Promise((resolve11) => setTimeout(resolve11, 500));
73164
- }
73165
- if (isProcessRunning(status.pid)) {
73166
- try {
73167
- if (process.platform === "win32") {
73168
- spawn3("taskkill", ["/PID", String(status.pid), "/F", "/T"], { stdio: "ignore" });
73169
- } else {
73170
- process.kill(status.pid, "SIGKILL");
73171
- }
73172
- } catch {
73173
- }
73174
- }
73175
- await rm(pidFile, { force: true });
73330
+ const port = Number.parseInt(args.port, 10);
73331
+ const host = args.host;
73332
+ const { pidFile, logFile } = getWebPaths();
73333
+ const status = await getWebStatus();
73334
+ if (status.running && status.pid !== null) {
73335
+ const stopSubsystem = createWebSubsystem({
73336
+ port: status.port ?? port,
73337
+ host: status.host ?? host
73338
+ });
73339
+ await stopSubsystem.shutdown({
73340
+ pid: status.pid,
73341
+ pidFile,
73342
+ logFile,
73343
+ port: status.port ?? port,
73344
+ host: status.host ?? host
73345
+ });
73176
73346
  }
73177
- await startWebServer(Number.parseInt(args.port, 10), args.host);
73347
+ const startSubsystem = createWebSubsystem({ port, host });
73348
+ const ctx = await startSubsystem.start();
73349
+ cliOutput(
73350
+ {
73351
+ pid: ctx.pid,
73352
+ port: ctx.port,
73353
+ host: ctx.host,
73354
+ url: `http://${ctx.host}:${ctx.port}`,
73355
+ logFile
73356
+ },
73357
+ { command: "web", message: `CLEO Web UI running on port ${ctx.port}` }
73358
+ );
73178
73359
  } catch (err) {
73179
73360
  if (err instanceof CleoError12) {
73180
73361
  console.error(formatError5(err));
@@ -73188,7 +73369,7 @@ var init_web = __esm({
73188
73369
  meta: { name: "status", description: "Check server status" },
73189
73370
  async run() {
73190
73371
  try {
73191
- const status = await getStatus();
73372
+ const status = await getWebStatus();
73192
73373
  cliOutput(status, { command: "web" });
73193
73374
  } catch (err) {
73194
73375
  if (err instanceof CleoError12) {
@@ -73203,7 +73384,7 @@ var init_web = __esm({
73203
73384
  meta: { name: "open", description: "Open browser to the UI" },
73204
73385
  async run() {
73205
73386
  try {
73206
- const status = await getStatus();
73387
+ const status = await getWebStatus();
73207
73388
  if (!status.running || !status.url) {
73208
73389
  throw new CleoError12(
73209
73390
  1 /* GENERAL_ERROR */,
@@ -73214,11 +73395,11 @@ var init_web = __esm({
73214
73395
  const platform = process.platform;
73215
73396
  try {
73216
73397
  if (platform === "linux") {
73217
- spawn3("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
73398
+ spawn4("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
73218
73399
  } else if (platform === "darwin") {
73219
- spawn3("open", [url], { detached: true, stdio: "ignore" }).unref();
73400
+ spawn4("open", [url], { detached: true, stdio: "ignore" }).unref();
73220
73401
  } else if (platform === "win32") {
73221
- spawn3("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
73402
+ spawn4("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
73222
73403
  }
73223
73404
  } catch {
73224
73405
  }
@@ -73257,7 +73438,7 @@ __export(workgraph_exports, {
73257
73438
  });
73258
73439
  import { readFileSync as readFileSync20 } from "node:fs";
73259
73440
  import { resolve as resolve10 } from "node:path";
73260
- var validateCommand10, applyCommand, planCommand4, structureCommand, workgraphCommand;
73441
+ var validateCommand10, applyCommand, planCommand4, structureCommand, statusCommand19, workgraphCommand;
73261
73442
  var init_workgraph2 = __esm({
73262
73443
  "packages/cleo/src/cli/commands/workgraph.ts"() {
73263
73444
  "use strict";
@@ -73401,16 +73582,64 @@ var init_workgraph2 = __esm({
73401
73582
  cliOutput2(result, { command: "workgraph", operation: "structure" });
73402
73583
  }
73403
73584
  });
73585
+ statusCommand19 = defineCommand({
73586
+ meta: {
73587
+ name: "status",
73588
+ description: "One-shot saga-to-saga workgraph dashboard (tracking checklist)"
73589
+ },
73590
+ async run() {
73591
+ const { sagas: sagas2, getProjectRoot: getProjectRoot57 } = await import("@cleocode/core");
73592
+ const { cliOutput: cliOutput2 } = await Promise.resolve().then(() => (init_renderers(), renderers_exports));
73593
+ const projectRoot = getProjectRoot57();
73594
+ const listResult = await sagas2.sagaList(projectRoot);
73595
+ if (!listResult.success) {
73596
+ cliOutput2(listResult, { command: "workgraph", operation: "workgraph.status" });
73597
+ return;
73598
+ }
73599
+ const allSagas = listResult.data?.sagas ?? [];
73600
+ const sagaMap = new Map(
73601
+ allSagas.map((s) => [s.id, s])
73602
+ );
73603
+ const TERMINAL = /* @__PURE__ */ new Set(["done", "cancelled", "deleted", "archived", "completed"]);
73604
+ const rows = [];
73605
+ for (let i = 0; i < sagas2.CANONICAL_SAGA_ORDER.length; i++) {
73606
+ const [id, label] = sagas2.CANONICAL_SAGA_ORDER[i];
73607
+ const saga = sagaMap.get(id);
73608
+ if (!saga) continue;
73609
+ const status = saga.status ?? "pending";
73610
+ const rollupResult = await sagas2.sagaRollup(projectRoot, { sagaId: id });
73611
+ const rollup = rollupResult.success ? rollupResult.data : { total: 0, done: 0, completionPct: 0 };
73612
+ rows.push({
73613
+ rank: i + 1,
73614
+ sagaId: id,
73615
+ status,
73616
+ completionPct: rollup.completionPct ?? 0,
73617
+ done: rollup.done ?? 0,
73618
+ total: rollup.total ?? 0,
73619
+ label: TERMINAL.has(status) ? `${label} \u2713` : label
73620
+ });
73621
+ }
73622
+ cliOutput2(
73623
+ {
73624
+ rows,
73625
+ total: rows.length,
73626
+ activeSagaCount: rows.filter((r) => !TERMINAL.has(r.status)).length
73627
+ },
73628
+ { command: "workgraph", operation: "workgraph.status" }
73629
+ );
73630
+ }
73631
+ });
73404
73632
  workgraphCommand = defineCommand({
73405
73633
  meta: {
73406
73634
  name: "workgraph",
73407
- description: "PM-Core V2 WorkGraph operations \u2014 validate, apply, plan, structure"
73635
+ description: "PM-Core V2 WorkGraph operations \u2014 validate, apply, plan, structure, status"
73408
73636
  },
73409
73637
  subCommands: {
73410
73638
  validate: validateCommand10,
73411
73639
  apply: applyCommand,
73412
73640
  plan: planCommand4,
73413
- structure: structureCommand
73641
+ structure: structureCommand,
73642
+ status: statusCommand19
73414
73643
  },
73415
73644
  async run({ cmd, rawArgs }) {
73416
73645
  const firstArg = rawArgs?.find((a) => !a.startsWith("-"));
@@ -73775,7 +74004,7 @@ init_field_context();
73775
74004
  init_format_context();
73776
74005
  import { readFileSync as readFileSync21 } from "node:fs";
73777
74006
  import { dirname as dirname11, join as join37 } from "node:path";
73778
- import { fileURLToPath as fileURLToPath7 } from "node:url";
74007
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
73779
74008
  import { enforceNodeVersion } from "@cleocode/paths";
73780
74009
 
73781
74010
  // packages/cleo/src/cli/generated/command-manifest.ts
@@ -74176,10 +74405,16 @@ var COMMAND_MANIFEST = [
74176
74405
  description: "Transcript garbage collection: manual trigger and status",
74177
74406
  load: async () => (await Promise.resolve().then(() => (init_gc(), gc_exports))).gcCommand
74178
74407
  },
74408
+ {
74409
+ exportName: "goCommand",
74410
+ name: "go",
74411
+ description: "SG-AUTOPILOT: run one turn of briefing\u2192sagaNext\u2192ready\u2192stage-branch\u2192ivtr loop",
74412
+ load: async () => (await Promise.resolve().then(() => (init_go(), go_exports))).goCommand
74413
+ },
74179
74414
  {
74180
74415
  exportName: "goalCommand",
74181
74416
  name: "goal",
74182
- description: "DB-persisted, per-agent, evidence-gate-aware goal loop (set/status/subgoal/append).",
74417
+ description: "DB-persisted, per-agent, evidence-gate-aware goal loop (set/status/advance/subgoal/append).",
74183
74418
  load: async () => (await Promise.resolve().then(() => (init_goal2(), goal_exports))).goalCommand
74184
74419
  },
74185
74420
  {
@@ -74659,7 +74894,7 @@ var COMMAND_MANIFEST = [
74659
74894
  {
74660
74895
  exportName: "workgraphCommand",
74661
74896
  name: "workgraph",
74662
- description: "PM-Core V2 WorkGraph operations \u2014 validate, apply, plan, structure",
74897
+ description: "PM-Core V2 WorkGraph operations \u2014 validate, apply, plan, structure, status",
74663
74898
  load: async () => (await Promise.resolve().then(() => (init_workgraph2(), workgraph_exports))).workgraphCommand
74664
74899
  },
74665
74900
  {
@@ -74940,7 +75175,7 @@ async function resolveSubCommandForHelp(cmd, rawArgs) {
74940
75175
  init_summary_context();
74941
75176
  enforceNodeVersion();
74942
75177
  function getPackageVersion() {
74943
- const pkgPath = join37(dirname11(fileURLToPath7(import.meta.url)), "../../package.json");
75178
+ const pkgPath = join37(dirname11(fileURLToPath6(import.meta.url)), "../../package.json");
74944
75179
  const pkg = JSON.parse(readFileSync21(pkgPath, "utf-8"));
74945
75180
  return pkg.version;
74946
75181
  }