@walwal-harness/cli 7.1.10 → 7.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -180,9 +180,10 @@ npx walwal-harness init --force --project-root /path/to/project
180
180
 
181
181
  Features:
182
182
 
183
- - **Org Tree** — live status of Owner → CEO → CXX → Workers hierarchy
183
+ - **Split Workspace** — full-viewport left/right panes keep Org Tree + Mission Timeline beside the selected mission detail; both panes scroll independently with hidden scrollbars, and the detail pane resets to top on selection changes
184
+ - **Org Tree** — live status of Owner → CEO → CXX → Workers hierarchy, with worker cards grouped under their owning CXX
184
185
  - **Mission Timeline** — clickable history of goal/submission/hot-fix missions showing the full dispatch chain
185
- - **Mission Flow tab** — per-mission flow: Owner prompt → CEO routing → CXX → worker files changed → CQO verdict
186
+ - **Mission Flow tab** — per-mission flow: Owner prompt → CEO routing → CXX → owner-specific worker evidence → CQO verdict
186
187
  - **History tab** — mission-specific Owner request (from CEO summary + closest progress.log match)
187
188
  - **Gotchas tab** — searchable `.harness/gotchas/*.md` knowledge base, click to read full markdown
188
189
  - **Document tab** — per-CXX markdown doc viewer
@@ -221,6 +222,10 @@ harness-hiring → register and onboard worker
221
222
 
222
223
  | Version | Summary |
223
224
  |---|---|
225
+ | 7.1.13 | Dashboard: full-viewport split workspace, hidden pane scrollbars, and detail scroll reset on selection |
226
+ | 7.1.12 | Dashboard: split workspace panes now scroll independently |
227
+ | 7.1.11 | Dashboard: persistent 50/50 split workspace and CXX-owned worker grouping in the org tree |
228
+ | 7.1.10 | Adds `/submission` and nested goal/submission/hot-fix mission history |
224
229
  | 7.1.9 | Dashboard is packaged with npm and auto-syncs isolated cache by package version; README documents dashboard port/reinstall and update/migrate commands |
225
230
  | 7.1.8 | HR-Resource: replace China-platform-specific workers with global equivalents; refine cross-border/video/livestream skills |
226
231
  | 7.1.7 | Implementation Notes mandatory in all CXX docs and worker reports; harness-worker-evidence-validate.sh |
@@ -15,3 +15,12 @@ body {
15
15
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
16
16
  sans-serif;
17
17
  }
18
+
19
+ .scrollbar-hidden {
20
+ scrollbar-width: none;
21
+ -ms-overflow-style: none;
22
+ }
23
+
24
+ .scrollbar-hidden::-webkit-scrollbar {
25
+ display: none;
26
+ }
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { useEffect } from "react";
2
+ import { useEffect, useRef } from "react";
3
3
 
4
4
  export type DrawerTab =
5
5
  | "mission-flow" // Mission document hierarchy (PRIMARY)
@@ -15,6 +15,8 @@ interface DrawerProps {
15
15
  onClose: () => void;
16
16
  onTabChange: (tab: DrawerTab) => void;
17
17
  children: React.ReactNode;
18
+ mode?: "overlay" | "inline";
19
+ scrollResetKey?: string;
18
20
  }
19
21
 
