@walwal-harness/cli 7.1.15 → 7.1.17

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
@@ -223,6 +223,8 @@ harness-hiring → register and onboard worker
223
223
 
224
224
  | Version | Summary |
225
225
  |---|---|
226
+ | 7.1.17 | Dashboard: fix /submission and /hot-fix missions displaying as /goal; runtime strip shows live owner command in mission flow tab |
227
+ | 7.1.16 | `migrate` now always fully replaces /goal, /submission, /hot-fix commands regardless of current state |
226
228
  | 7.1.15 | Dashboard: reset embedded View Doc state when the selected mission or document target changes |
227
229
  | 7.1.14 | Owner acceptance gate: Owner is final reviewer, not tester; dashboard org-node clicks initialize the detail pane |
228
230
  | 7.1.13 | Dashboard: full-viewport split workspace, hidden pane scrollbars, and detail scroll reset on selection |
@@ -155,6 +155,7 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
155
155
  <MissionFlowTab
156
156
  mission={selectedMission ?? snapshot.missions?.[0] ?? null}
157
157
  ownerHistory={snapshot.ownerHistory ?? []}
158
+ runtime={snapshot.runtime}
158
159
  />
159
160
  )}
160
161
  {drawerTab === "history" && (
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { useEffect, useState } from "react";
3
- import type { MissionDoc, OwnerPromptEntry } from "@/lib/types";
3
+ import type { MissionDoc, OwnerPromptEntry, RuntimeSnapshot } from "@/lib/types";
4
4
  import { MarkdownView } from "@/lib/markdown";
5
5
 
6
6
  type DocView = "overview" | "ceo" | "cto" | "cqo" | "coo" | "cdo" | "ops" | string;
@@ -8,6 +8,7 @@ type DocView = "overview" | "ceo" | "cto" | "cqo" | "coo" | "cdo" | "ops" | stri
8
8
  interface Props {
9
9
  mission: MissionDoc | null;
10
10
  ownerHistory: OwnerPromptEntry[];
11
+ runtime: RuntimeSnapshot;
11
12
  }
12
13
 
13
14
  // ---------------------------------------------------------------------------
@@ -222,7 +223,53 @@ function WorkerCard({
222
223
  // ---------------------------------------------------------------------------
223
224
  // Main component
224
225
  // ---------------------------------------------------------------------------
225
- export function MissionFlowTab({ mission, ownerHistory }: Props) {
226
+ function RuntimeStrip({ runtime }: { runtime: RuntimeSnapshot }) {
227
+ const isActive = runtime.agentStatus === "running";
228
+ const command = runtime.ownerPrompt?.command ?? "input";
229
+ const summary = runtime.ownerPrompt?.summary ?? null;
230
+ return (
231
+ <div
232
+ data-testid="runtime-strip"
233
+ data-current-agent={runtime.currentAgent ?? ""}
234
+ data-agent-status={runtime.agentStatus}
235
+ className={`rounded border px-2.5 py-2 ${
236
+ isActive
237
+ ? "border-cyan-400/30 bg-cyan-400/10"
238
+ : runtime.agentStatus === "blocked" || runtime.agentStatus === "failed"
239
+ ? "border-rose-400/30 bg-rose-400/10"
240
+ : "border-gray-700/60 bg-black/20"
241
+ }`}
242
+ >
243
+ <div className="flex flex-wrap items-center gap-1.5">
244
+ <span className="font-mono text-[9px] uppercase tracking-[0.18em] text-gray-500">
245
+ live runtime
246
+ </span>
247
+ <StepChip role={runtime.currentAgent ?? "worker"} label={runtime.currentAgent ?? "none"} />
248
+ <span
249
+ className={`font-mono text-[9px] ${
250
+ isActive ? "text-cyan-300" : "text-gray-400"
251
+ }`}
252
+ >
253
+ {runtime.agentStatus}
254
+ </span>
255
+ {runtime.nextAgent && (
256
+ <span className="font-mono text-[9px] text-gray-500">
257
+ next: {runtime.nextAgent}
258
+ </span>
259
+ )}
260
+ </div>
261
+ {summary && (
262
+ <p className="mt-1.5 text-[10px] leading-relaxed text-gray-400 line-clamp-2">
263
+ <span className="font-mono text-gray-500">{command}</span>
264
+ <span className="text-gray-600"> · </span>
265
+ {summary}
266
+ </p>
267
+ )}
268
+ </div>
269
+ );
270
+ }
271
+
272
+ export function MissionFlowTab({ mission, ownerHistory, runtime }: Props) {
226
273
  const [docView, setDocView] = useState<DocView>("overview");
227
274
 
228
275
  useEffect(() => {
@@ -231,8 +278,11 @@ export function MissionFlowTab({ mission, ownerHistory }: Props) {
231
278
 
232
279
  if (!mission) {
233
280
  return (
234
- <div className="text-gray-500 text-xs">
235
- No mission selected. Click a mission from the timeline below.
281
+ <div className="space-y-3">
282
+ <RuntimeStrip runtime={runtime} />
283
+ <div className="text-gray-500 text-xs">
284
+ No mission selected. Click a mission from the timeline below.
285
+ </div>
236
286
  </div>
237
287
  );
238
288
  }
@@ -253,6 +303,7 @@ export function MissionFlowTab({ mission, ownerHistory }: Props) {
253
303
 
254
304
  return (
255
305
  <div className="space-y-3">
306
+ <RuntimeStrip runtime={runtime} />
256
307
  <button
257
308
  type="button"
258
309
  onClick={() => setDocView("overview")}
@@ -339,6 +390,7 @@ export function MissionFlowTab({ mission, ownerHistory }: Props) {
339
390
 
340
391
  return (
341
392
  <div className="space-y-3">
393
+ <RuntimeStrip runtime={runtime} />
342
394
  {/* Mission header */}
343
395
  <div>
344
396
  <div className="flex items-center gap-2 flex-wrap mb-1">
@@ -15,6 +15,30 @@ describe("deriveMinifigState", () => {
15
15
  ).toBe("typing");
16
16
  });
17
17
 
18
+ it("maps v7 CEO alias to dispatcher and brainstormer", () => {
19
+ const progress = {
20
+ current_agent: "ceo",
21
+ agent_status: "running",
22
+ };
23
+ expect(deriveMinifigState("dispatcher", progress)).toBe("typing");
24
+ expect(deriveMinifigState("brainstormer", progress)).toBe("typing");
25
+ });
26
+
27
+ it("maps v7 CXX aliases to their dashboard departments", () => {
28
+ expect(
29
+ deriveMinifigState("generator-designer", {
30
+ current_agent: "cdo",
31
+ agent_status: "running",
32
+ })
33
+ ).toBe("typing");
34
+ expect(
35
+ deriveMinifigState("service-ops", {
36
+ next_agent: "ops",
37
+ agent_status: "pending",
38
+ })
39
+ ).toBe("queued");
40
+ });
41
+
18
42
  it("does not return typing when agent_status is not running", () => {
19
43
  expect(
20
44
  deriveMinifigState("generator-backend", {
@@ -33,6 +33,7 @@ import type {
33
33
  RoomId,
34
34
  RoomMetrics,
35
35
  RoomState,
36
+ RuntimeSnapshot,
36
37
  TrackStatus,
37
38
  WorkerDocEntry,
38
39
  WorkerSnapshot,
@@ -50,6 +51,13 @@ interface RawProgress {
50
51
  current_agent?: AgentId | null;
51
52
  next_agent?: AgentId | null;
52
53
  agent_status?: string;
54
+ updated_at?: string | null;
55
+ owner_prompt?: {
56
+ command?: string;
57
+ summary?: string;
58
+ received_at?: string | null;
59
+ status?: string;
60
+ } | null;
53
61
  pipeline?: Pipeline;
54
62
  failure?: { agent?: AgentId | null; message?: string | null; location?: string | null; retry_target?: AgentId | null } | null;
55
63
  meetings?: {
@@ -205,6 +213,13 @@ function emptySnapshot(banner: ErrorBanner | null = null, rootDir?: string): Har
205
213
  evalScores: null,
206
214
  errorBanner: banner,
207
215
  dashboard: { workers: [], features: [], recentMeetings: [], opsHealth: [], envFiles: [] },
216
+ runtime: {
217
+ currentAgent: null,
218
+ agentStatus: "unknown",
219
+ nextAgent: null,
220
+ updatedAt: null,
221
+ ownerPrompt: null,
222
+ },
208
223
  missions: [],
209
224
  ownerHistory: [],
210
225
  gotchas: [],
@@ -953,7 +968,25 @@ function buildDashboard(rootDir: string, progress: RawProgress | null): Operatio
953
968
  };
954
969
  }
955
970
 
956
- function readMissions(rootDir: string, limit = 15): MissionDoc[] {
971
+ function buildRuntime(progress: RawProgress | null): RuntimeSnapshot {
972
+ const ownerPrompt = progress?.owner_prompt
973
+ ? {
974
+ command: progress.owner_prompt.command ?? "input",
975
+ summary: progress.owner_prompt.summary ?? "",
976
+ receivedAt: progress.owner_prompt.received_at ?? null,
977
+ status: progress.owner_prompt.status ?? "unknown",
978
+ }
979
+ : null;
980
+ return {
981
+ currentAgent: progress?.current_agent ?? null,
982
+ agentStatus: progress?.agent_status ?? "unknown",
983
+ nextAgent: progress?.next_agent ?? null,
984
+ updatedAt: progress?.updated_at ?? null,
985
+ ownerPrompt,
986
+ };
987
+ }
988
+
989
+ function readMissions(rootDir: string, progress: RawProgress | null = null, limit = 15): MissionDoc[] {
957
990
  const docsDir = path.join(rootDir, ".harness", "documents");
958
991
  if (!existsSync(docsDir)) return [];
959
992
 
@@ -1050,6 +1083,19 @@ function readMissions(rootDir: string, limit = 15): MissionDoc[] {
1050
1083
  .sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())
1051
1084
  .slice(0, limit);
1052
1085
 
1086
+ // When the CEO updates a goal directory in-place for a submission or hot-fix
1087
+ // (instead of creating a submission-* or hotfix-* subdirectory), the most
1088
+ // recently modified mission still has type "goal" from its directory name.
1089
+ // Override it using owner_prompt.command so the display reflects the actual
1090
+ // command the Owner issued, not just the directory prefix.
1091
+ const cmd = progress?.owner_prompt?.command;
1092
+ if (missions.length > 0 && (cmd === "submission" || cmd === "hot-fix")) {
1093
+ const top = missions[0];
1094
+ if (top.type === "goal") {
1095
+ missions[0] = { ...top, type: cmd === "hot-fix" ? "hotfix" : "submission" };
1096
+ }
1097
+ }
1098
+
1053
1099
  return missions;
1054
1100
  }
1055
1101
 
@@ -1132,7 +1178,7 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
1132
1178
  };
1133
1179
  const snapshot = emptySnapshot(banner, rootDir);
1134
1180
  snapshot.archive = buildArchive(rootDir);
1135
- snapshot.missions = readMissions(rootDir);
1181
+ snapshot.missions = readMissions(rootDir, null);
1136
1182
  snapshot.ownerHistory = readOwnerHistory(rootDir);
1137
1183
  snapshot.gotchas = readGotchas(rootDir);
1138
1184
  return snapshot;
@@ -1183,7 +1229,8 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
1183
1229
  evalScores,
1184
1230
  errorBanner: null,
1185
1231
  dashboard,
1186
- missions: readMissions(rootDir),
1232
+ runtime: buildRuntime(progress),
1233
+ missions: readMissions(rootDir, progress),
1187
1234
  ownerHistory: readOwnerHistory(rootDir),
1188
1235
  gotchas: readGotchas(rootDir),
1189
1236
  };
@@ -1,11 +1,24 @@
1
1
  import type { AgentId, Dept, MinifigState } from "./types";
2
2
 
3
+ const AGENT_ALIASES: Record<string, AgentId[]> = {
4
+ ceo: ["dispatcher", "brainstormer"],
5
+ dispatcher: ["dispatcher"],
6
+ brainstormer: ["brainstormer"],
7
+ coo: ["planner", "coo-developer", "documentationer"],
8
+ planner: ["planner"],
9
+ cdo: ["generator-designer"],
10
+ cto: ["cto", "conductor", "generator-backend", "generator-frontend", "generator-designer", "generator-devops"],
11
+ cqo: ["cqo", "evaluator-functional", "evaluator-visual", "evaluator-code-quality", "evaluator-architecture", "evaluator-security"],
12
+ ops: ["service-ops"],
13
+ "service-ops": ["service-ops"],
14
+ };
15
+
3
16
  export interface ProgressSlice {
4
- current_agent?: AgentId | null;
5
- next_agent?: AgentId | null;
17
+ current_agent?: string | null;
18
+ next_agent?: string | null;
6
19
  agent_status?: string;
7
- failure?: { agent?: AgentId | null; message?: string | null } | null;
8
- meetings?: { active?: AgentId[] };
20
+ failure?: { agent?: string | null; message?: string | null } | null;
21
+ meetings?: { active?: string[] };
9
22
  service_ops?: {
10
23
  incident?: { open?: Array<{ id: string; dept?: string }> };
11
24
  monitor?: { stream_active?: boolean; stream_target?: string | null };
@@ -42,6 +55,13 @@ export function parseSpawnAgent(action: string | null | undefined): string | nul
42
55
  return m ? m[1].toLowerCase() : null;
43
56
  }
44
57
 
58
+ function agentMatches(agentId: string, raw: string | null | undefined): boolean {
59
+ if (!raw) return false;
60
+ const key = raw.toLowerCase();
61
+ if (key === agentId) return true;
62
+ return AGENT_ALIASES[key]?.includes(agentId as AgentId) ?? false;
63
+ }
64
+
45
65
  function isAgentRunningTrack(
46
66
  agentId: string,
47
67
  conductor: ProgressSlice["conductor"]
@@ -49,7 +69,7 @@ function isAgentRunningTrack(
49
69
  const tracks = conductor?.tracks ?? [];
50
70
  return tracks.some(
51
71
  (t) =>
52
- t.owner === agentId &&
72
+ agentMatches(agentId, t.owner) &&
53
73
  (t.status === "running" || t.status === "in_progress" || t.status === "active")
54
74
  );
55
75
  }
@@ -59,7 +79,7 @@ function isAgentQueuedTrack(
59
79
  conductor: ProgressSlice["conductor"]
60
80
  ): boolean {
61
81
  const tracks = conductor?.tracks ?? [];
62
- return tracks.some((t) => t.owner === agentId && t.status === "pending");
82
+ return tracks.some((t) => agentMatches(agentId, t.owner) && t.status === "pending");
63
83
  }
64
84
 
65
85
  // Priority: red-alert > talking > typing > queued > idle.
@@ -76,18 +96,18 @@ export function deriveMinifigState(
76
96
  ctx: DeriveContext = {}
77
97
  ): MinifigState {
78
98
  if (!progress) return "idle";
79
- if (progress.failure?.agent === agentId) return "red-alert";
99
+ if (agentMatches(agentId, progress.failure?.agent)) return "red-alert";
80
100
  if (ctx.dept && ctx.incidentDepts && ctx.incidentDepts.size > 0) {
81
101
  if (ctx.dept === "Operations" || ctx.incidentDepts.has(ctx.dept)) {
82
102
  return "red-alert";
83
103
  }
84
104
  }
85
- const inMeeting = progress.meetings?.active?.includes(agentId) ?? false;
105
+ const inMeeting = progress.meetings?.active?.some((id) => agentMatches(agentId, id)) ?? false;
86
106
  if (inMeeting) return "talking";
87
107
 
88
108
  // Parallel-typing signals (any one of these means the agent is working).
89
109
  if (
90
- progress.current_agent === agentId &&
110
+ agentMatches(agentId, progress.current_agent) &&
91
111
  progress.agent_status === "running"
92
112
  ) {
93
113
  return "typing";
@@ -95,7 +115,7 @@ export function deriveMinifigState(
95
115
  if (isAgentRunningTrack(agentId, progress.conductor)) {
96
116
  return "typing";
97
117
  }
98
- if (parseSpawnAgent(progress.conductor?.current_action) === agentId) {
118
+ if (agentMatches(agentId, parseSpawnAgent(progress.conductor?.current_action))) {
99
119
  return "typing";
100
120
  }
101
121
  // G-006: service-ops co-spawned in monitor mode is actively streaming.
@@ -106,7 +126,7 @@ export function deriveMinifigState(
106
126
  return "typing";
107
127
  }
108
128
  if (
109
- progress.next_agent === agentId &&
129
+ agentMatches(agentId, progress.next_agent) &&
110
130
  progress.agent_status !== "blocked" &&
111
131
  progress.agent_status !== "failed"
112
132
  ) {
@@ -257,6 +257,19 @@ export interface OperationsDashboard {
257
257
  envFiles: EnvFileSummary[];
258
258
  }
259
259
 
260
+ export interface RuntimeSnapshot {
261
+ currentAgent: string | null;
262
+ agentStatus: string;
263
+ nextAgent: string | null;
264
+ updatedAt: string | null;
265
+ ownerPrompt: {
266
+ command: string;
267
+ summary: string;
268
+ receivedAt: string | null;
269
+ status: string;
270
+ } | null;
271
+ }
272
+
260
273
  export interface HarnessSnapshot {
261
274
  version: string;
262
275
  ts: string;
@@ -275,6 +288,7 @@ export interface HarnessSnapshot {
275
288
  evalScores: EvalScores | null;
276
289
  errorBanner: ErrorBanner | null;
277
290
  dashboard: OperationsDashboard;
291
+ runtime: RuntimeSnapshot;
278
292
  missions: MissionDoc[];
279
293
  ownerHistory: OwnerPromptEntry[];
280
294
  gotchas: GotchaEntry[];
package/bin/init.js CHANGED
@@ -1459,6 +1459,14 @@ function showMigrationProposal(flags) {
1459
1459
 
1460
1460
  function runMigrate(opts = {}) {
1461
1461
  const dryRun = opts.dryRun || false;
1462
+
1463
+ // Always refresh owner-facing commands (full replacement regardless of current state).
1464
+ if (dryRun) {
1465
+ log(' (dry-run) commands: /goal, /submission, /hot-fix 교체 예정');
1466
+ } else {
1467
+ installCommands();
1468
+ }
1469
+
1462
1470
  const flags = detectMigrationNeeded();
1463
1471
  const gotchaMissingTotal = Object.values(flags.gotchaMissingEntries || {}).reduce((n, a) => n + a.length, 0);
1464
1472
  const conventionMissingTotal = Object.values(flags.conventionMissingEntries || {}).reduce((n, a) => n + a.length, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.15",
3
+ "version": "7.1.17",
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"
@@ -71,6 +71,28 @@ if [ -n "$PROMPT" ] && [ -d "$CWD/.harness" ]; then
71
71
  COMMAND_TYPE="hot-fix"
72
72
  fi
73
73
  echo "$(date +"%Y-%m-%d %H:%M") | user-prompt | ${COMMAND_TYPE} | ${PROMPT_SHORT}" >> "$PROGRESS_LOG"
74
+ if [ "$COMMAND_TYPE" = "goal" ] || [ "$COMMAND_TYPE" = "submission" ] || [ "$COMMAND_TYPE" = "hot-fix" ]; then
75
+ if [ -f "$CWD/.harness/progress.json" ] && command -v jq >/dev/null 2>&1; then
76
+ TMP_PROGRESS="${CWD}/.harness/progress.json.tmp.$$"
77
+ NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
78
+ jq \
79
+ --arg now "$NOW_ISO" \
80
+ --arg command "$COMMAND_TYPE" \
81
+ --arg prompt "$PROMPT_SHORT" \
82
+ '
83
+ .current_agent = "ceo" |
84
+ .agent_status = "running" |
85
+ .next_agent = "ceo" |
86
+ .updated_at = $now |
87
+ .owner_prompt = {
88
+ command: $command,
89
+ summary: $prompt,
90
+ received_at: $now,
91
+ status: "routing"
92
+ }
93
+ ' "$CWD/.harness/progress.json" > "$TMP_PROGRESS" && mv "$TMP_PROGRESS" "$CWD/.harness/progress.json"
94
+ fi
95
+ fi
74
96
  fi
75
97
  fi
76
98