Skip to content

Commit 1a1d0ff

Browse files
authored
feat: sharpen planner and compare launch states (#93)
* feat: sharpen planner and compare launch states * fix: bump pytest security patch
1 parent 0cc06d9 commit 1a1d0ff

11 files changed

Lines changed: 209 additions & 46 deletions

File tree

apps/dashboard/app/globals.feature.part04.css

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,63 @@
332332
gap: var(--space-5);
333333
}
334334

335+
.planner-empty-shell {
336+
display: grid;
337+
grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.18fr);
338+
gap: var(--space-4);
339+
align-items: start;
340+
}
341+
342+
.planner-empty-brief {
343+
display: grid;
344+
gap: var(--space-3);
345+
}
346+
347+
.planner-empty-title {
348+
font-size: var(--text-lg);
349+
line-height: 1.2;
350+
letter-spacing: -0.02em;
351+
}
352+
353+
.planner-empty-summary {
354+
color: var(--color-text-secondary);
355+
font-size: var(--text-sm);
356+
line-height: 1.65;
357+
}
358+
359+
.planner-empty-checklist {
360+
display: grid;
361+
gap: var(--space-2);
362+
}
363+
364+
.planner-empty-check {
365+
display: grid;
366+
grid-template-columns: auto minmax(0, 1fr);
367+
gap: var(--space-3);
368+
padding: var(--space-3) 0;
369+
border-top: 1px solid var(--color-border-subtle);
370+
}
371+
372+
.planner-empty-check:first-child {
373+
border-top: 0;
374+
padding-top: 0;
375+
}
376+
377+
.planner-empty-check-body {
378+
display: grid;
379+
gap: var(--space-1);
380+
}
381+
382+
.planner-empty-check-body strong {
383+
font-size: var(--text-sm);
384+
}
385+
386+
.planner-empty-check-body span {
387+
color: var(--color-text-muted);
388+
font-size: var(--text-xs);
389+
line-height: 1.55;
390+
}
391+
335392
.planner-empty-grid {
336393
display: grid;
337394
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -359,6 +416,7 @@
359416

360417
.planner-empty-card strong {
361418
font-size: var(--text-base);
419+
line-height: 1.3;
362420
}
363421

364422
.planner-empty-card span:last-child {
@@ -438,6 +496,7 @@
438496
@media (max-width: 1040px) {
439497
.home-briefing-shell,
440498
.planner-hero-shell,
499+
.planner-empty-shell,
441500
.compare-room-shell,
442501
.compare-primary-grid {
443502
grid-template-columns: 1fr;

apps/dashboard/app/planner/page.tsx

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,36 @@ export default async function PlannerPage() {
350350
const totalUnblockTasks = rows.reduce((sum, row) => sum + row.unblockTasks.length, 0);
351351
const wakeAnchoredRuns = rows.filter((row) => Boolean(row.wavePlan?.wake_policy_ref)).length;
352352
const priority = plannerPriorityState(text, sortedRows);
353+
const emptyChecklist =
354+
locale === "zh-CN"
355+
? [
356+
{
357+
title: "锁定目标和验收口径",
358+
desc: "先在 PM 入口把 objective、constraints 和 done signal 讲清楚,planner 才能开始工作。",
359+
},
360+
{
361+
title: "把第一条 wave 送进系统",
362+
desc: "没有真实 wave plan,就不会有 triage queue,也不会有 planner 的优先级排序。",
363+
},
364+
{
365+
title: "回到 planner 做 dispatch",
366+
desc: "等 planning artifact 出现后,再回来决定是去 tower、workflow case,还是 proof & replay。",
367+
},
368+
]
369+
: [
370+
{
371+
title: "Lock the objective and the done signal",
372+
desc: "Write the objective, constraints, and acceptance bar in PM intake before you expect the planner to triage anything.",
373+
},
374+
{
375+
title: "Send the first wave into the system",
376+
desc: "Without a real wave plan there is no triage queue, no worker-contract posture, and no planner priority order.",
377+
},
378+
{
379+
title: "Return here for dispatch",
380+
desc: "Once the planning artifacts exist, come back here to choose whether the next move belongs in tower, workflow cases, or proof.",
381+
},
382+
];
353383

354384
return (
355385
<main className="grid" aria-labelledby="planner-page-title">
@@ -424,26 +454,42 @@ export default async function PlannerPage() {
424454

425455
{rows.length === 0 ? (
426456
<Card className="planner-empty-stage">
427-
<div className="empty-state-stack">
428-
<span className="muted">{text.empty}</span>
429-
<span className="mono muted">{text.note}</span>
430-
</div>
431-
<div className="planner-empty-grid">
432-
<Link href="/pm" className="planner-empty-card">
433-
<span className="cell-sub mono muted">01</span>
457+
<div className="planner-empty-shell">
458+
<div className="planner-empty-brief">
459+
<span className="cell-sub mono muted">Planner launch checklist</span>
460+
<strong className="planner-empty-title">
461+
{locale === "zh-CN" ? "先把第一条规划 wave 发车,再回来做真正的 triage。" : "Start the first planning wave, then come back for real triage."}
462+
</strong>
463+
<p className="planner-empty-summary">{text.note}</p>
464+
<div className="planner-empty-checklist" aria-label="Planner launch checklist">
465+
{emptyChecklist.map((item, index) => (
466+
<div key={item.title} className="planner-empty-check">
467+
<span className="cell-sub mono muted">{String(index + 1).padStart(2, "0")}</span>
468+
<div className="planner-empty-check-body">
469+
<strong>{item.title}</strong>
470+
<span>{item.desc}</span>
471+
</div>
472+
</div>
473+
))}
474+
</div>
475+
</div>
476+
<div className="planner-empty-grid">
477+
<Link href="/pm" className="planner-empty-card">
478+
<span className="cell-sub mono muted">DISPATCH · 01</span>
434479
<strong>{text.openPm}</strong>
435480
<span>{text.title === "规划桌" ? "先把第一条目标、约束和验收口径写清,再回来让 planner desk 真正开机。" : "Start the first wave from PM intake, then return here once the planning surface exists."}</span>
436-
</Link>
437-
<Link href="/command-tower" className="planner-empty-card">
438-
<span className="cell-sub mono muted">02</span>
481+
</Link>
482+
<Link href="/command-tower" className="planner-empty-card">
483+
<span className="cell-sub mono muted">OBSERVE · 02</span>
439484
<strong>{text.openTower}</strong>
440485
<span>{text.title === "规划桌" ? "如果系统已经在跑,只是规划产物还没挂出来,就先回 tower 看当前谁在动。" : "If work is already running but planning artifacts are missing, scan the tower before you dispatch anything else."}</span>
441-
</Link>
442-
<Link href="/workflows" className="planner-empty-card">
443-
<span className="cell-sub mono muted">03</span>
486+
</Link>
487+
<Link href="/workflows" className="planner-empty-card">
488+
<span className="cell-sub mono muted">RESUME · 03</span>
444489
<strong>{text.openWorkflow}</strong>
445490
<span>{text.title === "规划桌" ? "Workflow Case 是 durable state,不是首页解释文;当 planning row 出现后,回这里继续追。" : "Workflow Cases keep the durable state once the planner row becomes real."}</span>
446-
</Link>
491+
</Link>
492+
</div>
447493
</div>
448494
</Card>
449495
) : (

apps/dashboard/app/runs/[id]/compare/page.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ function compareDecision(copy: Record<string, unknown>): {
7777
};
7878
}
7979

80+
function observationSignalCards(): Array<{ label: string; value: string }> {
81+
return [
82+
{ label: "Compare report", value: "Missing" },
83+
{ label: "Evidence posture", value: "Unavailable" },
84+
{ label: "Next move", value: "Replay compare" },
85+
];
86+
}
87+
88+
function observationDeltaRows(): Array<{ label: string; value: string }> {
89+
return [
90+
{ label: "Compare posture", value: "Awaiting report" },
91+
{ label: "Evidence chain", value: "Unavailable" },
92+
{ label: "LLM params", value: "Unavailable" },
93+
{ label: "LLM snapshot", value: "Unavailable" },
94+
];
95+
}
96+
8097
export default async function RunComparePage({
8198
params,
8299
}: {
@@ -93,6 +110,16 @@ export default async function RunComparePage({
93110
const compareSummary = asRecord(runCompareReport.compare_summary);
94111
const decision = compareDecision(compareSummary);
95112
const hasCompareReport = Object.keys(runCompareReport).length > 0;
113+
const signalCards = hasCompareReport
114+
? [
115+
{ label: "Hash deltas", value: compareSummary.mismatched_count },
116+
{ label: "Artifact gaps", value: compareSummary.missing_count },
117+
{ label: "Unexpected extras", value: compareSummary.extra_count },
118+
{ label: "Report gaps", value: compareSummary.missing_reports_count },
119+
{ label: "Check failures", value: compareSummary.failed_report_checks_count },
120+
]
121+
: observationSignalCards();
122+
const deltaRows = hasCompareReport ? decision.keyDeltas : observationDeltaRows();
96123
const evidenceStatus = hasCompareReport ? (asBoolean(compareSummary.evidence_ok) ? "OK" : "Needs review") : "Unavailable";
97124
const llmParamsStatus = hasCompareReport ? (asBoolean(compareSummary.llm_params_ok) ? "OK" : "Changed") : "Unavailable";
98125
const llmSnapshotStatus = hasCompareReport ? (asBoolean(compareSummary.llm_snapshot_ok) ? "OK" : "Changed") : "Unavailable";
@@ -146,16 +173,10 @@ export default async function RunComparePage({
146173
<h2 className="section-title">Decision summary</h2>
147174
<p>{displaySummary}</p>
148175
<div className="compare-signal-grid" aria-label="Compare signal highlights">
149-
{[
150-
{ label: "Hash deltas", value: compareSummary.mismatched_count },
151-
{ label: "Artifact gaps", value: compareSummary.missing_count },
152-
{ label: "Unexpected extras", value: compareSummary.extra_count },
153-
{ label: "Report gaps", value: compareSummary.missing_reports_count },
154-
{ label: "Check failures", value: compareSummary.failed_report_checks_count },
155-
].map((item) => (
176+
{signalCards.map((item) => (
156177
<div key={item.label} className="compare-signal-card">
157178
<span className="cell-sub mono muted">{item.label}</span>
158-
<strong>{String(item.value ?? 0)}</strong>
179+
<strong>{String(item.value)}</strong>
159180
</div>
160181
))}
161182
</div>
@@ -185,7 +206,7 @@ export default async function RunComparePage({
185206
<Card className="compare-delta-card">
186207
<h3>Key deltas</h3>
187208
<div className="data-list">
188-
{decision.keyDeltas.map((item) => (
209+
{deltaRows.map((item) => (
189210
<div key={item.label} className="data-list-row">
190211
<span className="data-list-label">{item.label}</span>
191212
<span className="data-list-value mono">{String(item.value)}</span>

apps/dashboard/tests/planner_page.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ describe("planner page", () => {
116116
render(await PlannerPage());
117117

118118
expect(screen.getByText("Seed the first planning wave")).toBeInTheDocument();
119-
expect(screen.getByText("No planning artifacts are visible yet.")).toBeInTheDocument();
119+
expect(screen.getByText("Planner launch checklist")).toBeInTheDocument();
120+
expect(screen.getByText("Start the first planning wave, then come back for real triage.")).toBeInTheDocument();
121+
expect(screen.getByText("Lock the objective and the done signal")).toBeInTheDocument();
120122
expect(screen.getByRole("link", { name: "Open PM intake" })).toHaveAttribute("href", "/pm");
121123
expect(screen.getByRole("link", { name: "Open Command Tower" })).toHaveAttribute("href", "/command-tower");
122124
expect(screen.getByRole("link", { name: "Open Workflow Cases" })).toHaveAttribute("href", "/workflows");

apps/dashboard/tests/run_compare_page.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,9 @@ describe("run compare decision surface", () => {
138138
expect(screen.getAllByText(/run replay compare, and refresh this page/i).length).toBeGreaterThan(0);
139139
expect(screen.getByText("Operator choreography")).toBeInTheDocument();
140140
expect(screen.getByText(/Evidence chain: Unavailable/i)).toBeInTheDocument();
141+
expect(screen.getByText("Compare posture")).toBeInTheDocument();
142+
expect(screen.getByText("Awaiting report")).toBeInTheDocument();
143+
expect(screen.getByText("Compare report")).toBeInTheDocument();
144+
expect(screen.getByText("Replay compare")).toBeInTheDocument();
141145
});
142146
});

apps/desktop/src/pages/RunComparePage.tsx

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ function asBoolean(value: JsonValue | undefined): boolean {
2727
return value === true;
2828
}
2929

30+
function observationSignalCards() {
31+
return [
32+
{ label: "Compare report", value: "Missing" },
33+
{ label: "Evidence posture", value: "Unavailable" },
34+
{ label: "Next move", value: "Replay compare" },
35+
];
36+
}
37+
38+
function observationDeltaRows() {
39+
return [
40+
{ label: "Compare posture", value: "Awaiting report" },
41+
{ label: "Evidence chain", value: "Unavailable" },
42+
{ label: "LLM params", value: "Unavailable" },
43+
{ label: "LLM snapshot", value: "Unavailable" },
44+
];
45+
}
46+
3047
export function RunComparePage({ runId, onBack }: Props) {
3148
const [run, setRun] = useState<RunDetailPayload | null>(null);
3249
const [reports, setReports] = useState<ReportRecord[]>([]);
@@ -101,6 +118,25 @@ export function RunComparePage({ runId, onBack }: Props) {
101118
const evidenceStatus = hasCompareReport ? (evidenceOk ? "OK" : "Needs review") : "Unavailable";
102119
const llmParamsStatus = hasCompareReport ? (llmParamsOk ? "OK" : "Changed") : "Unavailable";
103120
const llmSnapshotStatus = hasCompareReport ? (llmSnapshotOk ? "OK" : "Changed") : "Unavailable";
121+
const signalCards = hasCompareReport
122+
? [
123+
{ label: "Mismatched hashes", value: String(mismatchedCount) },
124+
{ label: "Missing artifacts", value: String(missingCount) },
125+
{ label: "Failed report checks", value: String(failedChecksCount) },
126+
]
127+
: observationSignalCards();
128+
const deltaRows = hasCompareReport
129+
? [
130+
{ label: "Mismatched", value: String(mismatchedCount) },
131+
{ label: "Missing", value: String(missingCount) },
132+
{ label: "Extra", value: String(extraCount) },
133+
{ label: "Missing reports", value: String(missingReportsCount) },
134+
{ label: "Failed checks", value: String(failedChecksCount) },
135+
{ label: "Evidence chain", value: evidenceStatus },
136+
{ label: "LLM params", value: llmParamsStatus },
137+
{ label: "LLM snapshot", value: llmSnapshotStatus },
138+
]
139+
: observationDeltaRows();
104140

105141
if (loading) {
106142
return <div className="content"><div className="skeleton-stack-lg"><div className="skeleton skeleton-row" /></div></div>;
@@ -153,18 +189,12 @@ export function RunComparePage({ runId, onBack }: Props) {
153189
<p>{displaySummary}</p>
154190
<p className="muted">{displayNextAction}</p>
155191
<div className="compare-signal-grid">
156-
<div className="compare-signal-card">
157-
<span className="cell-sub mono muted">Mismatched hashes</span>
158-
<strong>{mismatchedCount}</strong>
159-
</div>
160-
<div className="compare-signal-card">
161-
<span className="cell-sub mono muted">Missing artifacts</span>
162-
<strong>{missingCount}</strong>
163-
</div>
164-
<div className="compare-signal-card">
165-
<span className="cell-sub mono muted">Failed report checks</span>
166-
<strong>{failedChecksCount}</strong>
167-
</div>
192+
{signalCards.map((item) => (
193+
<div key={item.label} className="compare-signal-card">
194+
<span className="cell-sub mono muted">{item.label}</span>
195+
<strong>{item.value}</strong>
196+
</div>
197+
))}
168198
</div>
169199
</div>
170200
</CardBody>
@@ -173,14 +203,12 @@ export function RunComparePage({ runId, onBack }: Props) {
173203
<CardHeader><CardTitle>Key deltas</CardTitle></CardHeader>
174204
<CardBody>
175205
<div className="data-list">
176-
<div className="data-list-row"><span className="data-list-label">Mismatched</span><span className="data-list-value mono">{mismatchedCount}</span></div>
177-
<div className="data-list-row"><span className="data-list-label">Missing</span><span className="data-list-value mono">{missingCount}</span></div>
178-
<div className="data-list-row"><span className="data-list-label">Extra</span><span className="data-list-value mono">{extraCount}</span></div>
179-
<div className="data-list-row"><span className="data-list-label">Missing reports</span><span className="data-list-value mono">{missingReportsCount}</span></div>
180-
<div className="data-list-row"><span className="data-list-label">Failed checks</span><span className="data-list-value mono">{failedChecksCount}</span></div>
181-
<div className="data-list-row"><span className="data-list-label">Evidence chain</span><span className="data-list-value mono">{evidenceStatus}</span></div>
182-
<div className="data-list-row"><span className="data-list-label">LLM params</span><span className="data-list-value mono">{llmParamsStatus}</span></div>
183-
<div className="data-list-row"><span className="data-list-label">LLM snapshot</span><span className="data-list-value mono">{llmSnapshotStatus}</span></div>
206+
{deltaRows.map((item) => (
207+
<div key={item.label} className="data-list-row">
208+
<span className="data-list-label">{item.label}</span>
209+
<span className="data-list-value mono">{item.value}</span>
210+
</div>
211+
))}
184212
</div>
185213
{incidentPack.summary ? <p className="muted mt-2">Incident: {String(incidentPack.summary)}</p> : null}
186214
{proofPack.summary ? <p className="muted">Proof: {String(proofPack.summary)}</p> : null}

apps/orchestrator/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pydantic==2.12.5
1010
jsonschema==4.26.0
1111
typer==0.21.1
1212
rich==14.3.3
13-
pytest==9.0.2
13+
pytest==9.0.3
1414
fastapi==0.128.1
1515
uvicorn==0.42.0
1616
python-dotenv==1.2.1

apps/orchestrator/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

design-system/pages/desktop-run-compare.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Preserve the same decision language as web compare while taking advantage of des
1616
- The verdict card and the key-delta card should feel like one operator panel, not unrelated cards.
1717
- Back navigation belongs in chrome, not as the loudest action on the page.
1818
- Bring `evidence chain`, `LLM params`, and `LLM snapshot` into the desktop view so parity does not drift.
19+
- Observation mode must stay honest. Missing compare data should read as `Missing` or `Unavailable`, not as a false negative.
1920

2021
## Avoid
2122

design-system/pages/planner.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Make the operator understand, in one glance:
1616
- Metrics are supporting instrumentation, not the hero.
1717
- `worker contracts`, `unblock tasks`, and `wake policy` must read like planning posture, not like raw artifact counts.
1818
- Empty state should still feel operational: show the next three moves, not a blank warehouse.
19+
- Empty state should read like a launch checklist on the left and dispatch routes on the right, not three equal empty cards floating in space.
1920
- Inspection archive is second-layer only; it may deepen the planning picture, but it must never repeat the triage row one-to-one.
2021

2122
## Avoid

0 commit comments

Comments
 (0)