@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 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 * 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;
@@ -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(snapshot, lanes, [...persistedHeatSamples, ...heatSamples], heatmapStartedAt),
205
- [heatSamples, heatmapStartedAt, lanes, persistedHeatSamples, snapshot]
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 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
  })}
@@ -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: formatMinuteOffset(x),
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
- type RawWorker = NonNullable<NonNullable<RawProgress["company_state"]>["workers"]>[number];
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.16",
989
- "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.16.tgz",
990
- "integrity": "sha512-9QMKolCl+JnJtaRAQSXy4RQrhgfe8W7/G1+Hl3QSB/HZY7zQMzTwPDdTRwwio8BS96ps1MHpHhbS8qxoNV3JIQ==",
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.16",
995
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.16.tgz",
996
- "integrity": "sha512-wzdER4JZj+31vNkhaZ1Ght3IsNI8DMwj7VqadfIOqJB5sh8FiOqNSopYADQn6mgEPomzDd/DHqBcfo2fmVMYtg==",
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.16",
1011
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.16.tgz",
1012
- "integrity": "sha512-PPTo+cvcanxkuDEuDyZGk28ntmu0WjfkxqlG7hw9Mhsiribs4x1C6h2Culn0cJKqsne1gFjjZRK3ax7WYlSxgg==",
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.16",
1027
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.16.tgz",
1028
- "integrity": "sha512-Jl0IL9P7S8uNl5oI1TqrQmfmLp7OqjWM58000pVnUVIsHrvPP6m9QDW/uNWYUbmd+8IYvc6MTeZKICstBMBpew==",
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.16",
1043
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.16.tgz",
1044
- "integrity": "sha512-Zf0BIqv/o5uOWfyRkzgGhyV2Tky7HLt0bG+w7XWdaU1JpyX0tltM3TrSfa/Y9c597SJG4CzN47+u2InhgZZ4vg==",
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.16",
1059
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.16.tgz",
1060
- "integrity": "sha512-HCDDU1TRLeUDV180QQTWrs5Oa4lIcI7XH9nF0UVUVmYLN/boZ6LqyFtm3814gc1fv+lOVyKaw5B6bVC9BpXTSQ==",
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.16",
1075
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.16.tgz",
1076
- "integrity": "sha512-kvXUY1dn5wxKuMkXxQRUbPjEnKxW1PR9uKOm0zpIpj3574+cFfaePhYFmBVtrOuwt+w34OdDzNaJr5Iixf+HBQ==",
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.16",
1091
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.16.tgz",
1092
- "integrity": "sha512-zpOQuF+eyENMXRjglp2hZCIrUjTdO37suEBnDn1mX4PXSuetXZDMLpjKOh4dYSw3SiDTnOoOUwBl5i5Elr6nnQ==",
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.16",
1107
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.16.tgz",
1108
- "integrity": "sha512-LnwKYpiSmIzXlTq76hMeeIzZoDcFwu848p6H+QBkGFJIbZphgzNUPdHruJcHM/bFnaFeco0l1Frie5I27VKglA==",
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.17",
1691
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
1692
- "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
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.16",
3952
- "resolved": "https://registry.npmjs.org/next/-/next-15.5.16.tgz",
3953
- "integrity": "sha512-aZExBk/V6JCu3NCFc90twdj9L/M3y0+ukeQwUAZbOiqRhAX+h2oMEa0NZFhcpj6HYRYjVS3V2/3xvyOpNnmw7A==",
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.16",
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.16",
3970
- "@next/swc-darwin-x64": "15.5.16",
3971
- "@next/swc-linux-arm64-gnu": "15.5.16",
3972
- "@next/swc-linux-arm64-musl": "15.5.16",
3973
- "@next/swc-linux-x64-gnu": "15.5.16",
3974
- "@next/swc-linux-x64-musl": "15.5.16",
3975
- "@next/swc-win32-arm64-msvc": "15.5.16",
3976
- "@next/swc-win32-x64-msvc": "15.5.16",
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)) continue;
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에 lifecycle 보강`);
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.33",
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 = missions.find((m) => m.active) ?? missions.sort((a, b) => b.mtimeMs - a.mtimeMs)[0] ?? null;
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);
@@ -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