@walwal-harness/cli 7.1.36 → 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,13 @@ 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
+
34
41
  ## 7.1.36 — Stop hook active-loop guard + visible heatmap (2026-05-29)
35
42
 
36
43
  - Stop hook no longer runs worker evidence validation before deciding whether the company loop should chain.
@@ -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,7 @@ 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 |
286
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 |
287
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 |
288
291
  | 7.1.34 | Fix dashboard 500 when `company_state.workers` is recorded as a keyed worker map instead of an array |
@@ -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
@@ -204,10 +204,10 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
204
204
  () => buildHeatmap(
205
205
  snapshot,
206
206
  lanes,
207
- filterHeatSamplesForMission([...persistedHeatSamples, ...heatSamples], selectedMission),
207
+ [...persistedHeatSamples, ...heatSamples],
208
208
  heatmapStartedAt
209
209
  ),
210
- [heatSamples, heatmapStartedAt, lanes, persistedHeatSamples, selectedMission, snapshot]
210
+ [heatSamples, heatmapStartedAt, lanes, persistedHeatSamples, snapshot]
211
211
  );
212
212
  const [selectedLaneId, setSelectedLaneId] = useState(lanes[0]?.id ?? "ceo");
213
213
  const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null);
@@ -1806,14 +1806,39 @@ function heatColor(count: number) {
1806
1806
 
1807
1807
  // ===== Lane / Heatmap builders =====
1808
1808
 
1809
- function buildLanes(snapshot: HarnessSnapshot, selectedMission: MissionDoc | null): AgentLane[] {
1810
- 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
+
1811
1835
  // CXX → its workers, then next CXX → its workers (proper hierarchy).
1812
1836
  // No cap: heatmap container scrolls vertically so OPS at the bottom is always
1813
1837
  // reachable even when upstream CXX bring many workers.
1814
1838
  const lanes: AgentLane[] = [];
1815
1839
  for (const role of CXX_ROLES) {
1816
- const workers = activeMission?.workers.filter((w) => w.owner === role) ?? [];
1840
+ const workerEntries = workersByRole.get(role) ?? [];
1841
+ const workers = workerEntries.map((entry) => entry.worker);
1817
1842
  const todos = snapshot.todos.filter((t) => t.owner === role);
1818
1843
  const hasError =
1819
1844
  (role === "ops" && snapshot.incidents.length > 0) ||
@@ -1832,7 +1857,7 @@ function buildLanes(snapshot: HarnessSnapshot, selectedMission: MissionDoc | nul
1832
1857
  workers,
1833
1858
  mission: activeMission,
1834
1859
  });
1835
- for (const worker of workers) {
1860
+ for (const { worker, mission } of workerEntries) {
1836
1861
  lanes.push({
1837
1862
  id: `${role}:${worker.name}`,
1838
1863
  label: `└ ${worker.displayName.slice(0, 16)}`,
@@ -1843,24 +1868,13 @@ function buildLanes(snapshot: HarnessSnapshot, selectedMission: MissionDoc | nul
1843
1868
  todos: 0,
1844
1869
  workers: [],
1845
1870
  worker,
1846
- mission: activeMission,
1871
+ mission,
1847
1872
  });
1848
1873
  }
1849
1874
  }
1850
1875
  return lanes;
1851
1876
  }
1852
1877
 
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
-
1864
1878
  function buildHeatmap(
1865
1879
  snapshot: HarnessSnapshot,
1866
1880
  lanes: AgentLane[],
@@ -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";
@@ -110,6 +110,10 @@ CEO는 worker 기록 없이 완료된 산출물을 포함한 CXX 보고서를
110
110
 
111
111
  Owner는 최종 검수자다. Tester, QA worker, debugger, 배포 확인 담당자가 아니다.
112
112
 
113
+ - Owner는 방향 제시, 추가 요구, hot-fix, 최종 acceptance feedback을 제공한다. 회사 루프를 계속 돌리기 위한 진행 신호가 아니다.
114
+ - CEO는 Owner에게 계속 진행 여부, worker 고용 여부, 내부 옵션 선택, 다음 단계 시작 여부를 묻지 않는다.
115
+ - CEO가 혼자 판단하기 어려우면 관련 CXX 회의를 열고 written recommendation을 받아 결정한 뒤 계속 진행한다.
116
+ - Owner 입력 대기는 harness가 추론하거나 확보할 수 없는 외부 권한이 필요할 때만 허용한다: credentials/secrets, payment approval, legal/business acceptance, unavailable production access, destructive data action, 또는 명시된 Owner 방향과의 직접 충돌.
113
117
  - 동작하지 않거나 검증되지 않았거나 부분적으로만 실행되는 소프트웨어를 Owner에게 전달하며 "확인해 주세요"를 다음 액션으로 삼지 않는다.
114
118
  - Owner에게 개발자 테스트, 회귀 확인, 계정 생성/로그인 확인, Playwright 확인, E2E 순회, 로그 점검, 기본 기능 검증을 요구하지 않는다.
115
119
  - CEO와 CXX는 worker를 통해 자체 검증을 계획하고 실행한다: unit test, E2E test, Playwright/browser check, test account, seed data, build/run check, log evidence.
@@ -35,10 +35,11 @@ Codex does not need `.codex/agents/` to run this harness. The installed Codex co
35
35
 
36
36
  Don't assume. Don't hide confusion.
37
37
 
38
- - State assumptions before implementing. If uncertain, ask.
39
- - If multiple approaches exist, name them — don't pick silently.
38
+ - State assumptions before implementing.
39
+ - If multiple approaches exist, name them and choose the smallest reversible path from evidence.
40
40
  - Minimum change that solves the problem. No speculative scope.
41
- - If something is unclear, stop. Name what is unclear. Ask.
41
+ - Do not ask the Owner routine operating questions. CEO/CXX resolve uncertainty internally through written CXX recommendations.
42
+ - Stop for Owner input only when the next action requires external authority the harness cannot infer: credentials/secrets, payment approval, legal/business acceptance, unavailable production access, destructive data action, or a direct conflict with stated Owner direction. Record the recommended default when blocked.
42
43
 
43
44
  For hot-fix: smallest correct patch only. Do not "improve" adjacent code.
44
45
  For goal: define verifiable success criteria before CEO routes to CXX.
@@ -70,6 +71,9 @@ CEO rejects any CXX report that contains completed deliverables without matching
70
71
 
71
72
  The Owner is the final acceptance reviewer, not a tester, QA worker, debugger, or deployment verifier.
72
73
 
74
+ - The Owner sets direction, adds requirements, issues hot-fixes, and gives final acceptance feedback. The Owner is not the progress pump for the company loop.
75
+ - CEO must not ask the Owner whether to continue, which worker to hire, which internal option to choose, or whether to start the next planned step.
76
+ - If CEO cannot answer an operating question alone, CEO convenes the relevant CXX agents, records their recommendations, chooses a path, and continues.
73
77
  - Do not hand the Owner broken, unverified, or partially runnable software with "please check this" as the next action.
74
78
  - Do not ask the Owner to perform developer testing, regression checks, account setup checks, Playwright review, E2E traversal, log inspection, or basic functionality verification.
75
79
  - CEO and CXX must plan and execute self-verification through workers: unit tests, E2E tests, Playwright/browser checks, test accounts, seeded data, build/run checks, logs, and documented evidence.
@@ -240,7 +240,7 @@
240
240
  "command": ""
241
241
  },
242
242
  "production": {
243
- "comment": "Owner+CEO+CTO 합의로만 services[] 를 수정한다. local/docker/vm/aws/cloud 등 운영 서버 매핑을 Owner에게 받아 기록한다. live=true 이고 services[] 가 있으면 OPS 는 매 hourly review 마다 port/health/log 상태를 운영보고서에 남긴다.",
243
+ "comment": "CEO+CTO+OPS가 repo config, scripts, running process, Docker, logs, CXX report에서 services[]와 운영 서버 매핑을 우선 도출한다. Owner 입력 대기는 credentials/secrets, unavailable production access, legal/business acceptance 같은 외부 권한이 필요할 때만 허용한다. live=true 이고 services[] 가 있으면 OPS 는 매 hourly review 마다 port/health/log 상태를 운영보고서에 남긴다.",
244
244
  "live": false,
245
245
  "services": [
246
246
  {
@@ -1 +1,6 @@
1
- # For consumer projects, add .worktrees/ to .gitignore
1
+ # walwal-harness dashboard runtime
2
+ .harness/dashboard/apps/harness-dashboard/node_modules/
3
+ .harness/dashboard/apps/harness-dashboard/.next/
4
+ .harness/dashboard/apps/harness-dashboard/.turbo/
5
+ .harness/dashboard/apps/harness-dashboard/tsconfig.tsbuildinfo
6
+ .harness/dashboard/.download/
@@ -73,7 +73,7 @@ docmeta:
73
73
  - **TTL**: 영구
74
74
  - **Lesson**: walwal-harness 는 NEXUS 회사 메타포다. 다음 두 룰을 **절대로** 어기지 않는다.
75
75
  1. **정체성**: 사용자 = **Owner** (회사 외부 주주), Dispatcher = **CEO** (Owner 와의 유일한 대화 창구). 응답·GOAL·로그 어디서도 사용자를 "CEO" 로 호칭하지 않는다. 회사 내부 결정 (sprint 분할, mode 선택, agent 호출, 평가 점수) 을 Owner 에게 떠넘기지 않는다.
76
- 2. **자율 실행 (NEXUS P3)**: GOAL 확정 직후 회사는 **사용자 펌프 없이** 자율 진행. "다음 단계로 진행할까요?", "/harness-next 실행하시겠습니까?" 같은 진행 여부 질문은 자율성 위반. Owner 가 돌아오는 시점은 (a) GOAL 모호성 명료화 (1~2 개 객관식 질문, 한 번만), (b) 결과 보고, (c) escalation 셋 뿐.
76
+ 2. **자율 실행 (NEXUS P3)**: GOAL 확정 직후 회사는 **사용자 펌프 없이** 자율 진행. "다음 단계로 진행할까요?", "/harness-next 실행하시겠습니까?" 같은 진행 여부 질문은 자율성 위반. CEO가 혼자 결정하기 어려우면 Owner에게 묻지 말고 관련 CXX 회의를 열어 written recommendation을 받은 뒤 결정한다. Owner 가 돌아오는 시점은 (a) 목표 변경/추가 요구/hot-fix, (b) 결과 보고와 최종 acceptance feedback, (c) credentials/secrets, payment approval, legal/business acceptance, unavailable production access, destructive data action, stated direction conflict 같은 외부 권한 block 뿐.
77
77
  - **Context**: v6.0.0 publish 직후 dispatcher 응답이 사용자를 "CEO 직접 리뷰…" 로 호명하고 "/harness-next 자동 진행하시겠습니까?" 로 사용자 펌프를 요구함. Owner 의 명시적 교정.
78
78
  - **Applies to**:
79
79
  - **Dispatcher**: 사용자 호칭/응답 문구, GOAL 정립, 모든 inbound/outbound owner 통신. → anti-pattern: `.harness/gotchas/dispatcher.md` [G-001]~[G-004].
package/bin/init.js CHANGED
@@ -848,9 +848,9 @@ function ensureHarnessEnv() {
848
848
  if (re.test(content)) return;
849
849
 
850
850
  const prefix = content && !content.endsWith('\n') ? '\n' : '';
851
- const block = `${prefix}${content ? '\n' : ''}# Walwal Harness — CEO must agree on a {xx}000 base port with the Owner.\n# Set HARNESS_BASE_PORT before CXX agents allocate any service ports.\n# HARNESS_BASE_PORT=\n`;
851
+ const block = `${prefix}${content ? '\n' : ''}# Walwal Harness — CEO chooses an available {xx}000 base port unless Owner already specified one.\n# Set HARNESS_BASE_PORT before CXX agents allocate any service ports.\n# HARNESS_BASE_PORT=\n`;
852
852
  fs.writeFileSync(envPath, content + block);
853
- log('.env: HARNESS_BASE_PORT placeholder added — set value with Owner approval before starting services');
853
+ log('.env: HARNESS_BASE_PORT placeholder added — CEO/CXX must choose an available value before starting services');
854
854
  }
855
855
 
856
856
  function ensureStructuredRuntimeFiles({ dryRun = false } = {}) {
@@ -963,6 +963,27 @@ function refreshScriptsForMigrate({ dryRun = false, backupDir = null } = {}) {
963
963
  chmodShellScripts(scriptsDest);
964
964
  }
965
965
 
966
+ function ensureWakeScheduler({ dryRun = false } = {}) {
967
+ const wakeInstall = path.join(PROJECT_ROOT, 'scripts', 'harness-wake-install.sh');
968
+ if (!fs.existsSync(wakeInstall)) return;
969
+
970
+ log(' wake scheduler: project registration + launchd load check');
971
+ if (dryRun) return;
972
+
973
+ try {
974
+ execSync(`bash ${shellQuote(wakeInstall)} install ${shellQuote(PROJECT_ROOT)}`, {
975
+ cwd: PROJECT_ROOT,
976
+ stdio: 'pipe',
977
+ timeout: 15000,
978
+ });
979
+ log(' wake scheduler: registered');
980
+ } catch (e) {
981
+ const detail = String(e?.stderr || e?.message || '').trim().split('\n').slice(-1)[0];
982
+ log(` wake scheduler: registration skipped (${detail || 'install command failed'})`);
983
+ log(' wake scheduler: run manually if needed: bash scripts/harness-wake-install.sh install .');
984
+ }
985
+ }
986
+
966
987
  function migrateActivityHistory({ dryRun = false } = {}) {
967
988
  const recorder = path.join(PROJECT_ROOT, 'scripts', 'harness-activity-record.js');
968
989
  if (!fs.existsSync(recorder)) return;
@@ -2032,6 +2053,7 @@ function runMigrate(opts = {}) {
2032
2053
  // Runtime scripts are package-owned. Migrate must refresh them so existing
2033
2054
  // projects receive wake/meeting/OPS fixes without requiring a full init.
2034
2055
  refreshScriptsForMigrate({ dryRun, backupDir });
2056
+ ensureWakeScheduler({ dryRun });
2035
2057
  migrateActivityHistory({ dryRun });
2036
2058
  updateRuleRegistryLinks(flags.ruleRegistryLinks || [], { dryRun, backupDir });
2037
2059
 
@@ -2600,6 +2622,7 @@ function main() {
2600
2622
  scaffoldHarness();
2601
2623
  installSkills();
2602
2624
  installScripts();
2625
+ ensureWakeScheduler();
2603
2626
  installCommands();
2604
2627
  installSessionHook();
2605
2628
  installStatusline();
package/commands/goal.md CHANGED
@@ -16,7 +16,7 @@ Required flow:
16
16
  1. Create or update a goal under `.harness/documents/goal-{goal_index}-{goal_name}/`.
17
17
  2. Write `.harness/documents/goal-{goal_index}-{goal_name}/mission-state.json` with `{"lifecycle":"active","active":true}` before routing CXX.
18
18
  3. Record CEO decisions in `.harness/documents/goal-{goal_index}-{goal_name}/ceo.md`.
19
- 4. CEO decides whether brainstorming is needed or whether CXX questions can be issued immediately.
19
+ 4. CEO decides whether brainstorming is needed or whether CXX questions can be issued immediately. CEO must not ask the Owner which path to take.
20
20
  5. CEO asks COO, CDO, CTO, and CQO only the questions needed for this goal.
21
21
  6. CXX roles must start in a fresh session context. Do not let the default model impersonate a missing worker.
22
22
  7. CXX roles must not directly execute specialist deliverables. They must use hired workers for research, planning, design, implementation, QA, ops checks, and documentation.
@@ -24,6 +24,7 @@ Required flow:
24
24
  9. CEO must require a Worker Evidence Manifest and worker report paths under `.harness/documents/goal-{goal_index}-{goal_name}/{owning-cxx}/workers/` before accepting CXX completion.
25
25
  10. When the goal is accepted, cancelled, superseded, blocked, or closed, update `mission-state.json` to `complete`, `cancelled`, `superseded`, `blocked`, or `closed` and set `active:false`.
26
26
  11. Do not invoke internal roles through slash commands; commands are Owner entrypoints only.
27
+ 12. Do not ask the Owner whether to continue, hire workers, choose internal options, or start the next step. If CEO cannot decide alone, convene the relevant CXX agents and decide from their written recommendations. Stop only for external authority such as credentials/secrets, payment approval, legal/business acceptance, unavailable production access, destructive data action, or direct conflict with stated Owner direction.
27
28
 
28
29
  Note: A goal is the company's objective. Submissions and hot-fixes that happen while pursuing it should be recorded under that goal directory.
29
30
 
@@ -17,7 +17,7 @@ Required flow:
17
17
  2. If another child mission under the goal is active, update its `mission-state.json` to `closed`, `cancelled`, or `superseded` with `active:false` before starting the hot-fix.
18
18
  3. Create `.harness/documents/{goal_name}/hotfix-{hotfix_index}-{hotfix_name}/ceo.md`.
19
19
  4. Write `.harness/documents/{goal_name}/hotfix-{hotfix_index}-{hotfix_name}/mission-state.json` with `{"lifecycle":"active","active":true}`.
20
- 5. CEO summons CTO and CQO first; summon COO/CDO only when planning or UX decisions are involved.
20
+ 5. CEO summons CTO and CQO first; summon COO/CDO only when planning or UX decisions are involved. CEO must not ask the Owner whether to start the fix or which internal path to choose.
21
21
  6. CTO applies the smallest correct patch path through hired implementation skills.
22
22
  7. CXX roles must start in a fresh session context. Do not let the default model impersonate a missing CXX or worker.
23
23
  8. CXX roles must not directly execute specialist deliverables. They must use hired workers for implementation, QA, ops checks, and documentation even when the fix is small.
@@ -26,6 +26,7 @@ Required flow:
26
26
  11. CQO must register durable lessons in `.harness/gotchas/`, `.harness/conventions/`, or `.harness/memories/`. This step is mandatory, not optional, even for small fixes.
27
27
  12. Archive only after CQO has accepted the fix, then update `mission-state.json` to `complete` with `active:false`.
28
28
  13. Do not invoke internal roles through slash commands; commands are Owner entrypoints only.
29
+ 14. Do not ask the Owner whether to continue, hire workers, choose internal options, or start the next step. If CEO cannot decide alone, convene the relevant CXX agents and decide from their written recommendations. Stop only for external authority such as credentials/secrets, payment approval, legal/business acceptance, unavailable production access, destructive data action, or direct conflict with stated Owner direction.
29
30
 
30
31
  Note: `/hot-fix` is a problem-fix flow while pursuing the active goal. It belongs under that goal in history.
31
32
 
@@ -13,12 +13,12 @@ Codex adapter:
13
13
  - For CXX "fresh session context", use role-scoped context: read the CXX skill, active mission files, and required conventions/gotchas before writing that role's `{cxx}.md`.
14
14
 
15
15
  Required flow:
16
- 1. Locate the active goal document root under `.harness/documents/{goal_name}/`. If no active goal exists, CEO must ask the Owner to create or select one with `/goal` before proceeding.
16
+ 1. Locate the active goal document root under `.harness/documents/{goal_name}/`. If no active goal exists, CEO must create/select the best matching goal record from available mission context or mark the submission blocked with the recommended default; do not ask the Owner an open-ended setup question.
17
17
  2. If another child mission under the goal is active, update its `mission-state.json` to `closed`, `cancelled`, or `superseded` with `active:false` before starting this submission.
18
18
  3. Create a submission record under `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/`.
19
19
  4. Write `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/mission-state.json` with `{"lifecycle":"active","active":true}`.
20
20
  5. Record CEO decisions in `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/ceo.md`.
21
- 6. CEO routes only to the CXX agents needed for this additional requirement.
21
+ 6. CEO routes only to the CXX agents needed for this additional requirement. CEO must not ask the Owner which CXX or worker path to choose.
22
22
  7. CXX roles must start in a fresh session context. Do not let the default model impersonate a missing CXX or worker.
23
23
  8. CXX roles must not directly execute specialist deliverables. They must use hired workers for research, planning, design, implementation, QA, ops checks, and documentation.
24
24
  9. If a required worker is missing, the responsible CXX must invoke the installed `harness-hiring` skill before assigning the work.
@@ -26,6 +26,7 @@ Required flow:
26
26
  11. CEO must require a Worker Evidence Manifest and worker report paths under `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/{owning-cxx}/workers/` before accepting CXX completion.
27
27
  12. When the submission is accepted, cancelled, superseded, blocked, or closed, update `mission-state.json` to `complete`, `cancelled`, `superseded`, `blocked`, or `closed` and set `active:false`.
28
28
  13. Do not invoke internal roles through slash commands; commands are Owner entrypoints only.
29
+ 14. Do not ask the Owner whether to continue, hire workers, choose internal options, or start the next step. If CEO cannot decide alone, convene the relevant CXX agents and decide from their written recommendations. Stop only for external authority such as credentials/secrets, payment approval, legal/business acceptance, unavailable production access, destructive data action, or direct conflict with stated Owner direction.
29
30
 
30
31
  Note: `/submission` is not a new company goal and not an emergency fix. It is an additional requirement while pursuing the active goal. It belongs under that goal in history.
31
32
 
@@ -5,5 +5,7 @@
5
5
  - CEO routes work to CXX agents through agent/skill invocation, not slash commands.
6
6
  - CEO records mission framing and routing decisions in `.harness/documents/{goal-or-child-mission}/ceo.md`.
7
7
  - CEO asks only mission-relevant questions to COO, CDO, CTO, CQO, and OPS.
8
+ - CEO does not ask the Owner routine operating questions. CEO decides from evidence, or convenes CXX agents and then decides.
9
+ - CEO stops for Owner input only for external authority: credentials/secrets, payment approval, legal/business acceptance, unavailable production access, destructive data action, or direct conflict with stated Owner direction.
8
10
  - CEO does not accept CXX completion unless specialist deliverables were produced by hired workers.
9
11
  - CEO requires worker names and `.harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/{worker-name}.md` paths in CXX completion reports.
package/gotchas/ceo.md CHANGED
@@ -7,3 +7,7 @@ CEO must not call `/coo`, `/cto`, or similar slash commands. CXX routing is agen
7
7
  ## Over-broad Meeting
8
8
 
9
9
  Do not summon every CXX for every mission by default. Ask only the roles needed for the decision.
10
+
11
+ ## Owner As Progress Pump
12
+
13
+ Do not ask the Owner whether to continue, start the next step, hire a worker, choose between internal options, or verify routine technical behavior. Decide internally, record the rationale, and keep routing. If a decision is too ambiguous for CEO alone, convene the relevant CXX agents and decide from their recommendations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.36",
3
+ "version": "7.1.37",
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"
@@ -2,8 +2,8 @@
2
2
  # walwal-harness — Brick Office dashboard launcher
3
3
  #
4
4
  # 처음 쓰는 사용자도 한 줄로 대시보드를 띄워볼 수 있게 만든 helper.
5
- # npm 패키지에 포함된 apps/harness-dashboard 를 사용자 프로젝트 외부
6
- # (~/.walwal-harness/dashboard/<project-key>) 에 격리 복사 후 dev 실행한다.
5
+ # npm 패키지에 포함된 apps/harness-dashboard 를 프로젝트별 runtime 영역
6
+ # (.harness/dashboard/) 에 복사 후 dev 실행한다.
7
7
  # 구버전 호환을 위해 패키지에 dashboard 가 없으면 git sparse-checkout 으로 fallback 한다.
8
8
  #
9
9
  # Usage:
@@ -38,9 +38,7 @@ if [[ ! -d "${HARNESS_ROOT}/.harness" ]]; then
38
38
  exit 3
39
39
  fi
40
40
 
41
- PROJECT_KEY="$(printf '%s' "${HARNESS_ROOT}" | shasum -a 1 | awk '{print substr($1,1,12)}')"
42
- PROJECT_NAME="$(basename "${HARNESS_ROOT}")"
43
- LOCAL_DIR="${HOME}/.walwal-harness/dashboard/${PROJECT_NAME}-${PROJECT_KEY}"
41
+ LOCAL_DIR="${HARNESS_ROOT}/.harness/dashboard"
44
42
  VERSION_FILE="${LOCAL_DIR}/.walwal-dashboard-version"
45
43
 
46
44
  PACKAGE_ROOT="${HARNESS_ROOT}/node_modules/@walwal-harness/cli"
@@ -53,8 +51,33 @@ if [[ "$REINSTALL" == "true" ]]; then
53
51
  rm -rf "$LOCAL_DIR"
54
52
  fi
55
53
 
54
+ ensure_gitignore() {
55
+ local gitignore="${HARNESS_ROOT}/.gitignore"
56
+ local marker="# walwal-harness dashboard runtime"
57
+ local block
58
+ block="$(cat <<'EOF'
59
+ # walwal-harness dashboard runtime
60
+ .harness/dashboard/apps/harness-dashboard/node_modules/
61
+ .harness/dashboard/apps/harness-dashboard/.next/
62
+ .harness/dashboard/apps/harness-dashboard/.turbo/
63
+ .harness/dashboard/apps/harness-dashboard/tsconfig.tsbuildinfo
64
+ .harness/dashboard/.download/
65
+ EOF
66
+ )"
67
+ if [[ -f "$gitignore" ]] && grep -Fq "$marker" "$gitignore"; then
68
+ return
69
+ fi
70
+ mkdir -p "$(dirname "$gitignore")"
71
+ if [[ -s "$gitignore" ]] && [[ "$(tail -c 1 "$gitignore" 2>/dev/null || true)" != "" ]]; then
72
+ printf '\n' >> "$gitignore"
73
+ fi
74
+ printf '%s\n' "$block" >> "$gitignore"
75
+ }
76
+
77
+ ensure_gitignore
78
+
56
79
  echo "[brick-office] HARNESS_ROOT = ${HARNESS_ROOT}"
57
- echo "[brick-office] dashboard 격리 경로 = ${LOCAL_DIR}"
80
+ echo "[brick-office] dashboard runtime path = ${LOCAL_DIR}"
58
81
 
59
82
  if [[ -d "${PACKAGED_DASHBOARD}" ]]; then
60
83
  INSTALLED_VERSION=""
@@ -70,14 +93,19 @@ if [[ -d "${PACKAGED_DASHBOARD}" ]]; then
70
93
  fi
71
94
  elif [[ ! -d "${LOCAL_DIR}/${DASHBOARD_PATH}" ]]; then
72
95
  echo "[brick-office] 첫 실행 — git sparse-checkout 으로 dashboard 만 가져옵니다 (~5MB)."
73
- mkdir -p "${LOCAL_DIR}"
74
- cd "${LOCAL_DIR}"
96
+ rm -rf "${LOCAL_DIR}/.download"
97
+ mkdir -p "${LOCAL_DIR}/.download"
98
+ cd "${LOCAL_DIR}/.download"
75
99
  git init -q
76
100
  git remote add origin "${REPO_URL}" 2>/dev/null || true
77
101
  git config core.sparseCheckout true
78
102
  echo "${DASHBOARD_PATH}/" > .git/info/sparse-checkout
79
103
  git fetch --depth=1 origin main -q
80
104
  git checkout main -q
105
+ mkdir -p "${LOCAL_DIR}/apps"
106
+ rm -rf "${LOCAL_DIR:?}/${DASHBOARD_PATH}"
107
+ cp -R "${DASHBOARD_PATH}" "${LOCAL_DIR}/apps/"
108
+ rm -rf "${LOCAL_DIR}/.download"
81
109
  echo "[brick-office] 다운로드 완료."
82
110
  fi
83
111
 
@@ -397,7 +397,7 @@ meeting_json="$(jq -n \
397
397
  echo "### Dispatcher/CEO"
398
398
  case "$verdict" in
399
399
  owner_needed)
400
- echo "- 판단: escalation 상태이므로 Owner에게 외부 결정이 필요한지 확인한다."
400
+ echo "- 판단: escalation 상태다. 먼저 CEO/CXX가 내부 권장안을 확정하고, 외부 권한(credentials/secrets, payment approval, legal/business acceptance, production access, destructive data action)이 필요할 때만 Owner-facing BLOCKED로 보고한다."
401
401
  ;;
402
402
  *)
403
403
  echo "- 판단: 최초 GOAL 이후 Owner 입력은 interrupt다. 회사는 Owner를 기다리지 않고 GOAL 달성 루프를 계속한다."
@@ -38,7 +38,7 @@ base_port="${env_base_port:-$config_base_port}"
38
38
  if [ -z "$base_port" ]; then
39
39
  echo "ERROR: HARNESS_BASE_PORT is not set." >&2
40
40
  echo "" >&2
41
- echo " CEO must agree on a {xx}000 base port with the Owner and add it to .env:" >&2
41
+ echo " CEO/CXX must choose an available {xx}000 base port and add it to .env:" >&2
42
42
  echo " HARNESS_BASE_PORT=3000" >&2
43
43
  echo "" >&2
44
44
  echo " OPS cannot monitor ports without a declared base. Set the value and re-run." >&2
@@ -208,7 +208,7 @@ if [ "$live" != "true" ] || [ "${service_count:-0}" -eq 0 ]; then
208
208
  (if (.log.configured|not) then "not configured" elif (.log.exists|not) then "missing" else ((.log.recent_errors|tostring) + " recent errors") end) +
209
209
  " | \(.status) |"' <<<"$build_results_json"
210
210
  echo ""
211
- echo "No production service endpoints are configured. Owner+CEO+CTO must define runtime.production.services[] before OPS can monitor live servers."
211
+ echo "No production service endpoints are configured. CEO+CTO+OPS must derive runtime.production.services[] from repo config, scripts, running processes, Docker, logs, or CXX reports before OPS can monitor live servers. Escalate to Owner only for external authority such as missing credentials or unavailable production access."
212
212
  } > "$report_path"
213
213
  echo "$report_rel"
214
214
  exit 0
@@ -195,6 +195,8 @@ ${CONTEXT_WARNING}
195
195
  - CXX는 직접 전문 산출물을 만들지 않고 worker report를 \`.harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/\`에 남겨야 한다
196
196
  - Owner에게 "계속 진행", "진행할까요?", "다음 명령을 입력하세요"를 요구하지 않는다. GOAL이 있고 escalation이 아니면 즉시 next_agent/current_agent 업무를 수행한다
197
197
  - Owner 입력은 목표 변경·사고·결과 확인용이지 회사 진행을 펌프하는 신호가 아니다
198
+ - CEO가 혼자 결정하기 어려우면 Owner에게 묻지 말고 관련 CXX 회의를 열어 written recommendation 을 받고 결정한다
199
+ - Owner 입력 대기는 credentials/secrets, payment approval, legal/business acceptance, unavailable production access, destructive data action, stated direction conflict 같은 외부 권한이 필요할 때만 허용한다
198
200
  - CEO는 필요한 CXX만 호출하고, CXX는 resource-manager/hiring 없이 worker 업무를 수행하지 않는다
199
201
  - 목표 설정/변경 → goal flow | 추가 요구사항 → submission flow | 긴급 수정 → hot-fix flow | 메타 질문 → 짧게 응답 (skip)
200
202
  - 활성 pipeline → next_agent/current_agent 컨텍스트로 계속
@@ -26,6 +26,28 @@ LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
26
26
  PLIST_NAME="com.walwal.harness-wake"
27
27
  PLIST_PATH="$LAUNCH_AGENTS_DIR/$PLIST_NAME.plist"
28
28
 
29
+ launch_domain() {
30
+ echo "gui/$(id -u)"
31
+ }
32
+
33
+ launch_is_loaded() {
34
+ local domain
35
+ domain="$(launch_domain)"
36
+ launchctl print "$domain/$PLIST_NAME" >/dev/null 2>&1
37
+ }
38
+
39
+ launch_unload() {
40
+ local domain
41
+ domain="$(launch_domain)"
42
+ launchctl bootout "$domain" "$PLIST_PATH" >/dev/null 2>&1 || launchctl unload "$PLIST_PATH" 2>/dev/null || true
43
+ }
44
+
45
+ launch_load() {
46
+ local domain
47
+ domain="$(launch_domain)"
48
+ launchctl bootstrap "$domain" "$PLIST_PATH" >/dev/null 2>&1 || launchctl load "$PLIST_PATH" 2>/dev/null
49
+ }
50
+
29
51
  # 템플릿 위치 후보 — walwal-harness 패키지 안일 수도, 프로젝트 안일 수도.
30
52
  detect_template() {
31
53
  local candidates=(
@@ -39,8 +61,8 @@ detect_template() {
39
61
  }
40
62
 
41
63
  ensure_dirs() {
42
- mkdir -p "$HARNESS_DIR" "$LOG_DIR" "$LAUNCH_AGENTS_DIR"
43
- touch "$PROJECTS_LIST"
64
+ mkdir -p "$HARNESS_DIR" "$LOG_DIR" "$LAUNCH_AGENTS_DIR" || return 1
65
+ touch "$PROJECTS_LIST" || return 1
44
66
  }
45
67
 
46
68
  abs_path() {
@@ -53,19 +75,32 @@ abs_path() {
53
75
  }
54
76
 
55
77
  cmd_add() {
56
- ensure_dirs
78
+ ensure_dirs || {
79
+ echo "[wake] ERROR: cannot prepare $HARNESS_DIR" >&2
80
+ return 1
81
+ }
57
82
  local project
58
83
  project=$(abs_path "$1")
59
84
  if grep -Fxq "$project" "$PROJECTS_LIST" 2>/dev/null; then
60
85
  echo "[wake] already registered: $project"
61
86
  return 0
62
87
  fi
63
- echo "$project" >> "$PROJECTS_LIST"
88
+ echo "$project" >> "$PROJECTS_LIST" || {
89
+ echo "[wake] ERROR: cannot write $PROJECTS_LIST" >&2
90
+ return 1
91
+ }
92
+ if ! grep -Fxq "$project" "$PROJECTS_LIST" 2>/dev/null; then
93
+ echo "[wake] ERROR: registration did not persist: $project" >&2
94
+ return 1
95
+ fi
64
96
  echo "[wake] added: $project"
65
97
  }
66
98
 
67
99
  cmd_remove() {
68
- ensure_dirs
100
+ ensure_dirs || {
101
+ echo "[wake] ERROR: cannot prepare $HARNESS_DIR" >&2
102
+ return 1
103
+ }
69
104
  local project
70
105
  project=$(abs_path "$1")
71
106
  if ! grep -Fxq "$project" "$PROJECTS_LIST" 2>/dev/null; then
@@ -80,7 +115,10 @@ cmd_remove() {
80
115
  }
81
116
 
82
117
  cmd_list() {
83
- ensure_dirs
118
+ ensure_dirs || {
119
+ echo "[wake] ERROR: cannot prepare $HARNESS_DIR" >&2
120
+ return 1
121
+ }
84
122
  if [ ! -s "$PROJECTS_LIST" ]; then
85
123
  echo "(empty)"
86
124
  return 0
@@ -89,28 +127,41 @@ cmd_list() {
89
127
  }
90
128
 
91
129
  cmd_status() {
130
+ ensure_dirs || {
131
+ echo "[wake] ERROR: cannot prepare $HARNESS_DIR" >&2
132
+ return 1
133
+ }
134
+ local loaded=1
92
135
  if [ -f "$PLIST_PATH" ]; then
93
136
  echo "[wake] plist: $PLIST_PATH (exists)"
94
137
  else
95
138
  echo "[wake] plist: $PLIST_PATH (NOT installed)"
96
139
  fi
97
- if launchctl list 2>/dev/null | grep -q "$PLIST_NAME"; then
140
+ if launch_is_loaded; then
141
+ loaded=0
98
142
  echo "[wake] launchd: loaded"
99
- launchctl list | awk -v n="$PLIST_NAME" '$3==n {print " pid="$1" exit="$2" label="$3}'
143
+ launchctl print "$(launch_domain)/$PLIST_NAME" 2>/dev/null | awk '
144
+ /state =/ {print " " $0}
145
+ /run interval =/ {print " " $0}
146
+ /runs =/ {print " " $0}
147
+ '
100
148
  else
101
149
  echo "[wake] launchd: NOT loaded"
102
150
  fi
103
151
  echo "[wake] projects:"
104
152
  cmd_list | sed 's/^/ /'
153
+ return "$loaded"
105
154
  }
106
155
 
107
156
  cmd_install() {
108
- ensure_dirs
157
+ ensure_dirs || {
158
+ echo "[wake] ERROR: cannot prepare $HARNESS_DIR" >&2
159
+ return 1
160
+ }
109
161
 
110
162
  # 추가 인자가 있으면 add
111
- shift_count=0
112
163
  for p in "$@"; do
113
- cmd_add "$p"
164
+ cmd_add "$p" || return 1
114
165
  done
115
166
 
116
167
  local template
@@ -127,22 +178,24 @@ cmd_install() {
127
178
  "$template" > "$PLIST_PATH"
128
179
 
129
180
  # 기존 jobs 언로드 (idempotent)
130
- launchctl unload "$PLIST_PATH" 2>/dev/null || true
181
+ launch_unload
131
182
 
132
183
  # 로드
133
- if launchctl load "$PLIST_PATH" 2>/dev/null; then
184
+ local loaded=1
185
+ if launch_load; then
186
+ loaded=0
134
187
  echo "[wake] launchd loaded: $PLIST_PATH"
135
188
  else
136
189
  echo "[wake] WARN: launchctl load 실패. 다음 명령으로 직접 시도해 보세요:" >&2
137
190
  echo " launchctl load $PLIST_PATH" >&2
138
191
  fi
139
192
 
140
- cmd_status
193
+ cmd_status || return "$loaded"
141
194
  }
142
195
 
143
196
  cmd_uninstall() {
144
197
  if [ -f "$PLIST_PATH" ]; then
145
- launchctl unload "$PLIST_PATH" 2>/dev/null || true
198
+ launch_unload
146
199
  rm -f "$PLIST_PATH"
147
200
  echo "[wake] uninstalled: $PLIST_PATH"
148
201
  else