@walwal-harness/cli 7.1.34 → 7.1.37

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,6 +31,25 @@ docmeta:
31
31
 
32
32
  ## Unreleased
33
33
 
34
+ ## 7.1.37 — Project-local dashboard runtime + wake reliability (2026-05-31)
35
+
36
+ - Dashboard runtime now lives under each project’s `.harness/dashboard/` instead of the shared `~/.walwal-harness/dashboard/` cache, with generated dependencies/build output ignored via `.gitignore`.
37
+ - Dashboard heatmap is no longer filtered by the selected goal/submission/hot-fix; it shows all recorded activity while still preserving mission IDs on active cells.
38
+ - Dashboard worker parsing now accepts `name`, `progress`, `eta`, and `report_path` telemetry and no longer treats docmeta-only report drafts as proof of live worker activity.
39
+ - Init/migrate now best-effort registers the project with the hourly wake scheduler, and wake status now uses the macOS `launchctl print gui/$UID/...` state to avoid false `NOT loaded` reports.
40
+
41
+ ## 7.1.36 — Stop hook active-loop guard + visible heatmap (2026-05-29)
42
+
43
+ - Stop hook no longer runs worker evidence validation before deciding whether the company loop should chain.
44
+ - Worker evidence validation can be scoped to the newest active mission so legacy/archive mission records cannot keep Stop blocked forever.
45
+ - Dashboard heatmap now uses 10-minute buckets across 24 hours, making activity visible without horizontal scrolling through 1440 minute cells.
46
+
47
+ ## 7.1.35 — Dashboard heatmap mission scoping (2026-05-29)
48
+
49
+ - Dashboard heatmap samples are now scoped to the selected mission, so worker rows no longer inherit activity from unrelated stale-active missions.
50
+ - Activity recording now chooses the newest active mission when legacy runtime state contains multiple `active:true` mission-state files.
51
+ - Init/migrate now deactivates duplicate active submission/hot-fix children under the same goal instead of only creating missing mission-state files.
52
+
34
53
  ## 7.1.26 — Lazy rule links + MCP steward (2026-05-27)
35
54
 
36
55
  ### Added
@@ -9,6 +9,17 @@ disable-model-invocation: false
9
9
 
10
10
  You are the only direct conversation channel with the Owner.
11
11
 
12
+ ## Autonomous Operating Charter
13
+
14
+ The Owner gives direction and final acceptance, not routine operating answers.
15
+
16
+ - Do not ask the Owner whether to continue, which option to choose, which worker to hire, or whether to start the next planned step.
17
+ - When a decision is needed, decide from mission evidence, conventions, gotchas, CXX recommendations, and the smallest reversible path. Record the decision and rationale in `ceo.md`.
18
+ - If CEO cannot decide alone, convene the relevant CXX agents and use their written recommendations to decide. Do not turn the uncertainty into an Owner question.
19
+ - Treat Owner messages as direction changes, new requirements, hot-fixes, final acceptance feedback, or explicit constraints. They are not required to keep the company loop moving.
20
+ - Only stop for Owner input when the next action requires external authority the harness cannot infer or obtain: credentials/secrets, payment approval, legal/business acceptance, production access not already granted, destructive data action, or a goal conflict that would knowingly violate the Owner's stated direction.
21
+ - When stopped for external authority, mark the mission `blocked` with the exact missing authority and the internally recommended default. Do not ask an open-ended question.
22
+
12
23
  ## Lazy Rule Loading
13
24
 
14
25
  Before routing or accepting CXX work, enforce lazy loading:
@@ -29,7 +40,7 @@ Before routing or accepting CXX work, enforce lazy loading:
29
40
  - CQO: quality gates, e2e/backtest strategy, regression and archive criteria.
30
41
  - OPS: build/service environment monitoring, CQO verification watch, port map checks, launch observation, production watch, and exception monitoring.
31
42
  4. Route completed outputs to the next responsible CXX.
