@walwal-harness/cli 7.1.22 → 7.1.23

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/CHANGELOG.md CHANGED
@@ -31,6 +31,20 @@ docmeta:
31
31
 
32
32
  ## Unreleased
33
33
 
34
+ ## 7.1.23 — Dashboard CDO preview + radial org tree (2026-05-21)
35
+
36
+ ### Added
37
+ - Dashboard CDO Preview tab: clicking `harness-cdo` opens an iframe preview from `.harness/documents/{mission}/cdo/preview.html`.
38
+ - CDO skill and harness reference now require the preview artifact before CDO completion, with CEO/Owner handoff guidance to view it in the dashboard.
39
+ - Dashboard mission state now reads `cdo/preview.html` alongside CXX markdown docs.
40
+
41
+ ### Changed
42
+ - Dashboard Org Tree now uses a compact radial/sunflower layout with Owner/CEO centered and CXX roles arranged around them.
43
+ - Org Tree now has independent zoom controls, ctrl/meta-wheel zoom, and drag panning inside a reduced-height scroll surface.
44
+
45
+ ### Fixed
46
+ - CDO dashboard navigation now opens the visual Preview tab instead of only the markdown document flow.
47
+
34
48
  ### Fixed
35
49
  - v7 `/goal`, `/submission`, and `/hot-fix` now mark both `company_state.state` and `conductor.state` as `running`, so Claude Stop hook auto-chain does not stop after CEO routing or hiring-only summaries.
36
50
  - Stop hook now treats v7 CEO routing state (`current_agent=ceo`, `agent_status=running`, `owner_prompt.status=routing`) as an unfinished company loop, including migrated projects that do not yet have running conductor/company state.
@@ -18,13 +18,25 @@ Own design strategy for the mission.
18
18
  5. Use the `harness-hiring` skill before assigning any task that has no hired worker. Do not complete that task yourself.
19
19
  6. Delegate all design deliverables and review passes to hired workers in fresh sessions.
20
20
  7. Evaluate worker feedback for usefulness, discomfort, novelty, clarity, and differentiation.
21
- 8. Select the final direction and report to CEO and CTO.
21
+ 8. Select the final direction and write a CDO preview artifact at `.harness/documents/{mission_name}/cdo/preview.html`.
22
+ 9. Report to CEO and CTO with a concise preview summary and mention that the visual sample is available in the dashboard by clicking harness-cdo.
22
23
 
23
24
  ## Rule
24
25
 
25
26
  Design output must be usable by implementation teams, not just descriptive.
26
27
  CDO must not directly produce palettes, typography, layouts, mockups, interaction specs, or design-review findings. CDO may only brief workers, compare their outputs, decide, and document the accepted direction with worker names and report paths.
27
28
 
29
+ ## Preview Artifact Gate
30
+
31
+ CDO completion is invalid without `.harness/documents/{mission_name}/cdo/preview.html`.
32
+
33
+ - The preview must be a self-contained HTML document suitable for dashboard iframe rendering.
34
+ - The preview must summarize the worker-backed final direction visually, not just repeat prose from `cdo.md`.
35
+ - It must include the core screen/state CDO expects CTO to implement, using realistic layout, density, labels, and interaction affordances.
36
+ - It must not load remote scripts or depend on a dev server. Inline CSS is preferred.
37
+ - If the mission has no UI surface, CDO must still write a visual decision board preview explaining the non-UI design constraints.
38
+ - CDO's response to CEO must include a short note: "Dashboard: click harness-cdo to view the Preview."
39
+
28
40
  ## Owner Handoff Gate
29
41
 
30
42
  Owner is the final acceptance reviewer, not a design QA substitute. CDO must use design/review workers to validate UI/UX readiness, accessibility concerns, responsive behavior assumptions, and visual acceptance risks before handoff. Do not ask the Owner to discover whether the UI is usable or visually broken; ask only for final product or brand acceptance after worker-backed review.
@@ -34,7 +46,8 @@ Required output sections:
34
46
  1. Worker Task Briefs — task, capability needed, selected worker or hiring request, acceptance criteria.
35
47
  2. Worker Evidence Manifest — worker name, report path, status.
36
48
  3. CDO Decision — only decisions accepted from worker evidence.
