@walwal-harness/cli 7.1.40 → 7.1.42

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.
Files changed (84) hide show
  1. package/HR-Resource/cdo/SKILL.md +9 -1
  2. package/HR-Resource/ceo/SKILL.md +33 -2
  3. package/HR-Resource/coo/SKILL.md +9 -1
  4. package/HR-Resource/cqo/SKILL.md +9 -1
  5. package/HR-Resource/cto/SKILL.md +10 -2
  6. package/HR-Resource/ops/SKILL.md +12 -4
  7. package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
  8. package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
  9. package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
  10. package/apps/harness-dashboard/app/api/metrics/route.ts +17 -0
  11. package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
  12. package/apps/harness-dashboard/app/brick.css +482 -0
  13. package/apps/harness-dashboard/app/layout.tsx +13 -0
  14. package/apps/harness-dashboard/app/page.tsx +2 -2
  15. package/apps/harness-dashboard/components/Scene.tsx +690 -36
  16. package/apps/harness-dashboard/components/brick/BrickDashboard.tsx +149 -0
  17. package/apps/harness-dashboard/components/brick/reports.tsx +291 -0
  18. package/apps/harness-dashboard/components/brick/tweaks.tsx +738 -0
  19. package/apps/harness-dashboard/components/brick/ui.tsx +463 -0
  20. package/apps/harness-dashboard/components/brick/views-graph.tsx +305 -0
  21. package/apps/harness-dashboard/components/brick/views-grid.tsx +543 -0
  22. package/apps/harness-dashboard/components/brick/views-timeline.tsx +238 -0
  23. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
  24. package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
  25. package/apps/harness-dashboard/lib/brick/adapter.ts +423 -0
  26. package/apps/harness-dashboard/lib/brick/contract.ts +247 -0
  27. package/apps/harness-dashboard/lib/harness-state.ts +107 -796
  28. package/apps/harness-dashboard/lib/metrics/sampler.ts +545 -0
  29. package/apps/harness-dashboard/lib/types.ts +22 -222
  30. package/apps/harness-dashboard/playwright.config.ts +26 -7
  31. package/assets/templates/AGENTS-ko.md.template +1 -1
  32. package/assets/templates/AGENTS.md.template +12 -2
  33. package/assets/templates/HARNESS.md +2 -0
  34. package/assets/templates/company-pipeline-manifest.json +5 -14
  35. package/assets/templates/worker-report.md.template +38 -0
  36. package/bin/init.js +2 -1
  37. package/commands/goal.md +4 -2
  38. package/commands/hot-fix.md +1 -1
  39. package/commands/submission.md +1 -1
  40. package/package.json +1 -1
  41. package/scripts/conductor-tick.sh +12 -0
  42. package/scripts/harness-agenda.sh +105 -0
  43. package/scripts/harness-company-block.sh +89 -0
  44. package/scripts/harness-company-complete.sh +17 -8
  45. package/scripts/harness-company-cycle.sh +54 -0
  46. package/scripts/harness-progress-set.sh +8 -2
  47. package/scripts/harness-stop.sh +76 -0
  48. package/scripts/harness-wake-control.sh +151 -0
  49. package/scripts/harness-wake.sh +5 -0
  50. package/scripts/harness-worker-dispatch.sh +31 -7
  51. package/apps/harness-dashboard/components/Drawer.tsx +0 -146
  52. package/apps/harness-dashboard/components/Header.tsx +0 -40
  53. package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
  54. package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
  55. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
  56. package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
  57. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
  58. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
  59. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
  60. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
  61. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
  62. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
  63. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
  64. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
  65. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
  66. package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
  67. package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
  68. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
  69. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
  70. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
  71. package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
  72. package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
  73. package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
  74. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
  75. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
  76. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
  77. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
  78. package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
  79. package/apps/harness-dashboard/lib/furniture.ts +0 -165
  80. package/apps/harness-dashboard/lib/i18n.ts +0 -26
  81. package/apps/harness-dashboard/lib/iso.ts +0 -150
  82. package/apps/harness-dashboard/lib/markdown.tsx +0 -306
  83. package/apps/harness-dashboard/lib/path-planning.ts +0 -72
  84. package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
@@ -103,7 +103,7 @@ fi
103
103
 
104
104
  count="$(jq 'length' <<<"$pairs")"
105
105
  if [ "$count" -eq 0 ]; then