32
- 5. Report outcomes and escalations to the Owner.
43
+ 5. Report outcomes, final acceptance requests, and true external-authority blocks to the Owner.
33
44
 
34
45
  ## Hard Rules
35
46
 
@@ -41,9 +52,9 @@ Before routing or accepting CXX work, enforce lazy loading:
41
52
  - Every CXX starts from fresh context and records decisions in `.harness/documents/{goal-or-child-mission}/{cxx}.md`.
42
53
  - Hiring or resource-manager output is never a stopping point. After missing workers are registered, immediately continue routing to the responsible CXX fresh sessions and require those CXX agents to brief/run the hired workers. Do not end the turn with only a hiring summary while the Owner goal remains unfinished.
43
54
  - Preserve DDD boundaries: domain decisions, application wiring, infrastructure, and quality policy are separate responsibilities.
44
- - Before CTO/CDO/OPS allocate runnable services, agree with the Owner on a `{xx}000` base port and write it to project `.env` as `HARNESS_BASE_PORT={xx}000`. Mentioning the value in `ceo.md` is not sufficient.
55
+ - Before CTO/CDO/OPS allocate runnable services, choose a `{xx}000` base port from available local evidence unless the Owner already specified one, then write it to project `.env` as `HARNESS_BASE_PORT={xx}000`. Mentioning the value in `ceo.md` is not sufficient.
45
56
  - After writing `.env`, verify with `grep '^HARNESS_BASE_PORT=' .env` before routing service work.
46
- - For service monitoring, collect the Owner's server mapping first: local PC, Docker, VM, AWS/cloud, host, port, health path, log path, and contact/source.
57
+ - For service monitoring, derive the server mapping from repository config, running processes, Docker, scripts, logs, and CXX reports first: local PC, Docker, VM, AWS/cloud, host, port, health path, log path, and contact/source. If a production target or credential is unavailable, record a BLOCKED external-authority item instead of asking a broad Owner question.
47
58
  - For runnable verification, collect or require CTO to record the test runtime mapping before CQO starts evaluator work: command/service name, cwd, host, port, health path if any, log path if any, and owner. OPS must watch that runtime during CQO Playwright/E2E/API/visual/performance/regression checks.
48
59
  - CEO must not accept CQO PASS for a runnable product unless OPS has supplied clean verification-watch evidence or an explicit not-applicable reason. Open OPS incidents, missing runtime mapping, missing required logs, service down, or health mismatch block Owner acceptance.
49
60
  - After launch, CEO treats OPS production incidents as company events. CEO convenes CTO/CQO/OPS when user-impacting production signals appear; CTO owns recovery, CQO owns regression confirmation, and OPS owns evidence and close criteria.
@@ -94,7 +105,7 @@ If CEO needs implementation done, CEO routes to CTO. CTO then hires dev workers.
94
105
  If CEO needs QA done, CEO routes to CQO. CQO then hires evaluator/tester workers.
95
106
  CEO does not contact workers. CXX contact workers.
96
107
 
97
- **When a CXX is unavailable or unresponsive:** escalate to the Owner. Do not act on their behalf.
108
+ **When a CXX is unavailable or unresponsive:** retry with a fresh role-scoped context, route to another relevant CXX for recovery planning, or record an internal blocker with evidence. Escalate to the Owner only when the blocker requires external authority listed in the Autonomous Operating Charter.
98
109
 
99
110
  ## Worktree Isolation Failure — No git Repo
100
111
 
@@ -17,7 +17,7 @@ OPS owns three environment classes:
17
17
 
18
18
  - Build environment: command-based local execution such as `flutter run`, `npm run dev`, test watchers, local build scripts, and other foreground/background commands used by CXX workers.
19
19
  - Verification environment: the runnable local, preview, Docker, or cloud target used while CQO evaluator workers run Playwright, E2E, API, visual, accessibility, performance, or regression checks.