37
- 4. Next Handoff — CTO-ready design constraints, inputs, blockers.
38
- 5. Implementation Notes — in English, with `Design Decisions`, `Deviations`, `Tradeoffs`, and `Open Questions`.
49
+ 4. Preview Artifact — path `.harness/documents/{mission_name}/cdo/preview.html`, visual summary, and dashboard viewing note.
50
+ 5. Next Handoff — CTO-ready design constraints, inputs, blockers.
51
+ 6. Implementation Notes — in English, with `Design Decisions`, `Deviations`, `Tradeoffs`, and `Open Questions`.
39
52
 
40
53
  Every CDO worker brief must require the worker to append the same English `## Implementation Notes` block to the bottom of `.harness/documents/{mission_name}/cdo/workers/{worker-name}.md`, covering risks, self-corrections, chosen direction, and unresolved questions. Use `None` for empty subsections.
package/README.md CHANGED
@@ -203,6 +203,7 @@ Features:
203
203
  - **Org Tree** — live status of Owner → CEO → CXX → Workers hierarchy, with worker cards created only from hired `hr-roster.json` entries backed by `.harness/shared/HR-Resource/{worker}/SKILL.md`; running hired workers are highlighted under their owning CXX
204
204
  - **Mission Timeline** — clickable history of goal/submission/hot-fix missions showing the full dispatch chain
205
205
  - **Mission Flow tab** — per-mission flow: Owner prompt → CEO routing → CXX → owner-specific worker evidence → CQO verdict
206
+ - **CDO Preview** — clicking `harness-cdo` opens a dashboard iframe preview from `.harness/documents/{mission}/cdo/preview.html`; CDO completion requires this visual summary artifact
206
207
  - **History tab** — mission-specific Owner request (from CEO summary + closest progress.log match)
207
208
  - **Gotchas tab** — searchable `.harness/gotchas/*.md` knowledge base, click to read full markdown
208
209
  - **Document tab** — per-CXX markdown doc viewer
@@ -5,6 +5,7 @@ export type DrawerTab =
5
5
  | "mission-flow" // Mission document hierarchy (PRIMARY)
6
6
  | "history" // Owner prompt history
7
7
  | "gotchas" // Harness gotcha knowledge base
8
+ | "design-preview" // CDO visual sample iframe
8
9
  | "mission-doc" // Single doc markdown view
9
10
  | "logs"; // Agent log (technical)
10
11
 
@@ -23,6 +24,7 @@ const TABS: Array<{ id: DrawerTab; label: string }> = [
23
24
  { id: "mission-flow", label: "Mission Flow" },
24
25
  { id: "history", label: "History" },
25
26
  { id: "gotchas", label: "Gotchas" },
27
+ { id: "design-preview", label: "Preview" },
26
28
  { id: "mission-doc", label: "Document" },
27
29
  { id: "logs", label: "Agent Log" },
28
30
  ];
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { useCallback, useMemo, useRef, useState } from "react";
2
3
  import type { AgentId, HarnessSnapshot, MinifigState, OrgNodeDef } from "@/lib/types";
3
4
 
