@dadado/agent-kit-cli 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,6 +55,8 @@ NO_COLOR=1 agent-kit
55
55
 
56
56
  Subcommands and `agent-kit --version` are unchanged. Chat-only HITL flows (`/start-project`, `/git-staging`, `/git-prod`, `/run-plan-all`, backlog CRUD) are not CLI commands.
57
57
 
58
+ On an interactive TTY, long-running commands (`init`, `install`, `doctor`, `update`, `run-plan` ticks) show an in-process ANSI spinner plus a rotating Mission Kit tip. Set `AGENT_KIT_REDUCED_MOTION=1` for static text on a capable TTY. Runtime dependencies stay `@clack/prompts`, `citty`, and `kolorist` (no `ora` / `figlet` / `chalk` / `ink`). Window titles for `agent-kit dashboard` and `agent-kit dashboard-broadcast` use the workspace basename, not the CLI package folder.
59
+
58
60
  ## Common commands
59
61
 
60
62
  | Command | Purpose |
@@ -743,16 +743,19 @@ body.mc-fullscreen .top-tabs-row {
743
743
  }
744
744
 
745
745
  /* ===== Progress Bar ===== */
746
+ /* Track must stay visibly a track at 0% (contract item 5): --border-active
747
+ reads against --bg-card in both skins where --border blended in, and 6px
748
+ with rounded ends keeps mid fills from collapsing into a hairline. */
746
749
  .progress-bar {
747
- height: 4px;
748
- background: var(--border);
749
- border-radius: 2px;
750
+ height: 6px;
751
+ background: var(--border-active);
752
+ border-radius: 3px;
750
753
  overflow: hidden;
751
754
  margin-top: 8px;
752
755
  }
753
756
  .progress-fill {
754
757
  height: 100%;
755
- border-radius: 2px;
758
+ border-radius: 3px;
756
759
  transition: width 0.5s ease;
757
760
  }
758
761
  .progress-fill.green { background: var(--green); }
@@ -4267,11 +4270,11 @@ function fmtDate(iso) {
4267
4270
  return d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
4268
4271
  }
4269
4272
 
4273
+ // Presentation contract (Phase 2): progress is never an error signal. Any
4274
+ // in-flight percentage (0-99) renders the neutral accent; 100% renders green
4275
+ // (lifecycle completed with total > 0 implies 100 per the Phase 0 contract).
4270
4276
  function progressColor(pct) {
4271
- if (pct >= 100) return 'green';
4272
- if (pct >= 50) return 'blue';
4273
- if (pct >= 25) return 'yellow';
4274
- return 'red';
4277
+ return pct >= 100 ? 'green' : 'blue';
4275
4278
  }
4276
4279
 
4277
4280
  // Dot semantics: only state signals survive. completed = good, cancelled =
