@walwal-harness/cli 7.1.19 → 7.1.21

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/CHANGELOG.md CHANGED
@@ -31,13 +31,19 @@ docmeta:
31
31
 
32
32
  ## Unreleased
33
33
 
34
+ ### Changed
35
+ - Dashboard Org Tree now positions only hired HR-Resource workers from `hr-roster.json`, requiring a backing `.harness/shared/HR-Resource/{worker}/SKILL.md`.
36
+ - Running hired workers are highlighted under their owning CXX instead of showing queue jobs as worker seats.
37
+ - Dashboard SSE now watches mission documents, hired-worker roster, and HR-Resource changes so worker updates appear without browser refresh.
38
+ - Dashboard worker cards now prefer mission/report task labels and mark recently updated unfinished worker reports as active when runtime active-worker state is absent.
39
+
34
40
  ### Added
35
41
  - Hourly wake can now run either `claude -p` or `codex exec` via `HARNESS_WAKE_EXECUTOR=claude|codex`.
36
42
  - Hourly wake supports `HARNESS_WAKE_MODE=headless|tmux|record`; `tmux` creates a separate time-boxed session per hourly company tick for easier inspection.
37
43
  - Deterministic conductor fallback runs after each hourly review so meeting decisions can advance even when the LLM runtime is unavailable.
38
44
  - Hourly meeting minutes now split meaningful progress from paperwork-only artifacts and track strategy cadence drift.
39
45
 
40
- ### Changed
46
+ ### Runtime Changed
41
47
  - launchd wake PATH now includes `$HOME/.local/bin` so local Claude/Codex installs are found from non-interactive jobs.
42
48
  - Service-Ops records repeated incident signatures and marks repeated incidents as `recovery_required`, causing hourly meetings to route to CTO runtime recovery instead of repeating discussion.
43
49
 
package/README.md CHANGED
@@ -181,7 +181,7 @@ npx walwal-harness init --force --project-root /path/to/project
181
181
  Features:
182
182
 
183
183
  - **Split Workspace** — full-viewport left/right panes keep Org Tree + Mission Timeline beside the selected mission detail; both panes scroll independently with hidden scrollbars, and the detail pane resets to top on selection changes
184
- - **Org Tree** — live status of Owner → CEO → CXX → Workers hierarchy, with worker cards grouped under their owning CXX; clicking any node initializes the right pane to the relevant history, CXX document, or worker report
184
+ - **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
185
185
  - **Mission Timeline** — clickable history of goal/submission/hot-fix missions showing the full dispatch chain
186
186
  - **Mission Flow tab** — per-mission flow: Owner prompt → CEO routing → CXX → owner-specific worker evidence → CQO verdict
187
187
  - **History tab** — mission-specific Owner request (from CEO summary + closest progress.log match)
@@ -225,9 +225,9 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
225
225
  },
226
226
  ];
227
227
 
228
- // Workers are grouped by their owning CXX so evaluator reports sit under CQO,
229
- // implementation reports under CTO, and legacy flat reports stay visible.
230
- const workers = currentMission?.workers ?? [];
228
+ // Only hired HR-Resource workers are positioned in the org chart. Legacy
229
+ // report-only workers remain visible in mission documents, but not as seats.
230
+ const workers = (currentMission?.workers ?? []).filter((w) => w.hired);
231
231
 