4
5
  interface OrgTreeProps {
@@ -66,38 +67,12 @@ function OrgCard({
66
67
  );
67
68
  }
68
69
 
69
- function BranchConnector({ count }: { count: number }) {
70
- const h = 44;
71
- const stepPct = 100 / count;
72
- const midY = h * 0.5;
73
- const centers = Array.from({ length: count }, (_, i) => stepPct * i + stepPct / 2);
74
- return (
75
- <svg width="100%" height={h} className="overflow-visible pointer-events-none">
76
- <line x1="50%" y1="0" x2="50%" y2={midY} stroke="#374151" strokeWidth="1.5" />
77
- <line
78
- x1={`${centers[0]}%`}
79
- y1={midY}
80
- x2={`${centers[centers.length - 1]}%`}
81
- y2={midY}
82
- stroke="#374151"
83
- strokeWidth="1.5"
84
- />
85
- {centers.map((x, i) => (
86
- <line
87
- key={i}
88
- x1={`${x}%`}
89
- y1={midY}
90
- x2={`${x}%`}
91
- y2={h}
92
- stroke="#374151"
93
- strokeWidth="1.5"
94
- />
95
- ))}
96
- </svg>
97
- );
98
- }
99
-
100
70
  export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
71
+ const defaultZoom = 0.74;
72
+ const [zoom, setZoom] = useState(defaultZoom);
73
+ const scrollRef = useRef<HTMLDivElement | null>(null);
74
+ const dragRef = useRef<{ x: number; y: number; left: number; top: number } | null>(null);
75
+
101
76
  const agentMap = new Map(snapshot.agents.map((a) => [a.id, a]));
102
77
 
103
78
  const getStatus = (ids: AgentId[]): MinifigState => {
@@ -229,68 +204,205 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
229
204
  // report-only workers remain visible in mission documents, but not as seats.
230
205
  const workers = (currentMission?.workers ?? []).filter((w) => w.hired);
231
206
 
232
- return (
233
- <div className="w-full px-4 py-6 select-none">
234
- {/* Mission label */}
235
- {currentMission && (
236
- <div className="mb-4 text-center">
237
- <span className="font-mono text-[10px] uppercase tracking-[0.2em] text-cyan-300/60">
238
- current mission
239
- </span>
240
- <div className="mt-0.5 font-mono text-xs text-gray-400">{currentMission.missionId}</div>
241
- </div>
242
- )}
207
+ const setBoundedZoom = useCallback((next: number) => {
208
+ setZoom(Math.min(1.35, Math.max(0.62, Number(next.toFixed(2)))));
209
+ }, []);
243
210
 
244
- {/* Row 1: Owner */}
245
- <div className="flex justify-center">
246
- <OrgCard
247
- node={ownerNode}
248
- size="lg"
249
- active={activeNodeId === "owner"}
250
- onClick={onNodeClick}
251
- />
252
- </div>
211
+ const handleWheel = useCallback(
212
+ (event: React.WheelEvent<HTMLDivElement>) => {
213
+ if (!event.ctrlKey && !event.metaKey) return;
214
+ event.preventDefault();
215
+ setBoundedZoom(zoom + (event.deltaY > 0 ? -0.06 : 0.06));
216
+ },
217
+ [setBoundedZoom, zoom],
218
+ );
253
219
 
254
- {/* Connector: Owner → CEO */}
255
- <div className="flex justify-center">
256
- <div className="w-px h-6 bg-gray-700" />
257
- </div>
220
+ const handlePointerDown = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
221
+ if (event.button !== 0 || (event.target as HTMLElement).closest("button")) return;
222
+ const node = scrollRef.current;
223
+ if (!node) return;
224
+ dragRef.current = {
225
+ x: event.clientX,
226
+ y: event.clientY,
227
+ left: node.scrollLeft,
228
+ top: node.scrollTop,
229
+ };
230
+ node.setPointerCapture(event.pointerId);
231
+ }, []);
258
232
 
259
- {/* Row 2: CEO */}
260
- <div className="flex justify-center">
261
- <OrgCard
262
- node={ceoNode}
263
- size="lg"
264
- active={activeNodeId === "ceo"}
265
- onClick={onNodeClick}
266
- />
267
- </div>
233
+ const handlePointerMove = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
234
+ const start = dragRef.current;
235
+ const node = scrollRef.current;
236
+ if (!start || !node) return;
237
+ node.scrollLeft = start.left - (event.clientX - start.x);
238
+ node.scrollTop = start.top - (event.clientY - start.y);
239
+ }, []);
240
+
241
+ const endDrag = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
242
+ scrollRef.current?.releasePointerCapture(event.pointerId);
243
+ dragRef.current = null;
244
+ }, []);
245
+
246
+ const canvas = { width: 1160, height: 600 };
247
+ const center = { x: 500, y: 240 };
268
248
 