20
22
  const TABS: Array<{ id: DrawerTab; label: string }> = [
@@ -25,15 +27,60 @@ const TABS: Array<{ id: DrawerTab; label: string }> = [
25
27
  { id: "logs", label: "Agent Log" },
26
28
  ];
27
29
 
28
- export function Drawer({ open, tab, title, onClose, onTabChange, children }: DrawerProps) {
30
+ export function Drawer({ open, tab, title, onClose, onTabChange, children, mode = "overlay", scrollResetKey }: DrawerProps) {
31
+ const contentRef = useRef<HTMLDivElement | null>(null);
32
+
29
33
  useEffect(() => {
30
- if (!open) return;
34
+ if (!open || mode === "inline") return;
31
35
  const onKey = (e: KeyboardEvent) => {
32
36
  if (e.key === "Escape") onClose();
33
37
  };
34
38
  window.addEventListener("keydown", onKey);
35
39
  return () => window.removeEventListener("keydown", onKey);
36
- }, [open, onClose]);
40
+ }, [open, onClose, mode]);
41
+
42
+ useEffect(() => {
43
+ contentRef.current?.scrollTo({ top: 0, left: 0 });
44
+ }, [scrollResetKey]);
45
+
46
+ if (mode === "inline") {
47
+ return (
48
+ <aside
49
+ role="complementary"
50
+ aria-label="Detail panel"
51
+ data-testid="drawer"
52
+ data-open="true"
53
+ className="h-full min-h-0 rounded-md border border-brick-wall bg-brick-bg shadow-2xl flex flex-col overflow-hidden"
54
+ >
55
+ <header className="flex items-center justify-between border-b border-brick-wall px-4 py-3">
56
+ <h2 className="text-sm font-mono uppercase tracking-widest text-gray-200 truncate">
57
+ {title}
58
+ </h2>
59
+ </header>
60
+ <nav className="flex flex-wrap border-b border-brick-wall text-[11px] font-mono">
61
+ {TABS.map((t) => (
62
+ <button
63
+ key={t.id}
64
+ type="button"
65
+ data-testid={`drawer-tab-${t.id}`}
66
+ data-active={t.id === tab}
67
+ onClick={() => onTabChange(t.id)}
68
+ className={`min-w-[33%] flex-1 px-2 py-2 transition-colors ${
69
+ t.id === tab
70
+ ? "bg-brick-wall/50 text-gray-100 border-b-2 border-aura-typing"
71
+ : "text-gray-500 hover:text-gray-200"
72
+ }`}
73
+ >
74
+ {t.label}
75
+ </button>
76
+ ))}
77
+ </nav>
78
+ <div ref={contentRef} className="scrollbar-hidden flex-1 overflow-y-auto px-4 py-3 text-xs text-gray-200 font-mono">
79
+ {children}
80
+ </div>
81
+ </aside>
82
+ );
83
+ }
37
84
 
38
85
  return (
39
86
  <>
@@ -88,7 +135,7 @@ export function Drawer({ open, tab, title, onClose, onTabChange, children }: Dra
88
135
  </button>
89
136
  ))}
90
137
  </nav>
91
- <div className="flex-1 overflow-y-auto px-4 py-3 text-xs text-gray-200 font-mono">
138
+ <div ref={contentRef} className="scrollbar-hidden flex-1 overflow-y-auto px-4 py-3 text-xs text-gray-200 font-mono">
92
139
  {children}
93
140
  </div>
94
141
  </aside>
@@ -225,7 +225,8 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
225
225
  },
226
226
  ];
227
227
 
228
- // Workers from current mission's workers/, shown under CTO column
228
+ // Workers are grouped by their owning CXX so evaluator reports sit under CQO,
229
+ // implementation reports under CTO, and legacy flat reports stay visible.
229
230
  const workers = currentMission?.workers ?? [];
230
231
 