232
232
  return (
233
233
  <div className="w-full px-4 py-6 select-none">
@@ -272,14 +272,16 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
272
272
  {/* Row 3: CXX nodes with workers below */}
273
273
  <div className="flex gap-3 justify-center">
274
274
  {cxxNodes.map((node) => {
275
- const nodeWorkers = workers.filter(
276
- (w) => w.owner === node.role || (w.owner === "unknown" && node.role === "cto")
277
- );
275
+ const nodeWorkers = workers.filter((w) => w.owner === node.role);
276
+ const hasActiveWorker = nodeWorkers.some((w) => w.active);
277
+ const displayNode = hasActiveWorker
278
+ ? { ...node, status: "typing" as const, activity: "Worker active" }
279
+ : node;
278
280
 
279
281
  return (
280
282
  <div key={node.id} className="flex flex-col items-center gap-2">
281
283
  <OrgCard
282
- node={node}
284
+ node={displayNode}
283
285
  size="md"
284
286
  active={activeNodeId === node.id}
285
287
  onClick={onNodeClick}
@@ -289,38 +291,53 @@ export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
289
291
  <>
290
292
  <div className="w-px h-3 bg-gray-700/60" />
291
293
  <div className="flex flex-col gap-1.5 w-full">
292
- {nodeWorkers.map((w) => (
294
+ {nodeWorkers.map((w) => {
295
+ const workerNodeId = `worker-${node.role}-${w.name}`;
296
+ const workerActive = activeNodeId === workerNodeId || w.active;
297
+ return (
293
298
  <button
294
- key={w.name}
299
+ key={`${node.role}-${w.name}`}
295
300
  type="button"
296
301
  onClick={() =>
297
302
  onNodeClick({
298
- id: `worker-${w.name}`,
303
+ id: workerNodeId,
299
304
  role: node.role,
300
- label: w.name,
301
- sublabel: w.status,
302
- status: w.status === "COMPLETE" ? "idle" : "typing",
303
- activity: null,
305
+ label: w.displayName,
306
+ sublabel: w.active ? `running · ${w.name}` : `${w.status} · ${w.name}`,
307
+ status: w.active ? "typing" : w.status === "COMPLETE" ? "idle" : "queued",
308
+ activity: w.sourcePath ?? null,
304
309
  agentIds: [],
305
310
  })
306
311
  }
307
- className={`rounded border border-gray-700/40 bg-gray-800/30 px-2 py-1.5 text-left min-w-[130px] max-w-[160px] transition-all hover:border-white/20 ${
308
- activeNodeId === `worker-${w.name}` ? "ring-1 ring-cyan-300" : ""
309
- }`}
312
+ className={`relative rounded border px-2 py-1.5 text-left min-w-[130px] max-w-[160px] transition-all hover:border-white/20 ${
313
+ workerActive
314
+ ? "border-cyan-300/70 bg-cyan-400/10 shadow-[0_0_18px_rgba(34,211,238,0.18)]"
315
+ : "border-gray-700/40 bg-gray-800/30"
316
+ } ${activeNodeId === workerNodeId ? "ring-1 ring-cyan-300" : ""}`}
310
317
  >
311
- <div className="font-mono text-[9px] text-gray-500">worker</div>
312
- <div className="text-[11px] font-medium text-gray-200 truncate">
313
- {w.name}
318
+ {w.active && (
319
+ <span className="absolute right-2 top-2 h-1.5 w-1.5 rounded-full bg-cyan-300 animate-pulse" />
320
+ )}
321
+ <div className={`font-mono text-[9px] ${w.active ? "text-cyan-300/80" : "text-gray-500"}`}>
322
+ hired worker
323
+ </div>
324
+ <div className="text-[11px] font-medium text-gray-200 truncate pr-3">
325
+ {w.displayName}
314
326
  </div>
315
327
  <div
316
328
  className={`text-[9px] font-mono mt-0.5 ${
317
- w.status === "COMPLETE" ? "text-emerald-400" : "text-amber-400"
318
- }`}
329
+ w.active
330
+ ? "text-cyan-300"
331
+ : w.status === "COMPLETE"
332
+ ? "text-emerald-400"
333
+ : "text-amber-400"
334
+ }`}
319
335
  >
320
- {w.status}
336
+ {w.active ? "RUNNING" : w.status}
321
337
  </div>
322
338
  </button>
323
- ))}
339
+ );
340
+ })}
324
341
  </div>
325
342
  </>
326
343
  )}
@@ -77,7 +77,7 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
77
77
  selectedAgent?.id ?? "",
78
78
  ].join(":");
79
79
  const selectedWorkerName = selectedNode?.id.startsWith("worker-")
80
- ? selectedNode.label
80
+ ? selectedNode.id.replace(/^worker-[^-]+-/, "")
81
81
  : undefined;
82
82
  const selectedDocumentRole = selectedWorkerName
83
83
  ? "worker"
@@ -247,4 +247,54 @@ describe("readHarnessState", () => {
247
247
  expect(missions[0].type).toBe("submission");
248
248
  expect(missions.find((mission) => mission.missionId === "goal-1-auth-system")?.type).toBe("goal");
249
249
  });
250
+
251
+ it("positions only hired HR-Resource workers and marks active workers", () => {
252
+ const harnessDir = path.join(dir, ".harness");
253
+ const missionDir = path.join(harnessDir, "documents", "goal-1-dashboard");
254
+ mkdirSync(path.join(missionDir, "cto", "workers"), { recursive: true });
255
+ mkdirSync(path.join(harnessDir, "shared", "HR-Resource", "react-ui-worker"), { recursive: true });
256
+ writeFileSync(path.join(missionDir, "ceo.md"), "# Dashboard\n");
257
+ writeFileSync(
258
+ path.join(missionDir, "cto", "workers", "react-ui-worker.md"),
259
+ "## Status\nIN_PROGRESS\n"
260
+ );
261
+ writeFileSync(
262
+ path.join(harnessDir, "shared", "HR-Resource", "react-ui-worker", "SKILL.md"),
263
+ "# React UI Worker\n"
264
+ );
265
+ writeFileSync(
266
+ path.join(harnessDir, "shared", "hr-roster.json"),
267
+ JSON.stringify({
268
+ hired: [
269
+ {
270
+ worker: "react-ui-worker",
271
+ owner: "cto",
272
+ skillPath: ".harness/shared/HR-Resource/react-ui-worker/SKILL.md",
273
+ },
274
+ {
275
+ worker: "missing-worker",
276
+ owner: "cto",
277
+ skillPath: ".harness/shared/HR-Resource/missing-worker/SKILL.md",
278
+ },
279
+ ],
280
+ })
281
+ );
282
+ writeFileSync(
283
+ path.join(harnessDir, "progress.json"),
284
+ JSON.stringify({
285
+ company_state: {
286
+ workers: [{ agent: "react-ui-worker", status: "running", feature: "F1" }],
287
+ },
288
+ })
289
+ );
290
+
291
+ const workers = readHarnessState(dir).missions[0].workers;
292
+ expect(workers.map((w) => w.name)).toEqual(["react-ui-worker"]);
293
+ expect(workers[0]).toMatchObject({
294
+ owner: "cto",
295
+ hired: true,
296
+ active: true,
297
+ sourcePath: ".harness/shared/HR-Resource/react-ui-worker/SKILL.md",
298
+ });
299
+ });
250
300
  });
@@ -11,7 +11,10 @@ describe("watchHarness", () => {
11
11
  dir = mkdtempSync(path.join(tmpdir(), "watch-"));
12
12
  mkdirSync(path.join(dir, ".harness", "actions"), { recursive: true });
13
13
  mkdirSync(path.join(dir, ".harness", "archive"), { recursive: true });
14
+ mkdirSync(path.join(dir, ".harness", "documents"), { recursive: true });
15
+ mkdirSync(path.join(dir, ".harness", "shared", "HR-Resource"), { recursive: true });
14
16
  writeFileSync(path.join(dir, ".harness", "progress.json"), "{}");
17
+ writeFileSync(path.join(dir, ".harness", "shared", "hr-roster.json"), "{\"hired\":[]}");
15
18
  });
16
19
 
17
20
  afterEach(() => {
@@ -45,4 +48,43 @@ describe("watchHarness", () => {
45
48
  await handle.close();
46
49
  // No assertion — test passes if close() resolves without throwing.
47
50
  });
51
+
52
+ it("fires onChange when hired workers or mission documents change", async () => {
53
+ const events: string[] = [];
54
+ const handle = watchHarness(dir, (p) => events.push(p), { debounceMs: 30 });
55
+
56
+ await new Promise((r) => setTimeout(r, 250));
57
+
58
+ writeFileSync(
59
+ path.join(dir, ".harness", "shared", "hr-roster.json"),
60
+ JSON.stringify({ hired: [{ worker: "react-ui-worker", owner: "cto" }] })
61
+ );
62
+ await new Promise((r) => setTimeout(r, 300));
63
+
64
+ mkdirSync(
65
+ path.join(dir, ".harness", "shared", "HR-Resource", "react-ui-worker"),
66
+ { recursive: true }
67
+ );
68
+ writeFileSync(
69
+ path.join(dir, ".harness", "shared", "HR-Resource", "react-ui-worker", "SKILL.md"),
70
+ "# React UI Worker\n"
71
+ );
72
+ await new Promise((r) => setTimeout(r, 300));
73
+
74
+ mkdirSync(
75
+ path.join(dir, ".harness", "documents", "goal-1", "cto", "workers"),
76
+ { recursive: true }
77
+ );
78
+ writeFileSync(
79
+ path.join(dir, ".harness", "documents", "goal-1", "cto", "workers", "react-ui-worker.md"),
80
+ "## Status\nIN_PROGRESS\n"
81
+ );
82
+
83
+ await new Promise((r) => setTimeout(r, 500));
84
+
85
+ await handle.close();
86
+ expect(events.some((e) => e.endsWith("hr-roster.json"))).toBe(true);
87
+ expect(events.some((e) => e.endsWith("SKILL.md"))).toBe(true);
88
+ expect(events.some((e) => e.endsWith("react-ui-worker.md"))).toBe(true);
89
+ });
48
90
  });
@@ -41,6 +41,7 @@ import type {
41
41
 
42
42
  const SNAPSHOT_VERSION = "1.2.0";
43
43
  const GOAL_DESC_TRUNCATE = 200;
44
+ const RECENT_WORKER_ACTIVE_MS = 10 * 60 * 1000;
44
45
 
45
46
  interface RawProgress {
46
47
  goals?: {
@@ -775,6 +776,16 @@ function readFeatureTitles(rootDir: string): Map<string, string> {
775
776
  }
776
777
 
777
778
  type RawWorker = NonNullable<NonNullable<RawProgress["company_state"]>["workers"]>[number];
779
+ type WorkerOwner = WorkerDocEntry["owner"];
780
+
781
+ interface HiredWorkerEntry {
782
+ name: string;
783
+ owner: Exclude<WorkerOwner, "unknown">;
784
+ sourcePath: string | null;
785
+ mission: string | null;
786
+ task: string | null;
787
+ capability: string | null;
788
+ }
778
789
 
779
790
  function deriveWorkerProgress(worker: RawWorker): number | null {
780
791
  const status = (worker?.status ?? worker?.spawn_status ?? "").toLowerCase();
@@ -837,6 +848,109 @@ function buildWorkers(rootDir: string, progress: RawProgress | null): WorkerSnap
837
848
  });
838
849
  }
839
850
 
851
+ function normalizeWorkerName(value: string | null | undefined): string {
852
+ return (value ?? "")
853
+ .toLowerCase()
854
+ .replace(/^harness-/, "")
855
+ .replace(/\.md$/, "")
856
+ .trim();
857
+ }
858
+
859
+ function normalizeWorkerOwner(value: string | null | undefined): HiredWorkerEntry["owner"] | null {
860
+ const owner = (value ?? "").toLowerCase().replace(/^harness-/, "");
861
+ if (owner === "coo" || owner === "cdo" || owner === "cto" || owner === "cqo" || owner === "ops") {
862
+ return owner;
863
+ }
864
+ return null;
865
+ }
866
+
867
+ function readHiredWorkers(rootDir: string): HiredWorkerEntry[] {
868
+ const rosterPath = path.join(rootDir, ".harness", "shared", "hr-roster.json");
869
+ const roster = readJsonSafe<{
870
+ hired?: Array<{
871
+ worker?: string;
872
+ name?: string;
873
+ owner?: string;
874
+ owningCxx?: string;
875
+ mission?: string;
876
+ task?: string;
877
+ capability?: string;
878
+ skillPath?: string;
879
+ skillPaths?: { source?: string };
880
+ }>;
881
+ }>(rosterPath);
882
+ if (!roster.ok) return [];
883
+
884
+ const seen = new Set<string>();
885
+ const hired: HiredWorkerEntry[] = [];
886
+ for (const entry of roster.value.hired ?? []) {
887
+ const name = normalizeWorkerName(entry.worker ?? entry.name);
888
+ const owner = normalizeWorkerOwner(entry.owner ?? entry.owningCxx);
889
+ if (!name || !owner) continue;
890
+
891
+ const sourcePath = entry.skillPaths?.source ?? entry.skillPath ?? `.harness/shared/HR-Resource/${name}/SKILL.md`;
892
+ const sourceAbs = path.join(rootDir, sourcePath);
893
+ if (!existsSync(sourceAbs)) continue;
894
+
895
+ const key = `${owner}:${name}`;
896
+ if (seen.has(key)) continue;
897
+ seen.add(key);
898
+ hired.push({
899
+ name,
900
+ owner,
901
+ sourcePath,
902
+ mission: entry.mission ?? null,
903
+ task: entry.task ?? null,
904
+ capability: entry.capability ?? null,
905
+ });
906
+ }
907
+ return hired;
908
+ }
909
+
910
+ function hiredForMission(
911
+ hiredWorkers: HiredWorkerEntry[],
912
+ name: string,
913
+ owner: WorkerOwner,
914
+ missionId: string,
915
+ ): HiredWorkerEntry | undefined {
916
+ const candidates = hiredWorkers.filter(
917
+ (w) => w.name === name && (w.owner === owner || owner === "unknown")
918
+ );
919
+ return candidates.find((w) => w.mission === missionId) ?? candidates[0];
920
+ }
921
+
922
+ function workerDisplayName(content: string, hired: HiredWorkerEntry | undefined, fallback: string): string {
923
+ const title = content.match(/^\s*title:\s*["']?(.+?)["']?\s*$/m)?.[1]?.trim();
924
+ if (title) return truncateText(title.replace(/\s+—\s+Worker Report$/i, ""), 42);
925
+
926
+ const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
927
+ if (heading) return truncateText(heading.replace(/\s+—\s+.*$/i, ""), 42);
928
+
929
+ if (hired?.task) {
930
+ const capability = hired.capability ? ` · ${hired.capability}` : "";
931
+ return truncateText(`${hired.task}${capability}`, 42);
932
+ }
933
+
934
+ return fallback;
935
+ }
936
+
937
+ function activeWorkerNames(progress: RawProgress | null): Set<string> {
938
+ const active = new Set<string>();
939
+ const workers = [
940
+ ...(progress?.company_state?.workers ?? []),
941
+ ...(progress?.company_state?.last_dispatch ?? []),
942
+ ];
943
+ for (const worker of workers) {
944
+ const rawStatus = (worker.status ?? worker.spawn_status ?? "").toLowerCase();
945
+ if (["completed", "done", "idle"].includes(rawStatus)) continue;
946
+ for (const value of [worker.agent, worker.feature]) {
947
+ const normalized = normalizeWorkerName(value);
948
+ if (normalized) active.add(normalized);
949
+ }
950
+ }
951
+ return active;
952
+ }
953
+
840
954
  function meetingTitleFromMarkdown(text: string, fallback: string): string {
841
955
  const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim();
842
956
  return heading || fallback;
@@ -989,6 +1103,8 @@ function buildRuntime(progress: RawProgress | null): RuntimeSnapshot {
989
1103
  function readMissions(rootDir: string, progress: RawProgress | null = null, limit = 15): MissionDoc[] {
990
1104
  const docsDir = path.join(rootDir, ".harness", "documents");
991
1105
  if (!existsSync(docsDir)) return [];
1106
+ const hiredWorkers = readHiredWorkers(rootDir);
1107
+ const activeWorkers = activeWorkerNames(progress);
992
1108
 
993
1109
  const missionDirs: Array<{ rel: string; abs: string }> = [];
994
1110
  const cxxDocNames = new Set(["ceo.md", "cto.md", "cqo.md", "coo.md", "cdo.md", "ops.md"]);
@@ -1035,20 +1151,39 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
1035
1151
  };
1036
1152
 
1037
1153
  const workers: WorkerDocEntry[] = [];
1038
- const addWorkersFromDir = (owner: WorkerDocEntry["owner"], relDir: string) => {
1154
+ const workersByKey = new Map<string, WorkerDocEntry>();
1155
+ const addWorkersFromDir = (owner: WorkerOwner, relDir: string) => {
1039
1156
  const workersDir = path.join(missionPath, relDir);
1040
1157
  if (!existsSync(workersDir)) return;
1041
1158
  try {
1042
1159
  for (const f of readdirSync(workersDir)) {
1043
1160
  if (!f.endsWith(".md")) continue;
1161
+ const reportAbs = path.join(missionPath, relDir, f);
1044
1162
  const content = readMd(`${relDir}/${f}`) ?? "";
1045
1163
  const statusMatch = content.match(/##\s*Status\s*\n+([A-Z_]+)/);
1046
- workers.push({
1047
- name: f.replace(".md", ""),
1164
+ const status = (statusMatch?.[1] as WorkerDocEntry["status"]) ?? "unknown";
1165
+ const name = normalizeWorkerName(f);
1166
+ const hired = hiredForMission(hiredWorkers, name, owner, rel);
1167
+ let updatedAt: string | null = null;
1168
+ let recentlyTouched = false;
1169
+ try {
1170
+ const mtime = statSync(reportAbs).mtime;
1171
+ updatedAt = mtime.toISOString();
1172
+ recentlyTouched = Date.now() - mtime.getTime() < RECENT_WORKER_ACTIVE_MS;
1173
+ } catch { /* ignore */ }
1174
+ const entry: WorkerDocEntry = {
1175
+ name,
1176
+ displayName: workerDisplayName(content, hired, name),
1048
1177
  content,
1049
- status: (statusMatch?.[1] as WorkerDocEntry["status"]) ?? "unknown",
1050
- owner,
1051
- });
1178
+ status,
1179
+ owner: hired?.owner ?? owner,
1180
+ hired: !!hired,
1181
+ active: activeWorkers.has(name) || (recentlyTouched && status !== "COMPLETE"),
1182
+ sourcePath: hired?.sourcePath ?? null,
1183
+ reportPath: path.relative(rootDir, reportAbs),
1184
+ updatedAt,
1185
+ };
1186
+ workersByKey.set(`${entry.owner}:${entry.name}`, entry);
1052
1187
  }
1053
1188
  } catch { /* ignore */ }
1054
1189
  };
@@ -1060,6 +1195,25 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
1060
1195
  addWorkersFromDir("ops", "ops/workers");
1061
1196
  addWorkersFromDir("unknown", "workers");
1062
1197
 
1198
+ for (const hired of hiredWorkers) {
1199
+ const key = `${hired.owner}:${hired.name}`;
1200
+ if (workersByKey.has(key)) continue;
1201
+ workersByKey.set(key, {
1202
+ name: hired.name,
1203
+ displayName: workerDisplayName("", hired, hired.name),
1204
+ content: "",
1205
+ status: activeWorkers.has(hired.name) ? "IN_PROGRESS" : "unknown",
1206
+ owner: hired.owner,
1207
+ hired: true,
1208
+ active: activeWorkers.has(hired.name),
1209
+ sourcePath: hired.sourcePath,
1210
+ reportPath: null,
1211
+ updatedAt: null,
1212
+ });
1213
+ }
1214
+
1215
+ workers.push(...workersByKey.values());
1216
+
1063
1217
  const cxxRoles = ["ceo", "cto", "cqo", "coo", "cdo", "ops"] as const;
1064
1218
  const cxxPresent = cxxRoles.filter(role => existsSync(path.join(missionPath, `${role}.md`)));
1065
1219
 
@@ -297,9 +297,15 @@ export interface HarnessSnapshot {
297
297
  // Real harness mission documents
298
298
  export interface WorkerDocEntry {
299
299
  name: string;
300
+ displayName: string;
300
301
  content: string;
301
302
  status: "COMPLETE" | "IN_PROGRESS" | "unknown";
302
303
  owner: "cto" | "cqo" | "coo" | "cdo" | "ops" | "unknown";
304
+ hired: boolean;
305
+ active: boolean;
306
+ sourcePath?: string | null;
307
+ reportPath?: string | null;
308
+ updatedAt?: string | null;
303
309
  }
304
310
 
305
311
  export interface MissionDoc {
@@ -10,9 +10,8 @@ export interface WatchOptions {
10
10
  debounceMs?: number;
11
11
  }
12
12
 
13
- // Watches the .harness/ directory for changes that affect the dashboard
14
- // snapshot — progress.json (state), progress.log (activity), actions/* (sprint
15
- // artifacts), archive/* (sprint history).
13
+ // Watches .harness paths that affect the dashboard snapshot: runtime progress,
14
+ // actions, mission documents, hired-worker roster/resources, and archive state.
16
15
  export function watchHarness(
17
16
  rootDir: string,
18
17
  onChange: (changedPath: string) => void,
@@ -25,6 +24,9 @@ export function watchHarness(
25
24
  path.join(harnessDir, "progress.json"),
26
25
  path.join(harnessDir, "progress.log"),
27
26
  path.join(harnessDir, "actions"),
27
+ path.join(harnessDir, "documents"),
28
+ path.join(harnessDir, "shared", "hr-roster.json"),
29
+ path.join(harnessDir, "shared", "HR-Resource"),
28
30
  path.join(harnessDir, "archive"),
29
31
  ],
30
32
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.19",
3
+ "version": "7.1.21",
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"