@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 +2 -0
- package/dashboard/dashboard.html +58 -12
- package/dashboard/lib/guards.d.mts +103 -0
- package/dashboard/lib/guards.mjs +56 -2
- package/dashboard/lib/open-browser.mjs +0 -5
- package/dashboard/lib/semantic-model.mjs +7 -1
- package/dashboard/start-broadcast.mjs +4 -3
- package/dashboard/start.mjs +4 -3
- package/dist/index.js +621 -150
- package/package.json +3 -1
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 |
|
package/dashboard/dashboard.html
CHANGED
|
@@ -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:
|
|
748
|
-
background: var(--border);
|
|
749
|
-
border-radius:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/dashboard/lib/guards.mjs
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dashboard/start.mjs
CHANGED
|
@@ -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
|
}
|