@evo-dev/evodev 0.0.1-alpha.10 → 0.0.1-alpha.11

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.
@@ -4,13 +4,13 @@
4
4
  "name": "EvoDev"
5
5
  },
6
6
  "description": "EvoDev Claude Code hook plugins for AI-assisted R&D workflows.",
7
- "version": "0.0.1-alpha.10",
7
+ "version": "0.0.1-alpha.11",
8
8
  "plugins": [
9
9
  {
10
10
  "name": "evodev",
11
11
  "source": "./packages/plugin",
12
12
  "description": "EvoDev hook integration for disciplined AI coding workflows.",
13
- "version": "0.0.1-alpha.10",
13
+ "version": "0.0.1-alpha.11",
14
14
  "author": {
15
15
  "name": "EvoDev"
16
16
  },
package/dist/index.js CHANGED
@@ -29106,6 +29106,131 @@ async function fileExists2(path2) {
29106
29106
  }
29107
29107
  }
29108
29108
 
29109
+ // packages/cli/src/ui/runtime.ts
29110
+ import { mkdir as mkdir24, readFile as readFile35, rm as rm11, writeFile as writeFile24 } from "node:fs/promises";
29111
+ import { join as join35 } from "node:path";
29112
+ function resolveUiRuntimePaths(homeDir) {
29113
+ const rootDir = join35(resolveEvoDevPaths(homeDir).stateDir, "ui");
29114
+ return {
29115
+ rootDir,
29116
+ runtimePath: join35(rootDir, "runtime.json"),
29117
+ tokenPath: join35(rootDir, "token")
29118
+ };
29119
+ }
29120
+ async function readUiRuntimeBundle(homeDir) {
29121
+ const paths3 = resolveUiRuntimePaths(homeDir);
29122
+ const [runtimeText, tokenText] = await Promise.all([
29123
+ readOptionalFile(paths3.runtimePath),
29124
+ readOptionalFile(paths3.tokenPath)
29125
+ ]);
29126
+ return {
29127
+ state: runtimeText === null ? null : parseUiRuntimeState(runtimeText),
29128
+ token: tokenText === null ? null : parseUiRuntimeToken(tokenText)
29129
+ };
29130
+ }
29131
+ async function writeUiRuntimeState(input) {
29132
+ const state = parseUiRuntimeState(JSON.stringify(input.state));
29133
+ const token = parseUiRuntimeToken(input.token);
29134
+ const paths3 = resolveUiRuntimePaths(input.homeDir);
29135
+ let tokenCreated = false;
29136
+ let runtimeCreated = false;
29137
+ await mkdir24(paths3.rootDir, { recursive: true });
29138
+ try {
29139
+ await writeFile24(paths3.tokenPath, `${token}
29140
+ `, {
29141
+ encoding: "utf8",
29142
+ flag: "wx",
29143
+ mode: 384
29144
+ });
29145
+ tokenCreated = true;
29146
+ await writeFile24(paths3.runtimePath, `${JSON.stringify(state, null, 2)}
29147
+ `, {
29148
+ encoding: "utf8",
29149
+ flag: "wx",
29150
+ mode: 384
29151
+ });
29152
+ runtimeCreated = true;
29153
+ return paths3;
29154
+ } catch (error) {
29155
+ if (runtimeCreated)
29156
+ await rm11(paths3.runtimePath, { force: true }).catch(() => {
29157
+ return;
29158
+ });
29159
+ if (tokenCreated)
29160
+ await rm11(paths3.tokenPath, { force: true }).catch(() => {
29161
+ return;
29162
+ });
29163
+ throw error;
29164
+ }
29165
+ }
29166
+ async function removeUiRuntimeState(input) {
29167
+ const paths3 = resolveUiRuntimePaths(input.homeDir);
29168
+ const current = await readUiRuntimeBundle(input.homeDir);
29169
+ if (input.expectedInstanceId !== undefined && current.state?.instanceId !== input.expectedInstanceId) {
29170
+ return [];
29171
+ }
29172
+ if (input.expectedToken !== undefined && current.token !== input.expectedToken)
29173
+ return [];
29174
+ const removed = [];
29175
+ if (current.state !== null) {
29176
+ await rm11(paths3.runtimePath, { force: true });
29177
+ removed.push(paths3.runtimePath);
29178
+ }
29179
+ if (current.token !== null) {
29180
+ await rm11(paths3.tokenPath, { force: true });
29181
+ removed.push(paths3.tokenPath);
29182
+ }
29183
+ return removed;
29184
+ }
29185
+ function buildUiRuntimeBaseUrl(state) {
29186
+ return `http://${state.host}:${state.port}`;
29187
+ }
29188
+ function isMatchingUiRuntime(expected, actual) {
29189
+ return expected.component === actual.component && expected.instanceId === actual.instanceId && expected.pid === actual.pid && expected.host === actual.host && expected.port === actual.port && expected.startedAt === actual.startedAt;
29190
+ }
29191
+ function parseUiRuntimeState(value) {
29192
+ let parsed;
29193
+ try {
29194
+ parsed = JSON.parse(value);
29195
+ } catch {
29196
+ throw new Error("Invalid EvoDev UI runtime state.");
29197
+ }
29198
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
29199
+ throw new Error("Invalid EvoDev UI runtime state.");
29200
+ }
29201
+ const record = parsed;
29202
+ if (record.version !== 1 || record.component !== "evodev-ui" || typeof record.instanceId !== "string" || !/^[a-f0-9]{32}$/u.test(record.instanceId) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0 || record.host !== "127.0.0.1" && record.host !== "localhost" || typeof record.port !== "number" || !Number.isInteger(record.port) || record.port <= 0 || record.port > 65535 || typeof record.startedAt !== "string" || Number.isNaN(Date.parse(record.startedAt))) {
29203
+ throw new Error("Invalid EvoDev UI runtime state.");
29204
+ }
29205
+ return {
29206
+ version: 1,
29207
+ component: "evodev-ui",
29208
+ instanceId: record.instanceId,
29209
+ pid: record.pid,
29210
+ host: record.host,
29211
+ port: record.port,
29212
+ startedAt: record.startedAt
29213
+ };
29214
+ }
29215
+ function parseUiRuntimeToken(value) {
29216
+ const token = value.trim();
29217
+ if (!/^[a-f0-9]{64}$/u.test(token))
29218
+ throw new Error("Invalid EvoDev UI runtime token.");
29219
+ return token;
29220
+ }
29221
+ async function readOptionalFile(path2) {
29222
+ try {
29223
+ return await readFile35(path2, "utf8");
29224
+ } catch (error) {
29225
+ if (isNodeError3(error) && error.code === "ENOENT")
29226
+ return null;
29227
+ throw error;
29228
+ }
29229
+ }
29230
+ function isNodeError3(error) {
29231
+ return error instanceof Error && "code" in error;
29232
+ }
29233
+
29109
29234
  // packages/cli/src/ui/shell.ts