106
- active_workers="$(jq '[.teams | to_entries[] | select(.value.status == "busy") | {team:(.key|tonumber), feature:.value.feature, pid:.value.pid, phase:(.value.phase // "gen")}]' "$QUEUE" 2>/dev/null || echo "[]")"
106
+ active_workers="$(jq '[.teams | to_entries[] | select(.value.status == "busy") | {team:(.key|tonumber), feature:.value.feature, pid:.value.pid, tmux_session:.value.tmux_session, phase:(.value.phase // "gen")}]' "$QUEUE" 2>/dev/null || echo "[]")"
107
107
  jq --arg ts "$ts" --argjson workers "$active_workers" '
108
108
  .company_state.active_workers = ($workers | length) |
109
109
  .company_state.workers = $workers |
@@ -141,8 +141,30 @@ while [ "$i" -lt "$count" ]; do
141
141
  build_prompt "$team" "$fid" "$agent" "$prompt_path" "$log_path"
142
142
 
143
143
  pid="null"
144
+ tmux_session="null"
144
145
  status="recorded"
145
- if [ "$spawn_mode" = "claude" ] && command -v claude >/dev/null 2>&1; then
146
+ if [ "$spawn_mode" = "tmux" ] && command -v tmux >/dev/null 2>&1 && command -v claude >/dev/null 2>&1; then
147
+ session_safe="$(basename "$PROJECT_ROOT" | tr -c '[:alnum:]_' '_')"
148
+ session_name="walwal_${session_safe}_w${team}_${stamp}"
149
+ if tmux new-session -d -s "$session_name" "cd '$PROJECT_ROOT' && claude -p \"\$(cat '$prompt_path')\" > '$log_path' 2>&1"; then
150
+ pane_pid="$(tmux list-panes -t "$session_name" -F '#{pane_pid}' 2>/dev/null | head -n1)"
151
+ if [ -n "$pane_pid" ]; then
152
+ pid="$pane_pid"
153
+ fi
154
+ tmux_session="\"$session_name\""
155
+ status="spawned-tmux"
156
+ else
157
+ status="recorded tmux-spawn-failed"
158
+ fi
159
+ elif [ "$spawn_mode" = "tmux" ] && command -v claude >/dev/null 2>&1; then
160
+ # tmux requested but unavailable: fall back to background claude spawn
161
+ (
162
+ cd "$PROJECT_ROOT" || exit 1
163
+ claude -p "$(cat "$prompt_path")" > "$log_path" 2>&1
164
+ ) &
165
+ pid="$!"
166
+ status="spawned tmux-fallback-claude"
167
+ elif [ "$spawn_mode" = "claude" ] && command -v claude >/dev/null 2>&1; then
146
168
  (
147
169
  cd "$PROJECT_ROOT" || exit 1
148
170
  claude -p "$(cat "$prompt_path")" > "$log_path" 2>&1
@@ -152,8 +174,9 @@ while [ "$i" -lt "$count" ]; do
152
174
  fi
153
175
 
154
176
  tmpq="${QUEUE}.tmp.$$.$i"
155
- jq --arg tid "$team" --arg agent "$agent" --arg phase "$phase" --arg prompt "$prompt_rel" --arg log "$log_rel" --arg status "$status" --argjson pid "$pid" --arg ts "$ts" '
177
+ jq --arg tid "$team" --arg agent "$agent" --arg phase "$phase" --arg prompt "$prompt_rel" --arg log "$log_rel" --arg status "$status" --argjson pid "$pid" --argjson tmux_session "$tmux_session" --arg ts "$ts" '
156
178
  .teams[$tid].pid = $pid |
179
+ .teams[$tid].tmux_session = $tmux_session |
157
180
  .teams[$tid].agent = $agent |
158
181
  .teams[$tid].phase = $phase |
159
182
  .teams[$tid].prompt = $prompt |
@@ -164,18 +187,19 @@ while [ "$i" -lt "$count" ]; do
164
187
  .queue.in_progress[.teams[$tid].feature].phase = $phase |
165
188
  .queue.in_progress[.teams[$tid].feature].prompt = $prompt |
166
189
  .queue.in_progress[.teams[$tid].feature].log = $log |
167
- .queue.in_progress[.teams[$tid].feature].pid = $pid
190
+ .queue.in_progress[.teams[$tid].feature].pid = $pid |
191
+ .queue.in_progress[.teams[$tid].feature].tmux_session = $tmux_session
168
192
  ' "$QUEUE" > "$tmpq" && mv "$tmpq" "$QUEUE"
169
193
 
170
- jq -n --argjson team "$team" --arg feature "$fid" --arg agent "$agent" --arg phase "$phase" --arg prompt "$prompt_rel" --arg log "$log_rel" --arg status "$status" --argjson pid "$pid" \
171
- '{team:$team, feature:$feature, agent:$agent, phase:$phase, prompt:$prompt, log:$log, status:$status, pid:$pid}' >> "$dispatches_file"
194
+ jq -n --argjson team "$team" --arg feature "$fid" --arg agent "$agent" --arg phase "$phase" --arg prompt "$prompt_rel" --arg log "$log_rel" --arg status "$status" --argjson pid "$pid" --argjson tmux_session "$tmux_session" \
195
+ '{team:$team, feature:$feature, agent:$agent, phase:$phase, prompt:$prompt, log:$log, status:$status, pid:$pid, tmux_session:$tmux_session}' >> "$dispatches_file"
172
196
  echo "$ts | conductor | worker-dispatch | $status | T${team}/${fid} | $agent phase=$phase pid=$pid" >> "$PROJECT_ROOT/.harness/progress.log"
173
197
  i=$((i + 1))
174
198
  done
175
199
 
176
200
  dispatches="$(jq -s '.' "$dispatches_file")"
177
201
  rm -f "$dispatches_file"
178
- active_workers="$(jq '[.teams | to_entries[] | select(.value.status == "busy") | {team:(.key|tonumber), feature:.value.feature, agent:.value.agent, pid:.value.pid, phase:(.value.phase // "gen"), prompt:.value.prompt, log:.value.log, spawn_status:.value.spawn_status}]' "$QUEUE")"
202
+ active_workers="$(jq '[.teams | to_entries[] | select(.value.status == "busy") | {team:(.key|tonumber), feature:.value.feature, agent:.value.agent, pid:.value.pid, tmux_session:.value.tmux_session, phase:(.value.phase // "gen"), prompt:.value.prompt, log:.value.log, spawn_status:.value.spawn_status}]' "$QUEUE")"
179
203
 
180
204
  jq --arg ts "$ts" --argjson dispatches "$dispatches" --argjson workers "$active_workers" '
181
205
  .company_state.active_workers = ($workers | length) |
@@ -1,146 +0,0 @@
1
- "use client";
2
- import { useEffect, useRef } from "react";
3
-
4
- export type DrawerTab =
5
- | "mission-flow" // Mission document hierarchy (PRIMARY)
6
- | "history" // Owner prompt history
7
- | "gotchas" // Harness gotcha knowledge base
8
- | "design-preview" // CDO visual sample iframe
9
- | "mission-doc" // Single doc markdown view
10
- | "logs"; // Agent log (technical)
11
-
12
- interface DrawerProps {
13
- open: boolean;
14
- tab: DrawerTab;
15
- title: string;
16
- onClose: () => void;
17
- onTabChange: (tab: DrawerTab) => void;
18
- children: React.ReactNode;
19
- mode?: "overlay" | "inline";
20
- scrollResetKey?: string;
21
- }
22
-
23
- const TABS: Array<{ id: DrawerTab; label: string }> = [
24
- { id: "mission-flow", label: "Mission Flow" },
25
- { id: "history", label: "History" },
26
- { id: "gotchas", label: "Gotchas" },
27
- { id: "design-preview", label: "Preview" },
28
- { id: "mission-doc", label: "Document" },
29
- { id: "logs", label: "Agent Log" },
30
- ];
31
-
32
- export function Drawer({ open, tab, title, onClose, onTabChange, children, mode = "overlay", scrollResetKey }: DrawerProps) {
33
- const contentRef = useRef<HTMLDivElement | null>(null);
34
-
35
- useEffect(() => {
36
- if (!open || mode === "inline") return;
37
- const onKey = (e: KeyboardEvent) => {
38
- if (e.key === "Escape") onClose();
39
- };
40
- window.addEventListener("keydown", onKey);
41
- return () => window.removeEventListener("keydown", onKey);
42
- }, [open, onClose, mode]);
43
-
44
- useEffect(() => {
45
- contentRef.current?.scrollTo({ top: 0, left: 0 });
46
- }, [scrollResetKey]);
47
-
48
- if (mode === "inline") {
49
- return (
50
- <aside
51
- role="complementary"
52
- aria-label="Detail panel"
53
- data-testid="drawer"
54
- data-open="true"
55
- className="glass-panel h-full min-h-0 rounded-lg flex flex-col overflow-hidden"
56
- >
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
- {title}
60
- </h2>
61
- </header>
62
- <nav className="flex flex-wrap border-b border-white/10 px-2 py-2 text-[11px] font-mono">
63
- {TABS.map((t) => (
64
- <button
65
- key={t.id}
66
- type="button"
67
- data-testid={`drawer-tab-${t.id}`}
68
- data-active={t.id === tab}
69
- onClick={() => onTabChange(t.id)}
70
- className={`min-w-[33%] flex-1 rounded-md px-2 py-2 transition-colors ${
71
- t.id === tab
72
- ? "bg-white/12 text-gray-100 shadow-inner"
73
- : "text-gray-500 hover:bg-white/6 hover:text-gray-200"
74
- }`}
75
- >
76
- {t.label}
77
- </button>
78
- ))}
79
- </nav>
80
- <div ref={contentRef} className="scrollbar-hidden flex-1 overflow-y-auto px-4 py-3 text-xs text-gray-200 font-mono">
81
- {children}
82
- </div>
83
- </aside>
84
- );
85
- }
86
-
87
- return (
88
- <>
89
- {/* Click-outside backdrop. Pointer-events flip with `open`. */}
90
- <div
91
- data-testid="drawer-backdrop"
92
- aria-hidden="true"
93
- onClick={onClose}
94
- className={`fixed inset-0 bg-black/30 transition-opacity duration-200 ${
95
- open ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
96
- }`}
97
- />
98
- <aside
99
- role="complementary"
100
- aria-label="Detail panel"
101
- aria-hidden={!open}
102
- data-testid="drawer"
103
- data-open={open}
104
- className={`fixed top-0 right-0 h-[100dvh] w-full sm:w-[50vw] sm:min-w-[600px] bg-brick-bg border-l border-brick-wall shadow-2xl flex flex-col transition-transform duration-300 ease-out ${
105
- open ? "translate-x-0" : "translate-x-full"
106
- }`}
107
- >
108
- <header className="flex items-center justify-between border-b border-brick-wall px-4 py-3">
109
- <h2 className="text-sm font-mono uppercase tracking-widest text-gray-200">
110
- {title}
111
- </h2>
112
- <button
113
- type="button"
114
- onClick={onClose}
115
- aria-label="Close panel"
116
- data-testid="drawer-close"
117
- className="rounded p-1 text-gray-400 hover:text-gray-100 hover:bg-brick-wall/40"
118
- >
119
- ✕
120
- </button>
121
- </header>
122
- <nav className="flex flex-wrap border-b border-brick-wall text-[11px] font-mono">
123
- {TABS.map((t) => (
124
- <button
125
- key={t.id}
126
- type="button"
127
- data-testid={`drawer-tab-${t.id}`}
128
- data-active={t.id === tab}
129
- onClick={() => onTabChange(t.id)}
130
- className={`min-w-[33%] flex-1 px-2 py-2 transition-colors ${
131
- t.id === tab
132
- ? "bg-brick-wall/50 text-gray-100 border-b-2 border-aura-typing"
133
- : "text-gray-500 hover:text-gray-200"
134
- }`}
135
- >
136
- {t.label}
137
- </button>
138
- ))}
139
- </nav>
140
- <div ref={contentRef} className="scrollbar-hidden flex-1 overflow-y-auto px-4 py-3 text-xs text-gray-200 font-mono">
141
- {children}
142
- </div>
143
- </aside>
144
- </>
145
- );
146
- }
@@ -1,40 +0,0 @@
1
- import path from "node:path";
2
- import { Wordmark } from "./Wordmark";
3
- import { resolveHarnessRoot } from "@/lib/harness-root";
4
-
5
- export function Header() {
6
- const root = resolveHarnessRoot();
7
- const projectName = path.basename(root);
8
- return (
9
- <header className="border-b border-brick-wall bg-brick-bg/80 backdrop-blur">
10
- <div className="mx-auto flex max-w-[1280px] items-center justify-between px-6 py-4">
11
- <div className="flex items-baseline gap-4">
12
- <Wordmark />
13
- <span className="hidden items-baseline gap-2 sm:inline-flex">
14
- <span
15
- className="rounded bg-brick-wall/40 px-2 py-0.5 font-mono text-xs font-semibold text-amber-300"
16
- title={root}
17
- >
18
- {projectName}
19
- </span>
20
- <span className="text-xs text-gray-400">walwal-harness 라이브 운영 대시보드</span>
21
- </span>
22
- </div>
23
- <nav className="flex items-center gap-3 text-xs font-mono text-gray-500">
24
- <span className="inline-flex items-center gap-1">
25
- <span className="size-2 rounded-full bg-aura-idle" /> idle
26
- </span>
27
- <span className="inline-flex items-center gap-1">
28
- <span className="size-2 rounded-full bg-aura-typing" /> typing
29
- </span>
30
- <span className="inline-flex items-center gap-1">
31
- <span className="size-2 rounded-full bg-aura-talking" /> talking
32
- </span>
33
- <span className="inline-flex items-center gap-1">
34
- <span className="size-2 rounded-full bg-aura-alert" /> alert
35
- </span>
36
- </nav>
37
- </div>
38
- </header>
39
- );
40
- }
@@ -1,214 +0,0 @@
1
- "use client";
2
- import type { MissionDoc } from "@/lib/types";
3
-
4
- interface Props {
5
- missions: MissionDoc[];
6
- activeMissionId: string | null;
7
- onMissionClick: (mission: MissionDoc) => void;
8
- }
9
-
10
- type ChipRole =
11
- | "owner"
12
- | "ceo"
13
- | "coo"
14
- | "cdo"
15
- | "cto"
16
- | "cqo"
17
- | "ops"
18
- | "worker";
19
-
20
- function chipColor(role: ChipRole): string {
21
- switch (role) {
22
- case "owner":
23
- return "bg-amber-500/15 text-amber-300 border-amber-500/30";
24
- case "ceo":
25
- return "bg-cyan-400/15 text-cyan-300 border-cyan-400/30";
26
- case "coo":
27
- return "bg-emerald-500/15 text-emerald-300 border-emerald-500/30";
28
- case "cdo":
29
- return "bg-purple-500/15 text-purple-300 border-purple-500/30";
30
- case "cto":
31
- return "bg-blue-400/15 text-blue-300 border-blue-400/30";
32
- case "cqo":
33
- return "bg-amber-400/15 text-amber-300 border-amber-400/30";
34
- case "ops":
35
- return "bg-rose-400/15 text-rose-300 border-rose-400/30";
36
- case "worker":
37
- return "bg-gray-500/15 text-gray-300 border-gray-500/30";
38
- default:
39
- return "bg-gray-500/15 text-gray-400 border-gray-600/30";
40
- }
41
- }
42
-
43
- function Chip({
44
- role,
45
- label,
46
- badge,
47
- }: {
48
- role: ChipRole;
49
- label: string;
50
- badge?: string;
51
- }) {
52
- return (
53
- <span
54
- className={`inline-flex items-center gap-1 rounded border px-1.5 py-0.5 font-mono text-[9px] ${chipColor(role)}`}
55
- >
56
- {label}
57
- {badge && <span className="opacity-80">{badge}</span>}
58
- </span>
59
- );
60
- }
61
-
62
- function Arrow() {
63
- return <span className="text-[10px] text-gray-600 shrink-0">→</span>;
64
- }
65
-
66
- function buildFlowChain(mission: MissionDoc): React.ReactNode {
67
- // Pre-worker CXX: planning phase (before implementation)
68
- const preWorker: Array<{ key: "coo" | "cdo" | "cto"; role: ChipRole }> = [
69
- { key: "coo", role: "coo" },
70
- { key: "cdo", role: "cdo" },
71
- { key: "cto", role: "cto" },
72
- ];
73
- // Post-worker CXX: verification phase (after implementation)
74
- const postWorker: Array<{ key: "cqo" | "ops"; role: ChipRole }> = [
75
- { key: "cqo", role: "cqo" },
76
- { key: "ops", role: "ops" },
77
- ];
78
-
79
- const isCqoAccepted =
80
- typeof mission.cqo === "string" && mission.cqo.includes("ACCEPTED");
81
-
82
- const chips: React.ReactNode[] = [];
83
- let key = 0;
84
-
85
- chips.push(<Chip key={key++} role="owner" label="Owner" />);
86
- chips.push(<Arrow key={key++} />);
87
- chips.push(<Chip key={key++} role="ceo" label="CEO" />);
88
-
89
- // Planning phase: COO, CDO, CTO
90
- for (const { key: roleKey, role } of preWorker) {
91
- if (mission.cxxPresent.includes(roleKey)) {
92
- chips.push(<Arrow key={key++} />);
93
- chips.push(<Chip key={key++} role={role} label={roleKey.toUpperCase()} />);
94
- }
95
- }
96
-
97
- // CTO-owned implementation workers. Legacy flat workers remain visible here
98
- // with owner=unknown so protocol bypasses are not hidden.
99
- for (const worker of mission.workers.filter((w) => w.owner === "cto" || w.owner === "unknown")) {
100
- chips.push(<Arrow key={key++} />);
101
- const badge = worker.status === "COMPLETE" ? "✅" : "⏳";
102
- const shortName =
103
- worker.name.length > 12 ? worker.name.slice(0, 12) + "…" : worker.name;
104
- chips.push(
105
- <Chip key={key++} role="worker" label={shortName} badge={badge} />
106
- );
107
- }
108
-
109
- // Verification phase: CQO, OPS
110
- for (const { key: roleKey, role } of postWorker) {
111
- if (mission.cxxPresent.includes(roleKey)) {
112
- chips.push(<Arrow key={key++} />);
113
- const badge = roleKey === "cqo" && isCqoAccepted ? "✅" : undefined;
114
- chips.push(
115
- <Chip key={key++} role={role} label={roleKey.toUpperCase()} badge={badge} />
116
- );
117
- if (roleKey === "cqo") {
118
- for (const worker of mission.workers.filter((w) => w.owner === "cqo")) {
119
- chips.push(<Arrow key={key++} />);
120
- const workerBadge = worker.status === "COMPLETE" ? "✅" : "⏳";
121
- const shortName =
122
- worker.name.length > 12 ? worker.name.slice(0, 12) + "…" : worker.name;
123
- chips.push(
124
- <Chip key={key++} role="worker" label={shortName} badge={workerBadge} />
125
- );
126
- }
127
- }
128
- }
129
- }
130
-
131
- return (
132
- <div className="flex flex-wrap items-center gap-1 min-w-0">{chips}</div>
133
- );
134
- }
135
-
136
- export function MissionTimeline({
137
- missions,
138
- activeMissionId,
139
- onMissionClick,
140
- }: Props) {
141
- if (!missions.length) {
142
- return (
143
- <div className="glass-panel rounded-lg px-3 py-4 text-center text-[11px] text-gray-500">
144
- No missions found in .harness/documents/
145
- </div>
146
- );
147
- }
148
-
149
- return (
150
- <div className="space-y-1.5">
151
- {missions.map((mission) => {
152
- const isActive = mission.missionId === activeMissionId;
153
- const typeBadge =
154
- mission.type === "hotfix"
155
- ? "[🔥 hot-fix]"
156
- : mission.type === "submission"
157
- ? "[+ submission]"
158
- : mission.type === "goal" || mission.type === "feature"
159
- ? "[✦ goal]"
160
- : "[— unknown]";
161
- const shortId =
162
- mission.missionId.length > 28
163
- ? mission.missionId.slice(0, 28) + "…"
164
- : mission.missionId;
165
-
166
- let dateStr = "—";
167
- try {
168
- dateStr = new Date(mission.ts).toLocaleDateString(undefined, {
169
- month: "short",
170
- day: "numeric",
171
- });
172
- } catch {
173
- // ignore invalid date
174
- }
175
-
176
- return (
177
- <button
178
- key={mission.missionId}
179
- type="button"
180
- onClick={() => onMissionClick(mission)}
181
- className={`w-full rounded-lg px-3 py-2.5 text-left transition-colors ${
182
- isActive
183
- ? "glass-panel"
184
- : "border border-white/10 bg-white/[0.035] hover:border-white/20"
185
- }`}
186
- >
187
- <div className="flex items-start justify-between gap-2 mb-1.5">
188
- <div className="flex items-center gap-2 min-w-0">
189
- <span
190
- className={`shrink-0 font-mono text-[9px] rounded px-1.5 py-0.5 border ${
191
- mission.type === "hotfix"
192
- ? "bg-rose-500/15 text-rose-300 border-rose-500/30"
193
- : mission.type === "submission"
194
- ? "bg-sky-500/15 text-sky-300 border-sky-500/30"
195
- : "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
196
- }`}
197
- >
198
- {typeBadge}
199
- </span>
200
- <span className="font-mono text-[10px] text-gray-200 truncate">
201
- {shortId}
202
- </span>
203
- </div>
204
- <span className="shrink-0 font-mono text-[9px] text-gray-500">
205
- {dateStr}
206
- </span>
207
- </div>
208
- {buildFlowChain(mission)}
209
- </button>
210
- );
211
- })}
212
- </div>
213
- );
214
- }