@walwal-harness/cli 7.1.33 → 7.1.36
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 +12 -0
- package/README.md +3 -0
- package/apps/harness-dashboard/components/Scene.tsx +24 -7
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +37 -0
- package/apps/harness-dashboard/lib/harness-state.ts +44 -5
- package/apps/harness-dashboard/package-lock.json +42 -43
- package/bin/init.js +24 -2
- package/package.json +1 -1
- package/scripts/harness-activity-record.js +6 -1
- package/scripts/harness-stop.sh +18 -17
- package/scripts/harness-worker-evidence-validate.sh +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -31,6 +31,18 @@ docmeta:
|
|
|
31
31
|
|
|
32
32
|
## Unreleased
|
|
33
33
|
|
|
34
|
+
## 7.1.36 — Stop hook active-loop guard + visible heatmap (2026-05-29)
|
|
35
|
+
|
|
36
|
+
- Stop hook no longer runs worker evidence validation before deciding whether the company loop should chain.
|
|
37
|
+
- Worker evidence validation can be scoped to the newest active mission so legacy/archive mission records cannot keep Stop blocked forever.
|
|
38
|
+
- Dashboard heatmap now uses 10-minute buckets across 24 hours, making activity visible without horizontal scrolling through 1440 minute cells.
|
|
39
|
+
|
|
40
|
+
## 7.1.35 — Dashboard heatmap mission scoping (2026-05-29)
|
|
41
|
+
|
|
42
|
+
- Dashboard heatmap samples are now scoped to the selected mission, so worker rows no longer inherit activity from unrelated stale-active missions.
|
|
43
|
+
- Activity recording now chooses the newest active mission when legacy runtime state contains multiple `active:true` mission-state files.
|
|
44
|
+
- Init/migrate now deactivates duplicate active submission/hot-fix children under the same goal instead of only creating missing mission-state files.
|
|
45
|
+
|
|
34
46
|
## 7.1.26 — Lazy rule links + MCP steward (2026-05-27)
|
|
35
47
|
|
|
36
48
|
### Added
|
package/README.md
CHANGED
|
@@ -283,6 +283,9 @@ harness-hiring → register and onboard worker
|
|
|
283
283
|
|
|
284
284
|
| Version | Summary |
|
|
285
285
|
|---|---|
|
|
286
|
+
| 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 |
|
|
287
|
+
| 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 |
|
|
288
|
+
| 7.1.34 | Fix dashboard 500 when `company_state.workers` is recorded as a keyed worker map instead of an array |
|
|
286
289
|
| 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 |
|
|
287
290
|
| 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 |
|
|
288
291
|
| 7.1.29 | Idempotent AGENTS/CLAUDE doc merge with marked harness sections, mission lifecycle backfill for init/migrate, completion closes todos and mission-state, and Layer Activity now lists CXX/worker rows sorted by remaining work |
|
|
@@ -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;
|
|
@@ -201,8 +201,13 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
|
|
|
201
201
|
return () => clearInterval(id);
|
|
202
202
|
}, []);
|
|
203
203
|
const { cells: heatmap, totalBuckets } = useMemo(
|
|
204
|
-
() => buildHeatmap(
|
|
205
|
-
|
|
204
|
+
() => buildHeatmap(
|
|
205
|
+
snapshot,
|
|
206
|
+
lanes,
|
|
207
|
+
filterHeatSamplesForMission([...persistedHeatSamples, ...heatSamples], selectedMission),
|
|
208
|
+
heatmapStartedAt
|
|
209
|
+
),
|
|
210
|
+
[heatSamples, heatmapStartedAt, lanes, persistedHeatSamples, selectedMission, snapshot]
|
|
206
211
|
);
|
|
207
212
|
const [selectedLaneId, setSelectedLaneId] = useState(lanes[0]?.id ?? "ceo");
|
|
208
213
|
const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null);
|
|
@@ -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
|
})}
|
|
@@ -1844,6 +1850,17 @@ function buildLanes(snapshot: HarnessSnapshot, selectedMission: MissionDoc | nul
|
|
|
1844
1850
|
return lanes;
|
|
1845
1851
|
}
|
|
1846
1852
|
|
|
1853
|
+
function filterHeatSamplesForMission(samples: HeatSample[], selectedMission: MissionDoc | null) {
|
|
1854
|
+
if (!selectedMission) return samples;
|
|
1855
|
+
return samples.filter((sample) => {
|
|
1856
|
+
if (sample.missionId) return sample.missionId === selectedMission.missionId;
|
|
1857
|
+
// Legacy CXX samples from progress.log did not carry a mission id. Keep
|
|
1858
|
+
// role-level history, but do not project legacy worker rows onto unrelated
|
|
1859
|
+
// mission-specific worker lanes.
|
|
1860
|
+
return !sample.laneId.includes(":");
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1847
1864
|
function buildHeatmap(
|
|
1848
1865
|
snapshot: HarnessSnapshot,
|
|
1849
1866
|
lanes: AgentLane[],
|
|
@@ -1895,7 +1912,7 @@ function buildHeatmap(
|
|
|
1895
1912
|
y,
|
|
1896
1913
|
laneId: lane.id,
|
|
1897
1914
|
laneLabel: lane.label,
|
|
1898
|
-
bucketLabel:
|
|
1915
|
+
bucketLabel: formatBucketOffset(x),
|
|
1899
1916
|
count: agg?.count ?? 0,
|
|
1900
1917
|
hotfix: agg?.hotfix ?? false,
|
|
1901
1918
|
mission: missionId ? missionById.get(missionId) ?? null : null,
|
|
@@ -407,4 +407,41 @@ describe("readHarnessState", () => {
|
|
|
407
407
|
active: true,
|
|
408
408
|
});
|
|
409
409
|
});
|
|
410
|
+
|
|
411
|
+
it("accepts keyed company_state.workers maps from manual CXX telemetry", () => {
|
|
412
|
+
const harnessDir = path.join(dir, ".harness");
|
|
413
|
+
const missionDir = path.join(harnessDir, "documents", "goal-1-dashboard");
|
|
414
|
+
mkdirSync(path.join(missionDir, "cqo", "workers"), { recursive: true });
|
|
415
|
+
writeFileSync(path.join(missionDir, "ceo.md"), "# Dashboard\n");
|
|
416
|
+
writeFileSync(
|
|
417
|
+
path.join(missionDir, "cqo", "workers", "evaluator.md"),
|
|
418
|
+
"---\ndocmeta:\n id: evaluator\n---\n\n# Worker Report\n"
|
|
419
|
+
);
|
|
420
|
+
writeFileSync(
|
|
421
|
+
path.join(harnessDir, "progress.json"),
|
|
422
|
+
JSON.stringify({
|
|
423
|
+
agent_status: "running",
|
|
424
|
+
company_state: {
|
|
425
|
+
active_workers: 1,
|
|
426
|
+
workers: {
|
|
427
|
+
"engineering-engineering-code-reviewer": {
|
|
428
|
+
owner: "cqo",
|
|
429
|
+
report: ".harness/documents/goal-1-dashboard/cqo/workers/evaluator.md",
|
|
430
|
+
status: "running",
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const snap = readHarnessState(dir);
|
|
438
|
+
expect(snap.dashboard.workers[0]).toMatchObject({
|
|
439
|
+
agent: "engineering-engineering-code-reviewer",
|
|
440
|
+
status: "running",
|
|
441
|
+
});
|
|
442
|
+
expect(snap.missions[0].workers[0]).toMatchObject({
|
|
443
|
+
owner: "cqo",
|
|
444
|
+
active: true,
|
|
445
|
+
});
|
|
446
|
+
});
|
|
410
447
|
});
|
|
@@ -157,6 +157,18 @@ interface RawProgress {
|
|
|
157
157
|
spawn_status?: string;
|
|
158
158
|
status?: string;
|
|
159
159
|
pid?: number | null;
|
|
160
|
+
}> | Record<string, {
|
|
161
|
+
team?: number | string;
|
|
162
|
+
feature?: string;
|
|
163
|
+
agent?: string;
|
|
164
|
+
phase?: string;
|
|
165
|
+
prompt?: string | null;
|
|
166
|
+
log?: string | null;
|
|
167
|
+
spawn_status?: string;
|
|
168
|
+
status?: string;
|
|
169
|
+
pid?: number | null;
|
|
170
|
+
owner?: string;
|
|
171
|
+
report?: string | null;
|
|
160
172
|
}>;
|
|
161
173
|
last_dispatch?: Array<{
|
|
162
174
|
team?: number | string;
|
|
@@ -784,7 +796,19 @@ function readFeatureTitles(rootDir: string): Map<string, string> {
|
|
|
784
796
|
return map;
|
|
785
797
|
}
|
|
786
798
|
|
|
787
|
-
|
|
799
|
+
interface RawWorker {
|
|
800
|
+
team?: number | string;
|
|
801
|
+
feature?: string;
|
|
802
|
+
agent?: string;
|
|
803
|
+
phase?: string;
|
|
804
|
+
prompt?: string | null;
|
|
805
|
+
log?: string | null;
|
|
806
|
+
spawn_status?: string;
|
|
807
|
+
status?: string;
|
|
808
|
+
pid?: number | null;
|
|
809
|
+
owner?: string;
|
|
810
|
+
report?: string | null;
|
|
811
|
+
}
|
|
788
812
|
type WorkerOwner = WorkerDocEntry["owner"];
|
|
789
813
|
|
|
790
814
|
interface HiredWorkerEntry {
|
|
@@ -839,13 +863,28 @@ function deriveWorkerProgress(worker: RawWorker): number | null {
|
|
|
839
863
|
if (status === "blocked" || status === "failed") return 0.35;
|
|
840
864
|
if (status === "recorded") return 0.15;
|
|
841
865
|
if (status === "spawned" || status === "running") return 0.55;
|
|
842
|
-
if (status === "completed" || status === "done") return 1;
|
|
866
|
+
if (status === "complete" || status === "completed" || status === "done") return 1;
|
|
843
867
|
return null;
|
|
844
868
|
}
|
|
845
869
|
|
|
870
|
+
function normalizeRawWorkers(value: RawWorker[] | Record<string, RawWorker> | null | undefined): RawWorker[] {
|
|
871
|
+
if (Array.isArray(value)) return value;
|
|
872
|
+
if (!value || typeof value !== "object") return [];
|
|
873
|
+
return Object.entries(value).map(([name, worker], idx) => {
|
|
874
|
+
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
|
+
};
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
846
885
|
function buildWorkers(rootDir: string, progress: RawProgress | null): WorkerSnapshot[] {
|
|
847
886
|
const titles = readFeatureTitles(rootDir);
|
|
848
|
-
let workers = progress?.company_state?.workers
|
|
887
|
+
let workers = normalizeRawWorkers(progress?.company_state?.workers);
|
|
849
888
|
if (workers.length === 0) {
|
|
850
889
|
const queue = readJsonSafe<{
|
|
851
890
|
teams?: Record<string, {
|
|
@@ -996,12 +1035,12 @@ function activeWorkerNames(progress: RawProgress | null): Set<string> {
|
|
|
996
1035
|
const active = new Set<string>();
|
|
997
1036
|
if (isHarnessRuntimeIdle(progress)) return active;
|
|
998
1037
|
const workers = [
|
|
999
|
-
...(progress?.company_state?.workers
|
|
1038
|
+
...normalizeRawWorkers(progress?.company_state?.workers),
|
|
1000
1039
|
...(progress?.company_state?.last_dispatch ?? []),
|
|
1001
1040
|
];
|
|
1002
1041
|
for (const worker of workers) {
|
|
1003
1042
|
const rawStatus = (worker.status ?? worker.spawn_status ?? "").toLowerCase();
|
|
1004
|
-
if (["completed", "done", "idle"].includes(rawStatus)) continue;
|
|
1043
|
+
if (["complete", "completed", "done", "idle"].includes(rawStatus)) continue;
|
|
1005
1044
|
for (const value of [worker.agent, worker.feature]) {
|
|
1006
1045
|
const normalized = normalizeWorkerName(value);
|
|
1007
1046
|
if (normalized) active.add(normalized);
|
|
@@ -985,15 +985,15 @@
|
|
|
985
985
|
}
|
|
986
986
|
},
|
|
987
987
|
"node_modules/@next/env": {
|
|
988
|
-
"version": "15.5.
|
|
989
|
-
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.
|
|
990
|
-
"integrity": "sha512-
|
|
988
|
+
"version": "15.5.18",
|
|
989
|
+
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.18.tgz",
|
|
990
|
+
"integrity": "sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==",
|
|
991
991
|
"license": "MIT"
|
|
992
992
|
},
|
|
993
993
|
"node_modules/@next/swc-darwin-arm64": {
|
|
994
|
-
"version": "15.5.
|
|
995
|
-
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.
|
|
996
|
-
"integrity": "sha512-
|
|
994
|
+
"version": "15.5.18",
|
|
995
|
+
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.18.tgz",
|
|
996
|
+
"integrity": "sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==",
|
|
997
997
|
"cpu": [
|
|
998
998
|
"arm64"
|
|
999
999
|
],
|
|
@@ -1007,9 +1007,9 @@
|
|
|
1007
1007
|
}
|
|
1008
1008
|
},
|
|
1009
1009
|
"node_modules/@next/swc-darwin-x64": {
|
|
1010
|
-
"version": "15.5.
|
|
1011
|
-
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.
|
|
1012
|
-
"integrity": "sha512-
|
|
1010
|
+
"version": "15.5.18",
|
|
1011
|
+
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.18.tgz",
|
|
1012
|
+
"integrity": "sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==",
|
|
1013
1013
|
"cpu": [
|
|
1014
1014
|
"x64"
|
|
1015
1015
|
],
|
|
@@ -1023,9 +1023,9 @@
|
|
|
1023
1023
|
}
|
|
1024
1024
|
},
|
|
1025
1025
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
|
1026
|
-
"version": "15.5.
|
|
1027
|
-
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.
|
|
1028
|
-
"integrity": "sha512-
|
|
1026
|
+
"version": "15.5.18",
|
|
1027
|
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.18.tgz",
|
|
1028
|
+
"integrity": "sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==",
|
|
1029
1029
|
"cpu": [
|
|
1030
1030
|
"arm64"
|
|
1031
1031
|
],
|
|
@@ -1039,9 +1039,9 @@
|
|
|
1039
1039
|
}
|
|
1040
1040
|
},
|
|
1041
1041
|
"node_modules/@next/swc-linux-arm64-musl": {
|
|
1042
|
-
"version": "15.5.
|
|
1043
|
-
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.
|
|
1044
|
-
"integrity": "sha512-
|
|
1042
|
+
"version": "15.5.18",
|
|
1043
|
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.18.tgz",
|
|
1044
|
+
"integrity": "sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==",
|
|
1045
1045
|
"cpu": [
|
|
1046
1046
|
"arm64"
|
|
1047
1047
|
],
|
|
@@ -1055,9 +1055,9 @@
|
|
|
1055
1055
|
}
|
|
1056
1056
|
},
|
|
1057
1057
|
"node_modules/@next/swc-linux-x64-gnu": {
|
|
1058
|
-
"version": "15.5.
|
|
1059
|
-
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.
|
|
1060
|
-
"integrity": "sha512-
|
|
1058
|
+
"version": "15.5.18",
|
|
1059
|
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.18.tgz",
|
|
1060
|
+
"integrity": "sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==",
|
|
1061
1061
|
"cpu": [
|
|
1062
1062
|
"x64"
|
|
1063
1063
|
],
|
|
@@ -1071,9 +1071,9 @@
|
|
|
1071
1071
|
}
|
|
1072
1072
|
},
|
|
1073
1073
|
"node_modules/@next/swc-linux-x64-musl": {
|
|
1074
|
-
"version": "15.5.
|
|
1075
|
-
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.
|
|
1076
|
-
"integrity": "sha512-
|
|
1074
|
+
"version": "15.5.18",
|
|
1075
|
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.18.tgz",
|
|
1076
|
+
"integrity": "sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==",
|
|
1077
1077
|
"cpu": [
|
|
1078
1078
|
"x64"
|
|
1079
1079
|
],
|
|
@@ -1087,9 +1087,9 @@
|
|
|
1087
1087
|
}
|
|
1088
1088
|
},
|
|
1089
1089
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
|
1090
|
-
"version": "15.5.
|
|
1091
|
-
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.
|
|
1092
|
-
"integrity": "sha512-
|
|
1090
|
+
"version": "15.5.18",
|
|
1091
|
+
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.18.tgz",
|
|
1092
|
+
"integrity": "sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==",
|
|
1093
1093
|
"cpu": [
|
|
1094
1094
|
"arm64"
|
|
1095
1095
|
],
|
|
@@ -1103,9 +1103,9 @@
|
|
|
1103
1103
|
}
|
|
1104
1104
|
},
|
|
1105
1105
|
"node_modules/@next/swc-win32-x64-msvc": {
|
|
1106
|
-
"version": "15.5.
|
|
1107
|
-
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.
|
|
1108
|
-
"integrity": "sha512-
|
|
1106
|
+
"version": "15.5.18",
|
|
1107
|
+
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.18.tgz",
|
|
1108
|
+
"integrity": "sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==",
|
|
1109
1109
|
"cpu": [
|
|
1110
1110
|
"x64"
|
|
1111
1111
|
],
|
|
@@ -1687,9 +1687,9 @@
|
|
|
1687
1687
|
"license": "MIT"
|
|
1688
1688
|
},
|
|
1689
1689
|
"node_modules/@types/node": {
|
|
1690
|
-
"version": "22.19.
|
|
1691
|
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.
|
|
1692
|
-
"integrity": "sha512-
|
|
1690
|
+
"version": "22.19.18",
|
|
1691
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz",
|
|
1692
|
+
"integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==",
|
|
1693
1693
|
"dev": true,
|
|
1694
1694
|
"license": "MIT",
|
|
1695
1695
|
"dependencies": {
|
|
@@ -2660,7 +2660,6 @@
|
|
|
2660
2660
|
"version": "2.3.2",
|
|
2661
2661
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
2662
2662
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
2663
|
-
"dev": true,
|
|
2664
2663
|
"hasInstallScript": true,
|
|
2665
2664
|
"license": "MIT",
|
|
2666
2665
|
"optional": true,
|
|
@@ -3948,12 +3947,12 @@
|
|
|
3948
3947
|
}
|
|
3949
3948
|
},
|
|
3950
3949
|
"node_modules/next": {
|
|
3951
|
-
"version": "15.5.
|
|
3952
|
-
"resolved": "https://registry.npmjs.org/next/-/next-15.5.
|
|
3953
|
-
"integrity": "sha512-
|
|
3950
|
+
"version": "15.5.18",
|
|
3951
|
+
"resolved": "https://registry.npmjs.org/next/-/next-15.5.18.tgz",
|
|
3952
|
+
"integrity": "sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==",
|
|
3954
3953
|
"license": "MIT",
|
|
3955
3954
|
"dependencies": {
|
|
3956
|
-
"@next/env": "15.5.
|
|
3955
|
+
"@next/env": "15.5.18",
|
|
3957
3956
|
"@swc/helpers": "0.5.15",
|
|
3958
3957
|
"caniuse-lite": "^1.0.30001579",
|
|
3959
3958
|
"postcss": "8.4.31",
|
|
@@ -3966,14 +3965,14 @@
|
|
|
3966
3965
|
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
|
3967
3966
|
},
|
|
3968
3967
|
"optionalDependencies": {
|
|
3969
|
-
"@next/swc-darwin-arm64": "15.5.
|
|
3970
|
-
"@next/swc-darwin-x64": "15.5.
|
|
3971
|
-
"@next/swc-linux-arm64-gnu": "15.5.
|
|
3972
|
-
"@next/swc-linux-arm64-musl": "15.5.
|
|
3973
|
-
"@next/swc-linux-x64-gnu": "15.5.
|
|
3974
|
-
"@next/swc-linux-x64-musl": "15.5.
|
|
3975
|
-
"@next/swc-win32-arm64-msvc": "15.5.
|
|
3976
|
-
"@next/swc-win32-x64-msvc": "15.5.
|
|
3968
|
+
"@next/swc-darwin-arm64": "15.5.18",
|
|
3969
|
+
"@next/swc-darwin-x64": "15.5.18",
|
|
3970
|
+
"@next/swc-linux-arm64-gnu": "15.5.18",
|
|
3971
|
+
"@next/swc-linux-arm64-musl": "15.5.18",
|
|
3972
|
+
"@next/swc-linux-x64-gnu": "15.5.18",
|
|
3973
|
+
"@next/swc-linux-x64-musl": "15.5.18",
|
|
3974
|
+
"@next/swc-win32-arm64-msvc": "15.5.18",
|
|
3975
|
+
"@next/swc-win32-x64-msvc": "15.5.18",
|
|
3977
3976
|
"sharp": "^0.34.3"
|
|
3978
3977
|
},
|
|
3979
3978
|
"peerDependencies": {
|
package/bin/init.js
CHANGED
|
@@ -530,7 +530,29 @@ function ensureMissionLifecycleStates(opts = {}) {
|
|
|
530
530
|
|
|
531
531
|
for (const mission of group) {
|
|
532
532
|
const statePath = path.join(mission.abs, 'mission-state.json');
|
|
533
|
-
if (fs.existsSync(statePath))
|
|
533
|
+
if (fs.existsSync(statePath)) {
|
|
534
|
+
const state = readJsonFileSafe(statePath) || {};
|
|
535
|
+
const lifecycle = state.lifecycle || state.status;
|
|
536
|
+
const active = state.active === true;
|
|
537
|
+
const shouldDeactivate =
|
|
538
|
+
active &&
|
|
539
|
+
(isTerminalMissionLifecycle(lifecycle) ||
|
|
540
|
+
(mission.rel.includes('/') &&
|
|
541
|
+
(mission.type === 'submission' || mission.type === 'hotfix') &&
|
|
542
|
+
activeChild &&
|
|
543
|
+
activeChild.rel !== mission.rel));
|
|
544
|
+
if (shouldDeactivate) {
|
|
545
|
+
writes.push({
|
|
546
|
+
path: statePath,
|
|
547
|
+
state: {
|
|
548
|
+
...state,
|
|
549
|
+
lifecycle: isTerminalMissionLifecycle(lifecycle) ? lifecycle : 'superseded',
|
|
550
|
+
active: false,
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
534
556
|
const isChild = mission.rel.includes('/');
|
|
535
557
|
const isActive = isChild
|
|
536
558
|
? activeChild?.rel === mission.rel
|
|
@@ -555,7 +577,7 @@ function ensureMissionLifecycleStates(opts = {}) {
|
|
|
555
577
|
fs.writeFileSync(item.path, JSON.stringify(item.state, null, 2) + '\n');
|
|
556
578
|
}
|
|
557
579
|
if (writes.length) {
|
|
558
|
-
log(`mission-state.json: ${writes.length}개 기존 mission
|
|
580
|
+
log(`mission-state.json: ${writes.length}개 기존 mission lifecycle/active 보강`);
|
|
559
581
|
}
|
|
560
582
|
return { missing: writes.length, written: writes.length };
|
|
561
583
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.36",
|
|
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"
|
|
@@ -130,7 +130,12 @@ function runtimeIdle(root) {
|
|
|
130
130
|
|
|
131
131
|
function currentSamples(root, now = Date.now()) {
|
|
132
132
|
const missions = collectMissions(root);
|
|
133
|
-
const active =
|
|
133
|
+
const active =
|
|
134
|
+
missions
|
|
135
|
+
.filter((m) => m.active)
|
|
136
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0] ??
|
|
137
|
+
missions.sort((a, b) => b.mtimeMs - a.mtimeMs)[0] ??
|
|
138
|
+
null;
|
|
134
139
|
if (!active) return [];
|
|
135
140
|
const todos = readTodos(root);
|
|
136
141
|
const idle = runtimeIdle(root);
|
package/scripts/harness-stop.sh
CHANGED
|
@@ -28,23 +28,6 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
28
28
|
AUTO_CHAIN=$(jq -r '.behavior.auto_chain_on_stop // true' "$CONFIG" 2>/dev/null || echo "true")
|
|
29
29
|
if [ "$AUTO_CHAIN" != "true" ]; then exit 0; fi
|
|
30
30
|
|
|
31
|
-
# CXX direct-execution guard:
|
|
32
|
-
# If a mission has CXX documents but no worker reports, block the stop so the
|
|
33
|
-
# company loop must hire/dispatch workers instead of ending with CXX-only work.
|
|
34
|
-
if [ -x "$SCRIPT_DIR/harness-worker-evidence-validate.sh" ]; then
|
|
35
|
-
WORKER_EVIDENCE_JSON=$("$SCRIPT_DIR/harness-worker-evidence-validate.sh" "$CWD" json 2>/dev/null || true)
|
|
36
|
-
WORKER_EVIDENCE_OK=$(echo "$WORKER_EVIDENCE_JSON" | jq -r 'if has("ok") then .ok else true end' 2>/dev/null || echo true)
|
|
37
|
-
if [ "$WORKER_EVIDENCE_OK" != "true" ]; then
|
|
38
|
-
REASON=$(echo "$WORKER_EVIDENCE_JSON" | jq -r '
|
|
39
|
-
"CXX 직접 실행 차단: " +
|
|
40
|
-
([.violations[] | "\(.mission) has CXX docs without worker reports: \(.docs | join(","))"] | join("; ")) +
|
|
41
|
-
". harness-hiring/resource-manager로 전문 worker를 고용 또는 배정하고 .harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/{worker-name}.md 를 남긴 뒤 계속하라."
|
|
42
|
-
' 2>/dev/null || echo "CXX 직접 실행 차단: worker report가 없는 mission이 있습니다. hired worker 보고서를 먼저 생성하세요.")
|
|
43
|
-
jq -nc --arg reason "$REASON" '{decision:"block", reason:$reason}'
|
|
44
|
-
exit 0
|
|
45
|
-
fi
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
31
|
# 무한루프 방지: 한 sprint 안에서 stop_chain_count 가 상한을 넘으면 중단
|
|
49
32
|
STOP_CHAIN_MAX=$(jq -r '.behavior.auto_chain_max_per_sprint // 200' "$CONFIG" 2>/dev/null || echo 200)
|
|
50
33
|
STOP_CHAIN_COUNT=$(jq -r '.conductor.stop_chain_count // 0' "$PROGRESS" 2>/dev/null || echo 0)
|
|
@@ -96,6 +79,24 @@ fi
|
|
|
96
79
|
|
|
97
80
|
if [ "$should_chain" != "true" ]; then exit 0; fi
|
|
98
81
|
|
|
82
|
+
# CXX direct-execution guard:
|
|
83
|
+
# Only run this while an active company loop is actually chaining. Scope it to
|
|
84
|
+
# the newest active mission so legacy/archive documents cannot keep Stop
|
|
85
|
+
# blocked forever.
|
|
86
|
+
if [ -x "$SCRIPT_DIR/harness-worker-evidence-validate.sh" ]; then
|
|
87
|
+
WORKER_EVIDENCE_JSON=$("$SCRIPT_DIR/harness-worker-evidence-validate.sh" "$CWD" json latest-active 2>/dev/null || true)
|
|
88
|
+
WORKER_EVIDENCE_OK=$(echo "$WORKER_EVIDENCE_JSON" | jq -r 'if has("ok") then .ok else true end' 2>/dev/null || echo true)
|
|
89
|
+
if [ "$WORKER_EVIDENCE_OK" != "true" ]; then
|
|
90
|
+
REASON=$(echo "$WORKER_EVIDENCE_JSON" | jq -r '
|
|
91
|
+
"CXX 직접 실행 차단: " +
|
|
92
|
+
([.violations[] | "\(.mission) has CXX docs without worker reports: \(.docs | join(","))"] | join("; ")) +
|
|
93
|
+
". harness-hiring/resource-manager로 전문 worker를 고용 또는 배정하고 .harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/{worker-name}.md 를 남긴 뒤 계속하라."
|
|
94
|
+
' 2>/dev/null || echo "CXX 직접 실행 차단: worker report가 없는 active mission이 있습니다. hired worker 보고서를 먼저 생성하세요.")
|
|
95
|
+
jq -nc --arg reason "$REASON" '{decision:"block", reason:$reason}'
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
99
100
|
# stop_chain_count 증가 (느슨한 카운터 — race 허용)
|
|
100
101
|
NEW_COUNT=$((STOP_CHAIN_COUNT + 1))
|
|
101
102
|
TMP=$(mktemp)
|
|
@@ -8,6 +8,7 @@ DOC_ROOT="$PROJECT_ROOT/.harness/documents"
|
|
|
8
8
|
[ -d "$DOC_ROOT" ] || exit 0
|
|
9
9
|
|
|
10
10
|
mode="${2:-text}"
|
|
11
|
+
scope="${3:-all}"
|
|
11
12
|
violations=()
|
|
12
13
|
|
|
13
14
|
has_implementation_notes() {
|
|
@@ -37,6 +38,40 @@ has_legacy_flat_worker_report() {
|
|
|
37
38
|
|
|
38
39
|
mission_dirs=$(find "$DOC_ROOT" -type f \( -name 'ceo.md' -o -name 'coo.md' -o -name 'cdo.md' -o -name 'cto.md' -o -name 'cqo.md' -o -name 'ops.md' \) -exec dirname {} \; 2>/dev/null | sort -u)
|
|
39
40
|
|
|
41
|
+
mission_mtime() {
|
|
42
|
+
stat -f '%m' "$1" 2>/dev/null || stat -c '%Y' "$1" 2>/dev/null || echo 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
is_terminal_lifecycle() {
|
|
46
|
+
case "$1" in
|
|
47
|
+
closed|cancelled|superseded|complete|completed|blocked) return 0 ;;
|
|
48
|
+
*) return 1 ;;
|
|
49
|
+
esac
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
latest_active_mission=""
|
|
53
|
+
if [ "$scope" = "latest-active" ]; then
|
|
54
|
+
latest_active_mtime=0
|
|
55
|
+
while IFS= read -r mission_dir; do
|
|
56
|
+
[ -n "$mission_dir" ] || continue
|
|
57
|
+
state_path="$mission_dir/mission-state.json"
|
|
58
|
+
[ -f "$state_path" ] || continue
|
|
59
|
+
active=$(jq -r '.active // false' "$state_path" 2>/dev/null || echo false)
|
|
60
|
+
lifecycle=$(jq -r '.lifecycle // .status // "unknown"' "$state_path" 2>/dev/null || echo unknown)
|
|
61
|
+
if [ "$active" != "true" ] || is_terminal_lifecycle "$lifecycle"; then
|
|
62
|
+
continue
|
|
63
|
+
fi
|
|
64
|
+
mtime=$(mission_mtime "$mission_dir")
|
|
65
|
+
if [ "${mtime:-0}" -ge "${latest_active_mtime:-0}" ]; then
|
|
66
|
+
latest_active_mtime="$mtime"
|
|
67
|
+
latest_active_mission="$mission_dir"
|
|
68
|
+
fi
|
|
69
|
+
done <<EOF
|
|
70
|
+
$mission_dirs
|
|
71
|
+
EOF
|
|
72
|
+
mission_dirs="$latest_active_mission"
|
|
73
|
+
fi
|
|
74
|
+
|
|
40
75
|
while IFS= read -r mission_dir; do
|
|
41
76
|
[ -n "$mission_dir" ] || continue
|
|
42
77
|
[ -d "$mission_dir" ] || continue
|