231
232
  return (
@@ -271,7 +272,9 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
271
272
  {/* Row 3: CXX nodes with workers below */}
272
273
  <div className="flex gap-3 justify-center">
273
274
  {cxxNodes.map((node) => {
274
- const nodeWorkers = node.role === "cto" ? workers : [];
275
+ const nodeWorkers = workers.filter(
276
+ (w) => w.owner === node.role || (w.owner === "unknown" && node.role === "cto")
277
+ );
275
278
 
276
279
  return (
277
280
  <div key={node.id} className="flex flex-col items-center gap-2">
@@ -293,7 +296,7 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
293
296
  onClick={() =>
294
297
  onNodeClick({
295
298
  id: `worker-${w.name}`,
296
- role: "cto",
299
+ role: node.role,
297
300
  label: w.name,
298
301
  sublabel: w.status,
299
302
  status: w.status === "COMPLETE" ? "idle" : "typing",
@@ -27,7 +27,6 @@ interface SceneProps {
27
27
 
28
28
  export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
29
29
  const { snapshot, connectionState } = useHarnessStream(initial);
30
- const [drawerOpen, setDrawerOpen] = useState(false);
31
30
  const [drawerTab, setDrawerTab] = useState<DrawerTab>("mission-flow");
32
31
  const [selectedAgent, setSelectedAgent] = useState<AgentState | null>(null);
33
32
  const [selectedNode, setSelectedNode] = useState<OrgNodeDef | null>(null);
@@ -38,7 +37,6 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
38
37
  setSelectedAgent(a);
39
38
  setSelectedNode(null);
40
39
  setDrawerTab("logs");
41
- setDrawerOpen(true);
42
40
  };
43
41
 
44
42
  const handleNodeClick = (node: OrgNodeDef) => {
@@ -55,7 +53,6 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
55
53
  } else {
56
54
  setDrawerTab("mission-flow");
57
55
  }
58
- setDrawerOpen(true);
59
56
  };
60
57
 
61
58
  const handleMissionClick = (mission: MissionDoc) => {
@@ -63,7 +60,6 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
63
60
  setSelectedNode(null);
64
61
  setSelectedAgent(null);
65
62
  setDrawerTab("mission-flow");
66
- setDrawerOpen(true);
67
63
  };
68
64
 
69
65
  const drawerTitle =
@@ -74,60 +70,126 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
74
70
  : selectedNode
75
71
  ? selectedNode.label
76
72
  : "Mission Flow";
73
+ const drawerScrollResetKey = [
74
+ drawerTab,
75
+ selectedMission?.missionId ?? "",
76
+ selectedNode?.id ?? "",
77
+ selectedAgent?.id ?? "",
78
+ ].join(":");
77
79
 
78
80
  // Suppress unused lang warning — kept for future i18n
79
81
  void lang;
80
82
 
81
83
  return (
82
- <div className="mx-auto max-w-[1920px] px-4 py-5">
84
+ <div className="flex h-[100dvh] w-full flex-col overflow-hidden px-4 py-5">
83
85
  {snapshot.escalations.length > 0 && (
84
86
  <EscalationStrip escalations={snapshot.escalations} />
85
87
  )}
86
- <div className="min-w-0">
87
- <section className="min-w-0">
88
- <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
89
- <div>
90
- <p className="text-[11px] font-mono uppercase tracking-[0.24em] text-cyan-300/80">
91
- Company Structure
92
- </p>
93
- <h1 className="mt-1 text-2xl font-semibold text-gray-100">
94
- {snapshot.projectName || "walwal-harness"} · Organization
95
- </h1>
88
+ <div className="grid min-h-0 flex-1 min-w-0 gap-4 lg:grid-cols-2">
89
+ <div className="scrollbar-hidden min-h-0 min-w-0 overflow-y-auto pr-1">
90
+ <section className="min-w-0">
91
+ <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
92
+ <div>
93
+ <p className="text-[11px] font-mono uppercase tracking-[0.24em] text-cyan-300/80">
94
+ Company Structure
95
+ </p>
96
+ <h1 className="mt-1 text-2xl font-semibold text-gray-100">
97
+ {snapshot.projectName || "walwal-harness"} · Organization
98
+ </h1>
99
+ </div>
100
+ <ActivityIndicator snapshot={snapshot} connectionState={connectionState} />
96
101
  </div>
97
- <ActivityIndicator snapshot={snapshot} connectionState={connectionState} />
98
- </div>
99
- <div
100
- data-testid="brick-office-stage"
101
- className="w-full overflow-hidden rounded-md border border-gray-700/80 bg-[#12151b] shadow-2xl"
102
- >
103
- <div className="pointer-events-none flex items-center justify-between border-b border-white/10 bg-black/30 px-4 py-2 font-mono text-[10px] text-gray-300 backdrop-blur">
104
- <span>Owner → CEO → CXX → Workers</span>
105
- <span>
106
- {snapshot.missions?.[0]?.missionId ?? "no active mission"}
107
- {snapshot.missions?.[0]?.cxxPresent.length
108
- ? ` · ${snapshot.missions[0].cxxPresent.join(" / ")}`
109
- : ""}
110
- </span>
102
+ <div
103
+ data-testid="brick-office-stage"
104
+ className="w-full overflow-x-auto rounded-md border border-gray-700/80 bg-[#12151b] shadow-2xl"
105
+ >
106
+ <div className="pointer-events-none flex items-center justify-between gap-3 border-b border-white/10 bg-black/30 px-4 py-2 font-mono text-[10px] text-gray-300 backdrop-blur">
107
+ <span>Owner → CEO → CXX → Workers</span>
108
+ <span className="truncate">
109
+ {snapshot.missions?.[0]?.missionId ?? "no active mission"}
110
+ {snapshot.missions?.[0]?.cxxPresent.length
111
+ ? ` · ${snapshot.missions[0].cxxPresent.join(" / ")}`
112
+ : ""}
113
+ </span>
114
+ </div>
115
+ <OrgTree
116
+ snapshot={snapshot}
117
+ activeNodeId={selectedNode?.id ?? null}
118
+ onNodeClick={handleNodeClick}
119
+ />
120
+ </div>
121
+ </section>
122
+
123
+ <section className="mt-4">
124
+ <div className="mb-2">
125
+ <p className="font-mono text-[10px] uppercase tracking-[0.2em] text-cyan-300/60">Mission History</p>
126
+ <p className="text-[11px] text-gray-500 mt-0.5">goal · submission · hot-fix 명령별 파생 작업 흐름</p>
111
127
  </div>
112
- <OrgTree
113
- snapshot={snapshot}
114
- activeNodeId={selectedNode?.id ?? null}
115
- onNodeClick={handleNodeClick}
128
+ <MissionTimeline
129
+ missions={snapshot.missions ?? []}
130
+ activeMissionId={selectedMission?.missionId ?? null}
131
+ onMissionClick={handleMissionClick}
116
132
  />
117
- </div>
118
- </section>
133
+ </section>
134
+ </div>
119
135
 
120
- <section className="mt-4">
121
- <div className="mb-2">
122
- <p className="font-mono text-[10px] uppercase tracking-[0.2em] text-cyan-300/60">Mission History</p>
123
- <p className="text-[11px] text-gray-500 mt-0.5">goal · submission · hot-fix 명령별 파생 작업 흐름</p>
124
- </div>
125
- <MissionTimeline
126
- missions={snapshot.missions ?? []}
127
- activeMissionId={selectedMission?.missionId ?? null}
128
- onMissionClick={handleMissionClick}
129
- />
130
- </section>
136
+ <div className="min-h-0 min-w-0">
137
+ <Drawer
138
+ mode="inline"
139
+ open={true}
140
+ tab={drawerTab}
141
+ title={drawerTitle}
142
+ onClose={() => undefined}
143
+ onTabChange={setDrawerTab}
144
+ scrollResetKey={drawerScrollResetKey}
145
+ >
146
+ {drawerTab === "mission-flow" && (
147
+ <MissionFlowTab
148
+ mission={selectedMission ?? snapshot.missions?.[0] ?? null}
149
+ ownerHistory={snapshot.ownerHistory ?? []}
150
+ />
151
+ )}
152
+ {drawerTab === "history" && (
153
+ <OwnerHistoryTab
154
+ ownerHistory={snapshot.ownerHistory ?? []}
155
+ mission={selectedMission ?? snapshot.missions?.[0] ?? null}
156
+ />
157
+ )}
158
+ {drawerTab === "gotchas" && (
159
+ <GotchasTab gotchas={snapshot.gotchas ?? []} />
160
+ )}
161
+ {drawerTab === "mission-doc" && selectedNode && (
162
+ <MissionDocTab
163
+ missions={snapshot.missions ?? []}
164
+ role={
165
+ selectedNode.role === "owner"
166
+ ? "ceo"
167
+ : (selectedNode.role as "ceo" | "cto" | "cqo" | "coo" | "cdo" | "ops")
168
+ }
169
+ fromLabel={selectedNode.role === "ceo" ? "Owner" : "CEO"}
170
+ toLabel={
171
+ selectedNode.role === "cto"
172
+ ? "Workers"
173
+ : selectedNode.role === "cqo"
174
+ ? "Owner Report"
175
+ : "—"
176
+ }
177
+ />
178
+ )}
179
+ {drawerTab === "logs" && selectedNode && (
180
+ <AgentLogTab agentId={selectedNode.agentIds[0] ?? selectedNode.id} />
181
+ )}
182
+ {drawerTab === "logs" && selectedAgent && !selectedNode && (
183
+ <AgentLogTab agentId={selectedAgent.id} />
184
+ )}
185
+ {drawerTab === "logs" && !selectedNode && !selectedAgent && (
186
+ <div className="text-gray-500 text-xs">Select a node to view logs.</div>
187
+ )}
188
+ {drawerTab === "mission-flow" && !selectedMission && !snapshot.missions?.length && (
189
+ <div className="text-gray-500 text-xs">No missions found in .harness/documents/</div>
190
+ )}
191
+ </Drawer>
192
+ </div>
131
193
  </div>
132
194
 
133
195
  {/* Hidden DOM index for E2E + a11y */}
@@ -153,60 +215,6 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
153
215
  </li>
154
216
  ))}
155
217
  </ul>
156
-
157
- <Drawer
158
- open={drawerOpen}
159
- tab={drawerTab}
160
- title={drawerTitle}
161
- onClose={() => setDrawerOpen(false)}
162
- onTabChange={setDrawerTab}
163
- >
164
- {drawerTab === "mission-flow" && (
165
- <MissionFlowTab
166
- mission={selectedMission ?? snapshot.missions?.[0] ?? null}
167
- ownerHistory={snapshot.ownerHistory ?? []}
168
- />
169
- )}
170
- {drawerTab === "history" && (
171
- <OwnerHistoryTab
172
- ownerHistory={snapshot.ownerHistory ?? []}
173
- mission={selectedMission ?? snapshot.missions?.[0] ?? null}
174
- />
175
- )}
176
- {drawerTab === "gotchas" && (
177
- <GotchasTab gotchas={snapshot.gotchas ?? []} />
178
- )}
179
- {drawerTab === "mission-doc" && selectedNode && (
180
- <MissionDocTab
181
- missions={snapshot.missions ?? []}
182
- role={
183
- selectedNode.role === "owner"
184
- ? "ceo"
185
- : (selectedNode.role as "ceo" | "cto" | "cqo" | "coo" | "cdo" | "ops")
186
- }
187
- fromLabel={selectedNode.role === "ceo" ? "Owner" : "CEO"}
188
- toLabel={
189
- selectedNode.role === "cto"
190
- ? "Workers"
191
- : selectedNode.role === "cqo"
192
- ? "Owner Report"
193
- : "—"
194
- }
195
- />
196
- )}
197
- {drawerTab === "logs" && selectedNode && (
198
- <AgentLogTab agentId={selectedNode.agentIds[0] ?? selectedNode.id} />
199
- )}
200
- {drawerTab === "logs" && selectedAgent && !selectedNode && (
201
- <AgentLogTab agentId={selectedAgent.id} />
202
- )}
203
- {drawerTab === "logs" && !selectedNode && !selectedAgent && (
204
- <div className="text-gray-500 text-xs">Select a node to view logs.</div>
205
- )}
206
- {drawerTab === "mission-flow" && !selectedMission && !snapshot.missions?.length && (
207
- <div className="text-gray-500 text-xs">No missions found in .harness/documents/</div>
208
- )}
209
- </Drawer>
210
218
  </div>
211
219
  );
212
220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.10",
3
+ "version": "7.1.13",
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"