269
- {/* Branch connector: CEO → CXX */}
270
- <BranchConnector count={cxxNodes.length} />
249
+ const layout = useMemo(() => {
250
+ const cxxAngles = [-150, -78, 0, 78, 150];
251
+ const cxxRadius = 280;
252
+ const workerRadius = 150;
271
253
 
272
- {/* Row 3: CXX nodes with workers below */}
273
- <div className="flex gap-3 justify-center">
274
- {cxxNodes.map((node) => {
275
- const nodeWorkers = workers.filter((w) => w.owner === node.role);
276
- const hasActiveWorker = nodeWorkers.some((w) => w.active);
277
- const displayNode = hasActiveWorker
278
- ? { ...node, status: "typing" as const, activity: "Worker active" }
279
- : node;
254
+ return cxxNodes.map((node, index) => {
255
+ const angle = (cxxAngles[index] * Math.PI) / 180;
256
+ const x = center.x + Math.cos(angle) * cxxRadius;
257
+ const y = center.y + Math.sin(angle) * cxxRadius * 0.58;
258
+ const outwardX = Math.cos(angle) * workerRadius;
259
+ const outwardY = Math.sin(angle) * workerRadius * 0.48;
260
+ return {
261
+ node,
262
+ x,
263
+ y,
264
+ workerX: x + outwardX,
265
+ workerY: y + outwardY,
266
+ };
267
+ });
268
+ }, [cxxNodes]);
280
269
 
281
- return (
282
- <div key={node.id} className="flex flex-col items-center gap-2">
270
+ return (
271
+ <div className="select-none">
272
+ <div className="flex items-center justify-between border-b border-white/10 bg-black/20 px-3 py-2">
273
+ <div className="min-w-0">
274
+ {currentMission && (
275
+ <div className="truncate font-mono text-[10px] text-cyan-300/70">
276
+ current mission · {currentMission.missionId}
277
+ </div>
278
+ )}
279
+ </div>
280
+ <div className="flex items-center gap-1">
281
+ <button
282
+ type="button"
283
+ onClick={() => setBoundedZoom(zoom - 0.08)}
284
+ className="h-7 w-7 rounded border border-gray-700 bg-gray-900/80 font-mono text-xs text-gray-200 hover:border-cyan-300/60"
285
+ title="Zoom out"
286
+ >
287
+ -
288
+ </button>
289
+ <button
290
+ type="button"
291
+ onClick={() => setBoundedZoom(defaultZoom)}
292
+ className="h-7 min-w-12 rounded border border-gray-700 bg-gray-900/80 px-2 font-mono text-[10px] text-gray-300 hover:border-cyan-300/60"
293
+ title="Reset zoom"
294
+ >
295
+ {Math.round(zoom * 100)}%
296
+ </button>
297
+ <button
298
+ type="button"
299
+ onClick={() => setBoundedZoom(zoom + 0.08)}
300
+ className="h-7 w-7 rounded border border-gray-700 bg-gray-900/80 font-mono text-xs text-gray-200 hover:border-cyan-300/60"
301
+ title="Zoom in"
302
+ >
303
+ +
304
+ </button>
305
+ </div>
306
+ </div>
307
+
308
+ <div
309
+ ref={scrollRef}
310
+ onWheel={handleWheel}
311
+ onPointerDown={handlePointerDown}
312
+ onPointerMove={handlePointerMove}
313
+ onPointerUp={endDrag}
314
+ onPointerCancel={endDrag}
315
+ className="scrollbar-hidden h-[390px] cursor-grab overflow-auto active:cursor-grabbing"
316
+ >
317
+ <div
318
+ className="relative"
319
+ style={{
320
+ width: canvas.width * zoom,
321
+ height: canvas.height * zoom,
322
+ }}
323
+ >
324
+ <div
325
+ className="absolute left-0 top-0 origin-top-left"
326
+ style={{
327
+ width: canvas.width,
328
+ height: canvas.height,
329
+ transform: `scale(${zoom})`,
330
+ }}
331
+ >
332
+ <svg
333
+ className="pointer-events-none absolute inset-0"
334
+ width={canvas.width}
335
+ height={canvas.height}
336
+ >
337
+ <line x1={center.x} y1={center.y - 26} x2={center.x} y2={center.y + 26} stroke="#374151" strokeWidth="1.5" />
338
+ {layout.map(({ node, x, y }) => (
339
+ <line
340
+ key={node.id}
341
+ x1={center.x}
342
+ y1={center.y}
343
+ x2={x}
344
+ y2={y}
345
+ stroke="#374151"
346
+ strokeDasharray="4 5"
347
+ strokeWidth="1.5"
348
+ />
349
+ ))}
350
+ {layout.map(({ node, x, y, workerX, workerY }) => {
351
+ const nodeWorkers = workers.filter((w) => w.owner === node.role);
352
+ if (nodeWorkers.length === 0) return null;
353
+ return (
354
+ <line
355
+ key={`${node.id}-workers`}
356
+ x1={x}
357
+ y1={y}
358
+ x2={workerX}
359
+ y2={workerY}
360
+ stroke="#374151"
361
+ strokeWidth="1.5"
362
+ />
363
+ );
364
+ })}
365
+ </svg>
366
+
367
+ <div className="absolute left-[390px] top-[170px] grid w-[220px] gap-2">
283
368
  <OrgCard
284
- node={displayNode}
285
- size="md"
286
- active={activeNodeId === node.id}
369
+ node={ownerNode}
370
+ size="lg"
371
+ active={activeNodeId === "owner"}
287
372
  onClick={onNodeClick}
288
373
  />
289
- {/* Worker mini-cards under CTO */}
290
- {nodeWorkers.length > 0 && (
291
- <>
292
- <div className="w-px h-3 bg-gray-700/60" />
293
- <div className="flex flex-col gap-1.5 w-full">
374
+ <OrgCard
375
+ node={ceoNode}
376
+ size="lg"
377
+ active={activeNodeId === "ceo"}
378
+ onClick={onNodeClick}
379
+ />
380
+ </div>
381
+
382
+ {layout.map(({ node, x, y, workerX, workerY }) => {
383
+ const nodeWorkers = workers.filter((w) => w.owner === node.role);
384
+ const hasActiveWorker = nodeWorkers.some((w) => w.active);
385
+ const displayNode = hasActiveWorker
386
+ ? { ...node, status: "typing" as const, activity: "Worker active" }
387
+ : node;
388
+
389
+ return (
390
+ <div
391
+ key={node.id}
392
+ className="absolute"
393
+ style={{ left: x - 82, top: y - 38 }}
394
+ >
395
+ <OrgCard
396
+ node={displayNode}
397
+ size="md"
398
+ active={activeNodeId === node.id}
399
+ onClick={onNodeClick}
400
+ />
401
+ {nodeWorkers.length > 0 && (
402
+ <div
403
+ className="absolute flex w-[170px] flex-col gap-1.5"
404
+ style={{ left: workerX - x, top: workerY - y }}
405
+ >
294
406
  {nodeWorkers.map((w) => {
295
407
  const workerNodeId = `worker-${node.role}-${w.name}`;
296
408
  const workerActive = activeNodeId === workerNodeId || w.active;
@@ -339,11 +451,12 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
339
451
  );
340
452
  })}
341
453
  </div>
342
- </>
343
- )}
344
- </div>
345
- );
346
- })}
454
+ )}
455
+ </div>
456
+ );
457
+ })}
458
+ </div>
459
+ </div>
347
460
  </div>
