@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 +7 -0
- package/HR-Resource/ceo/SKILL.md +15 -4
- package/HR-Resource/ops/SKILL.md +3 -3
- package/README.md +5 -2
- package/apps/harness-dashboard/components/Scene.tsx +38 -24
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +36 -3
- package/apps/harness-dashboard/lib/harness-state.ts +44 -20
- package/assets/templates/AGENTS-ko.md.template +4 -0
- package/assets/templates/AGENTS.md.template +7 -3
- package/assets/templates/config.json +1 -1
- package/assets/templates/gitignore-append.txt +6 -1
- package/assets/templates/memory.md +1 -1
- package/bin/init.js +25 -2
- package/commands/goal.md +2 -1
- package/commands/hot-fix.md +2 -1
- package/commands/submission.md +3 -2
- package/conventions/ceo.md +2 -0
- package/gotchas/ceo.md +4 -0
- package/package.json +1 -1
- package/scripts/harness-dashboard-up.sh +36 -8
- package/scripts/harness-hourly-review.sh +1 -1
- package/scripts/harness-service-ops-monitor.sh +2 -2
- package/scripts/harness-user-prompt-submit.sh +2 -0
- package/scripts/harness-wake-install.sh +68 -15
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.
|
package/HR-Resource/ceo/SKILL.md
CHANGED
|
@@ -9,6 +9,17 @@ disable-model-invocation: false
|
|
|
9
9
|
|
|
10
10
|
You are the only direct conversation channel with the Owner.
|
|
11
11
|
|
|
12
|
+
## Autonomous Operating Charter
|
|
13
|
+
|
|
14
|
+
The Owner gives direction and final acceptance, not routine operating answers.
|
|
15
|
+
|
|
16
|
+
- Do not ask the Owner whether to continue, which option to choose, which worker to hire, or whether to start the next planned step.
|
|
17
|
+
- When a decision is needed, decide from mission evidence, conventions, gotchas, CXX recommendations, and the smallest reversible path. Record the decision and rationale in `ceo.md`.
|
|
18
|
+
- If CEO cannot decide alone, convene the relevant CXX agents and use their written recommendations to decide. Do not turn the uncertainty into an Owner question.
|
|
19
|
+
- Treat Owner messages as direction changes, new requirements, hot-fixes, final acceptance feedback, or explicit constraints. They are not required to keep the company loop moving.
|
|
20
|
+
- Only stop for Owner input when the next action requires external authority the harness cannot infer or obtain: credentials/secrets, payment approval, legal/business acceptance, production access not already granted, destructive data action, or a goal conflict that would knowingly violate the Owner's stated direction.
|
|
21
|
+
- When stopped for external authority, mark the mission `blocked` with the exact missing authority and the internally recommended default. Do not ask an open-ended question.
|
|
22
|
+
|
|
12
23
|
## Lazy Rule Loading
|
|
13
24
|
|
|
14
25
|
Before routing or accepting CXX work, enforce lazy loading:
|
|
@@ -29,7 +40,7 @@ Before routing or accepting CXX work, enforce lazy loading:
|
|
|
29
40
|
- CQO: quality gates, e2e/backtest strategy, regression and archive criteria.
|
|
30
41
|
- OPS: build/service environment monitoring, CQO verification watch, port map checks, launch observation, production watch, and exception monitoring.
|
|
31
42
|
4. Route completed outputs to the next responsible CXX.
|
|
32
|
-
5. Report outcomes and
|
|
43
|
+
5. Report outcomes, final acceptance requests, and true external-authority blocks to the Owner.
|
|
33
44
|
|
|
34
45
|
## Hard Rules
|
|
35
46
|
|
|
@@ -41,9 +52,9 @@ Before routing or accepting CXX work, enforce lazy loading:
|
|
|
41
52
|
- Every CXX starts from fresh context and records decisions in `.harness/documents/{goal-or-child-mission}/{cxx}.md`.
|
|
42
53
|
- Hiring or resource-manager output is never a stopping point. After missing workers are registered, immediately continue routing to the responsible CXX fresh sessions and require those CXX agents to brief/run the hired workers. Do not end the turn with only a hiring summary while the Owner goal remains unfinished.
|
|
43
54
|
- Preserve DDD boundaries: domain decisions, application wiring, infrastructure, and quality policy are separate responsibilities.
|
|
44
|
-
- Before CTO/CDO/OPS allocate runnable services,
|
|
55
|
+
- Before CTO/CDO/OPS allocate runnable services, choose a `{xx}000` base port from available local evidence unless the Owner already specified one, then write it to project `.env` as `HARNESS_BASE_PORT={xx}000`. Mentioning the value in `ceo.md` is not sufficient.
|
|
45
56
|
- After writing `.env`, verify with `grep '^HARNESS_BASE_PORT=' .env` before routing service work.
|
|
46
|
-
- For service monitoring,
|
|
57
|
+
- For service monitoring, derive the server mapping from repository config, running processes, Docker, scripts, logs, and CXX reports first: local PC, Docker, VM, AWS/cloud, host, port, health path, log path, and contact/source. If a production target or credential is unavailable, record a BLOCKED external-authority item instead of asking a broad Owner question.
|
|
47
58
|
- For runnable verification, collect or require CTO to record the test runtime mapping before CQO starts evaluator work: command/service name, cwd, host, port, health path if any, log path if any, and owner. OPS must watch that runtime during CQO Playwright/E2E/API/visual/performance/regression checks.
|
|
48
59
|
- CEO must not accept CQO PASS for a runnable product unless OPS has supplied clean verification-watch evidence or an explicit not-applicable reason. Open OPS incidents, missing runtime mapping, missing required logs, service down, or health mismatch block Owner acceptance.
|
|
49
60
|
- After launch, CEO treats OPS production incidents as company events. CEO convenes CTO/CQO/OPS when user-impacting production signals appear; CTO owns recovery, CQO owns regression confirmation, and OPS owns evidence and close criteria.
|
|
@@ -94,7 +105,7 @@ If CEO needs implementation done, CEO routes to CTO. CTO then hires dev workers.
|
|
|
94
105
|
If CEO needs QA done, CEO routes to CQO. CQO then hires evaluator/tester workers.
|
|
95
106
|
CEO does not contact workers. CXX contact workers.
|
|
96
107
|
|
|
97
|
-
**When a CXX is unavailable or unresponsive:**
|
|
108
|
+
**When a CXX is unavailable or unresponsive:** retry with a fresh role-scoped context, route to another relevant CXX for recovery planning, or record an internal blocker with evidence. Escalate to the Owner only when the blocker requires external authority listed in the Autonomous Operating Charter.
|
|
98
109
|
|
|
99
110
|
## Worktree Isolation Failure — No git Repo
|
|
100
111
|
|
package/HR-Resource/ops/SKILL.md
CHANGED
|
@@ -17,7 +17,7 @@ OPS owns three environment classes:
|
|
|
17
17
|
|
|
18
18
|
- Build environment: command-based local execution such as `flutter run`, `npm run dev`, test watchers, local build scripts, and other foreground/background commands used by CXX workers.
|
|
19
19
|
- Verification environment: the runnable local, preview, Docker, or cloud target used while CQO evaluator workers run Playwright, E2E, API, visual, accessibility, performance, or regression checks.
|
|
20
|
-
- Service environment: an actual server endpoint that can be monitored. The server may be the Owner's local PC, Docker, a VM, AWS, or another cloud/server target. OPS does not invent this mapping; CEO must
|
|
20
|
+
- Service environment: an actual server endpoint that can be monitored. The server may be the Owner's local PC, Docker, a VM, AWS, or another cloud/server target. OPS does not invent this mapping; CEO/CTO/OPS must derive it from repo config, scripts, running processes, Docker, logs, and CXX reports before OPS treats it as live. Escalate to Owner only when external authority is missing, such as credentials or unavailable production access.
|
|
21
21
|
|
|
22
22
|
OPS is not the implementation owner. CTO/DevOps workers start or change systems; OPS observes whether the declared build/service environments are healthy and raises evidence-backed events.
|
|
23
23
|
OPS must not directly perform DevOps implementation, service fixes, config rewrites, deployment changes, or recovery work. OPS may only monitor, classify, brief hired Ops/DevOps workers, review their reports, and escalate evidence-backed events.
|
|
@@ -46,8 +46,8 @@ After service launch, OPS continues the same monitoring duty against `runtime.pr
|
|
|
46
46
|
|
|
47
47
|
## Port Policy
|
|
48
48
|
|
|
49
|
-
- CEO must
|
|
50
|
-
- The
|
|
49
|
+
- CEO/CTO/OPS must choose an available `{xx}000` base port before CXX services are allocated, unless the Owner already specified one.
|
|
50
|
+
- The selected base port is recorded in project `.env` as `HARNESS_BASE_PORT`.
|
|
51
51
|
- Service-specific ports must be derived above that base port, for example `HARNESS_FRONTEND_PORT`, `HARNESS_API_PORT`, and `HARNESS_DASHBOARD_PORT`.
|
|
52
52
|
- CXX agents must read `.env` or `.harness/config.json runtime.ports.base` before assigning ports.
|
|
53
53
|
- OPS reports any service using an unmapped or off-range port as an ops drift.
|
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ Run on a different port:
|
|
|
164
164
|
bash scripts/harness-dashboard-up.sh --port 3002
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
Force-refresh the
|
|
167
|
+
Force-refresh the project-local dashboard runtime when an older dashboard is still shown:
|
|
168
168
|
|
|
169
169
|
```bash
|
|
170
170
|
bash scripts/harness-dashboard-up.sh --port 3002 --reinstall
|
|
@@ -175,7 +175,9 @@ Dashboard options:
|
|
|
175
175
|
| Option | Role |
|
|
176
176
|
|---|---|
|
|
177
177
|
| `--port <port>` | Run Brick Office on a custom port, e.g. `3002` |
|
|
178
|
-
| `--reinstall` | Delete and rebuild the
|
|
178
|
+
| `--reinstall` | Delete and rebuild the project-local dashboard runtime under `.harness/dashboard/` |
|
|
179
|
+
|
|
180
|
+
The dashboard app copy lives under `.harness/dashboard/` for the current project. Generated dependencies and build output are ignored via `.gitignore`; the runtime files the dashboard reads remain the normal `.harness/` files.
|
|
179
181
|
|
|
180
182
|
### Company Auto-Chain State
|
|
181
183
|
|
|
@@ -283,6 +285,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
|
|
119
|
+
const lanes = useMemo(() => buildLanes(snapshot), [snapshot]);
|
|
120
120
|
const heatmapStartedAt = useMemo(() => Date.now() - VISIBLE_BUCKETS * BUCKET_MS, [snapshot.ts]);
|
|
121
121
|
const persistedHeatSamples = useMemo<HeatSample[]>(
|
|
122
122
|
() =>
|
|
@@ -130,13 +130,13 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
|
|
|
130
130
|
[snapshot.activitySamples]
|
|
131
131
|
);
|
|
132
132
|
const [heatSamples, setHeatSamples] = useState<HeatSample[]>([]);
|
|
133
|
-
const sampleSourceRef = useRef({ lanes, snapshot
|
|
133
|
+
const sampleSourceRef = useRef({ lanes, snapshot });
|
|
134
134
|
useEffect(() => {
|
|
135
|
-
sampleSourceRef.current = { lanes, snapshot
|
|
136
|
-
});
|
|
135
|
+
sampleSourceRef.current = { lanes, snapshot };
|
|
136
|
+
}, [lanes, snapshot]);
|
|
137
137
|
useEffect(() => {
|
|
138
138
|
const tick = () => {
|
|
139
|
-
const { lanes, snapshot
|
|
139
|
+
const { lanes, snapshot } = sampleSourceRef.current;
|
|
140
140
|
// Gate live samples on the harness runtime — once the session reports
|
|
141
141
|
// completion (or there's no current/next agent), every "active" signal
|
|
142
142
|
// is stale (todos stay status="active", worker mtimes stay recent), so
|
|
@@ -156,7 +156,7 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
|
|
|
156
156
|
}
|
|
157
157
|
setHeatSamples((prev) => {
|
|
158
158
|
const now = Date.now();
|
|
159
|
-
const activeMission =
|
|
159
|
+
const activeMission = snapshot.missions.find((mission) => mission.active) ?? snapshot.missions[0] ?? null;
|
|
160
160
|
const next: HeatSample[] = [];
|
|
161
161
|
for (const lane of lanes) {
|
|
162
162
|
// Live overlay records active work only. Historical participation is
|
|
@@ -204,10 +204,10 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
|
|
|
204
204
|
() => buildHeatmap(
|
|
205
205
|
snapshot,
|
|
206
206
|
lanes,
|
|
207
|
-
|
|
207
|
+
[...persistedHeatSamples, ...heatSamples],
|
|
208
208
|
heatmapStartedAt
|
|
209
209
|
),
|
|
210
|
-
[heatSamples, heatmapStartedAt, lanes, persistedHeatSamples,
|
|
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
|
|
1810
|
-
const activeMission =
|
|
1809
|
+
function buildLanes(snapshot: HarnessSnapshot): AgentLane[] {
|
|
1810
|
+
const activeMission = snapshot.missions.find((mission) => mission.active) ?? snapshot.missions[0] ?? null;
|
|
1811
|
+
const workersByRole = new Map<CxxRole, Array<{ worker: WorkerDocEntry; mission: MissionDoc }>>();
|
|
1812
|
+
for (const role of CXX_ROLES) workersByRole.set(role, []);
|
|
1813
|
+
|
|
1814
|
+
const workerIndex = new Map<string, { worker: WorkerDocEntry; mission: MissionDoc }>();
|
|
1815
|
+
for (const mission of snapshot.missions) {
|
|
1816
|
+
for (const worker of mission.workers) {
|
|
1817
|
+
if (!CXX_ROLES.includes(worker.owner as CxxRole)) continue;
|
|
1818
|
+
const role = worker.owner as CxxRole;
|
|
1819
|
+
const key = `${role}:${worker.name}`;
|
|
1820
|
+
const existing = workerIndex.get(key);
|
|
1821
|
+
if (
|
|
1822
|
+
!existing ||
|
|
1823
|
+
(!existing.worker.active && worker.active) ||
|
|
1824
|
+
(existing.worker.status === "COMPLETE" && worker.status !== "COMPLETE")
|
|
1825
|
+
) {
|
|
1826
|
+
workerIndex.set(key, { worker, mission });
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
for (const entry of workerIndex.values()) {
|
|
1832
|
+
workersByRole.get(entry.worker.owner as CxxRole)?.push(entry);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
385
|
+
it("does not treat a recently written docmeta draft as live worker activity without runtime state", () => {
|
|
386
386
|
const harnessDir = path.join(dir, ".harness");
|
|
387
387
|
const missionDir = path.join(harnessDir, "documents", "goal-1-dashboard");
|
|
388
388
|
mkdirSync(path.join(missionDir, "cto", "workers"), { recursive: true });
|
|
@@ -403,9 +403,42 @@ describe("readHarnessState", () => {
|
|
|
403
403
|
const workers = readHarnessState(dir).missions[0].workers;
|
|
404
404
|
expect(workers[0]).toMatchObject({
|
|
405
405
|
name: "frontend-worker",
|
|
406
|
-
status: "
|
|
407
|
-
active:
|
|
406
|
+
status: "COMPLETE",
|
|
407
|
+
active: false,
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("reads manual worker telemetry with name, progress, eta, and report_path", () => {
|
|
412
|
+
const harnessDir = path.join(dir, ".harness");
|
|
413
|
+
mkdirSync(harnessDir, { recursive: true });
|
|
414
|
+
writeFileSync(
|
|
415
|
+
path.join(harnessDir, "progress.json"),
|
|
416
|
+
JSON.stringify({
|
|
417
|
+
agent_status: "running",
|
|
418
|
+
company_state: {
|
|
419
|
+
active_workers: 2,
|
|
420
|
+
workers: [
|
|
421
|
+
{
|
|
422
|
+
name: "frontend-engineer-multi-strategy",
|
|
423
|
+
status: "running",
|
|
424
|
+
progress: "70%",
|
|
425
|
+
eta: "2026-05-31 17:00",
|
|
426
|
+
report_path: ".harness/documents/goal-1/cto/workers/frontend-engineer-multi-strategy.md",
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const worker = readHarnessState(dir).dashboard.workers[0];
|
|
434
|
+
expect(worker).toMatchObject({
|
|
435
|
+
agent: "frontend-engineer-multi-strategy",
|
|
436
|
+
feature: "frontend-engineer-multi-strategy",
|
|
437
|
+
status: "running",
|
|
438
|
+
log: ".harness/documents/goal-1/cto/workers/frontend-engineer-multi-strategy.md",
|
|
439
|
+
progress: 0.7,
|
|
408
440
|
});
|
|
441
|
+
expect(worker.summary).toContain("2026-05-31 17:00");
|
|
409
442
|
});
|
|
410
443
|
|
|
411
444
|
it("accepts keyed company_state.workers maps from manual CXX telemetry", () => {
|
|
@@ -45,7 +45,6 @@ import type {
|
|
|
45
45
|
|
|
46
46
|
const SNAPSHOT_VERSION = "1.2.0";
|
|
47
47
|
const GOAL_DESC_TRUNCATE = 200;
|
|
48
|
-
const RECENT_WORKER_ACTIVE_MS = 10 * 60 * 1000;
|
|
49
48
|
const ACTIVITY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
50
49
|
|
|
51
50
|
interface RawProgress {
|
|
@@ -148,22 +147,31 @@ interface RawProgress {
|
|
|
148
147
|
company_state?: {
|
|
149
148
|
active_workers?: number;
|
|
150
149
|
workers?: Array<{
|
|
150
|
+
name?: string;
|
|
151
151
|
team?: number | string;
|
|
152
152
|
feature?: string;
|
|
153
153
|
agent?: string;
|
|
154
154
|
phase?: string;
|
|
155
155
|
prompt?: string | null;
|
|
156
156
|
log?: string | null;
|
|
157
|
+
report?: string | null;
|
|
158
|
+
report_path?: string | null;
|
|
159
|
+
progress?: string | number | null;
|
|
160
|
+
eta?: string | null;
|
|
157
161
|
spawn_status?: string;
|
|
158
162
|
status?: string;
|
|
159
163
|
pid?: number | null;
|
|
160
164
|
}> | Record<string, {
|
|
165
|
+
name?: string;
|
|
161
166
|
team?: number | string;
|
|
162
167
|
feature?: string;
|
|
163
168
|
agent?: string;
|
|
164
169
|
phase?: string;
|
|
165
170
|
prompt?: string | null;
|
|
166
171
|
log?: string | null;
|
|
172
|
+
report_path?: string | null;
|
|
173
|
+
progress?: string | number | null;
|
|
174
|
+
eta?: string | null;
|
|
167
175
|
spawn_status?: string;
|
|
168
176
|
status?: string;
|
|
169
177
|
pid?: number | null;
|
|
@@ -797,6 +805,7 @@ function readFeatureTitles(rootDir: string): Map<string, string> {
|
|
|
797
805
|
}
|
|
798
806
|
|
|
799
807
|
interface RawWorker {
|
|
808
|
+
name?: string;
|
|
800
809
|
team?: number | string;
|
|
801
810
|
feature?: string;
|
|
802
811
|
agent?: string;
|
|
@@ -808,6 +817,9 @@ interface RawWorker {
|
|
|
808
817
|
pid?: number | null;
|
|
809
818
|
owner?: string;
|
|
810
819
|
report?: string | null;
|
|
820
|
+
report_path?: string | null;
|
|
821
|
+
progress?: string | number | null;
|
|
822
|
+
eta?: string | null;
|
|
811
823
|
}
|
|
812
824
|
type WorkerOwner = WorkerDocEntry["owner"];
|
|
813
825
|
|
|
@@ -859,6 +871,14 @@ function readMissionState(missionPath: string): {
|
|
|
859
871
|
}
|
|
860
872
|
|
|
861
873
|
function deriveWorkerProgress(worker: RawWorker): number | null {
|
|
874
|
+
const explicit = worker?.progress;
|
|
875
|
+
if (typeof explicit === "number" && Number.isFinite(explicit)) {
|
|
876
|
+
return explicit > 1 ? Math.max(0, Math.min(1, explicit / 100)) : Math.max(0, Math.min(1, explicit));
|
|
877
|
+
}
|
|
878
|
+
if (typeof explicit === "string") {
|
|
879
|
+
const match = explicit.match(/(\d+(?:\.\d+)?)/);
|
|
880
|
+
if (match) return Math.max(0, Math.min(1, Number(match[1]) / 100));
|
|
881
|
+
}
|
|
862
882
|
const status = (worker?.status ?? worker?.spawn_status ?? "").toLowerCase();
|
|
863
883
|
if (status === "blocked" || status === "failed") return 0.35;
|
|
864
884
|
if (status === "recorded") return 0.15;
|
|
@@ -868,17 +888,24 @@ function deriveWorkerProgress(worker: RawWorker): number | null {
|
|
|
868
888
|
}
|
|
869
889
|
|
|
870
890
|
function normalizeRawWorkers(value: RawWorker[] | Record<string, RawWorker> | null | undefined): RawWorker[] {
|
|
871
|
-
|
|
891
|
+
const normalizeOne = (worker: RawWorker, fallbackName: string | null = null, idx = 0): RawWorker => {
|
|
892
|
+
const name = worker.name ?? worker.agent ?? worker.feature ?? fallbackName ?? `worker-${idx + 1}`;
|
|
893
|
+
const report = worker.report ?? worker.report_path ?? null;
|
|
894
|
+
return {
|
|
895
|
+
...worker,
|
|
896
|
+
name,
|
|
897
|
+
team: worker.team ?? idx + 1,
|
|
898
|
+
agent: worker.agent ?? name,
|
|
899
|
+
feature: worker.feature ?? name,
|
|
900
|
+
log: worker.log ?? report,
|
|
901
|
+
report,
|
|
902
|
+
};
|
|
903
|
+
};
|
|
904
|
+
if (Array.isArray(value)) return value.map((worker, idx) => normalizeOne(worker, null, idx));
|
|
872
905
|
if (!value || typeof value !== "object") return [];
|
|
873
906
|
return Object.entries(value).map(([name, worker], idx) => {
|
|
874
907
|
const w = worker && typeof worker === "object" ? worker : {};
|
|
875
|
-
return
|
|
876
|
-
...w,
|
|
877
|
-
team: (w as RawWorker).team ?? idx + 1,
|
|
878
|
-
agent: (w as RawWorker).agent ?? name,
|
|
879
|
-
feature: (w as RawWorker).feature ?? name,
|
|
880
|
-
log: (w as RawWorker).log ?? (w as RawWorker & { report?: string | null }).report ?? null,
|
|
881
|
-
};
|
|
908
|
+
return normalizeOne(w as RawWorker, name, idx);
|
|
882
909
|
});
|
|
883
910
|
}
|
|
884
911
|
|
|
@@ -911,7 +938,8 @@ function buildWorkers(rootDir: string, progress: RawProgress | null): WorkerSnap
|
|
|
911
938
|
const feature = w.feature ?? `worker-${idx + 1}`;
|
|
912
939
|
const promptText = w.prompt ? readTextSafe(path.join(rootDir, w.prompt)) : null;
|
|
913
940
|
const logText = w.log ? readTextSafe(path.join(rootDir, w.log)) : null;
|
|
914
|
-
const
|
|
941
|
+
const eta = w.eta ? ` ETA ${w.eta}` : "";
|
|
942
|
+
const summarySource = firstLine(logText) || firstLine(promptText) || `${w.agent ?? w.name ?? "worker"} ${w.status ?? "assigned"}${eta}`;
|
|
915
943
|
const material = w.prompt ?? w.log ?? null;
|
|
916
944
|
const rawStatus = (w.status ?? w.spawn_status ?? "unknown").toLowerCase();
|
|
917
945
|
const status = ["spawned", "recorded", "running", "idle", "blocked"].includes(rawStatus)
|
|
@@ -1036,13 +1064,13 @@ function activeWorkerNames(progress: RawProgress | null): Set<string> {
|
|
|
1036
1064
|
if (isHarnessRuntimeIdle(progress)) return active;
|
|
1037
1065
|
const workers = [
|
|
1038
1066
|
...normalizeRawWorkers(progress?.company_state?.workers),
|
|
1039
|
-
...(progress?.company_state?.last_dispatch
|
|
1067
|
+
...normalizeRawWorkers(progress?.company_state?.last_dispatch),
|
|
1040
1068
|
];
|
|
1041
1069
|
for (const worker of workers) {
|
|
1042
1070
|
const rawStatus = (worker.status ?? worker.spawn_status ?? "").toLowerCase();
|
|
1043
1071
|
if (["complete", "completed", "done", "idle"].includes(rawStatus)) continue;
|
|
1044
|
-
for (const value of [worker.agent, worker.feature]) {
|
|
1045
|
-
const normalized = normalizeWorkerName(value);
|
|
1072
|
+
for (const value of [worker.name, worker.agent, worker.feature, worker.report, worker.report_path, worker.log]) {
|
|
1073
|
+
const normalized = normalizeWorkerName(value && /[\\/]/.test(value) ? path.basename(value) : value);
|
|
1046
1074
|
if (normalized) active.add(normalized);
|
|
1047
1075
|
}
|
|
1048
1076
|
}
|
|
@@ -1262,21 +1290,17 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1262
1290
|
const name = normalizeWorkerName(f);
|
|
1263
1291
|
const hired = hiredForMission(hiredWorkers, name, owner, rel);
|
|
1264
1292
|
let updatedAt: string | null = null;
|
|
1265
|
-
let recentlyTouched = false;
|
|
1266
1293
|
try {
|
|
1267
1294
|
const mtime = statSync(reportAbs).mtime;
|
|
1268
1295
|
updatedAt = mtime.toISOString();
|
|
1269
|
-
recentlyTouched = Date.now() - mtime.getTime() < RECENT_WORKER_ACTIVE_MS;
|
|
1270
1296
|
} catch { /* ignore */ }
|
|
1271
1297
|
const statusMatch = content.match(/##\s*Status\s*\n+([A-Z_]+)/);
|
|
1272
1298
|
const hasDocmeta = /^---[\s\S]*?docmeta:[\s\S]*?---/.test(content);
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1299
|
+
// Runtime state is the source of truth for live activity. A
|
|
1300
|
+
// recently touched docmeta-only draft is not enough to prove that a
|
|
1301
|
+
// detached worker session is still running.
|
|
1276
1302
|
const status: WorkerDocEntry["status"] = statusMatch
|
|
1277
1303
|
? (statusMatch[1] as WorkerDocEntry["status"])
|
|
1278
|
-
: hasDocmeta && recentlyTouched && !runtimeIdle
|
|
1279
|
-
? "IN_PROGRESS"
|
|
1280
1304
|
: hasDocmeta
|
|
1281
1305
|
? "COMPLETE"
|
|
1282
1306
|
: "unknown";
|
|
@@ -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.
|
|
39
|
-
- If multiple approaches exist, name them
|
|
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
|
-
-
|
|
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": "
|
|
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
|
-
#
|
|
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)
|
|
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
|
|
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 —
|
|
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
|
|
package/commands/hot-fix.md
CHANGED
|
@@ -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
|
|
package/commands/submission.md
CHANGED
|
@@ -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
|
|
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
|
|
package/conventions/ceo.md
CHANGED
|
@@ -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.
|
|
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
|
-
# (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
140
|
+
if launch_is_loaded; then
|
|
141
|
+
loaded=0
|
|
98
142
|
echo "[wake] launchd: loaded"
|
|
99
|
-
launchctl
|
|
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
|
-
|
|
181
|
+
launch_unload
|
|
131
182
|
|
|
132
183
|
# 로드
|
|
133
|
-
|
|
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
|
-
|
|
198
|
+
launch_unload
|
|
146
199
|
rm -f "$PLIST_PATH"
|
|
147
200
|
echo "[wake] uninstalled: $PLIST_PATH"
|
|
148
201
|
else
|