@walwal-harness/cli 7.1.21 → 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,29 @@ 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
+
48
+ ### Fixed
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.
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.
51
+ - CEO skill now explicitly forbids ending a mission turn with only hiring/resource-manager output while the Owner goal remains unfinished.
52
+
53
+ ### Added
54
+ - `scripts/harness-company-complete.sh` provides the shared running → idle/completed transition for dashboard auto-mode, external runners, hooks, and final CEO handoff paths.
55
+ - New progress templates include `company_state.state` and default `conductor` fields for consistent runner/dashboard state.
56
+
34
57
  ### Changed
35
58
  - Dashboard Org Tree now positions only hired HR-Resource workers from `hr-roster.json`, requiring a backing `.harness/shared/HR-Resource/{worker}/SKILL.md`.
36
59
  - Running hired workers are highlighted under their owning CXX instead of showing queue jobs as worker seats.
@@ -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.
@@ -30,6 +30,7 @@ You are the only direct conversation channel with the Owner.
30
30
  - If a suitable hired worker is absent, invoke the `harness-hiring` skill before the CXX proceeds with that deliverable.
31
31
  - CEO must reject CXX reports that contain completed specialist deliverables without matching worker records under `.harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/`.
32
32
  - Every CXX starts from fresh context and records decisions in `.harness/documents/{goal-or-child-mission}/{cxx}.md`.
33
+ - Hiring or resource-manager output is never a stopping point. After missing workers are registered, immediately continue routing to the responsible CXX fresh sessions and require those CXX agents to brief/run the hired workers. Do not end the turn with only a hiring summary while the Owner goal remains unfinished.
33
34
  - Preserve DDD boundaries: domain decisions, application wiring, infrastructure, and quality policy are separate responsibilities.
34
35
  - Before CTO/CDO/OPS allocate runnable services, agree with the Owner on a `{xx}000` base port and write it to project `.env` as `HARNESS_BASE_PORT={xx}000`. Mentioning the value in `ceo.md` is not sufficient.
35
36
  - After writing `.env`, verify with `grep '^HARNESS_BASE_PORT=' .env` before routing service work.
package/README.md CHANGED
@@ -158,6 +158,25 @@ Dashboard options:
158
158
  | `--port <port>` | Run Brick Office on a custom port, e.g. `3002` |
159
159
  | `--reinstall` | Delete and rebuild the isolated dashboard cache under `~/.walwal-harness/dashboard/` |
160
160
 
161
+ ### Company Auto-Chain State
162
+
163
+ `/goal`, `/submission`, and `/hot-fix` mark the v7 company loop as running in `.harness/progress.json`:
164
+
165
+ - `company_state.state = "running"`
166
+ - `conductor.state = "running"`
167
+ - `current_agent = "ceo"`
168
+ - `next_agent = "ceo"`
169
+
170
+ Claude's Stop hook uses this state to prevent an unfinished CEO/CXX/worker loop from ending after a partial routing step such as hiring. Hookless executors such as Codex should drive the same state through an external runner or dashboard auto-mode process.
171
+
172
+ When the mission is genuinely complete, the common completion transition is:
173
+
174
+ ```bash
175
+ bash scripts/harness-company-complete.sh . mission-complete
176
+ ```
177
+
178
+ This sets `company_state.state = "idle"`, `conductor.state = "completed"`, `next_agent = "none"`, and `owner_prompt.status = "completed"` so dashboards, runners, and hooks agree that auto-mode can stop.
179
+
161
180
  Existing users should update and migrate from the target project:
162
181
 