20
- - Service environment: an actual server endpoint that can be monitored. The server may be the Owner's local PC, Docker, a VM, AWS, or another cloud/server target. OPS does not invent this mapping; CEO must collect it from the Owner and record it before OPS treats it as live.
20
+ - Service environment: an actual server endpoint that can be monitored. The server may be the Owner's local PC, Docker, a VM, AWS, or another cloud/server target. OPS does not invent this mapping; CEO/CTO/OPS must derive it from repo config, scripts, running processes, Docker, logs, and CXX reports before OPS treats it as live. Escalate to Owner only when external authority is missing, such as credentials or unavailable production access.
21
21
 
22
22
  OPS is not the implementation owner. CTO/DevOps workers start or change systems; OPS observes whether the declared build/service environments are healthy and raises evidence-backed events.
23
23
  OPS must not directly perform DevOps implementation, service fixes, config rewrites, deployment changes, or recovery work. OPS may only monitor, classify, brief hired Ops/DevOps workers, review their reports, and escalate evidence-backed events.
@@ -46,8 +46,8 @@ After service launch, OPS continues the same monitoring duty against `runtime.pr
46
46
 
47
47
  ## Port Policy
48
48
 
49
- - CEO must agree on a `{xx}000` base port with the Owner before CXX services are allocated.
50
- - The agreed base port is recorded in project `.env` as `HARNESS_BASE_PORT`.
49
+ - CEO/CTO/OPS must choose an available `{xx}000` base port before CXX services are allocated, unless the Owner already specified one.
50
+ - The selected base port is recorded in project `.env` as `HARNESS_BASE_PORT`.
51
51
  - Service-specific ports must be derived above that base port, for example `HARNESS_FRONTEND_PORT`, `HARNESS_API_PORT`, and `HARNESS_DASHBOARD_PORT`.
52
52
  - CXX agents must read `.env` or `.harness/config.json runtime.ports.base` before assigning ports.
53
53
  - OPS reports any service using an unmapped or off-range port as an ops drift.
package/README.md CHANGED
@@ -164,7 +164,7 @@ Run on a different port:
164
164
  bash scripts/harness-dashboard-up.sh --port 3002
165
165
  ```
166
166
 
167
- Force-refresh the isolated dashboard cache when an older dashboard is still shown:
167
+ Force-refresh the project-local dashboard runtime when an older dashboard is still shown:
168
168
 
