@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 +14 -0
- package/HR-Resource/cdo/SKILL.md +16 -3
- package/README.md +1 -0
- package/apps/harness-dashboard/components/Drawer.tsx +2 -0
- package/apps/harness-dashboard/components/OrgTree.tsx +202 -89
- package/apps/harness-dashboard/components/Scene.tsx +6 -0
- package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +160 -0
- package/apps/harness-dashboard/lib/harness-state.ts +1 -0
- package/apps/harness-dashboard/lib/types.ts +1 -0
- package/assets/templates/HARNESS.md +1 -0
- package/package.json +1 -1
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.
|
package/HR-Resource/cdo/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
|
38
|
-
5.
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
270
|
-
|
|
249
|
+
const layout = useMemo(() => {
|
|
250
|
+
const cxxAngles = [-150, -78, 0, 78, 150];
|
|
251
|
+
const cxxRadius = 280;
|
|
252
|
+
const workerRadius = 150;
|
|
271
253
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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={
|
|
285
|
-
size="
|
|
286
|
-
active={activeNodeId ===
|
|
369
|
+
node={ownerNode}
|
|
370
|
+
size="lg"
|
|
371
|
+
active={activeNodeId === "owner"}
|
|
287
372
|
onClick={onNodeClick}
|
|
288
373
|
/>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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("&", "&")
|
|
11
|
+
.replaceAll("<", "<")
|
|
12
|
+
.replaceAll(">", ">")
|
|
13
|
+
.replaceAll('"', """);
|
|
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],
|
|
@@ -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.
|
|
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"
|