163
182
  ```bash
@@ -184,6 +203,7 @@ Features:
184
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
185
204
  - **Mission Timeline** — clickable history of goal/submission/hot-fix missions showing the full dispatch chain
186
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
187
207
  - **History tab** — mission-specific Owner request (from CEO summary + closest progress.log match)
188
208
  - **Gotchas tab** — searchable `.harness/gotchas/*.md` knowledge base, click to read full markdown
189
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:
@@ -9,6 +9,7 @@
9
9
  "scripts/harness-meeting-doc.sh",
10
10
  "scripts/harness-next.sh",
11
11
  "scripts/harness-worker-dispatch.sh",
12
+ "scripts/harness-company-complete.sh",
12
13
  "scripts/harness-stop.sh",
13
14
  "scripts/harness-session-start.sh",
14
15
  "scripts/harness-progress-set.sh",
@@ -48,11 +48,19 @@
48
48
  "sprint-contract.md": { "status": "pending", "updated_by": null, "updated_at": null }
49
49
  },
50
50
  "company_state": {
51
+ "state": "idle",
51
52
  "active_workers": 0,
52
53
  "concurrency": 3,
53
54
  "paused_at": null,
54
55
  "resume_from": null
55
56
  },
57
+ "conductor": {
58
+ "state": "idle",
59
+ "tracks": [],
60
+ "rendezvous": null,
61
+ "fork_meeting_id": null,
62
+ "stop_chain_count": 0
63
+ },
56
64
  "updated_at": "{{DATE}}",
57
65
  "history": []
58
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.21",
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"
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # harness-company-complete.sh — mark the current v7 company mission complete.
3
+ #
4
+ # Usage:
5
+ # bash scripts/harness-company-complete.sh <project-root> [reason]
6
+ #
7
+ # This is the explicit running -> idle/done transition used by dashboard,
8
+ # runner, hooks, and final CEO handoff paths. It intentionally only updates
9
+ # runtime state; it does not archive or rewrite mission documents.
10
+
11
+ set -uo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+ PROJECT_ROOT="${1:-.}"
15
+ REASON="${2:-mission-complete}"
16
+
17
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
18
+ [ -f "$PROGRESS" ] || {
19
+ echo "[company-complete] not found: $PROGRESS" >&2
20
+ exit 1
21
+ }
22
+ command -v jq >/dev/null 2>&1 || {
23
+ echo "[company-complete] jq is required" >&2
24
+ exit 1
25
+ }
26
+
27
+ bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
28
+ '.company_state.state = "idle" |
29
+ .company_state.active_workers = 0 |
30
+ .company_state.completed_at = (now | todate) |
31
+ .conductor.state = "completed" |
32
+ .conductor.current_action = "complete:'"$REASON"'" |
33
+ .conductor.completed_at = (now | todate) |
34
+ .conductor.tracks = [] |
35
+ .conductor.rendezvous = null |
36
+ .conductor.fork_meeting_id = null |
37
+ .current_agent = null |
38
+ .next_agent = "none" |
39
+ .agent_status = "completed" |
40
+ .owner_prompt.status = "completed" |
41
+ .owner_prompt.completed_at = (now | todate)'
42
+
43
+ echo "[company-complete] marked complete: $REASON"
@@ -52,8 +52,11 @@ if [ "${STOP_CHAIN_COUNT:-0}" -ge "${STOP_CHAIN_MAX:-200}" ]; then exit 0; fi
52
52
 
53
53
  # 회사 루프 상태 확인
54
54
  CONDUCTOR_STATE=$(jq -r '.conductor.state // "idle"' "$PROGRESS" 2>/dev/null || echo "idle")
55
+ COMPANY_STATE=$(jq -r '.company_state.state // "idle"' "$PROGRESS" 2>/dev/null || echo "idle")
56
+ CURRENT_AGENT=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null || echo "none")
55
57
  NEXT_AGENT=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null || echo "none")
56
58
  AGENT_STATUS=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null || echo "pending")
59
+ OWNER_PROMPT_STATUS=$(jq -r '.owner_prompt.status // "none"' "$PROGRESS" 2>/dev/null || echo "none")
57
60
  TASK_STOP_ACTIVE=$(jq -r '.task_stop.active // false' "$PROGRESS" 2>/dev/null || echo "false")
58
61
  ESCALATION=$(jq -r '.conductor.escalation // "null"' "$PROGRESS" 2>/dev/null || echo "null")
59
62
  SPRINT_STATUS=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null || echo "init")
@@ -66,19 +69,30 @@ if [ "$HOOK_EVENT" = "Stop" ]; then
66
69
  fi
67
70
 
68
71
  # 자동 연쇄 조건:
69
- # conductor 가 running
70
- # AND task_stop 비활성
72
+ # conductor/company loop 가 running
71
73
  # AND task_stop 비활성
72
74
  # AND (next_agent 가 명시되어 있거나, agent_status=completed 라서 다음 tick 결정 필요)
75
+ # v7 CEO 루프는 conductor-tick 없이 CEO/CXX 상태로 시작될 수 있다.
76
+ # current_agent=ceo running + owner_prompt.routing 은 기존 설치본이 conductor/company
77
+ # state를 아직 쓰지 않은 stuck 상태에서도 turn 종료 차단 대상으로 본다.
73
78
  should_chain="false"
74
- if [ "$CONDUCTOR_STATE" = "running" ] \
79
+ if { [ "$CONDUCTOR_STATE" = "running" ] || [ "$COMPANY_STATE" = "running" ]; } \
75
80
  && [ "$TASK_STOP_ACTIVE" != "true" ]; then
76
81
  if [ "$NEXT_AGENT" != "none" ] && [ "$NEXT_AGENT" != "null" ]; then
77
82
  should_chain="true"
78
83
  elif [ "$AGENT_STATUS" = "completed" ]; then
79
84
  should_chain="true"
85
+ elif [ "$CURRENT_AGENT" = "ceo" ] && [ "$AGENT_STATUS" = "running" ] && [ "$OWNER_PROMPT_STATUS" = "routing" ]; then
86
+ should_chain="true"
80
87
  fi
81
88
  fi
89
+ if [ "$should_chain" != "true" ] \
90
+ && [ "$TASK_STOP_ACTIVE" != "true" ] \
91
+ && [ "$CURRENT_AGENT" = "ceo" ] \
92
+ && [ "$AGENT_STATUS" = "running" ] \
93
+ && [ "$OWNER_PROMPT_STATUS" = "routing" ]; then
94
+ should_chain="true"
95
+ fi
82
96
 
83
97
  if [ "$should_chain" != "true" ]; then exit 0; fi
84
98
 
@@ -91,9 +105,9 @@ jq --argjson n "$NEW_COUNT" '.conductor.stop_chain_count = $n |
91
105
 
92
106
  # Claude 에게 다음 행동 지시
93
107
  if [ "$ESCALATION" != "null" ]; then
94
- REASON="자율 회사 루프 진행 중. escalation=${ESCALATION} 는 정지 사유가 아니라 meeting-manager/service-ops 공유 대상이다. next_agent=${NEXT_AGENT}, agent_status=${AGENT_STATUS}, conductor=${CONDUCTOR_STATE}. SKILL 에 따라 conductor-tick 을 굴려 meeting-manager 회의로 라우팅하거나 다음 부서를 spawn 하라. progress.log 에는 절대 미래 시각을 쓰지 말 것. 진짜로 끝났을 때만 turn 을 종료하라."
108
+ REASON="자율 회사 루프 진행 중. escalation=${ESCALATION} 는 정지 사유가 아니라 meeting-manager/service-ops 공유 대상이다. current_agent=${CURRENT_AGENT}, next_agent=${NEXT_AGENT}, agent_status=${AGENT_STATUS}, conductor=${CONDUCTOR_STATE}, company=${COMPANY_STATE}. SKILL 에 따라 CEO/CXX/worker 흐름을 계속하거나 conductor-tick 을 굴려 meeting-manager 회의로 라우팅하라. progress.log 에는 절대 미래 시각을 쓰지 말 것. 진짜로 끝났을 때만 turn 을 종료하라."
95
109
  else
96
- REASON="자율 회사 루프 진행 중. next_agent=${NEXT_AGENT}, agent_status=${AGENT_STATUS}, conductor=${CONDUCTOR_STATE}. SKILL 에 따라 다음 부서를 spawn 하거나 conductor-tick 을 한 번 더 굴려라. progress.log 에는 절대 미래 시각을 쓰지 말 것. 진짜로 끝났을 때만 turn 을 종료하라."
110
+ REASON="자율 회사 루프 진행 중. current_agent=${CURRENT_AGENT}, next_agent=${NEXT_AGENT}, agent_status=${AGENT_STATUS}, conductor=${CONDUCTOR_STATE}, company=${COMPANY_STATE}. SKILL 에 따라 CEO/CXX/worker 흐름을 계속하고, 필요한 CXX/worker fresh session 을 실행하라. progress.log 에는 절대 미래 시각을 쓰지 말 것. 진짜로 끝났을 때만 turn 을 종료하라."
97
111
  fi
98
112
 
99
113
  jq -nc \
@@ -83,6 +83,10 @@ if [ -n "$PROMPT" ] && [ -d "$CWD/.harness" ]; then
83
83
  .current_agent = "ceo" |
84
84
  .agent_status = "running" |
85
85
  .next_agent = "ceo" |
86
+ .company_state.state = "running" |
87
+ .conductor.state = "running" |
88
+ .conductor.current_action = ("owner-" + $command + "-to-ceo") |
89
+ .conductor.stop_chain_count = 0 |
86
90
  .updated_at = $now |
87
91
  .owner_prompt = {
88
92
  command: $command,