169
169
  ```bash
170
170
  bash scripts/harness-dashboard-up.sh --port 3002 --reinstall
@@ -175,7 +175,9 @@ Dashboard options:
175
175
  | Option | Role |
176
176
  |---|---|
177
177
  | `--port <port>` | Run Brick Office on a custom port, e.g. `3002` |
178
- | `--reinstall` | Delete and rebuild the isolated dashboard cache under `~/.walwal-harness/dashboard/` |
178
+ | `--reinstall` | Delete and rebuild the project-local dashboard runtime under `.harness/dashboard/` |
179
+
180
+ The dashboard app copy lives under `.harness/dashboard/` for the current project. Generated dependencies and build output are ignored via `.gitignore`; the runtime files the dashboard reads remain the normal `.harness/` files.
179
181
 
180
182
  ### Company Auto-Chain State
181
183
 
@@ -283,6 +285,9 @@ harness-hiring → register and onboard worker
283
285
 
284
286
  | Version | Summary |
285
287
  |---|---|
288
+ | 7.1.37 | Project-local dashboard runtime under `.harness/dashboard/`, unfiltered all-mission heatmap, worker telemetry parsing, and more reliable hourly wake registration/status |
289
+ | 7.1.36 | Stop hook only validates worker evidence while chaining the active loop, scopes validation to the newest active mission, and makes the 24h heatmap visible at 10-minute resolution |
290
+ | 7.1.35 | Fix dashboard heatmap mission scoping, choose the newest active mission for activity recording, and normalize duplicate active child mission states during init/migrate |
286
291
  | 7.1.34 | Fix dashboard 500 when `company_state.workers` is recorded as a keyed worker map instead of an array |
287
292
  | 7.1.33 | Dashboard keeps recently written worker reports active even when docmeta is present, and CXX skills now record worker activity telemetry for live dashboard state |
288
293
  | 7.1.31 | Persist dashboard heatmap activity to 7-day JSONL logs, load persisted samples in the dashboard, and backfill activity during migrate from existing mission docs/progress logs |
@@ -59,8 +59,8 @@ type CxxRole = typeof CXX_ROLES[number];
59
59
  // Workers always sit under a non-CEO CXX (CEO delegates, never owns workers).
60
60
  const WORKER_OWNERS = ["coo", "cdo", "cto", "cqo", "ops"] as const;
61
61
  type WorkerOwnerRole = typeof WORKER_OWNERS[number];
62
- const BUCKET_MS = 60_000;
63
- const VISIBLE_BUCKETS = 24 * 60;
62
+ const BUCKET_MS = 10 * 60_000;
63
+ const VISIBLE_BUCKETS = 24 * 6;
64
64
  const LANE_LABEL_WIDTH = 110;
65
65
  const TIME_LABEL_HEIGHT = 18;
66
66
  const LANE_ROW_HEIGHT = 18;
@@ -116,7 +116,7 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
116
116
  );
117
117
  }, [snapshot.missions, selectedGoal]);
118
118
 
119
- const lanes = useMemo(() => buildLanes(snapshot, selectedMission), [selectedMission, snapshot]);
119
+ const lanes = useMemo(() => buildLanes(snapshot), [snapshot]);
120
120
  const heatmapStartedAt = useMemo(() => Date.now() - VISIBLE_BUCKETS * BUCKET_MS, [snapshot.ts]);
121
121
  const persistedHeatSamples = useMemo<HeatSample[]>(
122
122
  () =>
@@ -130,13 +130,13 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
130
130
  [snapshot.activitySamples]
131
131
  );
132
132
  const [heatSamples, setHeatSamples] = useState<HeatSample[]>([]);
133
- const sampleSourceRef = useRef({ lanes, snapshot, selectedMission });
133
+ const sampleSourceRef = useRef({ lanes, snapshot });
134
134
  useEffect(() => {
135
- sampleSourceRef.current = { lanes, snapshot, selectedMission };
136
- });
135
+ sampleSourceRef.current = { lanes, snapshot };
136
+ }, [lanes, snapshot]);
137
137
  useEffect(() => {
138
138
  const tick = () => {
139
- const { lanes, snapshot, selectedMission } = sampleSourceRef.current;
139
+ const { lanes, snapshot } = sampleSourceRef.current;
140
140
  // Gate live samples on the harness runtime — once the session reports
141
141
  // completion (or there's no current/next agent), every "active" signal
142
142
  // is stale (todos stay status="active", worker mtimes stay recent), so
@@ -156,7 +156,7 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
156
156
  }
157
157
  setHeatSamples((prev) => {
158
158
  const now = Date.now();
159
- const activeMission = selectedMission ?? snapshot.missions[0] ?? null;
159
+ const activeMission = snapshot.missions.find((mission) => mission.active) ?? snapshot.missions[0] ?? null;
160
160
  const next: HeatSample[] = [];
161
161
  for (const lane of lanes) {
162
162
  // Live overlay records active work only. Historical participation is
@@ -201,7 +201,12 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
201
201
  return () => clearInterval(id);
202
202
  }, []);
203
203
  const { cells: heatmap, totalBuckets } = useMemo(
204
- () => buildHeatmap(snapshot, lanes, [...persistedHeatSamples, ...heatSamples], heatmapStartedAt),
204
+ () => buildHeatmap(
205
+ snapshot,
206
+ lanes,
207
+ [...persistedHeatSamples, ...heatSamples],
208
+ heatmapStartedAt
209
+ ),
205
210
  [heatSamples, heatmapStartedAt, lanes, persistedHeatSamples, snapshot]
206
211
  );
207
212
  const [selectedLaneId, setSelectedLaneId] = useState(lanes[0]?.id ?? "ceo");
@@ -1374,7 +1379,8 @@ const CadencePromptTooltip = (() => {
1374
1379
 
1375
1380
  // ===== Heatmap =====
1376
1381
 
1377
- function formatMinuteOffset(minutes: number) {
1382
+ function formatBucketOffset(bucket: number) {
1383
+ const minutes = bucket * (BUCKET_MS / 60_000);
1378
1384
  if (minutes === 0) return "now";
1379
1385
  if (minutes < 60) return `-${minutes}m`;
1380
1386
  const h = Math.floor(minutes / 60);
@@ -1486,7 +1492,7 @@ function WorkflowHeatmap({
1486
1492
  className="grid place-items-center font-mono text-[9px] leading-none text-slate-500"
1487
1493
  style={{ gridColumn: idx + 2, gridRow: 1 }}
1488
1494
  >
1489
- {showLabel ? formatMinuteOffset(idx) : ""}
1495
+ {showLabel ? formatBucketOffset(idx) : ""}
1490
1496
  </div>
1491
1497
  );
1492
1498
  })}
@@ -1800,14 +1806,39 @@ function heatColor(count: number) {
1800
1806
 
1801
1807
  // ===== Lane / Heatmap builders =====
1802
1808
 
1803
- function buildLanes(snapshot: HarnessSnapshot, selectedMission: MissionDoc | null): AgentLane[] {
1804
- const activeMission = selectedMission ?? snapshot.missions[0] ?? null;
1809
+ function buildLanes(snapshot: HarnessSnapshot): AgentLane[] {
1810
+ const activeMission = snapshot.missions.find((mission) => mission.active) ?? snapshot.missions[0] ?? null;
1811
+ const workersByRole = new Map<CxxRole, Array<{ worker: WorkerDocEntry; mission: MissionDoc }>>();
1812
+ for (const role of CXX_ROLES) workersByRole.set(role, []);
1813
+
1814
+ const workerIndex = new Map<string, { worker: WorkerDocEntry; mission: MissionDoc }>();
1815
+ for (const mission of snapshot.missions) {
1816
+ for (const worker of mission.workers) {
1817
+ if (!CXX_ROLES.includes(worker.owner as CxxRole)) continue;
1818
+ const role = worker.owner as CxxRole;
1819
+ const key = `${role}:${worker.name}`;
1820
+ const existing = workerIndex.get(key);
1821
+ if (
1822
+ !existing ||
1823
+ (!existing.worker.active && worker.active) ||
1824
+ (existing.worker.status === "COMPLETE" && worker.status !== "COMPLETE")
1825
+ ) {
1826
+ workerIndex.set(key, { worker, mission });
1827
+ }
1828
+ }
1829
+ }
1830
+
1831
+ for (const entry of workerIndex.values()) {
1832
+ workersByRole.get(entry.worker.owner as CxxRole)?.push(entry);
1833
+ }
1834
+
1805
1835
  // CXX → its workers, then next CXX → its workers (proper hierarchy).
1806
1836
  // No cap: heatmap container scrolls vertically so OPS at the bottom is always
1807
1837
  // reachable even when upstream CXX bring many workers.
1808
1838
  const lanes: AgentLane[] = [];
1809
1839
  for (const role of CXX_ROLES) {
1810
- const workers = activeMission?.workers.filter((w) => w.owner === role) ?? [];
1840
+ const workerEntries = workersByRole.get(role) ?? [];
1841
+ const workers = workerEntries.map((entry) => entry.worker);
1811
1842
  const todos = snapshot.todos.filter((t) => t.owner === role);
1812
1843
  const hasError =
1813
1844
  (role === "ops" && snapshot.incidents.length > 0) ||
@@ -1826,7 +1857,7 @@ function buildLanes(snapshot: HarnessSnapshot, selectedMission: MissionDoc | nul
1826
1857
  workers,
1827
1858
  mission: activeMission,
1828
1859
  });
1829
- for (const worker of workers) {
1860
+ for (const { worker, mission } of workerEntries) {
1830
1861
  lanes.push({
1831
1862
  id: `${role}:${worker.name}`,
1832
1863
  label: `└ ${worker.displayName.slice(0, 16)}`,
@@ -1837,7 +1868,7 @@ function buildLanes(snapshot: HarnessSnapshot, selectedMission: MissionDoc | nul
1837
1868
  todos: 0,
1838
1869
  workers: [],
1839
1870
  worker,
1840
- mission: activeMission,
1871
+ mission,
1841
1872
  });
1842
1873
  }
1843
1874
  }
@@ -1895,7 +1926,7 @@ function buildHeatmap(
1895
1926
  y,
1896
1927
  laneId: lane.id,
1897
1928
  laneLabel: lane.label,
1898
- bucketLabel: formatMinuteOffset(x),
1929
+ bucketLabel: formatBucketOffset(x),
1899
1930
  count: agg?.count ?? 0,
1900
1931
  hotfix: agg?.hotfix ?? false,
1901
1932
  mission: missionId ? missionById.get(missionId) ?? null : null,
@@ -382,7 +382,7 @@ describe("readHarnessState", () => {
382
382
  });
383
383
  });
384
384
 
385
- it("keeps recently written docmeta worker reports active without an explicit COMPLETE marker", () => {
385
+ it("does not treat a recently written docmeta draft as live worker activity without runtime state", () => {
386
386
  const harnessDir = path.join(dir, ".harness");
387
387
  const missionDir = path.join(harnessDir, "documents", "goal-1-dashboard");
388
388
  mkdirSync(path.join(missionDir, "cto", "workers"), { recursive: true });
@@ -403,9 +403,42 @@ describe("readHarnessState", () => {
403
403
  const workers = readHarnessState(dir).missions[0].workers;
404
404
  expect(workers[0]).toMatchObject({
405
405
  name: "frontend-worker",
406
- status: "IN_PROGRESS",
407
- active: true,
406
+ status: "COMPLETE",
407
+ active: false,
408
+ });
409
+ });
410
+
411
+ it("reads manual worker telemetry with name, progress, eta, and report_path", () => {
412
+ const harnessDir = path.join(dir, ".harness");
413
+ mkdirSync(harnessDir, { recursive: true });
414
+ writeFileSync(
415
+ path.join(harnessDir, "progress.json"),
416
+ JSON.stringify({
417
+ agent_status: "running",
418
+ company_state: {
419
+ active_workers: 2,
420
+ workers: [
421
+ {
422
+ name: "frontend-engineer-multi-strategy",
423
+ status: "running",
424
+ progress: "70%",
425
+ eta: "2026-05-31 17:00",
426
+ report_path: ".harness/documents/goal-1/cto/workers/frontend-engineer-multi-strategy.md",
427
+ },
428
+ ],
429
+ },
430
+ })
431
+ );
432
+
433
+ const worker = readHarnessState(dir).dashboard.workers[0];
434
+ expect(worker).toMatchObject({
435
+ agent: "frontend-engineer-multi-strategy",
436
+ feature: "frontend-engineer-multi-strategy",
437
+ status: "running",
438
+ log: ".harness/documents/goal-1/cto/workers/frontend-engineer-multi-strategy.md",
439
+ progress: 0.7,
408
440
  });
441
+ expect(worker.summary).toContain("2026-05-31 17:00");
409
442
  });
410
443
 
411
444
  it("accepts keyed company_state.workers maps from manual CXX telemetry", () => {
@@ -45,7 +45,6 @@ import type {
45
45
 
46
46
  const SNAPSHOT_VERSION = "1.2.0";
47
47
  const GOAL_DESC_TRUNCATE = 200;
48
- const RECENT_WORKER_ACTIVE_MS = 10 * 60 * 1000;
49
48
  const ACTIVITY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
50
49
 
51
50
  interface RawProgress {
@@ -148,22 +147,31 @@ interface RawProgress {
148
147
  company_state?: {
149
148
  active_workers?: number;
150
149
  workers?: Array<{
150
+ name?: string;
151
151
  team?: number | string;
152
152
  feature?: string;
153
153
  agent?: string;
154
154
  phase?: string;
155
155
  prompt?: string | null;
156
156
  log?: string | null;
157
+ report?: string | null;
158
+ report_path?: string | null;
159
+ progress?: string | number | null;
160
+ eta?: string | null;
157
161
  spawn_status?: string;
158
162
  status?: string;
159
163
  pid?: number | null;
160
164
  }> | Record<string, {
165
+ name?: string;
161
166
  team?: number | string;
162
167
  feature?: string;
163
168
  agent?: string;
164
169
  phase?: string;
165
170
  prompt?: string | null;
166
171
  log?: string | null;
172
+ report_path?: string | null;
173
+ progress?: string | number | null;
174
+ eta?: string | null;
167
175
  spawn_status?: string;
168
176
  status?: string;
169
177
  pid?: number | null;
@@ -797,6 +805,7 @@ function readFeatureTitles(rootDir: string): Map<string, string> {
797
805
  }
798
806
 
799
807
  interface RawWorker {
808
+ name?: string;
800
809
  team?: number | string;
801
810
  feature?: string;
802
811
  agent?: string;
@@ -808,6 +817,9 @@ interface RawWorker {
808
817
  pid?: number | null;
809
818
  owner?: string;
810
819
  report?: string | null;
820
+ report_path?: string | null;
821
+ progress?: string | number | null;
822
+ eta?: string | null;
811
823
  }
812
824
  type WorkerOwner = WorkerDocEntry["owner"];
813
825
 
@@ -859,6 +871,14 @@ function readMissionState(missionPath: string): {
859
871
  }
860
872
 
861
873
  function deriveWorkerProgress(worker: RawWorker): number | null {
874
+ const explicit = worker?.progress;
875
+ if (typeof explicit === "number" && Number.isFinite(explicit)) {
876
+ return explicit > 1 ? Math.max(0, Math.min(1, explicit / 100)) : Math.max(0, Math.min(1, explicit));
877
+ }
878
+ if (typeof explicit === "string") {
879
+ const match = explicit.match(/(\d+(?:\.\d+)?)/);
880
+ if (match) return Math.max(0, Math.min(1, Number(match[1]) / 100));
881
+ }
862
882
  const status = (worker?.status ?? worker?.spawn_status ?? "").toLowerCase();
863
883
  if (status === "blocked" || status === "failed") return 0.35;
864
884
  if (status === "recorded") return 0.15;
@@ -868,17 +888,24 @@ function deriveWorkerProgress(worker: RawWorker): number | null {
868
888
  }
869
889
 
870
890
  function normalizeRawWorkers(value: RawWorker[] | Record<string, RawWorker> | null | undefined): RawWorker[] {
871
- if (Array.isArray(value)) return value;
891
+ const normalizeOne = (worker: RawWorker, fallbackName: string | null = null, idx = 0): RawWorker => {
892
+ const name = worker.name ?? worker.agent ?? worker.feature ?? fallbackName ?? `worker-${idx + 1}`;
893
+ const report = worker.report ?? worker.report_path ?? null;
894
+ return {
895
+ ...worker,
896
+ name,
897
+ team: worker.team ?? idx + 1,
898
+ agent: worker.agent ?? name,
899
+ feature: worker.feature ?? name,
900
+ log: worker.log ?? report,
901
+ report,
902
+ };
903
+ };
904
+ if (Array.isArray(value)) return value.map((worker, idx) => normalizeOne(worker, null, idx));
872
905
  if (!value || typeof value !== "object") return [];
873
906
  return Object.entries(value).map(([name, worker], idx) => {
874
907
  const w = worker && typeof worker === "object" ? worker : {};
875
- return {
876
- ...w,
877
- team: (w as RawWorker).team ?? idx + 1,
878
- agent: (w as RawWorker).agent ?? name,
879
- feature: (w as RawWorker).feature ?? name,
880
- log: (w as RawWorker).log ?? (w as RawWorker & { report?: string | null }).report ?? null,
881
- };
908
+ return normalizeOne(w as RawWorker, name, idx);
882
909
  });
883
910
  }
884
911
 
@@ -911,7 +938,8 @@ function buildWorkers(rootDir: string, progress: RawProgress | null): WorkerSnap
911
938
  const feature = w.feature ?? `worker-${idx + 1}`;
912
939
  const promptText = w.prompt ? readTextSafe(path.join(rootDir, w.prompt)) : null;
913
940
  const logText = w.log ? readTextSafe(path.join(rootDir, w.log)) : null;
914
- const summarySource = firstLine(logText) || firstLine(promptText) || `${w.agent ?? "worker"} assigned`;
941
+ const eta = w.eta ? ` ETA ${w.eta}` : "";
942
+ const summarySource = firstLine(logText) || firstLine(promptText) || `${w.agent ?? w.name ?? "worker"} ${w.status ?? "assigned"}${eta}`;
915
943
  const material = w.prompt ?? w.log ?? null;
916
944
  const rawStatus = (w.status ?? w.spawn_status ?? "unknown").toLowerCase();
917
945
  const status = ["spawned", "recorded", "running", "idle", "blocked"].includes(rawStatus)
@@ -1036,13 +1064,13 @@ function activeWorkerNames(progress: RawProgress | null): Set<string> {
1036
1064
  if (isHarnessRuntimeIdle(progress)) return active;
1037
1065
  const workers = [
1038
1066
  ...normalizeRawWorkers(progress?.company_state?.workers),
1039
- ...(progress?.company_state?.last_dispatch ?? []),
1067
+ ...normalizeRawWorkers(progress?.company_state?.last_dispatch),
1040
1068
  ];
1041
1069
  for (const worker of workers) {
1042
1070
  const rawStatus = (worker.status ?? worker.spawn_status ?? "").toLowerCase();
1043
1071
  if (["complete", "completed", "done", "idle"].includes(rawStatus)) continue;
1044
- for (const value of [worker.agent, worker.feature]) {
1045
- const normalized = normalizeWorkerName(value);
1072
+ for (const value of [worker.name, worker.agent, worker.feature, worker.report, worker.report_path, worker.log]) {
1073
+ const normalized = normalizeWorkerName(value && /[\\/]/.test(value) ? path.basename(value) : value);
1046
1074
  if (normalized) active.add(normalized);
1047
1075
  }
1048
1076
  }
@@ -1262,21 +1290,17 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
1262
1290
  const name = normalizeWorkerName(f);
1263
1291
  const hired = hiredForMission(hiredWorkers, name, owner, rel);
1264
1292
  let updatedAt: string | null = null;
1265
- let recentlyTouched = false;
1266
1293
  try {
1267
1294
  const mtime = statSync(reportAbs).mtime;
1268
1295
  updatedAt = mtime.toISOString();
1269
- recentlyTouched = Date.now() - mtime.getTime() < RECENT_WORKER_ACTIVE_MS;
1270
1296
  } catch { /* ignore */ }
1271
1297
  const statusMatch = content.match(/##\s*Status\s*\n+([A-Z_]+)/);
1272
1298
  const hasDocmeta = /^---[\s\S]*?docmeta:[\s\S]*?---/.test(content);
1273
- // Worker docs often write docmeta before appending final evidence.
1274
- // Without an explicit COMPLETE marker, keep recent writes visible as
1275
- // active; after the activity window they become historical reports.
1299
+ // Runtime state is the source of truth for live activity. A
1300
+ // recently touched docmeta-only draft is not enough to prove that a
1301
+ // detached worker session is still running.
1276
1302
  const status: WorkerDocEntry["status"] = statusMatch
1277
1303
  ? (statusMatch[1] as WorkerDocEntry["status"])
1278
- : hasDocmeta && recentlyTouched && !runtimeIdle
1279
- ? "IN_PROGRESS"
1280
1304
  : hasDocmeta
1281
1305
  ? "COMPLETE"
1282
1306
  : "unknown";