@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 +19 -0
- package/HR-Resource/ceo/SKILL.md +15 -4
- package/HR-Resource/ops/SKILL.md +3 -3
- package/README.md +7 -2
- package/apps/harness-dashboard/components/Scene.tsx +48 -17
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +36 -3
- package/apps/harness-dashboard/lib/harness-state.ts +44 -20
- package/apps/harness-dashboard/package-lock.json +42 -43
- package/assets/templates/AGENTS-ko.md.template +4 -0
- package/assets/templates/AGENTS.md.template +7 -3
- package/assets/templates/config.json +1 -1
- package/assets/templates/gitignore-append.txt +6 -1
- package/assets/templates/memory.md +1 -1
- package/bin/init.js +49 -4
- package/commands/goal.md +2 -1
- package/commands/hot-fix.md +2 -1
- package/commands/submission.md +3 -2
- package/conventions/ceo.md +2 -0
- package/gotchas/ceo.md +4 -0
- package/package.json +1 -1
- package/scripts/harness-activity-record.js +6 -1
- package/scripts/harness-dashboard-up.sh +36 -8
- package/scripts/harness-hourly-review.sh +1 -1
- package/scripts/harness-service-ops-monitor.sh +2 -2
- package/scripts/harness-stop.sh +18 -17
- package/scripts/harness-user-prompt-submit.sh +2 -0
- package/scripts/harness-wake-install.sh +68 -15
- package/scripts/harness-worker-evidence-validate.sh +35 -0
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
|
package/HR-Resource/ceo/SKILL.md
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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:**
|
|
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
|
|
package/HR-Resource/ops/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
50
|
-
- The
|
|
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
|
|
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
|
|
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 *
|
|
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
|
|
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
|
|
133
|
+
const sampleSourceRef = useRef({ lanes, snapshot });
|
|
134
134
|
useEffect(() => {
|
|
135
|
-
sampleSourceRef.current = { lanes, snapshot
|
|
136
|
-
});
|
|
135
|
+
sampleSourceRef.current = { lanes, snapshot };
|
|
136
|
+
}, [lanes, snapshot]);
|
|
137
137
|
useEffect(() => {
|
|
138
138
|
const tick = () => {
|
|
139
|
-
const { lanes, snapshot
|
|
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 =
|
|
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(
|
|
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
|
|
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 ?
|
|
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
|
|
1804
|
-
const activeMission =
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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("
|
|
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: "
|
|
407
|
-
active:
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
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";
|