@walwal-harness/cli 7.1.15 → 7.1.16
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 +1 -0
- package/apps/harness-dashboard/components/Scene.tsx +1 -0
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +56 -4
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +24 -0
- package/apps/harness-dashboard/lib/harness-state.ts +34 -0
- package/apps/harness-dashboard/lib/state-mapping.ts +31 -11
- package/apps/harness-dashboard/lib/types.ts +14 -0
- package/bin/init.js +8 -0
- package/package.json +1 -1
- package/scripts/harness-user-prompt-submit.sh +22 -0
package/README.md
CHANGED
|
@@ -223,6 +223,7 @@ harness-hiring → register and onboard worker
|
|
|
223
223
|
|
|
224
224
|
| Version | Summary |
|
|
225
225
|
|---|---|
|
|
226
|
+
| 7.1.16 | `migrate` now always fully replaces /goal, /submission, /hot-fix commands regardless of current state |
|
|
226
227
|
| 7.1.15 | Dashboard: reset embedded View Doc state when the selected mission or document target changes |
|
|
227
228
|
| 7.1.14 | Owner acceptance gate: Owner is final reviewer, not tester; dashboard org-node clicks initialize the detail pane |
|
|
228
229
|
| 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
|
-
|
|
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="
|
|
235
|
-
|
|
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,6 +968,24 @@ function buildDashboard(rootDir: string, progress: RawProgress | null): Operatio
|
|
|
953
968
|
};
|
|
954
969
|
}
|
|
955
970
|
|
|
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
|
+
|
|
956
989
|
function readMissions(rootDir: string, limit = 15): MissionDoc[] {
|
|
957
990
|
const docsDir = path.join(rootDir, ".harness", "documents");
|
|
958
991
|
if (!existsSync(docsDir)) return [];
|
|
@@ -1183,6 +1216,7 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
|
1183
1216
|
evalScores,
|
|
1184
1217
|
errorBanner: null,
|
|
1185
1218
|
dashboard,
|
|
1219
|
+
runtime: buildRuntime(progress),
|
|
1186
1220
|
missions: readMissions(rootDir),
|
|
1187
1221
|
ownerHistory: readOwnerHistory(rootDir),
|
|
1188
1222
|
gotchas: readGotchas(rootDir),
|
|
@@ -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?:
|
|
5
|
-
next_agent?:
|
|
17
|
+
current_agent?: string | null;
|
|
18
|
+
next_agent?: string | null;
|
|
6
19
|
agent_status?: string;
|
|
7
|
-
failure?: { agent?:
|
|
8
|
-
meetings?: { active?:
|
|
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
|
|
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
|
|
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
|
|
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?.
|
|
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
|
|
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)
|
|
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
|
|
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.
|
|
3
|
+
"version": "7.1.16",
|
|
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
|
|