@walwal-harness/cli 7.1.26 → 7.1.27

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
@@ -52,6 +52,9 @@ What `init` installs:
52
52
  | `.claude/commands/hot-fix.md` | `/hot-fix` Owner command |
53
53
  | `.claude/skills/harness-{ceo,coo,cdo,cto,cqo,ops}/` | CXX agent skills |
54
54
  | `.harness/shared/HR-Resource/` | Hireable worker skill pool |
55
+ | `.harness/events.jsonl` | Structured append-only runtime event stream |
56
+ | `.harness/todos/state.json` | Machine-readable CXX todo queues |
57
+ | `.harness/todos/events.jsonl` | Structured todo transition history |
55
58
  | `AGENTS.md` ← `CLAUDE.md` symlink | Project harness config |
56
59
 
57
60
  ---
@@ -106,6 +109,7 @@ Owner /submission → CEO → [relevant CXX] → [workers] → [convention updat
106
109
  | 8 | No verdict without worker evidence — CQO self-inspection is not valid |
107
110
  | 9 | Hierarchical worker ownership — worker reports live under `.harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/` |
108
111
  | 10 | Implementation Notes required — `ceo.md`, every `{cxx}.md`, and every worker report must end with one English `## Implementation Notes` section in the same file |
112
+ | 11 | Structured runtime state — queues, heartbeats, preemption/resume state, and harness-parsed evidence belong in JSON/JSONL, not free-form Markdown |
109
113
 
110
114
  ### Implementation Notes Format
111
115
 
@@ -133,7 +137,7 @@ Use `None` when a subsection has no entries. This section is mandatory even for
133
137
 
134
138
  ## Harness Dashboard
135
139
 
136
- The harness ships with a real-time dashboard that reads `.harness/documents/` directly.
140
+ The harness ships with a real-time dashboard that reads `.harness/` runtime state directly. The current dashboard is process-oriented: it highlights CXX todo readiness, active workers, incidents, mission flow, and structured runtime events alongside the organization view.
137
141
 
138
142
  ```bash
139
143
  bash scripts/harness-dashboard-up.sh
@@ -199,6 +203,8 @@ npx walwal-harness init --force --project-root /path/to/project
199
203
 
200
204
  Features:
201
205
 
206
+ - **Company Harness Observatory** — glass/aurora process monitor showing active CXX todos, workers, incidents, missions, and current loop state
207
+ - **Structured Runtime State** — `/goal`, `/submission`, and `/hot-fix` append to `.harness/events.jsonl` and update `.harness/todos/state.json`; `migrate` backfills these files for existing projects
202
208
  - **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
203
209
  - **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
210
  - **Mission Timeline** — clickable history of goal/submission/hot-fix missions showing the full dispatch chain
@@ -5,9 +5,10 @@
5
5
  html,
6
6
  body {
7
7
  background:
8
- radial-gradient(circle at 12% 0%, rgba(34, 211, 238, 0.08), transparent 26rem),
9
- radial-gradient(circle at 88% 6%, rgba(251, 191, 36, 0.06), transparent 24rem),
10
- #101216;
8
+ radial-gradient(circle at 16% -8%, rgba(125, 211, 252, 0.24), transparent 30rem),
9
+ radial-gradient(circle at 78% 2%, rgba(216, 180, 254, 0.2), transparent 28rem),
10
+ radial-gradient(circle at 52% 104%, rgba(52, 211, 153, 0.14), transparent 34rem),
11
+ linear-gradient(135deg, #080b13 0%, #101728 48%, #070910 100%);
11
12
  color: #e5e7eb;
12
13
  }
13
14
 
@@ -24,3 +25,34 @@ body {
24
25
  .scrollbar-hidden::-webkit-scrollbar {
25
26
  display: none;
26
27
  }
28
+
29
+ .glass-panel {
30
+ border: 1px solid rgba(255, 255, 255, 0.14);
31
+ background:
32
+ linear-gradient(135deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.045)),
33
+ rgba(7, 10, 18, 0.58);
34
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.16);
35
+ backdrop-filter: blur(22px) saturate(150%);
36
+ }
37
+
38
+ .brutal-tile {
39
+ border: 1px solid rgba(255, 255, 255, 0.18);
40
+ background: rgba(3, 7, 18, 0.64);
41
+ box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.34);
42
+ }
43
+
44
+ .aurora-line {
45
+ background: linear-gradient(90deg, #67e8f9, #c084fc, #f0abfc, #34d399);
46
+ }
47
+
48
+ .isometric-stage {
49
+ background:
50
+ linear-gradient(30deg, rgba(255, 255, 255, 0.035) 12%, transparent 12.5%, transparent 87%, rgba(255, 255, 255, 0.035) 87.5%, rgba(255, 255, 255, 0.035)),
51
+ linear-gradient(150deg, rgba(255, 255, 255, 0.035) 12%, transparent 12.5%, transparent 87%, rgba(255, 255, 255, 0.035) 87.5%, rgba(255, 255, 255, 0.035)),
52
+ linear-gradient(30deg, rgba(255, 255, 255, 0.035) 12%, transparent 12.5%, transparent 87%, rgba(255, 255, 255, 0.035) 87.5%, rgba(255, 255, 255, 0.035)),
53
+ linear-gradient(150deg, rgba(255, 255, 255, 0.035) 12%, transparent 12.5%, transparent 87%, rgba(255, 255, 255, 0.035) 87.5%, rgba(255, 255, 255, 0.035)),
54
+ linear-gradient(60deg, rgba(103, 232, 249, 0.08) 25%, transparent 25.5%, transparent 75%, rgba(103, 232, 249, 0.08) 75%, rgba(103, 232, 249, 0.08)),
55
+ linear-gradient(60deg, rgba(103, 232, 249, 0.08) 25%, transparent 25.5%, transparent 75%, rgba(103, 232, 249, 0.08) 75%, rgba(103, 232, 249, 0.08));
56
+ background-position: 0 0, 0 0, 20px 35px, 20px 35px, 0 0, 20px 35px;
57
+ background-size: 40px 70px;
58
+ }
@@ -52,14 +52,14 @@ export function Drawer({ open, tab, title, onClose, onTabChange, children, mode
52
52
  aria-label="Detail panel"
53
53
  data-testid="drawer"
54
54
  data-open="true"
55
- className="h-full min-h-0 rounded-md border border-brick-wall bg-brick-bg shadow-2xl flex flex-col overflow-hidden"
55
+ className="glass-panel h-full min-h-0 rounded-lg flex flex-col overflow-hidden"
56
56
  >
57
- <header className="flex items-center justify-between border-b border-brick-wall px-4 py-3">
58
- <h2 className="text-sm font-mono uppercase tracking-widest text-gray-200 truncate">
57
+ <header className="flex items-center justify-between border-b border-white/10 px-4 py-3">
58
+ <h2 className="text-sm font-semibold tracking-tight text-gray-100 truncate">
59
59
  {title}
60
60
  </h2>
61
61
  </header>
62
- <nav className="flex flex-wrap border-b border-brick-wall text-[11px] font-mono">
62
+ <nav className="flex flex-wrap border-b border-white/10 px-2 py-2 text-[11px] font-mono">
63
63
  {TABS.map((t) => (
64
64
  <button
65
65
  key={t.id}
@@ -67,10 +67,10 @@ export function Drawer({ open, tab, title, onClose, onTabChange, children, mode
67
67
  data-testid={`drawer-tab-${t.id}`}
68
68
  data-active={t.id === tab}
69
69
  onClick={() => onTabChange(t.id)}
70
- className={`min-w-[33%] flex-1 px-2 py-2 transition-colors ${
70
+ className={`min-w-[33%] flex-1 rounded-md px-2 py-2 transition-colors ${
71
71
  t.id === tab
72
- ? "bg-brick-wall/50 text-gray-100 border-b-2 border-aura-typing"
73
- : "text-gray-500 hover:text-gray-200"
72
+ ? "bg-white/12 text-gray-100 shadow-inner"
73
+ : "text-gray-500 hover:bg-white/6 hover:text-gray-200"
74
74
  }`}
75
75
  >
76
76
  {t.label}
@@ -140,7 +140,7 @@ export function MissionTimeline({
140
140
  }: Props) {
141
141
  if (!missions.length) {
142
142
  return (
143
- <div className="rounded border border-dashed border-gray-700 px-3 py-4 text-center text-[11px] text-gray-500">
143
+ <div className="glass-panel rounded-lg px-3 py-4 text-center text-[11px] text-gray-500">
144
144
  No missions found in .harness/documents/
145
145
  </div>
146
146
  );
@@ -178,10 +178,10 @@ export function MissionTimeline({
178
178
  key={mission.missionId}
179
179
  type="button"
180
180
  onClick={() => onMissionClick(mission)}
181
- className={`w-full rounded border px-3 py-2.5 text-left transition-colors ${
181
+ className={`w-full rounded-lg px-3 py-2.5 text-left transition-colors ${
182
182
  isActive
183
- ? "border-cyan-400/40 bg-cyan-400/5"
184
- : "border-gray-700/40 bg-black/10 hover:border-gray-600/50"
183
+ ? "glass-panel"
184
+ : "border border-white/10 bg-white/[0.035] hover:border-white/20"
185
185
  }`}
186
186
  >
187
187
  <div className="flex items-start justify-between gap-2 mb-1.5">
@@ -50,7 +50,7 @@ function OrgCard({
50
50
  <button
51
51
  type="button"
52
52
  onClick={() => onClick(node)}
53
- className={`relative rounded-lg border p-3 text-left transition-all cursor-pointer ${roleColor} ${widths[size]} ${active ? "ring-2 ring-cyan-300 border-cyan-300/60" : "hover:border-white/30"}`}
53
+ className={`brutal-tile relative rounded-md p-3 text-left transition-all cursor-pointer ${roleColor} ${widths[size]} ${active ? "ring-2 ring-cyan-300 border-cyan-300/80" : "hover:border-white/35"}`}
54
54
  >
55
55
  <StatusDot status={node.status} />
56
56
  <div className="font-mono text-[9px] uppercase tracking-[0.2em] text-gray-500 mb-0.5">{node.role}</div>
@@ -263,7 +263,7 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
263
263
 
264
264
  return (
265
265
  <div className="select-none">
266
- <div className="flex items-center justify-between border-b border-white/10 bg-black/20 px-3 py-2">
266
+ <div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2 backdrop-blur">
267
267
  <div className="min-w-0">
268
268
  {currentMission && (
269
269
  <div className="truncate font-mono text-[10px] text-cyan-300/70">
@@ -275,7 +275,7 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
275
275
  <button
276
276
  type="button"
277
277
  onClick={() => setBoundedZoom(zoom - 0.08)}
278
- 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"
278
+ className="h-7 w-7 rounded-md border border-white/15 bg-black/35 font-mono text-xs text-gray-200 hover:border-cyan-300/60"
279
279
  title="Zoom out"
280
280
  >
281
281
  -
@@ -283,7 +283,7 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
283
283
  <button
284
284
  type="button"
285
285
  onClick={() => setBoundedZoom(defaultZoom)}
286
- 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"
286
+ className="h-7 min-w-12 rounded-md border border-white/15 bg-black/35 px-2 font-mono text-[10px] text-gray-300 hover:border-cyan-300/60"
287
287
  title="Reset zoom"
288
288
  >
289
289
  {Math.round(zoom * 100)}%
@@ -291,7 +291,7 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
291
291
  <button
292
292
  type="button"
293
293
  onClick={() => setBoundedZoom(zoom + 0.08)}
294
- 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"
294
+ className="h-7 w-7 rounded-md border border-white/15 bg-black/35 font-mono text-xs text-gray-200 hover:border-cyan-300/60"
295
295
  title="Zoom in"
296
296
  >
297
297
  +
@@ -306,7 +306,7 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
306
306
  onPointerMove={handlePointerMove}
307
307
  onPointerUp={endDrag}
308
308
  onPointerCancel={endDrag}
309
- className="scrollbar-hidden h-[390px] cursor-grab overflow-auto active:cursor-grabbing"
309
+ className="scrollbar-hidden isometric-stage h-[390px] cursor-grab overflow-auto active:cursor-grabbing"
310
310
  >
311
311
  <div
312
312
  className="relative"
@@ -417,7 +417,7 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
417
417
  agentIds: [],
418
418
  })
419
419
  }
420
- className={`relative rounded border px-2 py-1.5 text-left min-w-[130px] max-w-[160px] transition-all hover:border-white/20 ${
420
+ className={`brutal-tile relative rounded-md px-2 py-1.5 text-left min-w-[130px] max-w-[160px] transition-all hover:border-white/25 ${
421
421
  workerActive
422
422
  ? "border-cyan-300/70 bg-cyan-400/10 shadow-[0_0_18px_rgba(34,211,238,0.18)]"
423
423
  : "border-gray-700/40 bg-gray-800/30"
@@ -96,7 +96,11 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
96
96
  {snapshot.escalations.length > 0 && (
97
97
  <EscalationStrip escalations={snapshot.escalations} />
98
98
  )}
99
- <div className="grid min-h-0 flex-1 min-w-0 gap-4 lg:grid-cols-2">
99
+ <div className="mb-4 grid gap-3 xl:grid-cols-[1.25fr_1fr]">
100
+ <CommandDeck snapshot={snapshot} connectionState={connectionState} />
101
+ <ProcessPulse snapshot={snapshot} />
102
+ </div>
103
+ <div className="grid min-h-0 flex-1 min-w-0 gap-4 lg:grid-cols-[1.12fr_0.88fr]">
100
104
  <div className="scrollbar-hidden min-h-0 min-w-0 overflow-y-auto pr-1">
101
105
  <section className="min-w-0">
102
106
  <div className="mb-3 flex flex-wrap items-end justify-between gap-3">
@@ -112,9 +116,9 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
112
116
  </div>
113
117
  <div
114
118
  data-testid="brick-office-stage"
115
- className="w-full overflow-x-auto rounded-md border border-gray-700/80 bg-[#12151b] shadow-2xl"
119
+ className="glass-panel w-full overflow-x-auto rounded-lg"
116
120
  >
117
- <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">
121
+ <div className="pointer-events-none flex items-center justify-between gap-3 border-b border-white/10 bg-white/5 px-4 py-2 font-mono text-[10px] text-gray-300 backdrop-blur">
118
122
  <span>Owner → CEO → CXX → Workers</span>
119
123
  <span className="truncate">
120
124
  {snapshot.missions?.[0]?.missionId ?? "no active mission"}
@@ -239,6 +243,133 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
239
243
  );
240
244
  }
241
245
 
246
+ function CommandDeck({
247
+ snapshot,
248
+ connectionState,
249
+ }: {
250
+ snapshot: HarnessSnapshot;
251
+ connectionState: ConnectionState;
252
+ }) {
253
+ const activeMission = snapshot.missions?.[0];
254
+ const cxxCount = activeMission?.cxxPresent.length ?? 0;
255
+ const activeWorkers = activeMission?.workers.filter((w) => w.active).length ?? 0;
256
+ const activeTodos = snapshot.todos.filter((t) => t.status === "active").length;
257
+ const blockedSignals =
258
+ snapshot.escalations.length +
259
+ snapshot.incidents.length +
260
+ snapshot.dashboard.features.filter((f) => f.status === "blocked" || f.status === "failed").length;
261
+ const stateLabel = blockedSignals > 0 ? "Needs Attention" : activeWorkers > 0 ? "In Motion" : "Observing";
262
+
263
+ return (
264
+ <section className="glass-panel relative overflow-hidden rounded-lg px-5 py-4">
265
+ <div className="aurora-line absolute inset-x-0 top-0 h-1" />
266
+ <div className="relative flex flex-wrap items-start justify-between gap-4">
267
+ <div className="min-w-0">
268
+ <p className="font-mono text-[10px] uppercase tracking-[0.28em] text-cyan-200/80">
269
+ Company Harness Observatory
270
+ </p>
271
+ <h1 className="mt-1 text-2xl font-semibold tracking-tight text-white sm:text-3xl">
272
+ {snapshot.projectName || "walwal-harness"}
273
+ </h1>
274
+ <p className="mt-2 max-w-2xl text-sm leading-6 text-slate-300">
275
+ {activeMission?.missionId ?? snapshot.goal?.title ?? "No active mission detected"}
276
+ </p>
277
+ </div>
278
+ <div className="brutal-tile min-w-[170px] rounded-md px-3 py-2">
279
+ <div className="font-mono text-[9px] uppercase tracking-[0.2em] text-slate-500">state</div>
280
+ <div className="mt-1 text-lg font-semibold text-white">{stateLabel}</div>
281
+ <div className="mt-1 font-mono text-[10px] text-slate-400">
282
+ {connectionState} · {snapshot.runtime.currentAgent ?? snapshot.runtime.nextAgent ?? "idle"}
283
+ </div>
284
+ </div>
285
+ </div>
286
+ <div className="relative mt-4 grid gap-2 sm:grid-cols-4">
287
+ <MetricTile label="CXX Present" value={String(cxxCount)} tone="cyan" />
288
+ <MetricTile label="Active Todos" value={String(activeTodos)} tone="green" />
289
+ <MetricTile label="Open Incidents" value={String(snapshot.incidents.length)} tone="rose" />
290
+ <MetricTile label="Missions" value={String(snapshot.missions.length)} tone="violet" />
291
+ </div>
292
+ </section>
293
+ );
294
+ }
295
+
296
+ function MetricTile({ label, value, tone }: { label: string; value: string; tone: "cyan" | "green" | "rose" | "violet" }) {
297
+ const toneClass = {
298
+ cyan: "text-cyan-200 border-cyan-300/35",
299
+ green: "text-emerald-200 border-emerald-300/35",
300
+ rose: "text-rose-200 border-rose-300/35",
301
+ violet: "text-violet-200 border-violet-300/35",
302
+ }[tone];
303
+ return (
304
+ <div className={`brutal-tile rounded-md px-3 py-2 ${toneClass}`}>
305
+ <div className="font-mono text-[9px] uppercase tracking-[0.18em] text-slate-500">{label}</div>
306
+ <div className="mt-1 text-2xl font-semibold">{value}</div>
307
+ </div>
308
+ );
309
+ }
310
+
311
+ function ProcessPulse({ snapshot }: { snapshot: HarnessSnapshot }) {
312
+ const roles = ["ceo", "coo", "cdo", "cto", "cqo", "ops"] as const;
313
+ const activeMission = snapshot.missions?.[0];
314
+ const activeRoles = new Set(activeMission?.cxxPresent ?? []);
315
+ const roleStatus = roles.map((role) => {
316
+ const todos = snapshot.todos.filter((t) => t.owner === role);
317
+ const activeTodos = todos.filter((t) => t.status === "active").length;
318
+ const blockedTodos = todos.filter((t) => t.status === "blocked").length;
319
+ const workerCount = activeMission?.workers.filter((w) => w.owner === role).length ?? 0;
320
+ const activeCount = activeMission?.workers.filter((w) => w.owner === role && w.active).length ?? 0;
321
+ return { role, workerCount, activeCount, activeTodos, blockedTodos, present: activeRoles.has(role) || todos.length > 0 };
322
+ });
323
+ return (
324
+ <section className="glass-panel rounded-lg px-4 py-4">
325
+ <div className="flex items-center justify-between gap-3">
326
+ <div>
327
+ <p className="font-mono text-[10px] uppercase tracking-[0.24em] text-fuchsia-200/75">
328
+ Process Monitor
329
+ </p>
330
+ <h2 className="mt-1 text-base font-semibold text-white">CXX queue readiness</h2>
331
+ </div>
332
+ <div className="rounded-full border border-white/15 bg-white/8 px-3 py-1 font-mono text-[10px] text-slate-300">
333
+ {snapshot.runtime.agentStatus}
334
+ </div>
335
+ </div>
336
+ <div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-3">
337
+ {roleStatus.map((item) => (
338
+ <div key={item.role} className="brutal-tile rounded-md px-3 py-2">
339
+ <div className="flex items-center justify-between gap-2">
340
+ <span className="font-mono text-[10px] uppercase tracking-[0.18em] text-slate-400">{item.role}</span>
341
+ <span
342
+ className={`h-2 w-2 rounded-full ${
343
+ item.blockedTodos > 0
344
+ ? "bg-rose-400"
345
+ : item.activeTodos > 0 || item.activeCount > 0
346
+ ? "bg-cyan-300 animate-pulse"
347
+ : item.present
348
+ ? "bg-emerald-300"
349
+ : "bg-slate-600"
350
+ }`}
351
+ />
352
+ </div>
353
+ <div className="mt-2 text-xs text-slate-300">
354
+ {item.blockedTodos > 0
355
+ ? `${item.blockedTodos} blocked`
356
+ : item.activeTodos > 0
357
+ ? `${item.activeTodos} todo`
358
+ : item.activeCount > 0
359
+ ? `${item.activeCount} active`
360
+ : item.workerCount > 0
361
+ ? `${item.workerCount} workers`
362
+ : item.present
363
+ ? "documented"
364
+ : "waiting"}
365
+ </div>
366
+ </div>
367
+ ))}
368
+ </div>
369
+ </section>
370
+ );
371
+ }
372
+
242
373
  function ActivityIndicator({
243
374
  snapshot,
244
375
  connectionState,
@@ -18,6 +18,7 @@ import type {
18
18
  FeatureSummary,
19
19
  GoalCard,
20
20
  HarnessSnapshot,
21
+ HarnessEvent,
21
22
  HypothesisEntry,
22
23
  IncidentEntry,
23
24
  MeetingCadence,
@@ -37,6 +38,7 @@ import type {
37
38
  TrackStatus,
38
39
  WorkerDocEntry,
39
40
  WorkerSnapshot,
41
+ CxxTodo,
40
42
  } from "./types";
41
43
 
42
44
  const SNAPSHOT_VERSION = "1.2.0";
@@ -224,6 +226,8 @@ function emptySnapshot(banner: ErrorBanner | null = null, rootDir?: string): Har
224
226
  missions: [],
225
227
  ownerHistory: [],
226
228
  gotchas: [],
229
+ todos: [],
230
+ events: [],
227
231
  };
228
232
  }
229
233
 
@@ -1315,6 +1319,89 @@ function readGotchas(rootDir: string): GotchaEntry[] {
1315
1319
  return results.sort((a, b) => a.id.localeCompare(b.id));
1316
1320
  }
1317
1321
 
1322
+ function normalizeTodoStatus(raw: unknown): CxxTodo["status"] {
1323
+ const value = String(raw ?? "pending").toLowerCase();
1324
+ if (["pending", "active", "paused", "blocked", "done"].includes(value)) return value;
1325
+ return value;
1326
+ }
1327
+
1328
+ function readTodos(rootDir: string): CxxTodo[] {
1329
+ const todoPath = path.join(rootDir, ".harness", "todos", "state.json");
1330
+ const result = readJsonSafe<{
1331
+ owners?: Record<string, Array<{
1332
+ id?: string;
1333
+ owner?: string;
1334
+ title?: string;
1335
+ status?: string;
1336
+ priority?: number;
1337
+ kind?: string;
1338
+ mission_path?: string | null;
1339
+ missionPath?: string | null;
1340
+ required_artifacts?: string[];
1341
+ requiredArtifacts?: string[];
1342
+ created_at?: string | null;
1343
+ createdAt?: string | null;
1344
+ updated_at?: string | null;
1345
+ updatedAt?: string | null;
1346
+ last_heartbeat_at?: string | null;
1347
+ lastHeartbeatAt?: string | null;
1348
+ blocked_reason?: string | null;
1349
+ blockedReason?: string | null;
1350
+ }>>;
1351
+ }>(todoPath);
1352
+ if (!result.ok) return [];
1353
+ const todos: CxxTodo[] = [];
1354
+ for (const [owner, list] of Object.entries(result.value.owners ?? {})) {
1355
+ for (const item of list ?? []) {
1356
+ const id = item.id ?? `${owner}-${todos.length + 1}`;
1357
+ todos.push({
1358
+ id,
1359
+ owner: item.owner ?? owner,
1360
+ title: item.title ?? id,
1361
+ status: normalizeTodoStatus(item.status),
1362
+ priority: item.priority ?? 0,
1363
+ kind: item.kind ?? "task",
1364
+ missionPath: item.mission_path ?? item.missionPath ?? null,
1365
+ requiredArtifacts: item.required_artifacts ?? item.requiredArtifacts ?? [],
1366
+ createdAt: item.created_at ?? item.createdAt ?? null,
1367
+ updatedAt: item.updated_at ?? item.updatedAt ?? null,
1368
+ lastHeartbeatAt: item.last_heartbeat_at ?? item.lastHeartbeatAt ?? null,
1369
+ blockedReason: item.blocked_reason ?? item.blockedReason ?? null,
1370
+ });
1371
+ }
1372
+ }
1373
+ return todos.sort((a, b) => (b.priority - a.priority) || ((b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")));
1374
+ }
1375
+
1376
+ function readJsonlTail<T extends Record<string, unknown>>(filePath: string, limit: number): T[] {
1377
+ if (!existsSync(filePath)) return [];
1378
+ let raw = "";
1379
+ try { raw = readFileSync(filePath, "utf8"); } catch { return []; }
1380
+ const rows: T[] = [];
1381
+ for (const line of raw.split("\n")) {
1382
+ const trimmed = line.trim();
1383
+ if (!trimmed) continue;
1384
+ try {
1385
+ rows.push(JSON.parse(trimmed) as T);
1386
+ } catch {
1387
+ // Ignore malformed legacy/manual lines in append-only logs.
1388
+ }
1389
+ }
1390
+ return rows.slice(-limit).reverse();
1391
+ }
1392
+
1393
+ function readEvents(rootDir: string, limit = 40): HarnessEvent[] {
1394
+ return readJsonlTail<Record<string, unknown>>(path.join(rootDir, ".harness", "events.jsonl"), limit).map((e) => ({
1395
+ ts: typeof e.ts === "string" ? e.ts : null,
1396
+ type: typeof e.type === "string" ? e.type : "event",
1397
+ owner: typeof e.owner === "string" ? e.owner : null,
1398
+ command: typeof e.command === "string" ? e.command : null,
1399
+ summary: typeof e.summary === "string" ? e.summary : null,
1400
+ title: typeof e.title === "string" ? e.title : null,
1401
+ status: typeof e.status === "string" ? e.status : null,
1402
+ }));
1403
+ }
1404
+
1318
1405
  export function readHarnessState(rootDir: string): HarnessSnapshot {
1319
1406
  const harnessDir = path.join(rootDir, ".harness");
1320
1407
  if (!existsSync(harnessDir)) {
@@ -1344,6 +1431,8 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
1344
1431
  snapshot.missions = readMissions(rootDir, null);
1345
1432
  snapshot.ownerHistory = readOwnerHistory(rootDir);
1346
1433
  snapshot.gotchas = readGotchas(rootDir);
1434
+ snapshot.todos = readTodos(rootDir);
1435
+ snapshot.events = readEvents(rootDir);
1347
1436
  return snapshot;
1348
1437
  }
1349
1438
 
@@ -1396,6 +1485,8 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
1396
1485
  missions: readMissions(rootDir, progress),
1397
1486
  ownerHistory: readOwnerHistory(rootDir),
1398
1487
  gotchas: readGotchas(rootDir),
1488
+ todos: readTodos(rootDir),
1489
+ events: readEvents(rootDir),
1399
1490
  };
1400
1491
  }
1401
1492
 
@@ -292,6 +292,33 @@ export interface HarnessSnapshot {
292
292
  missions: MissionDoc[];
293
293
  ownerHistory: OwnerPromptEntry[];
294
294
  gotchas: GotchaEntry[];
295
+ todos: CxxTodo[];
296
+ events: HarnessEvent[];
297
+ }
298
+
299
+ export interface CxxTodo {
300
+ id: string;
301
+ owner: "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops" | string;
302
+ title: string;
303
+ status: "pending" | "active" | "paused" | "blocked" | "done" | string;
304
+ priority: number;
305
+ kind: string;
306
+ missionPath?: string | null;
307
+ requiredArtifacts: string[];
308
+ createdAt: string | null;
309
+ updatedAt: string | null;
310
+ lastHeartbeatAt?: string | null;
311
+ blockedReason?: string | null;
312
+ }
313
+
314
+ export interface HarnessEvent {
315
+ ts: string | null;
316
+ type: string;
317
+ owner?: string | null;
318
+ command?: string | null;
319
+ summary?: string | null;
320
+ title?: string | null;
321
+ status?: string | null;
295
322
  }
296
323
 
297
324
  // Real harness mission documents
@@ -121,6 +121,9 @@ Note: A docmeta skip decision on harness documents (ceo.md, cto.md, cqo.md, work
121
121
  | `.harness/documents/goal-{index}-{name}/` | Goal-level CXX decisions and worker reports |
122
122
  | `.harness/documents/goal-{index}-{name}/submission-{index}-{name}/` | Additional requirement under the active goal |
123
123
  | `.harness/documents/goal-{index}-{name}/hotfix-{index}-{name}/` | Emergency fix under the active goal |
124
+ | `.harness/events.jsonl` | Structured append-only runtime event stream |
125
+ | `.harness/todos/state.json` | Machine-readable CXX todo queues |
126
+ | `.harness/todos/events.jsonl` | Structured todo transition history |
124
127
  | `.harness/conventions/` | Durable rules (CQO writes, survives missions) |
125
128
  | `.harness/gotchas/` | Recurrence-prevention records (CQO writes per hot-fix) |
126
129
  | `.harness/memories/` | Long-term shared context |
@@ -145,10 +148,11 @@ Note: A docmeta skip decision on harness documents (ceo.md, cto.md, cqo.md, work
145
148
  9. **No verdict without worker evidence** — CQO cannot issue ACCEPTED/REJECTED without a Worker Evidence Manifest referencing at least one evaluator worker. Self-inspection by CQO is not valid evidence.
146
149
  10. **Hierarchical worker ownership** — Hired workers are installed under `.claude/skills/{owning-cxx}/{worker}/` and `.codex/skills/{owning-cxx}/{worker}/`; mission worker reports live under `.harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/`. Flat `{mission}/workers/` reports are legacy and signal an ownership violation unless explicitly migrated.
147
150
  11. **Lazy convention/gotcha loading** — CXX roles read `.harness/conventions/shared.md`, `.harness/conventions/{cxx}.md`, `.harness/gotchas/shared.md`, and `.harness/gotchas/{cxx}.md`, then follow only the related topic links listed in those CXX files. Workers read only the related links supplied by their owning CXX.
148
- 11. **Implementation Notes required** — `ceo.md`, every `{cxx}.md`, and every worker report must end with one English `## Implementation Notes` section containing `Design Decisions`, `Deviations`, `Tradeoffs`, and `Open Questions`. Use `None` for empty subsections. Do not create a separate sidecar notes file; the notes belong at the bottom of the same role or worker report that produced the decision/evidence.
149
- 12. **Owner is final acceptance only** — Owner is not a tester or QA substitute. CEO/CXX must not report "done, please check" until worker-backed verification proves the goal can be completed. Use unit tests, E2E, Playwright, test accounts, seeded data, build/run checks, logs, and CQO evidence before requesting Owner acceptance.
150
- 13. **OPS watches CQO verification** — For runnable products, CQO PASS is invalid unless OPS has monitored the verification runtime or documented why OPS watch is not applicable. Open OPS incidents, missing runtime mapping, required log gaps, service down, health mismatch, or an unmonitored tested service block Owner acceptance.
151
- 14. **Production incidents are company events** — After launch, user-impacting OPS signals route through CEO to CTO/CQO/OPS. CTO owns recovery, CQO owns regression confirmation, and OPS owns evidence plus close criteria.
151
+ 12. **Implementation Notes required** — `ceo.md`, every `{cxx}.md`, and every worker report must end with one English `## Implementation Notes` section containing `Design Decisions`, `Deviations`, `Tradeoffs`, and `Open Questions`. Use `None` for empty subsections. Do not create a separate sidecar notes file; the notes belong at the bottom of the same role or worker report that produced the decision/evidence.
152
+ 13. **Structured runtime state** — If the harness must parse it, record it in JSON or JSONL. CXX todo queues, event history, heartbeat timestamps, preemption/resume state, and completion evidence belong in `.harness/todos/*.json*` or `.harness/events.jsonl`, not in free-form Markdown tables. Markdown remains for instructions, conventions, gotchas, skills, and human-readable mission narrative.
153
+ 14. **Owner is final acceptance only** — Owner is not a tester or QA substitute. CEO/CXX must not report "done, please check" until worker-backed verification proves the goal can be completed. Use unit tests, E2E, Playwright, test accounts, seeded data, build/run checks, logs, and CQO evidence before requesting Owner acceptance.
154
+ 15. **OPS watches CQO verification** — For runnable products, CQO PASS is invalid unless OPS has monitored the verification runtime or documented why OPS watch is not applicable. Open OPS incidents, missing runtime mapping, required log gaps, service down, health mismatch, or an unmonitored tested service block Owner acceptance.
155
+ 16. **Production incidents are company events** — After launch, user-impacting OPS signals route through CEO to CTO/CQO/OPS. CTO owns recovery, CQO owns regression confirmation, and OPS owns evidence plus close criteria.
152
156
 
153
157
  ---
154
158
 
@@ -51,11 +51,16 @@ Owner is the final acceptance reviewer, not a tester, QA substitute, debugger, o
51
51
  | `.harness/shared/HR-Resource/` | Hireable worker skill pool |
52
52
  | `.harness/shared/hr-roster.json` | Hired worker roster |
53
53
  | `.harness/archive/` | CQO-approved completed mission records |
54
- | `.harness/progress.log` | Append-only command and status history |
54
+ | `.harness/events.jsonl` | Structured append-only runtime event stream |
55
+ | `.harness/todos/state.json` | Machine-readable CXX todo queues |
56
+ | `.harness/todos/events.jsonl` | Structured todo transition history |
57
+ | `.harness/progress.log` | Legacy human-readable command and status history |
55
58
  | `.harness/progress.json` | Machine-readable current runtime state |
56
59
 
57
60
  Flat `.harness/documents/{mission}/workers/` reports are legacy. New worker evidence must be under `{owning-cxx}/workers/`.
58
61
 
62
+ Runtime state that the harness must parse belongs in JSON or JSONL, not Markdown. Markdown remains valid for instructions, skills, conventions, gotchas, and human-facing mission narrative, but queues, events, heartbeats, preemption/resume state, and completion evidence must be machine-readable.
63
+
59
64
  ## Required Flow
60
65
 
61
66
  ### Goal
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 1,
3
+ "updated_at": "{{DATE_ISO}}",
4
+ "owners": {
5
+ "ceo": [],
6
+ "coo": [],
7
+ "cdo": [],
8
+ "cto": [],
9
+ "cqo": [],
10
+ "ops": []
11
+ }
12
+ }
package/bin/init.js CHANGED
@@ -562,6 +562,7 @@ function scaffoldHarness() {
562
562
  // Copy templates as initial files
563
563
  const templateMap = {
564
564
  'progress.json.template': path.join(HARNESS_DIR, 'progress.json'),
565
+ 'todos-state.json.template': path.join(HARNESS_DIR, 'todos', 'state.json'),
565
566
  };
566
567
 
567
568
  const templatesDir = path.join(PKG_ROOT, 'assets', 'templates');
@@ -569,13 +570,22 @@ function scaffoldHarness() {
569
570
  for (const [template, dest] of Object.entries(templateMap)) {
570
571
  const src = path.join(templatesDir, template);
571
572
  if (fs.existsSync(src) && (!fileExists(dest) || isForce)) {
573
+ ensureDir(path.dirname(dest));
572
574
  let content = fs.readFileSync(src, 'utf8');
573
575
  content = content.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0]);
576
+ content = content.replace(/\{\{DATE_ISO\}\}/g, new Date().toISOString());
574
577
  fs.writeFileSync(dest, content);
575
578
  }
576
579
  }
577
580
  }
578
581
 
582
+ ensureDir(path.join(HARNESS_DIR, 'todos'));
583
+ for (const rel of ['events.jsonl', path.join('todos', 'events.jsonl')]) {
584
+ const p = path.join(HARNESS_DIR, rel);
585
+ if (!fileExists(p) || isForce) fs.writeFileSync(p, '');
586
+ }
587
+ ensureStructuredRuntimeFiles();
588
+
579
589
  // Migrate progress.json v1 → v2 (add mode + team_state fields)
580
590
  const progressPath = path.join(HARNESS_DIR, 'progress.json');
581
591
  if (fileExists(progressPath)) {
@@ -711,6 +721,38 @@ function ensureHarnessEnv() {
711
721
  log('.env: HARNESS_BASE_PORT placeholder added — set value with Owner approval before starting services');
712
722
  }
713
723
 
724
+ function ensureStructuredRuntimeFiles({ dryRun = false } = {}) {
725
+ const todosDir = path.join(HARNESS_DIR, 'todos');
726
+ const statePath = path.join(todosDir, 'state.json');
727
+ const eventsPath = path.join(HARNESS_DIR, 'events.jsonl');
728
+ const todoEventsPath = path.join(todosDir, 'events.jsonl');
729
+ const todoTplPath = path.join(PKG_ROOT, 'assets', 'templates', 'todos-state.json.template');
730
+
731
+ const missing = [eventsPath, statePath, todoEventsPath].filter((p) => !fileExists(p));
732
+ if (!missing.length) return false;
733
+
734
+ log(` structured runtime: ${missing.map((p) => path.relative(HARNESS_DIR, p)).join(', ')} 생성`);
735
+ if (dryRun) return true;
736
+
737
+ ensureDir(todosDir);
738
+ if (!fileExists(eventsPath)) fs.writeFileSync(eventsPath, '');
739
+ if (!fileExists(todoEventsPath)) fs.writeFileSync(todoEventsPath, '');
740
+ if (!fileExists(statePath)) {
741
+ let content;
742
+ if (fileExists(todoTplPath)) {
743
+ content = fs.readFileSync(todoTplPath, 'utf8').replace(/\{\{DATE_ISO\}\}/g, new Date().toISOString());
744
+ } else {
745
+ content = JSON.stringify({
746
+ version: 1,
747
+ updated_at: new Date().toISOString(),
748
+ owners: { ceo: [], coo: [], cdo: [], cto: [], cqo: [], ops: [] },
749
+ }, null, 2) + '\n';
750
+ }
751
+ fs.writeFileSync(statePath, content);
752
+ }
753
+ return true;
754
+ }
755
+
714
756
  // ─────────────────────────────────────────
715
757
  // 2. Skills → .claude/skills/
716
758
  // ─────────────────────────────────────────
@@ -1742,6 +1784,7 @@ function runMigrate(opts = {}) {
1742
1784
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
1743
1785
  const backupDir = path.join(HARNESS_DIR, 'archive', `migration-${ts}`);
1744
1786
  if (!dryRun) ensureDir(backupDir);
1787
+ ensureStructuredRuntimeFiles({ dryRun });
1745
1788
 
1746
1789
  // Runtime scripts are package-owned. Migrate must refresh them so existing
1747
1790
  // projects receive wake/meeting/OPS fixes without requiring a full init.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.26",
3
+ "version": "7.1.27",
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"
@@ -10,6 +10,8 @@ if [ -z "$CWD" ]; then CWD="$PWD"; fi
10
10
  # 조건 1: 하네스 초기화 확인
11
11
  if [ ! -f "$CWD/.harness/config.json" ]; then exit 0; fi
12
12
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ STRUCTURED_LIB="$SCRIPT_DIR/lib/harness-structured-log.sh"
14
+ [ -f "$STRUCTURED_LIB" ] && source "$STRUCTURED_LIB"
13
15
 
14
16
  # 조건 2: opt-out 플래그 확인
15
17
  AUTO_ROUTE="true"
@@ -71,7 +73,47 @@ if [ -n "$PROMPT" ] && [ -d "$CWD/.harness" ]; then
71
73
  COMMAND_TYPE="hot-fix"
72
74
  fi
73
75
  echo "$(date +"%Y-%m-%d %H:%M") | user-prompt | ${COMMAND_TYPE} | ${PROMPT_SHORT}" >> "$PROGRESS_LOG"
76
+ if declare -f harness_emit_event >/dev/null 2>&1; then
77
+ NOW_ISO_EVENT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
78
+ EVENT_JSON=$(jq -nc \
79
+ --arg ts "$NOW_ISO_EVENT" \
80
+ --arg command "$COMMAND_TYPE" \
81
+ --arg summary "$PROMPT_SHORT" \
82
+ '{ts:$ts,type:"user_prompt",command:$command,summary:$summary,source:"UserPromptSubmit"}')
83
+ harness_emit_event "$CWD" "$EVENT_JSON"
84
+ fi
74
85
  if [ "$COMMAND_TYPE" = "goal" ] || [ "$COMMAND_TYPE" = "submission" ] || [ "$COMMAND_TYPE" = "hot-fix" ]; then
86
+ if declare -f harness_todo_upsert >/dev/null 2>&1; then
87
+ NOW_ISO_TODO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
88
+ PRIORITY=50
89
+ KIND="$COMMAND_TYPE"
90
+ [ "$COMMAND_TYPE" = "hot-fix" ] && PRIORITY=100 && KIND="hotfix"
91
+ TODO_ID="ceo-${COMMAND_TYPE}-$(date -u +%Y%m%dT%H%M%SZ)"
92
+ TODO_JSON=$(jq -nc \
93
+ --arg id "$TODO_ID" \
94
+ --arg owner "ceo" \
95
+ --arg kind "$KIND" \
96
+ --arg title "$PROMPT_SHORT" \
97
+ --arg status "active" \
98
+ --arg now "$NOW_ISO_TODO" \
99
+ --argjson priority "$PRIORITY" \
100
+ '{
101
+ id:$id,
102
+ owner:$owner,
103
+ kind:$kind,
104
+ title:$title,
105
+ status:$status,
106
+ priority:$priority,
107
+ command:$kind,
108
+ mission_path:null,
109
+ required_artifacts:[],
110
+ created_at:$now,
111
+ updated_at:$now,
112
+ last_heartbeat_at:$now,
113
+ blocked_reason:null
114
+ }')
115
+ harness_todo_upsert "$CWD" "$TODO_JSON"
116
+ fi
75
117
  if [ -f "$CWD/.harness/progress.json" ] && command -v jq >/dev/null 2>&1; then
76
118
  TMP_PROGRESS="${CWD}/.harness/progress.json.tmp.$$"
77
119
  NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+ # Structured runtime helpers for events.jsonl and todos/state.json.
3
+
4
+ harness_json_escape() {
5
+ jq -Rs . 2>/dev/null
6
+ }
7
+
8
+ harness_ensure_structured_runtime() {
9
+ local project_root="${1:-.}"
10
+ local harness_dir="$project_root/.harness"
11
+ [ -d "$harness_dir" ] || return 0
12
+ mkdir -p "$harness_dir/todos"
13
+ [ -f "$harness_dir/events.jsonl" ] || : > "$harness_dir/events.jsonl"
14
+ [ -f "$harness_dir/todos/events.jsonl" ] || : > "$harness_dir/todos/events.jsonl"
15
+ if [ ! -f "$harness_dir/todos/state.json" ]; then
16
+ jq -n --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '{
17
+ version: 1,
18
+ updated_at: $now,
19
+ owners: {ceo: [], coo: [], cdo: [], cto: [], cqo: [], ops: []}
20
+ }' > "$harness_dir/todos/state.json"
21
+ fi
22
+ }
23
+
24
+ harness_emit_event() {
25
+ local project_root="$1"
26
+ local event_json="$2"
27
+ harness_ensure_structured_runtime "$project_root"
28
+ local path="$project_root/.harness/events.jsonl"
29
+ jq -c --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
30
+ . + {ts: (.ts // $now)}
31
+ ' <<<"$event_json" >> "$path" 2>/dev/null || true
32
+ }
33
+
34
+ harness_todo_upsert() {
35
+ local project_root="$1"
36
+ local todo_json="$2"
37
+ harness_ensure_structured_runtime "$project_root"
38
+ local state="$project_root/.harness/todos/state.json"
39
+ local events="$project_root/.harness/todos/events.jsonl"
40
+ local tmp="$state.tmp.$$"
41
+ local now
42
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
43
+
44
+ jq -c --arg now "$now" '. + {ts: (.ts // $now), type: (.type // "todo_upsert")}' <<<"$todo_json" >> "$events" 2>/dev/null || true
45
+ if jq --argjson todo "$todo_json" --arg now "$now" '
46
+ .version = (.version // 1) |
47
+ .updated_at = $now |
48
+ .owners = (.owners // {ceo: [], coo: [], cdo: [], cto: [], cqo: [], ops: []}) |
49
+ ($todo.owner // "ceo") as $owner |
50
+ .owners[$owner] = (.owners[$owner] // []) |
51
+ .owners[$owner] = ((.owners[$owner] | map(select(.id != $todo.id))) + [
52
+ ($todo + {updated_at: ($todo.updated_at // $now), created_at: ($todo.created_at // $now)})
53
+ ] | sort_by(-(.priority // 0), .created_at))
54
+ ' "$state" > "$tmp" 2>/dev/null; then
55
+ mv "$tmp" "$state"
56
+ else
57
+ rm -f "$tmp"
58
+ fi
59
+ }