@@ -5606,9 +5609,31 @@ function renderConfigSection(d) {
5606
5609
  <div class="config-row">
5607
5610
  <label for="config-epr-backend">Backend</label>
5608
5611
  <select id="config-epr-backend" name="eprBackend" data-focus-key="config-epr-backend">
5612
+ <option value="auto"${epr.backend === 'auto' ? ' selected' : ''}>auto</option>
5609
5613
  <option value="claude"${epr.backend === 'claude' || !epr.backend ? ' selected' : ''}>claude</option>
5614
+ <option value="cursor"${epr.backend === 'cursor' ? ' selected' : ''}>cursor</option>
5610
5615
  </select>
5611
- <span class="config-hint">claude is the only backend today.</span>
5616
+ <span class="config-hint">auto uses Claude when usable, else Cursor Agent. Pin claude or cursor to force one backend.</span>
5617
+ </div>
5618
+ <div class="config-row">
5619
+ <label for="config-epr-reviewerModel">Reviewer model</label>
5620
+ <input type="text" id="config-epr-reviewerModel" name="eprReviewerModel" value="${escapeAttr(epr.reviewerModel || 'sonnet')}" maxlength="64" data-focus-key="config-epr-reviewerModel" />
5621
+ <span class="config-hint">Claude default sonnet (auto permission mode). Must differ from the implementer stamp (Auto/Auto refused).</span>
5622
+ </div>
5623
+ <div class="config-row">
5624
+ <label for="config-epr-advisorModel">Advisor model</label>
5625
+ <input type="text" id="config-epr-advisorModel" name="eprAdvisorModel" value="${escapeAttr(epr.advisorModel || 'opus')}" maxlength="64" data-focus-key="config-epr-advisorModel" />
5626
+ <span class="config-hint">Opus only when the monitor marks high severity or uncertainty.</span>
5627
+ </div>
5628
+ <div class="config-row">
5629
+ <label for="config-epr-waitSlice">Wait slice (seconds)</label>
5630
+ <input type="number" id="config-epr-waitSlice" name="eprWaitSlice" min="1" max="3600" step="1" value="${escapeAttr(String(epr.waitSliceSeconds || 90))}" data-focus-key="config-epr-waitSlice" />
5631
+ <span class="config-hint">Chat AwaitShell budget per session. Default 90.</span>
5632
+ </div>
5633
+ <div class="config-row">
5634
+ <label for="config-epr-waitTimeout">Wait timeout (seconds)</label>
5635
+ <input type="number" id="config-epr-waitTimeout" name="eprWaitTimeout" min="1" max="86400" step="1" value="${escapeAttr(String(epr.waitTimeoutSeconds || 900))}" data-focus-key="config-epr-waitTimeout" />
5636
+ <span class="config-hint">Total remaining budget across slices and CI. Default 900.</span>
5612
5637
  </div>
5613
5638
  <div class="config-row">
5614
5639
  <label for="config-epr-mode">Arming mode</label>
@@ -5680,6 +5705,18 @@ function collectMissionConfigPayload() {
5680
5705
  offerOnExhausted: !!document.getElementById('config-epr-offer')?.checked,
5681
5706
  autoRemediate: !!document.getElementById('config-epr-auto')?.checked,
5682
5707
  backend: document.getElementById('config-epr-backend')?.value || 'claude',
5708
+ reviewerModel: (document.getElementById('config-epr-reviewerModel')?.value || 'sonnet').trim(),
5709
+ advisorModel: (document.getElementById('config-epr-advisorModel')?.value || 'opus').trim(),
5710
+ waitSliceSeconds: (() => {
5711
+ const raw = document.getElementById('config-epr-waitSlice')?.value;
5712
+ const n = Number.parseInt(String(raw || '90'), 10);
5713
+ return Number.isFinite(n) && n >= 1 ? n : 90;
5714
+ })(),
5715
+ waitTimeoutSeconds: (() => {
5716
+ const raw = document.getElementById('config-epr-waitTimeout')?.value;
5717
+ const n = Number.parseInt(String(raw || '900'), 10);
5718
+ return Number.isFinite(n) && n >= 1 ? n : 900;
5719
+ })(),
5683
5720
  mode: document.getElementById('config-epr-mode')?.value || 'paste',
5684
5721
  midBatchAudits: !!document.getElementById('config-epr-midBatch')?.checked,
5685
5722
  preflight: document.getElementById('config-epr-preflight')?.value || 'off',
@@ -6300,10 +6337,18 @@ function mergePlansForUi(d) {
6300
6337
  const inProgress = fromItems
6301
6338
  ? items.filter((t) => t.status === 'in_progress').length
6302
6339
  : (raw.todos?.inProgress ?? 0);
6340
+ // Cancelled counts toward the fill numerator (terminal work, mirrors
6341
+ // TERMINAL_TODO_STATUSES / todoStats in dashboard/lib/semantic-model.mjs)
6342
+ // so the bar reaches 100% whenever the lifecycle pill says COMPLETED.
6343
+ const cancelled = fromItems
6344
+ ? items.filter((t) => t.status === 'cancelled').length
6345
+ : (raw.todos?.cancelled ?? enriched.progress?.cancelled ?? 0);
6303
6346
  const nextActionTodo = planNextActionTodo(items);
6304
6347
  let lifecycle = enriched.lifecycle;
6305
6348
  if (!lifecycle) {
6306
- if (total > 0 && completed >= total && (raw.todos?.inProgress || 0) === 0) {
6349
+ // Mirrors classifyPlan: terminal (completed + cancelled) exhausting the
6350
+ // list means todoStats.open === 0 → completed.
6351
+ if (total > 0 && completed + cancelled >= total && inProgress === 0) {
6307
6352
  lifecycle = 'completed';
6308
6353
  } else {
6309
6354
  lifecycle = 'incomplete';
@@ -6317,12 +6362,13 @@ function mergePlansForUi(d) {
6317
6362
  modifiedAt: raw.modifiedAt || enriched.modifiedAt || null,
6318
6363
  progressPct:
6319
6364
  total > 0
6320
- ? Math.round((completed / total) * 100)
6365
+ ? Math.round(((completed + cancelled) / total) * 100)
6321
6366
  : typeof raw.progress === 'number'
6322
6367
  ? raw.progress
6323
6368
  : 0,
6324
- progressLabel: `${completed} of ${total} complete`,
6369
+ progressLabel: `${completed} of ${total} complete` + (cancelled > 0 ? ` · ${cancelled} cancelled` : ''),
6325
6370
  progressCompleted: completed,
6371
+ progressCancelled: cancelled,
6326
6372
  progressTotal: total,
6327
6373
  progressInProgress: inProgress,
6328
6374
  nextActionTodo,
@@ -1,5 +1,84 @@
1
1
  /** Ambient types for dashboard/lib/guards.mjs (consumed by CLI TypeScript). */
2
2
 
3
+ type ProcessEnvLike = NodeJS.ProcessEnv | Record<string, string | undefined>;
4
+ type HeaderMap = Record<string, string | string[] | undefined>;
5
+ type GuardRequest = { headers?: HeaderMap };
6
+ type PortOpts = { base?: number; range?: number };
7
+ type GitStatusFile = {
8
+ path: string;
9
+ status: string;
10
+ staged: boolean;
11
+ unstaged: boolean;
12
+ untracked: boolean;
13
+ oldPath?: string;
14
+ renamed?: boolean;
15
+ };
16
+
17
+ export const DEFAULT_HOST: "127.0.0.1";
18
+ export const BROADCAST_TOKEN_ENV: "MISSION_CONTROL_TOKEN";
19
+ export const REPO_ROOT_ENV: "MISSION_CONTROL_REPO_ROOT";
20
+ export const KIT_ROOT_ENV_KEYS: readonly ["MISSION_CONTROL_KIT_ROOT", "AGENT_KIT_HOME"];
21
+ export const BROADCAST_TOKEN_MIN_LEN: 16;
22
+ export const BROADCAST_TOKEN_COOKIE: "mc_token";
23
+ export const DEFAULT_PORT_BASE: 3333;
24
+ export const DEFAULT_PORT_RANGE: 256;
25
+ export const MAX_STRING: {
26
+ branch: number;
27
+ lastCommit: number;
28
+ terminalCwd: number;
29
+ terminalCommand: number;
30
+ processCommand: number;
31
+ };
32
+ export const MAX_GIT_FILES: 50;
33
+ export const MAX_GIT_PATH: 240;
34
+ export const CONTEXT_CONFIG_REL: ".cursor/context/config.json";
35
+ export const CONFIG_PERSONA_IDS: readonly ["autopilot", "night-shift", "ghost-runner"];
36
+ export const CONFIG_PERSONA_MODES: readonly ["continue-plan", "run-plan", "cli-run-plan"];
37
+ export const CONFIG_REVIEW_BACKENDS: readonly ["auto", "claude", "cursor"];
38
+ export const CONFIG_REVIEW_MODES: readonly ["paste", "autonomous"];
39
+ export const CONFIG_REVIEW_PREFLIGHT: readonly ["off", "warn", "block"];
40
+
41
+ export function escapePerlDoubleQuoted(value: string): string;
42
+ export function resolveSnapshotRepoRoot(env: ProcessEnvLike | undefined, kitRoot: string): string;
43
+ export function normalizeRepoRootKey(repoRoot: string): string;
44
+ export function hashRepoRoot(repoRoot: string): number;
45
+ export function repoRootLogId(repoRoot: string): string;
46
+ export function preferredPortForRepoRoot(repoRoot: string, opts?: PortOpts): number;
47
+ export function portCandidatesForRepoRoot(repoRoot: string, opts?: PortOpts): number[];
48
+ export function sameRepoRoot(a: string | null | undefined, b: string | null | undefined): boolean;
49
+ export function resolveMissionControlPort(args: {
50
+ repoRoot: string;
51
+ envPort?: string | number | null;
52
+ probe: (port: number) => { listening: boolean; repoRoot: string | null };
53
+ opts?: PortOpts;
54
+ }): { port: number; reuse: boolean; explicit: boolean };
55
+ export function isSafeRepoRelativePath(relPath: unknown): boolean;
56
+ export function resolveBindHost(envHost?: string | null): string;
57
+ export function isLoopbackBindHost(host: string | undefined | null): boolean;
58
+ export function normalizeAuthToken(raw: unknown): string;
59
+ export function isValidBroadcastToken(token: unknown): boolean;
60
+ export function generateBroadcastToken(): string;
61
+ export function tokensMatch(a: unknown, b: unknown): boolean;
62
+ export function resolveBroadcastAuth(
63
+ env?: ProcessEnvLike,
64
+ ):
65
+ | { ok: true; host: string; tokenRequired: boolean; token: string | null; broadcast: boolean }
66
+ | { ok: false; error: string };
67
+ export function extractRequestToken(req: GuardRequest, url: URL): string;
68
+ export function authorizeMissionControlRequest(
69
+ req: GuardRequest,
70
+ url: URL,
71
+ opts: { tokenRequired: boolean; expectedToken: string | null },
72
+ ): { ok: true; viaQuery: boolean } | { ok: false; status: number; error: string };
73
+ export function broadcastAuthCookieHeader(token: string): string;
74
+ export function listLanIPv4Addresses(): string[];
75
+ export function truncateStr(value: unknown, maxLen: number): unknown;
76
+ export function parseGitStatusShort(output: unknown): {
77
+ files: GitStatusFile[];
78
+ total: number;
79
+ truncated: boolean;
80
+ };
81
+ export function isLoopbackAddress(addr: string | undefined | null): boolean;
3
82
  export function resolveContextConfigPath(
4
83
  repoRoot: string,
5
84
  fsHooks?: {
@@ -8,3 +87,27 @@ export function resolveContextConfigPath(
8
87
  mkdirSync?: (path: string, opts?: { recursive?: boolean }) => void;
9
88
  },
10
89
  ): { ok: true; path: string } | { ok: false; error: string };
90
+ export function validateConfigWriteBody(
91
+ body: unknown,
92
+ ): { ok: true; patch: Record<string, unknown> } | { ok: false; error: string };
93
+ export function mergeConfigAllowlist(
94
+ existing: Record<string, unknown> | object,
95
+ patch: Record<string, unknown> | object,
96
+ ): Record<string, unknown>;
97
+ export function allowlistConfig(raw: unknown): Record<string, unknown>;
98
+ export function isAllowedOrigin(origin: unknown, port: unknown): boolean;
99
+ export function applyCorsHeaders(
100
+ req: GuardRequest,
101
+ res: { setHeader: (name: string, value: string) => unknown },
102
+ port: unknown,
103
+ ): boolean;
104
+ export function isUnderDashboard(resolvedPath: string, dashboardReal: string): boolean;
105
+ export function resolveDashboardStatic(
106
+ pathname: string,
107
+ hooks: {
108
+ dashboardDir: string;
109
+ dashboardReal: string;
110
+ existsSync: (path: string) => boolean;
111
+ realpathSync: (path: string) => string;
112
+ },
113
+ ): string | null;
@@ -445,7 +445,10 @@ export const CONFIG_PERSONA_IDS = Object.freeze(["autopilot", "night-shift", "gh
445
445
  export const CONFIG_PERSONA_MODES = Object.freeze(["continue-plan", "run-plan", "cli-run-plan"]);
446
446
 
447
447
  /** Allowed externalPlanReview.backend values. */
448
- export const CONFIG_REVIEW_BACKENDS = Object.freeze(["claude"]);
448
+ export const CONFIG_REVIEW_BACKENDS = Object.freeze(["auto", "claude", "cursor"]);
449
+
450
+ /** Named reviewer / advisor / implementer model id (no secrets). */
451
+ const REVIEW_MODEL_ID = /^[A-Za-z0-9._:+-]{1,64}$/;
449
452
 
450
453
  /** Allowed externalPlanReview.mode values (audits arming path). */
451
454
  export const CONFIG_REVIEW_MODES = Object.freeze(["paste", "autonomous"]);
@@ -591,6 +594,10 @@ export function validateConfigWriteBody(body) {
591
594
  const eprAllowed = new Set([
592
595
  "enabled",
593
596
  "backend",
597
+ "reviewerModel",
598
+ "advisorModel",
599
+ "waitSliceSeconds",
600
+ "waitTimeoutSeconds",
594
601
  "autoRemediate",
595
602
  "offerOnExhausted",
596
603
  "mode",
@@ -612,10 +619,45 @@ export function validateConfigWriteBody(body) {
612
619
  }
613
620
  if ("backend" in epr) {
614
621
  if (typeof epr.backend !== "string" || !CONFIG_REVIEW_BACKENDS.includes(epr.backend)) {
615
- return { ok: false, error: "externalPlanReview.backend must be a known backend" };
622
+ return { ok: false, error: "externalPlanReview.backend must be auto, claude, or cursor" };
616
623
  }
617
624
  eprPatch.backend = epr.backend;
618
625
  }
626
+ if ("reviewerModel" in epr) {
627
+ if (
628
+ typeof epr.reviewerModel !== "string" ||
629
+ !REVIEW_MODEL_ID.test(epr.reviewerModel.trim())
630
+ ) {
631
+ return { ok: false, error: "externalPlanReview.reviewerModel must be a model id" };
632
+ }
633
+ eprPatch.reviewerModel = epr.reviewerModel.trim();
634
+ }
635
+ if ("advisorModel" in epr) {
636
+ if (typeof epr.advisorModel !== "string" || !REVIEW_MODEL_ID.test(epr.advisorModel.trim())) {
637
+ return { ok: false, error: "externalPlanReview.advisorModel must be a model id" };
638
+ }
639
+ eprPatch.advisorModel = epr.advisorModel.trim();
640
+ }
641
+ if ("waitSliceSeconds" in epr) {
642
+ const n = epr.waitSliceSeconds;
643
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 3600) {
644
+ return {
645
+ ok: false,
646
+ error: "externalPlanReview.waitSliceSeconds must be an integer 1..3600",
647
+ };
648
+ }
649
+ eprPatch.waitSliceSeconds = n;
650
+ }
651
+ if ("waitTimeoutSeconds" in epr) {
652
+ const n = epr.waitTimeoutSeconds;
653
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 86400) {
654
+ return {
655
+ ok: false,
656
+ error: "externalPlanReview.waitTimeoutSeconds must be an integer 1..86400",
657
+ };
658
+ }
659
+ eprPatch.waitTimeoutSeconds = n;
660
+ }
619
661
  if ("autoRemediate" in epr) {
620
662
  if (typeof epr.autoRemediate !== "boolean") {
621
663
  return { ok: false, error: "externalPlanReview.autoRemediate must be boolean" };
@@ -847,6 +889,18 @@ export function allowlistConfig(raw) {
847
889
  if (typeof raw.externalPlanReview.preflight === "string") {
848
890
  epr.preflight = truncateStr(raw.externalPlanReview.preflight, 16);
849
891
  }
892
+ if (typeof raw.externalPlanReview.reviewerModel === "string") {
893
+ epr.reviewerModel = truncateStr(raw.externalPlanReview.reviewerModel, 64);
894
+ }
895
+ if (typeof raw.externalPlanReview.advisorModel === "string") {
896
+ epr.advisorModel = truncateStr(raw.externalPlanReview.advisorModel, 64);
897
+ }
898
+ if (typeof raw.externalPlanReview.waitSliceSeconds === "number") {
899
+ epr.waitSliceSeconds = raw.externalPlanReview.waitSliceSeconds;
900
+ }
901
+ if (typeof raw.externalPlanReview.waitTimeoutSeconds === "number") {
902
+ epr.waitTimeoutSeconds = raw.externalPlanReview.waitTimeoutSeconds;
903
+ }
850
904
  summary.externalPlanReview = epr;
851
905
  }
852
906
  if (raw.agentPersona && typeof raw.agentPersona === "object") {
@@ -201,16 +201,11 @@ export function openBrowser(url, options = {}) {
201
201
 
202
202
  /**
203
203
  * Preferred open: detect failure before claiming success, then caller may fall back.
204
- * Hermetic tests that only inject spawnFn use the detached path (throw = fail).
205
204
  *
206
205
  * @param {{ command: string, args: string[] }} built
207
206
  * @returns {{ opened: boolean, reason?: string, command: string, args: string[] }}
208
207
  */
209
208
  function runPreferred(built) {
210
- if (options.spawnFn && !spawnSyncFn) {
211
- return runDetached(built);
212
- }
213
-
214
209
  const sync = spawnSyncFn ?? spawnSync;
215
210
 
216
211
  if (platform !== "darwin" && platform !== "win32") {
@@ -3064,10 +3064,16 @@ export function enrichPlans(plans, handoff) {
3064
3064
  path: plan.path,
3065
3065
  overview: truncateStr(plan.overview || "", MAX_SEMANTIC_LABEL),
3066
3066
  modifiedAt: plan.modifiedAt || null,
3067
+ // Counter SoT for plan progress (todoStats / TERMINAL_TODO_STATUSES).
3068
+ // `terminal` (completed + cancelled) is the fill numerator so the bar can
3069
+ // reach 100% exactly when classifyPlan says completed (open === 0).
3070
+ // Label format is mirrored by mergePlansForUi in dashboard/dashboard.html.
3067
3071
  progress: {
3068
3072
  completed: stats.completed,
3073
+ cancelled: stats.cancelled,
3074
+ terminal: stats.completed + stats.cancelled,
3069
3075
  total: stats.total,
3070
- label: `${stats.completed} of ${stats.total}`,
3076
+ label: `${stats.completed} of ${stats.total} complete${stats.cancelled > 0 ? ` · ${stats.cancelled} cancelled` : ""}`,
3071
3077
  },
3072
3078
  lifecycle: classifyPlan(plan, handoff),
3073
3079
  // Preserved when lifecycle is completed so UI/sort can still know provenance.
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  import { execFileSync, execSync, spawn } from "node:child_process";
11
- import { existsSync, openSync } from "node:fs";
12
- import { dirname, join } from "node:path";
11
+ import { existsSync, openSync, realpathSync } from "node:fs";
12
+ import { basename, dirname, join } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import {
15
15
  buildBroadcastShareUrl,
@@ -35,6 +35,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
35
35
  const KIT_ROOT = join(__dirname, "..");
36
36
  /** Workspace snapshots / preference config. Defaults to KIT_ROOT. */
37
37
  const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT);
38
+ process.title = `Mission Control · ${basename(ROOT) || "workspace"}`;
38
39
  const SERVE = join(__dirname, "serve.mjs");
39
40
  const LOG = process.env.MISSION_CONTROL_LOG || "/tmp/mission-control-broadcast.log";
40
41
  const PORT = Number.parseInt(process.env.PORT || "3333", 10);
@@ -240,7 +241,7 @@ async function main() {
240
241
  return;
241
242
  }
242
243
  let configValue = null;
243
- const cfg = resolveContextConfigPath(ROOT, { existsSync });
244
+ const cfg = resolveContextConfigPath(ROOT, { existsSync, realpathSync });
244
245
  if (cfg.ok) {
245
246
  configValue = readPreferredBrowserFromConfig(cfg.path);
246
247
  }
@@ -17,8 +17,8 @@
17
17
  */
18
18
 
19
19
  import { execFileSync, execSync, spawn } from "node:child_process";
20
- import { existsSync, openSync } from "node:fs";
21
- import { dirname, join, resolve } from "node:path";
20
+ import { existsSync, openSync, realpathSync } from "node:fs";
21
+ import { basename, dirname, join, resolve } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import {
24
24
  REPO_ROOT_ENV,
@@ -34,6 +34,7 @@ import { openBrowser, readPreferredBrowserFromConfig } from "./lib/open-browser.
34
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
35
35
  const KIT_ROOT = join(__dirname, "..");
36
36
  const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT);
37
+ process.title = `Mission Control · ${basename(ROOT) || "workspace"}`;
37
38
  const SERVE = join(__dirname, "serve.mjs");
38
39
  const HOST = process.env.HOST || "127.0.0.1";
39
40
  const DISPLAY_HOST = HOST === "0.0.0.0" ? "127.0.0.1" : HOST;
@@ -256,7 +257,7 @@ async function main() {
256
257
  return;
257
258
  }
258
259
  let configValue = null;
259
- const cfg = resolveContextConfigPath(ROOT, { existsSync });
260
+ const cfg = resolveContextConfigPath(ROOT, { existsSync, realpathSync });
260
261
  if (cfg.ok) {
261
262
  configValue = readPreferredBrowserFromConfig(cfg.path);
262
263
  }