348
461
  </div>
349
462
  );
@@ -17,6 +17,7 @@ import { OwnerHistoryTab } from "./drawer/OwnerHistoryTab";
17
17
  import { MissionDocTab } from "./drawer/MissionDocTab";
18
18
  import { MissionFlowTab } from "./drawer/MissionFlowTab";
19
19
  import { GotchasTab } from "./drawer/GotchasTab";
20
+ import { DesignPreviewTab } from "./drawer/DesignPreviewTab";
20
21
  import { MissionTimeline } from "./MissionTimeline";
21
22
  import { OrgTree } from "./OrgTree";
22
23
 
@@ -48,6 +49,8 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
48
49
  }
49
50
  if (node.id === "owner") {
50
51
  setDrawerTab("history");
52
+ } else if (node.id === "cdo") {
53
+ setDrawerTab("design-preview");
51
54
  } else if (node.id.startsWith("worker-")) {
52
55
  setDrawerTab("mission-doc");
53
56
  } else {
@@ -167,6 +170,9 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
167
170
  {drawerTab === "gotchas" && (
168
171
  <GotchasTab gotchas={snapshot.gotchas ?? []} />
169
172
  )}
173
+ {drawerTab === "design-preview" && (
174
+ <DesignPreviewTab mission={selectedMission ?? snapshot.missions?.[0] ?? null} />
175
+ )}
170
176
  {drawerTab === "mission-doc" && selectedNode && (
171
177
  <MissionDocTab
172
178
  missions={snapshot.missions ?? []}
@@ -0,0 +1,160 @@
1
+ "use client";
2
+ import type { MissionDoc } from "@/lib/types";
3
+
4
+ interface DesignPreviewTabProps {
5
+ mission: MissionDoc | null;
6
+ }
7
+
8
+ function escapeHtml(value: string) {
9
+ return value
10
+ .replaceAll("&", "&amp;")
11
+ .replaceAll("<", "&lt;")
12
+ .replaceAll(">", "&gt;")
13
+ .replaceAll('"', "&quot;");
14
+ }
15
+
16
+ function extractFirstBullet(markdown: string | null | undefined) {
17
+ if (!markdown) return null;
18
+ return (
19
+ markdown
20
+ .split("\n")
21
+ .map((line) => line.trim())
22
+ .find((line) => /^[-*]\s+\S/.test(line))
23
+ ?.replace(/^[-*]\s+/, "")
24
+ .slice(0, 140) ?? null
25
+ );
26
+ }
27
+
28
+ function buildPreviewDoc(mission: MissionDoc | null) {
29
+ if (mission?.cdoPreview?.trim()) {
30
+ return mission.cdoPreview;
31
+ }
32
+
33
+ const missionId = mission?.missionId ?? "No active mission";
34
+ const brief =
35
+ extractFirstBullet(mission?.cdo) ??
36
+ extractFirstBullet(mission?.ceo) ??
37
+ "Design direction will appear here after harness-cdo writes cdo.md.";
38
+
39
+ return `<!doctype html>
40
+ <html lang="en">
41
+ <head>
42
+ <meta charset="utf-8" />
43
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
44
+ <style>
45
+ :root {
46
+ color-scheme: dark;
47
+ --bg: #101216;
48
+ --panel: #171b22;
49
+ --line: #2d3543;
50
+ --text: #eef2f7;
51
+ --muted: #8a94a6;
52
+ --cyan: #22d3ee;
53
+ --amber: #fbbf24;
54
+ --rose: #fb7185;
55
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
56
+ }
57
+ * { box-sizing: border-box; }
58
+ body {
59
+ margin: 0;
60
+ min-height: 100vh;
61
+ background: radial-gradient(circle at 18% 0%, rgba(34, 211, 238, .12), transparent 26rem), var(--bg);
62
+ color: var(--text);
63
+ }
64
+ .shell { min-height: 100vh; display: grid; grid-template-rows: auto 1fr; }
65
+ header {
66
+ display: flex; align-items: center; justify-content: space-between; gap: 16px;
67
+ padding: 18px 22px; border-bottom: 1px solid var(--line); background: rgba(10, 12, 16, .72);
68
+ }
69
+ .brand { font-size: 12px; letter-spacing: .22em; text-transform: uppercase; color: var(--cyan); }
70
+ .mission { max-width: 54ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-size: 12px; }
71
+ main { display: grid; grid-template-columns: 280px 1fr; min-height: 0; }
72
+ aside { border-right: 1px solid var(--line); padding: 18px; background: rgba(255,255,255,.02); }
73
+ .nav { display: grid; gap: 8px; }
74
+ .nav div { padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; color: var(--muted); font-size: 12px; }
75
+ .nav div:first-child { color: var(--text); border-color: rgba(34,211,238,.55); background: rgba(34,211,238,.08); }
76
+ section { padding: 22px; min-width: 0; }
77
+ .hero {
78
+ border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--panel);
79
+ display: grid; grid-template-columns: minmax(260px, 1fr) 280px;
80
+ }
81
+ .hero-copy { padding: 28px; }
82
+ h1 { margin: 0; font-size: 34px; line-height: 1.08; letter-spacing: 0; }
83
+ p { color: var(--muted); line-height: 1.55; }
84
+ .actions { display: flex; gap: 10px; margin-top: 22px; }
85
+ button { border: 0; border-radius: 8px; padding: 10px 14px; font-weight: 700; }
86
+ .primary { background: var(--cyan); color: #061014; }
87
+ .secondary { background: #242a35; color: var(--text); }
88
+ .visual { padding: 20px; background: linear-gradient(135deg, rgba(34,211,238,.18), rgba(251,191,36,.12)); display: grid; align-content: center; gap: 12px; }
89
+ .card { border: 1px solid rgba(255,255,255,.16); border-radius: 8px; padding: 14px; background: rgba(16,18,22,.72); box-shadow: 0 18px 40px rgba(0,0,0,.22); }
90
+ .metric { font-size: 26px; font-weight: 800; }
91
+ .grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 16px; }
92
+ .tile { border: 1px solid var(--line); border-radius: 8px; padding: 14px; background: rgba(255,255,255,.03); min-height: 112px; }
93
+ .tile b { display: block; margin-bottom: 8px; }
94
+ @media (max-width: 760px) {
95
+ main, .hero { grid-template-columns: 1fr; }
96
+ aside { display: none; }
97
+ h1 { font-size: 28px; }
98
+ .grid { grid-template-columns: 1fr; }
99
+ }
100
+ </style>
101
+ </head>
102
+ <body>
103
+ <div class="shell">
104
+ <header>
105
+ <div class="brand">CDO Visual Sample</div>
106
+ <div class="mission">${escapeHtml(missionId)}</div>
107
+ </header>
108
+ <main>
109
+ <aside>
110
+ <div class="nav">
111
+ <div>Overview</div>
112
+ <div>Interaction</div>
113
+ <div>Accessibility</div>
114
+ <div>Review</div>
115
+ </div>
116
+ </aside>
117
+ <section>
118
+ <div class="hero">
119
+ <div class="hero-copy">
120
+ <h1>${escapeHtml(brief)}</h1>
121
+ <p>Structured sample generated from the active CDO/CEO mission context. Replace this with a richer cdo.md mockup artifact when available.</p>
122
+ <div class="actions">
123
+ <button class="primary">Primary action</button>
124
+ <button class="secondary">Secondary</button>
125
+ </div>
126
+ </div>
127
+ <div class="visual">
128
+ <div class="card"><div class="metric">92%</div><p>Readiness score</p></div>
129
+ <div class="card"><b>State</b><p>Focused, scannable, worker-backed.</p></div>
130
+ </div>
131
+ </div>
132
+ <div class="grid">
133
+ <div class="tile"><b>Layout</b><p>Quiet hierarchy, constrained content, no nested cards.</p></div>
134
+ <div class="tile"><b>Flow</b><p>Primary workflow remains visible without explanation copy.</p></div>
135
+ <div class="tile"><b>Quality</b><p>Designed for CDO review before CQO evidence collection.</p></div>
136
+ </div>
137
+ </section>
138
+ </main>
139
+ </div>
140
+ </body>
141
+ </html>`;
142
+ }
143
+
144
+ export function DesignPreviewTab({ mission }: DesignPreviewTabProps) {
145
+ const srcDoc = buildPreviewDoc(mission);
146
+
147
+ return (
148
+ <div className="flex h-full min-h-[620px] flex-col gap-3">
149
+ <div className="rounded border border-purple-400/30 bg-purple-500/10 px-3 py-2 text-[11px] text-purple-200">
150
+ harness-cdo visual preview
151
+ </div>
152
+ <iframe
153
+ title="CDO visual sample"
154
+ sandbox=""
155
+ srcDoc={srcDoc}
156
+ className="min-h-0 flex-1 rounded-md border border-gray-700 bg-[#101216]"
157
+ />
158
+ </div>
159
+ );
160
+ }
@@ -1229,6 +1229,7 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
1229
1229
  cqo: readMd("cqo.md"),
1230
1230
  coo: readMd("coo.md"),
1231
1231
  cdo: readMd("cdo.md"),
1232
+ cdoPreview: readMd("cdo/preview.html"),
1232
1233
  ops: readMd("ops.md"),
1233
1234
  workers,
1234
1235
  cxxPresent: [...cxxPresent],
@@ -318,6 +318,7 @@ export interface MissionDoc {
318
318
  cqo: string | null;
319
319
  coo: string | null;
320
320
  cdo: string | null;
321
+ cdoPreview: string | null;
321
322
  ops: string | null;
322
323
  workers: WorkerDocEntry[];
323
324
  cxxPresent: string[];
@@ -89,6 +89,7 @@ Flat `.harness/documents/{mission}/workers/` reports are legacy. New worker evid
89
89
  - CEO must not dispatch, hire, or brief workers directly.
90
90
  - CXX agents must not directly execute specialist deliverables.
91
91
  - CTO worker dispatch requires `cto.md` first.
92
+ - CDO completion requires `.harness/documents/{mission}/cdo/preview.html`; CEO/Owner can view it from the dashboard by clicking `harness-cdo`.
92
93
  - CQO verdicts require evaluator/tester worker evidence.
93
94
  - Owner is final acceptance only. Do not report "done, please check" until worker-backed verification proves the goal is complete.
94
95
  - Every `ceo.md`, `{cxx}.md`, and worker report must end with:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.22",
3
+ "version": "7.1.23",
4
4
  "description": "Company-style AI agent harness for Claude and Codex. Installs commands, CXX agents, skills, HR-Resource hiring pool, and project-local .harness runtime state.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"