29110
29235
  function renderUiDashboardHtml() {
29111
29236
  return `<!doctype html>
@@ -30265,6 +30390,7 @@ function sanitizeText4(value) {
30265
30390
 
30266
30391
  // packages/cli/src/ui/server.ts
30267
30392
  var DEFAULT_UI_PORT = 37646;
30393
+ var UI_RUNTIME_REQUEST_TIMEOUT_MS = 2000;
30268
30394
  var MAX_UI_REQUEST_BODY_BYTES = 8 * 1024;
30269
30395
  var UI_RESPONSE_HEADERS = {
30270
30396
  "cache-control": "no-store",
@@ -30290,9 +30416,15 @@ function getUiHelpText() {
30290
30416
  "",
30291
30417
  "Usage:",
30292
30418
  " evodev ui [--host 127.0.0.1|localhost] [--port <port>] [--no-open]",
30419
+ " evodev ui status",
30420
+ " evodev ui open",
30421
+ " evodev ui stop",
30293
30422
  " evodev ui snapshot",
30294
30423
  "",
30295
30424
  "Commands:",
30425
+ " status Show the managed dashboard process status",
30426
+ " open Open the authenticated URL for the running dashboard",
30427
+ " stop Gracefully stop the running dashboard",
30296
30428
  " snapshot Print the local metadata dashboard snapshot as JSON",
30297
30429
  "",
30298
30430
  "Options:",
@@ -30300,6 +30432,7 @@ function getUiHelpText() {
30300
30432
  " --port Local bind port; defaults to 37646",
30301
30433
  " --open Open the dashboard URL with the OS default browser (default)",
30302
30434
  " --no-open Keep the dashboard server in the terminal without opening a browser",
30435
+ " Press Ctrl+C to stop a dashboard attached to the current terminal.",
30303
30436
  " Evolution automation is owned by evodev schedule; the dashboard can trigger a manual full evolution job."
30304
30437
  ].join(`
30305
30438
  `);
@@ -30323,6 +30456,31 @@ async function runUiCommand(argv, options = {}) {
30323
30456
  write(json);
30324
30457
  return 0;
30325
30458
  }
30459
+ if (command === "status") {
30460
+ requireNoUiSubcommandArgs(command, argv.slice(1));
30461
+ return await runUiStatusCommand({
30462
+ homeDir,
30463
+ write,
30464
+ fetchImpl: options.fetchImpl ?? globalThis.fetch
30465
+ });
30466
+ }
30467
+ if (command === "open") {
30468
+ requireNoUiSubcommandArgs(command, argv.slice(1));
30469
+ return await runUiOpenCommand({
30470
+ homeDir,
30471
+ write,
30472
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
30473
+ openBrowser: options.openBrowser ?? openUiBrowser
30474
+ });
30475
+ }
30476
+ if (command === "stop") {
30477
+ requireNoUiSubcommandArgs(command, argv.slice(1));
30478
+ return await runUiStopCommand({
30479
+ homeDir,
30480
+ write,
30481
+ fetchImpl: options.fetchImpl ?? globalThis.fetch
30482
+ });
30483
+ }
30326
30484
  const flags = parseServeFlags(argv);
30327
30485
  return await startUiServer({
30328
30486
  homeDir,
@@ -30330,11 +30488,215 @@ async function runUiCommand(argv, options = {}) {
30330
30488
  flags,
30331
30489
  signal: options.signal,
30332
30490
  write,
30491
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
30333
30492
  openBrowser: options.openBrowser ?? openUiBrowser,
30334
30493
  evolutionJobService: options.evolutionJobService,
30335
30494
  proposalExecutionService: options.proposalExecutionService
30336
30495
  });
30337
30496
  }
30497
+ async function runUiStatusCommand(input) {
30498
+ const runtime = await readUiRuntimeBundle(input.homeDir);
30499
+ if (runtime.state === null) {
30500
+ input.write(runtime.token === null ? "EvoDev UI: stopped" : "EvoDev UI: stopped (stale authentication state present)");
30501
+ return 0;
30502
+ }
30503
+ const publicProbe = await probeUiRuntime({
30504
+ state: runtime.state,
30505
+ fetchImpl: input.fetchImpl
30506
+ });
30507
+ if (publicProbe.kind !== "running") {
30508
+ input.write(`EvoDev UI: stale pid=${runtime.state.pid} url=${buildUiRuntimeBaseUrl(runtime.state)} (${publicProbe.reason})`);
30509
+ return 0;
30510
+ }
30511
+ let authentication = "ready";
30512
+ if (runtime.token === null) {
30513
+ authentication = "missing";
30514
+ } else {
30515
+ const authenticatedProbe = await probeUiRuntime({
30516
+ state: runtime.state,
30517
+ token: runtime.token,
30518
+ fetchImpl: input.fetchImpl
30519
+ });
30520
+ if (authenticatedProbe.kind !== "running")
30521
+ authentication = "unavailable";
30522
+ }
30523
+ input.write(`EvoDev UI: running pid=${runtime.state.pid} url=${buildUiRuntimeBaseUrl(runtime.state)} authentication=${authentication}`);
30524
+ return 0;
30525
+ }
30526
+ async function runUiOpenCommand(input) {
30527
+ const runtime = await readUiRuntimeBundle(input.homeDir);
30528
+ if (runtime.state === null)
30529
+ throw new Error("EvoDev UI is not running. Start it with evodev ui.");
30530
+ if (runtime.token === null) {
30531
+ throw new Error("EvoDev UI authentication state is missing. Stop the stale runtime and restart it.");
30532
+ }
30533
+ const probe = await probeUiRuntime({
30534
+ state: runtime.state,
30535
+ token: runtime.token,
30536
+ fetchImpl: input.fetchImpl
30537
+ });
30538
+ if (probe.kind !== "running") {
30539
+ throw new Error(`Cannot open EvoDev UI: ${probe.reason}.`);
30540
+ }
30541
+ const baseUrl = buildUiRuntimeBaseUrl(runtime.state);
30542
+ input.openBrowser(`${baseUrl}/#token=${runtime.token}`, input.write);
30543
+ input.write(`EvoDev UI opened: ${baseUrl}`);
30544
+ return 0;
30545
+ }
30546
+ async function runUiStopCommand(input) {
30547
+ const runtime = await readUiRuntimeBundle(input.homeDir);
30548
+ if (runtime.state === null) {
30549
+ if (runtime.token !== null) {
30550
+ await removeUiRuntimeState({
30551
+ homeDir: input.homeDir,
30552
+ expectedToken: runtime.token
30553
+ });
30554
+ input.write("EvoDev UI: stopped; removed stale authentication state.");
30555
+ } else {
30556
+ input.write("EvoDev UI: already stopped.");
30557
+ }
30558
+ return 0;
30559
+ }
30560
+ if (runtime.token === null) {
30561
+ const probe = await probeUiRuntime({
30562
+ state: runtime.state,
30563
+ fetchImpl: input.fetchImpl
30564
+ });
30565
+ if (probe.kind === "running") {
30566
+ throw new Error("Cannot safely stop EvoDev UI because its authentication state is missing. Stop the owning process manually.");
30567
+ }
30568
+ if (probe.kind === "unavailable" && probe.timedOut) {
30569
+ throw new Error("Cannot safely stop EvoDev UI because its health check timed out.");
30570
+ }
30571
+ await removeUiRuntimeState({
30572
+ homeDir: input.homeDir,
30573
+ expectedInstanceId: runtime.state.instanceId
30574
+ });
30575
+ input.write("EvoDev UI: stopped; removed stale runtime state.");
30576
+ return 0;
30577
+ }
30578
+ const shutdown = await requestUiShutdown({
30579
+ state: runtime.state,
30580
+ token: runtime.token,
30581
+ fetchImpl: input.fetchImpl
30582
+ });
30583
+ if (!shutdown.accepted) {
30584
+ const probe = await probeUiRuntime({
30585
+ state: runtime.state,
30586
+ fetchImpl: input.fetchImpl
30587
+ });
30588
+ if (probe.kind === "running") {
30589
+ throw new Error(`Cannot safely stop EvoDev UI: ${shutdown.reason}.`);
30590
+ }
30591
+ if (shutdown.timedOut || probe.kind === "unavailable" && probe.timedOut) {
30592
+ throw new Error("Cannot safely stop EvoDev UI because its shutdown request timed out.");
30593
+ }
30594
+ await removeUiRuntimeState({
30595
+ homeDir: input.homeDir,
30596
+ expectedInstanceId: runtime.state.instanceId,
30597
+ expectedToken: runtime.token
30598
+ });
30599
+ input.write("EvoDev UI: stopped; removed stale runtime state.");
30600
+ return 0;
30601
+ }
30602
+ const stopped = await waitForUiRuntimeRemoval({
30603
+ homeDir: input.homeDir,
30604
+ instanceId: runtime.state.instanceId
30605
+ });
30606
+ if (!stopped)
30607
+ throw new Error("EvoDev UI accepted shutdown but did not stop within 2 seconds.");
30608
+ input.write("EvoDev UI: stopped.");
30609
+ return 0;
30610
+ }
30611
+ async function probeUiRuntime(input) {
30612
+ const controller = new AbortController;
30613
+ const timeout = setTimeout(() => controller.abort(), UI_RUNTIME_REQUEST_TIMEOUT_MS);
30614
+ try {
30615
+ const headers = { accept: "application/json" };
30616
+ if (input.token !== undefined)
30617
+ headers["x-evodev-token"] = input.token;
30618
+ const response = await input.fetchImpl(new URL("/api/health", buildUiRuntimeBaseUrl(input.state)), { headers, signal: controller.signal });
30619
+ if (response.status === 401) {
30620
+ return { kind: "unauthorized", reason: "authentication failed" };
30621
+ }
30622
+ if (!response.ok) {
30623
+ return {
30624
+ kind: "unavailable",
30625
+ reason: `health check returned HTTP ${response.status}`,
30626
+ timedOut: false
30627
+ };
30628
+ }
30629
+ const body = await response.json();
30630
+ const actual = parseUiRuntimeState(JSON.stringify(body.data));
30631
+ if (!isMatchingUiRuntime(input.state, actual)) {
30632
+ return { kind: "unavailable", reason: "runtime identity changed", timedOut: false };
30633
+ }
30634
+ return { kind: "running", state: actual };
30635
+ } catch (error) {
30636
+ const timedOut = controller.signal.aborted;
30637
+ return {
30638
+ kind: "unavailable",
30639
+ reason: timedOut ? "health check timed out" : `health check failed: ${describeError10(error)}`,
30640
+ timedOut
30641
+ };
30642
+ } finally {
30643
+ clearTimeout(timeout);
30644
+ }
30645
+ }
30646
+ async function requestUiShutdown(input) {
30647
+ const controller = new AbortController;
30648
+ const timeout = setTimeout(() => controller.abort(), UI_RUNTIME_REQUEST_TIMEOUT_MS);
30649
+ try {
30650
+ const response = await input.fetchImpl(new URL("/api/shutdown", buildUiRuntimeBaseUrl(input.state)), {
30651
+ method: "POST",
30652
+ headers: {
30653
+ accept: "application/json",
30654
+ "content-type": "application/json",
30655
+ "x-evodev-token": input.token
30656
+ },
30657
+ body: "{}",
30658
+ signal: controller.signal
30659
+ });
30660
+ if (response.status !== 202) {
30661
+ return {
30662
+ accepted: false,
30663
+ reason: response.status === 401 ? "authentication failed" : `shutdown returned HTTP ${response.status}`,
30664
+ timedOut: false
30665
+ };
30666
+ }
30667
+ return { accepted: true };
30668
+ } catch (error) {
30669
+ const timedOut = controller.signal.aborted;
30670
+ return {
30671
+ accepted: false,
30672
+ reason: timedOut ? "shutdown request timed out" : `shutdown failed: ${describeError10(error)}`,
30673
+ timedOut
30674
+ };
30675
+ } finally {
30676
+ clearTimeout(timeout);
30677
+ }
30678
+ }
30679
+ async function waitForUiRuntimeRemoval(input) {
30680
+ const deadline = Date.now() + UI_RUNTIME_REQUEST_TIMEOUT_MS;
30681
+ while (Date.now() < deadline) {
30682
+ const runtime = await readUiRuntimeBundle(input.homeDir);
30683
+ if (runtime.state === null) {
30684
+ if (runtime.token === null)
30685
+ return true;
30686
+ } else if (runtime.state.instanceId !== input.instanceId) {
30687
+ return true;
30688
+ }
30689
+ await delay(25);
30690
+ }
30691
+ return false;
30692
+ }
30693
+ function requireNoUiSubcommandArgs(command, argv) {
30694
+ if (argv.length > 0)
30695
+ throw new Error(`Unknown ui ${command} option: ${argv[0] ?? ""}`.trim());
30696
+ }
30697
+ async function delay(milliseconds) {
30698
+ await new Promise((resolve11) => setTimeout(resolve11, milliseconds));
30699
+ }
30338
30700
  async function writeProcessStdout(value) {
30339
30701
  await new Promise((resolve11, reject) => {
30340
30702
  process.stdout.write(value, (error) => {
@@ -30347,10 +30709,27 @@ async function writeProcessStdout(value) {
30347
30709
  }
30348
30710
  async function startUiServer(input) {
30349
30711
  const token = randomBytes4(32).toString("hex");
30712
+ const instanceId = randomBytes4(16).toString("hex");
30713
+ const existingRuntime = await readUiRuntimeBundle(input.homeDir);
30714
+ if (existingRuntime.state !== null) {
30715
+ const existingProbe = await probeUiRuntime({
30716
+ state: existingRuntime.state,
30717
+ fetchImpl: input.fetchImpl
30718
+ });
30719
+ if (existingProbe.kind === "running") {
30720
+ throw new Error(`EvoDev UI is already running at ${buildUiRuntimeBaseUrl(existingRuntime.state)}. Use evodev ui open or evodev ui stop.`);
30721
+ }
30722
+ if (existingProbe.kind === "unavailable" && existingProbe.timedOut) {
30723
+ throw new Error("Existing EvoDev UI runtime state could not be verified because its health check timed out. Use evodev ui status or stop the owning process manually.");
30724
+ }
30725
+ }
30350
30726
  const html = renderUiDashboardHtml();
30351
30727
  const webAssets = await loadUiWebAssets();
30352
30728
  const proposalExecution = input.proposalExecutionService ?? new RepoProposalExecutionService;
30353
30729
  const evolutionJobs = input.evolutionJobService ?? new EvolutionJobService({ homeDir: input.homeDir });
30730
+ let runtimeState = null;
30731
+ let requestClose = null;
30732
+ let shutdownRequested = false;
30354
30733
  const server = createServer3(async (request, response) => {
30355
30734
  try {
30356
30735
  const url2 = new URL(request.url ?? "/", `http://${input.flags.host}:${input.flags.port}`);
@@ -30396,6 +30775,32 @@ async function startUiServer(input) {
30396
30775
  response.end(isScript ? webAssets.script : webAssets.styles);
30397
30776
  return;
30398
30777
  }
30778
+ if (url2.pathname === "/api/health") {
30779
+ if (request.method !== "GET") {
30780
+ writeMethodNotAllowed(response, "GET");
30781
+ return;
30782
+ }
30783
+ if (hasRequestToken(request) && !hasValidRequestToken(request, token)) {
30784
+ writeJsonResponse(response, 401, { ok: false, error: "unauthorized" });
30785
+ return;
30786
+ }
30787
+ if (runtimeState === null) {
30788
+ writeJsonResponse(response, 503, { ok: false, error: "dashboard is starting" });
30789
+ return;
30790
+ }
30791
+ writeJsonResponse(response, 200, { ok: true, data: runtimeState });
30792
+ return;
30793
+ }
30794
+ if (url2.pathname === "/api/shutdown") {
30795
+ requireAuthenticatedJsonPost({ request, response, token });
30796
+ if (response.writableEnded)
30797
+ return;
30798
+ await readRequestJson(request);
30799
+ writeJsonResponse(response, 202, { ok: true, data: { stopping: true } });
30800
+ shutdownRequested = true;
30801
+ queueMicrotask(() => requestClose?.());
30802
+ return;
30803
+ }
30399
30804
  if (url2.pathname === "/api/snapshot") {
30400
30805
  if (request.method !== "GET") {
30401
30806
  writeMethodNotAllowed(response, "GET");
@@ -30582,7 +30987,7 @@ async function startUiServer(input) {
30582
30987
  await new Promise((resolve11, reject) => {
30583
30988
  const onError = (error) => {
30584
30989
  server.off("listening", onListening);
30585
- reject(error);
30990
+ reject(error.code === "EADDRINUSE" ? new Error(`Cannot start EvoDev UI: ${input.flags.host}:${input.flags.port} is already in use and no running managed UI matched it.`) : error);
30586
30991
  };
30587
30992
  const onListening = () => {
30588
30993
  server.off("error", onError);
@@ -30594,6 +30999,31 @@ async function startUiServer(input) {
30594
30999
  });
30595
31000
  const address = server.address();
30596
31001
  const port = typeof address === "object" && address !== null ? address.port : input.flags.port;
31002
+ if (existingRuntime.state !== null || existingRuntime.token !== null) {
31003
+ await removeUiRuntimeState({
31004
+ homeDir: input.homeDir,
31005
+ expectedInstanceId: existingRuntime.state?.instanceId,
31006
+ expectedToken: existingRuntime.token ?? undefined
31007
+ });
31008
+ }
31009
+ runtimeState = {
31010
+ version: 1,
31011
+ component: "evodev-ui",
31012
+ instanceId,
31013
+ pid: process.pid,
31014
+ host: input.flags.host,
31015
+ port,
31016
+ startedAt: new Date().toISOString()
31017
+ };
31018
+ try {
31019
+ await writeUiRuntimeState({ homeDir: input.homeDir, state: runtimeState, token });
31020
+ } catch (error) {
31021
+ await Promise.all([proposalExecution.shutdown(), evolutionJobs.shutdown()]).catch(() => {
31022
+ return;
31023
+ });
31024
+ await new Promise((resolve11) => server.close(() => resolve11()));
31025
+ throw error;
31026
+ }
30597
31027
  const url = `http://${input.flags.host}:${port}/#token=${token}`;
30598
31028
  input.write(`EvoDev UI: ${url}`);
30599
31029
  input.write("Mode: local-only dashboard; only concrete repo proposals require review. Press Ctrl+C to stop.");
@@ -30605,7 +31035,13 @@ async function startUiServer(input) {
30605
31035
  process.off("SIGINT", close);
30606
31036
  process.off("SIGTERM", close);
30607
31037
  input.signal?.removeEventListener("abort", close);
30608
- resolve11(0);
31038
+ removeUiRuntimeState({
31039
+ homeDir: input.homeDir,
31040
+ expectedInstanceId: instanceId,
31041
+ expectedToken: token
31042
+ }).catch((error) => {
31043
+ input.write(`Warning: dashboard stopped, but runtime state cleanup failed: ${describeError10(error)}`);
31044
+ }).finally(() => resolve11(0));
30609
31045
  };
30610
31046
  const close = () => {
30611
31047
  if (closing)
@@ -30615,10 +31051,11 @@ async function startUiServer(input) {
30615
31051
  return;
30616
31052
  }).finally(() => server.close(finish));
30617
31053
  };
31054
+ requestClose = close;
30618
31055
  process.once("SIGINT", close);
30619
31056
  process.once("SIGTERM", close);
30620
31057
  input.signal?.addEventListener("abort", close, { once: true });
30621
- if (input.signal?.aborted)
31058
+ if (input.signal?.aborted || shutdownRequested)
30622
31059
  close();
30623
31060
  });
30624
31061
  }
@@ -30668,13 +31105,19 @@ function readHeader2(request, name) {
30668
31105
  return value ?? null;
30669
31106
  }
30670
31107
  function hasValidRequestToken(request, expectedToken) {
30671
- const requestToken = readHeader2(request, "authorization")?.replace(/^Bearer\s+/i, "") ?? readHeader2(request, "x-evodev-token");
31108
+ const requestToken = readRequestToken(request);
30672
31109
  if (requestToken === null)
30673
31110
  return false;
30674
31111
  const actual = Buffer.from(requestToken);
30675
31112
  const expected = Buffer.from(expectedToken);
30676
31113
  return actual.length === expected.length && timingSafeEqual(actual, expected);
30677
31114
  }
31115
+ function hasRequestToken(request) {
31116
+ return readRequestToken(request) !== null;
31117
+ }
31118
+ function readRequestToken(request) {
31119
+ return readHeader2(request, "authorization")?.replace(/^Bearer\s+/i, "") ?? readHeader2(request, "x-evodev-token");
31120
+ }
30678
31121
  function writeJsonResponse(response, status, body) {
30679
31122
  response.writeHead(status, {
30680
31123
  ...UI_RESPONSE_HEADERS,
@@ -30864,10 +31307,13 @@ function openUiBrowser(url, write, spawnProcess = spawn8) {
30864
31307
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
30865
31308
  const child = spawnProcess(command, args, { detached: true, stdio: "ignore" });
30866
31309
  child.once("error", (error) => {
30867
- write(`Warning: dashboard started, but the browser could not be opened: ${error.message}`);
31310
+ write(`Warning: dashboard URL is available, but the browser could not be opened: ${error.message}`);
30868
31311
  });
30869
31312
  child.unref();
30870
31313
  }
31314
+ function describeError10(error) {
31315
+ return error instanceof Error ? error.message : String(error);
31316
+ }
30871
31317
  function resolveHomeDir13(homeDir) {
30872
31318
  if (homeDir !== undefined)
30873
31319
  return homeDir;
@@ -30878,7 +31324,7 @@ function resolveHomeDir13(homeDir) {
30878
31324
  return envHome;
30879
31325
  }
30880
31326
  // packages/cli/src/workflow.ts
30881
- import { join as join36 } from "node:path";
31327
+ import { join as join37 } from "node:path";
30882
31328
  function getWorkflowHelpText() {
30883
31329
  return [
30884
31330
  "EvoDev workflow",
@@ -30941,7 +31387,7 @@ function parseWorkflowDryRunFlags(argv) {
30941
31387
  return { workflowId };
30942
31388
  }
30943
31389
  function resolveDefaultWorkflowsDir() {
30944
- return join36(resolveDefaultAssetsRootDir(), "workflows");
31390
+ return join37(resolveDefaultAssetsRootDir(), "workflows");
30945
31391
  }
30946
31392
  function isHelpArg3(arg) {
30947
31393
  return arg === undefined || arg === "help" || arg === "--help" || arg === "-h";
@@ -30978,6 +31424,9 @@ function getHelpText() {
30978
31424
  " workflow list List built-in workflow manifests",
30979
31425
  " workflow dry-run Plan workflow steps without execution",
30980
31426
  " ui Start the local-only EvoDev web dashboard",
31427
+ " ui status Show the managed dashboard process status",
31428
+ " ui open Open the authenticated URL for the running dashboard",
31429
+ " ui stop Gracefully stop the running dashboard",
30981
31430
  " ui snapshot Print dashboard metadata without starting a server",
30982
31431
  " learn review Preview learning candidates without writing memory",
30983
31432
  " learn lint Validate learning candidate privacy/provenance fields",
@@ -31023,7 +31472,7 @@ function getHelpText() {
31023
31472
  ].join(`
31024
31473
  `);
31025
31474
  }
31026
- var CLI_VERSION = "0.0.1-alpha.10";
31475
+ var CLI_VERSION = "0.0.1-alpha.11";
31027
31476
  function getVersionText() {
31028
31477
  return `evodev ${CLI_VERSION}`;
31029
31478
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "evodev",
3
3
  "description": "EvoDev hook, skill, and Teams MCP integration for disciplined AI coding workflows.",
4
- "version": "0.0.1-alpha.10",
4
+ "version": "0.0.1-alpha.11",
5
5
  "author": {
6
6
  "name": "EvoDev"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evodev",
3
- "version": "0.0.1-alpha.10",
3
+ "version": "0.0.1-alpha.11",
4
4
  "description": "EvoDev hook, skill, and Teams MCP integration for disciplined AI coding workflows.",
5
5
  "author": {
6
6
  "name": "EvoDev"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-dev/plugin",
3
- "version": "0.0.1-alpha.10",
3
+ "version": "0.0.1-alpha.11",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./hooks/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-dev/evodev",
3
- "version": "0.0.1-alpha.10",
3
+ "version": "0.0.1-alpha.11",
4
4
  "description": "AI Coding infrastructure CLI for real R&D workflows.",
5
5
  "type": "module",
6
6
  "repository": {