@walwal-harness/cli 7.1.48 → 7.1.49
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 +1 -1
- package/HR-Resource/support-support-mcp-registry-steward/SKILL.md +0 -1
- package/README.md +2 -1
- package/apps/harness-dashboard/lib/__tests__/brick-adapter.test.ts +44 -0
- package/apps/harness-dashboard/lib/__tests__/company-flow.test.ts +33 -1
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +55 -0
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +67 -2
- package/apps/harness-dashboard/lib/brick/adapter.ts +5 -1
- package/apps/harness-dashboard/lib/harness-state.ts +12 -2
- package/apps/harness-dashboard/next.config.mjs +1 -3
- package/apps/harness-dashboard/package.json +1 -1
- package/bin/init.js +10 -5
- package/package.json +1 -1
- package/scripts/harness-company-block.sh +30 -9
- package/scripts/harness-company-complete.sh +32 -11
- package/scripts/harness-dashboard-up.sh +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -47,7 +47,7 @@ docmeta:
|
|
|
47
47
|
- 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`.
|
|
48
48
|
- 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.
|
|
49
49
|
- 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.
|
|
50
|
-
-
|
|
50
|
+
- Hourly wake registration is opt-in during init/migrate (`--enable-wake` or `WALWAL_ENABLE_WAKE=1`), and wake status now uses the macOS `launchctl print gui/$UID/...` state to avoid false `NOT loaded` reports.
|
|
51
51
|
|
|
52
52
|
## 7.1.36 — Stop hook active-loop guard + visible heatmap (2026-05-29)
|
|
53
53
|
|
package/README.md
CHANGED
|
@@ -243,6 +243,7 @@ Features:
|
|
|
243
243
|
|
|
244
244
|
Hourly autonomous wake:
|
|
245
245
|
|
|
246
|
+
- Init/migrate does not auto-register launchd wake by default. Use `--enable-wake` or `WALWAL_ENABLE_WAKE=1` when you want installation/migration to register it; otherwise run `bash scripts/harness-wake-install.sh install .` from the target project.
|
|
246
247
|
- `HARNESS_WAKE_EXECUTOR=claude|codex` selects `claude -p` or `codex exec`.
|
|
247
248
|
- `HARNESS_WAKE_MODEL=<model>` optionally pins the Claude model for wake ticks.
|
|
248
249
|
- Each wake tick prompts CEO to convene CXX + OPS, collect progress and decisions, dispatch the next action without asking Owner, and require CQO/OPS verification until the active goal works.
|
|
@@ -286,7 +287,7 @@ harness-hiring → register and onboard worker
|
|
|
286
287
|
| Version | Summary |
|
|
287
288
|
|---|---|
|
|
288
289
|
| 7.1.38 | CEO can approve reversible routine operations without Owner confirmation, including monitoring cadence, local scheduler activation, Telegram briefing formats with existing credentials, dashboard refresh, and consolidation/supersede cleanup |
|
|
289
|
-
| 7.1.37 | Project-local dashboard runtime under `.harness/dashboard/`, unfiltered all-mission heatmap, worker telemetry parsing, and
|
|
290
|
+
| 7.1.37 | Project-local dashboard runtime under `.harness/dashboard/`, unfiltered all-mission heatmap, worker telemetry parsing, and opt-in hourly wake registration/status |
|
|
290
291
|
| 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 |
|
|
291
292
|
| 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 |
|
|
292
293
|
| 7.1.34 | Fix dashboard 500 when `company_state.workers` is recorded as a keyed worker map instead of an array |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toContractState } from "../brick/adapter";
|
|
3
|
+
import type { HarnessSnapshot } from "../types";
|
|
4
|
+
|
|
5
|
+
function snapshotWithActivity(ts: string): HarnessSnapshot {
|
|
6
|
+
return {
|
|
7
|
+
version: "test",
|
|
8
|
+
ts: new Date().toISOString(),
|
|
9
|
+
projectName: "demo",
|
|
10
|
+
projectPath: "/tmp/demo",
|
|
11
|
+
errorBanner: null,
|
|
12
|
+
runtime: {
|
|
13
|
+
currentAgent: null,
|
|
14
|
+
agentStatus: "pending",
|
|
15
|
+
nextAgent: null,
|
|
16
|
+
updatedAt: null,
|
|
17
|
+
conductorState: "running",
|
|
18
|
+
ownerPrompt: null,
|
|
19
|
+
},
|
|
20
|
+
incidents: [],
|
|
21
|
+
missions: [],
|
|
22
|
+
ownerHistory: [],
|
|
23
|
+
gotchas: [],
|
|
24
|
+
conventions: [],
|
|
25
|
+
todos: [],
|
|
26
|
+
events: [],
|
|
27
|
+
activitySamples: [{ ts, laneId: "cto", count: 1, hotfix: false, missionId: "goal-1" }],
|
|
28
|
+
files: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("toContractState", () => {
|
|
33
|
+
it("does not mark a role active from stale historical activity samples", () => {
|
|
34
|
+
const now = Date.parse("2026-06-05T00:10:00Z");
|
|
35
|
+
const state = toContractState(snapshotWithActivity("2026-06-05T00:00:00Z"), true, now);
|
|
36
|
+
expect(state.agents.find((agent) => agent.id === "cto")?.status).toBe("idle");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("marks a role active from recent activity samples", () => {
|
|
40
|
+
const now = Date.parse("2026-06-05T00:01:00Z");
|
|
41
|
+
const state = toContractState(snapshotWithActivity("2026-06-05T00:00:30Z"), true, now);
|
|
42
|
+
expect(state.agents.find((agent) => agent.id === "cto")?.status).toBe("active");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -267,4 +267,36 @@ describeHarnessShell("company-loop shell flows", () => {
|
|
|
267
267
|
expect(progress.planner.requested_mode).toBeNull();
|
|
268
268
|
expect(progress.meetings.requested_type).toBe("followup-review");
|
|
269
269
|
});
|
|
270
|
+
|
|
271
|
+
it("company terminal transitions only update the newest current mission state", () => {
|
|
272
|
+
writeState(dir, {
|
|
273
|
+
current_agent: "ceo",
|
|
274
|
+
agent_status: "running",
|
|
275
|
+
next_agent: "none",
|
|
276
|
+
owner_prompt: { status: "routing" },
|
|
277
|
+
company_state: { state: "running", active_workers: 0 },
|
|
278
|
+
conductor: { state: "running", tracks: [] },
|
|
279
|
+
});
|
|
280
|
+
const docs = path.join(dir, ".harness", "documents");
|
|
281
|
+
const oldMission = path.join(docs, "goal-1-old");
|
|
282
|
+
const newMission = path.join(docs, "goal-2-new");
|
|
283
|
+
mkdirSync(oldMission, { recursive: true });
|
|
284
|
+
mkdirSync(newMission, { recursive: true });
|
|
285
|
+
const oldState = path.join(oldMission, "mission-state.json");
|
|
286
|
+
const newState = path.join(newMission, "mission-state.json");
|
|
287
|
+
writeFileSync(oldState, JSON.stringify({ lifecycle: "active", active: true }));
|
|
288
|
+
writeFileSync(newState, JSON.stringify({ lifecycle: "active", active: true }));
|
|
289
|
+
const oldDate = new Date("2026-05-01T00:00:00Z");
|
|
290
|
+
const newDate = new Date("2026-05-02T00:00:00Z");
|
|
291
|
+
utimesSync(oldState, oldDate, oldDate);
|
|
292
|
+
utimesSync(newState, newDate, newDate);
|
|
293
|
+
|
|
294
|
+
runBash("scripts/harness-company-block.sh", dir, ["needs-key"]);
|
|
295
|
+
expect(JSON.parse(readFileSync(oldState, "utf8")).lifecycle).toBe("active");
|
|
296
|
+
expect(JSON.parse(readFileSync(newState, "utf8")).lifecycle).toBe("blocked");
|
|
297
|
+
|
|
298
|
+
runBash("scripts/harness-company-complete.sh", dir, ["provided-key"]);
|
|
299
|
+
expect(JSON.parse(readFileSync(oldState, "utf8")).lifecycle).toBe("active");
|
|
300
|
+
expect(JSON.parse(readFileSync(newState, "utf8")).lifecycle).toBe("complete");
|
|
301
|
+
});
|
|
270
302
|
});
|
|
@@ -123,6 +123,61 @@ describe("readHarnessState", () => {
|
|
|
123
123
|
expect(mission.protocolViolations).toContain("cto:missing-worker-evidence");
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
it("does not count roster-only hired workers as CXX evidence without a report file", () => {
|
|
127
|
+
const harnessDir = path.join(dir, ".harness");
|
|
128
|
+
const missionDir = path.join(harnessDir, "documents", "goal-1-dashboard");
|
|
129
|
+
mkdirSync(path.join(harnessDir, "shared", "HR-Resource", "react-ui-worker"), { recursive: true });
|
|
130
|
+
mkdirSync(missionDir, { recursive: true });
|
|
131
|
+
writeFileSync(path.join(missionDir, "ceo.md"), "# Dashboard\n");
|
|
132
|
+
writeFileSync(
|
|
133
|
+
path.join(missionDir, "cto.md"),
|
|
134
|
+
"# CTO\n\nWorker Evidence Manifest: cto/workers/react-ui-worker.md\n"
|
|
135
|
+
);
|
|
136
|
+
writeFileSync(
|
|
137
|
+
path.join(harnessDir, "shared", "HR-Resource", "react-ui-worker", "SKILL.md"),
|
|
138
|
+
"# React UI Worker\n"
|
|
139
|
+
);
|
|
140
|
+
writeFileSync(
|
|
141
|
+
path.join(harnessDir, "shared", "hr-roster.json"),
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
hired: [
|
|
144
|
+
{
|
|
145
|
+
worker: "react-ui-worker",
|
|
146
|
+
owner: "cto",
|
|
147
|
+
skillPath: ".harness/shared/HR-Resource/react-ui-worker/SKILL.md",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const mission = readHarnessState(dir).missions[0];
|
|
154
|
+
expect(mission.workers[0]).toMatchObject({ name: "react-ui-worker", reportPath: null });
|
|
155
|
+
expect(mission.protocolViolations).toContain("cto:missing-worker-evidence");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("keeps an explicit active mission visible even when it is older than the latest page", () => {
|
|
159
|
+
const harnessDir = path.join(dir, ".harness");
|
|
160
|
+
const docsDir = path.join(harnessDir, "documents");
|
|
161
|
+
const activeDir = path.join(docsDir, "goal-0-operating");
|
|
162
|
+
mkdirSync(activeDir, { recursive: true });
|
|
163
|
+
writeFileSync(path.join(activeDir, "ceo.md"), "# Operating\n");
|
|
164
|
+
writeFileSync(path.join(activeDir, "mission-state.json"), JSON.stringify({ lifecycle: "operating", active: true }));
|
|
165
|
+
const oldDate = new Date("2026-05-01T00:00:00Z");
|
|
166
|
+
utimesSync(activeDir, oldDate, oldDate);
|
|
167
|
+
|
|
168
|
+
for (let i = 1; i <= 16; i += 1) {
|
|
169
|
+
const missionDir = path.join(docsDir, `goal-${i}-done`);
|
|
170
|
+
mkdirSync(missionDir, { recursive: true });
|
|
171
|
+
writeFileSync(path.join(missionDir, "ceo.md"), `# Done ${i}\n`);
|
|
172
|
+
writeFileSync(path.join(missionDir, "mission-state.json"), JSON.stringify({ lifecycle: "complete", active: false }));
|
|
173
|
+
const ts = new Date(`2026-05-${String(i + 1).padStart(2, "0")}T00:00:00Z`);
|
|
174
|
+
utimesSync(missionDir, ts, ts);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const missions = readHarnessState(dir).missions;
|
|
178
|
+
expect(missions.some((mission) => mission.missionId === "goal-0-operating" && mission.active)).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
126
181
|
it("positions only hired HR-Resource workers and marks active workers", () => {
|
|
127
182
|
const harnessDir = path.join(dir, ".harness");
|
|
128
183
|
const missionDir = path.join(harnessDir, "documents", "goal-1-dashboard");
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the values the live Scene.tsx renders, plus the Stop-hook autonomy behavior.
|
|
9
9
|
*
|
|
10
10
|
* Gated behind WALWAL_E2E=1 so it stays out of the fast unit suite and only
|
|
11
|
-
* runs (and writes to
|
|
11
|
+
* runs (and writes to the OS temp dir, or WALWAL_E2E_ROOT when set) when explicitly requested:
|
|
12
12
|
* WALWAL_E2E=1 npx vitest run sandbox-e2e
|
|
13
13
|
*/
|
|
14
14
|
import { describe, it, expect, beforeAll } from "vitest";
|
|
@@ -111,7 +111,7 @@ function stopTick(sbx: string): { blocked: boolean; raw: string } {
|
|
|
111
111
|
return { blocked: raw.includes('"decision"') && raw.includes('"block"'), raw };
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
const SBX_ROOT = path.join(os.
|
|
114
|
+
const SBX_ROOT = process.env.WALWAL_E2E_ROOT ?? path.join(os.tmpdir(), "walwal-e2e");
|
|
115
115
|
|
|
116
116
|
gate("walwal-harness sandbox e2e — two usage modes", () => {
|
|
117
117
|
beforeAll(() => {
|
|
@@ -135,6 +135,71 @@ gate("walwal-harness sandbox e2e — two usage modes", () => {
|
|
|
135
135
|
expect(lstatSync(path.join(sbx, "CLAUDE.md")).isSymbolicLink()).toBe(true);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
it("migrate preserves user AGENTS.md and hired roster while refreshing package-owned state", () => {
|
|
139
|
+
const sbx = path.join(SBX_ROOT, "walwal-sandbox-migrate-preserve");
|
|
140
|
+
init(sbx);
|
|
141
|
+
appendFileSync(path.join(sbx, "AGENTS.md"), "\n## User Rule\n\nKeep this project-specific rule.\n");
|
|
142
|
+
writeFileSync(path.join(sbx, ".harness", ".bundle-version"), "7.1.1\n");
|
|
143
|
+
mkdirSync(path.join(sbx, ".harness", "shared", "HR-Resource", "react-ui-worker"), { recursive: true });
|
|
144
|
+
writeFileSync(path.join(sbx, ".harness", "shared", "HR-Resource", "react-ui-worker", "SKILL.md"), "# React UI Worker\n");
|
|
145
|
+
const rosterPath = path.join(sbx, ".harness", "shared", "hr-roster.json");
|
|
146
|
+
writeFileSync(
|
|
147
|
+
rosterPath,
|
|
148
|
+
JSON.stringify(
|
|
149
|
+
{
|
|
150
|
+
hired: [
|
|
151
|
+
{
|
|
152
|
+
worker: "react-ui-worker",
|
|
153
|
+
owner: "cto",
|
|
154
|
+
skillPath: ".claude/skills/react-ui-worker/SKILL.md",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
null,
|
|
159
|
+
2
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
execFileSync("node", [path.join(repoRoot, "bin", "init.js"), "migrate", "--dry-run", "--project-root", sbx], {
|
|
164
|
+
cwd: repoRoot,
|
|
165
|
+
encoding: "utf8",
|
|
166
|
+
stdio: "pipe",
|
|
167
|
+
});
|
|
168
|
+
expect(readFileSync(path.join(sbx, ".harness", ".bundle-version"), "utf8")).toBe("7.1.1\n");
|
|
169
|
+
expect(JSON.parse(readFileSync(rosterPath, "utf8")).hired[0].skillPaths).toBeUndefined();
|
|
170
|
+
|
|
171
|
+
execFileSync("node", [path.join(repoRoot, "bin", "init.js"), "migrate", "--project-root", sbx], {
|
|
172
|
+
cwd: repoRoot,
|
|
173
|
+
encoding: "utf8",
|
|
174
|
+
stdio: "pipe",
|
|
175
|
+
});
|
|
176
|
+
const agents = readFileSync(path.join(sbx, "AGENTS.md"), "utf8");
|
|
177
|
+
expect(agents).toContain("Keep this project-specific rule.");
|
|
178
|
+
const roster = JSON.parse(readFileSync(rosterPath, "utf8"));
|
|
179
|
+
expect(roster.hired[0].skillPaths.codex).toContain(".codex/skills/");
|
|
180
|
+
expect(roster.hired[0].skillPaths.source).toBe(".harness/shared/HR-Resource/react-ui-worker/SKILL.md");
|
|
181
|
+
expect(readFileSync(path.join(sbx, ".harness", ".bundle-version"), "utf8")).toBe("7.1.48\n");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("gitignore management untracks runtime state without untracking shared harness files", () => {
|
|
185
|
+
const sbx = path.join(SBX_ROOT, "walwal-sandbox-gitignore");
|
|
186
|
+
init(sbx);
|
|
187
|
+
execFileSync("git", ["init"], { cwd: sbx, encoding: "utf8", stdio: "pipe" });
|
|
188
|
+
execFileSync("git", ["add", "-f", ".harness/progress.json"], { cwd: sbx, encoding: "utf8", stdio: "pipe" });
|
|
189
|
+
execFileSync("git", ["add", ".harness/memory.md"], { cwd: sbx, encoding: "utf8", stdio: "pipe" });
|
|
190
|
+
expect(execFileSync("git", ["ls-files", ".harness/progress.json"], { cwd: sbx, encoding: "utf8" }).trim()).toBe(".harness/progress.json");
|
|
191
|
+
expect(execFileSync("git", ["ls-files", ".harness/memory.md"], { cwd: sbx, encoding: "utf8" }).trim()).toBe(".harness/memory.md");
|
|
192
|
+
|
|
193
|
+
execFileSync("node", [path.join(repoRoot, "bin", "init.js"), "init", "--force", "--project-root", sbx], {
|
|
194
|
+
cwd: repoRoot,
|
|
195
|
+
encoding: "utf8",
|
|
196
|
+
stdio: "pipe",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(execFileSync("git", ["ls-files", ".harness/progress.json"], { cwd: sbx, encoding: "utf8" }).trim()).toBe("");
|
|
200
|
+
expect(execFileSync("git", ["ls-files", ".harness/memory.md"], { cwd: sbx, encoding: "utf8" }).trim()).toBe(".harness/memory.md");
|
|
201
|
+
});
|
|
202
|
+
|
|
138
203
|
// ───────────────────────────────────────────────────────────────────────
|
|
139
204
|
// MODE 1 — AUTONOMY: Owner sets a broad goal, the company runs itself, and
|
|
140
205
|
// the loop must (a) keep going while work remains and (b) stop CLEANLY at
|
|
@@ -186,7 +186,11 @@ export function toContractState(
|
|
|
186
186
|
// ---- agents --------------------------------------------------------
|
|
187
187
|
const hasRecentSample = (role: AgentRole): boolean =>
|
|
188
188
|
snapshot.activitySamples.some(
|
|
189
|
-
(s) =>
|
|
189
|
+
(s) => {
|
|
190
|
+
if ((s.laneId !== role && !s.laneId.startsWith(role + ":")) || s.count <= 0) return false;
|
|
191
|
+
const t = parseTs(s.ts);
|
|
192
|
+
return t != null && nowMs - t <= STALE_MS;
|
|
193
|
+
}
|
|
190
194
|
);
|
|
191
195
|
const runtimeBlocked =
|
|
192
196
|
(rt.agentStatus ?? "").toLowerCase() === "blocked" ||
|
|
@@ -615,7 +615,7 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
615
615
|
const protocolViolations: string[] = [];
|
|
616
616
|
for (const role of cxxPresent) {
|
|
617
617
|
if (role === "ceo") continue;
|
|
618
|
-
const roleWorkers = workers.filter((w) => w.owner === role);
|
|
618
|
+
const roleWorkers = workers.filter((w) => w.owner === role && w.reportPath);
|
|
619
619
|
const roleDoc = readMd(`${role}.md`) ?? "";
|
|
620
620
|
const hasManifest = /Worker Evidence Manifest/i.test(roleDoc);
|
|
621
621
|
const mentionsWorkerReport = new RegExp(`${role}/workers/`, "i").test(roleDoc);
|
|
@@ -677,7 +677,17 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
|
|
680
|
+
const activePinned = missions.filter((m) => m.active);
|
|
681
|
+
const limited = missions.slice(0, limit);
|
|
682
|
+
if (activePinned.length) {
|
|
683
|
+
const seen = new Set(limited.map((m) => m.missionId));
|
|
684
|
+
for (const mission of activePinned) {
|
|
685
|
+
if (!seen.has(mission.missionId)) limited.push(mission);
|
|
686
|
+
}
|
|
687
|
+
missions = limited;
|
|
688
|
+
} else {
|
|
689
|
+
missions = limited;
|
|
690
|
+
}
|
|
681
691
|
|
|
682
692
|
// When the CEO updates a goal directory in-place for a submission or hot-fix
|
|
683
693
|
// (instead of creating a submission-* or hotfix-* subdirectory), the most
|
package/bin/init.js
CHANGED
|
@@ -20,6 +20,7 @@ const subcommandArgs = args.filter(a => !a.startsWith('-') && a !== subcommand);
|
|
|
20
20
|
const isAuto = args.includes('--auto');
|
|
21
21
|
const isForce = args.includes('--force');
|
|
22
22
|
const isHelp = args.includes('--help') || args.includes('-h');
|
|
23
|
+
const isWakeEnabled = args.includes('--enable-wake') || process.env.WALWAL_ENABLE_WAKE === '1';
|
|
23
24
|
|
|
24
25
|
function getOptionValue(name) {
|
|
25
26
|
const eqPrefix = `${name}=`;
|
|
@@ -1059,7 +1060,10 @@ function refreshScriptsForMigrate({ dryRun = false, backupDir = null } = {}) {
|
|
|
1059
1060
|
chmodShellScripts(scriptsDest);
|
|
1060
1061
|
}
|
|
1061
1062
|
|
|
1062
|
-
function ensureWakeScheduler({ dryRun = false } = {}) {
|
|
1063
|
+
function ensureWakeScheduler({ dryRun = false, enabled = false } = {}) {
|
|
1064
|
+
if (!enabled || process.env.WALWAL_SKIP_WAKE === '1') {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1063
1067
|
const wakeInstall = path.join(PROJECT_ROOT, 'scripts', 'harness-wake-install.sh');
|
|
1064
1068
|
if (!fs.existsSync(wakeInstall)) return;
|
|
1065
1069
|
|
|
@@ -1589,6 +1593,7 @@ Usage:
|
|
|
1589
1593
|
Runtime (always-on company mode):
|
|
1590
1594
|
- Stop 훅이 매 turn 종료 시 다음 부서로 자동 연쇄.
|
|
1591
1595
|
- 1시간 안전망 wake (선택): bash scripts/harness-wake-install.sh install .
|
|
1596
|
+
init/migrate 중 자동 등록하려면 --enable-wake 를 명시하세요.
|
|
1592
1597
|
- 3D 대시보드 (선택): bash scripts/harness-dashboard-up.sh
|
|
1593
1598
|
- Owner 입력은 GOAL 모호성, submission, hot-fix, escalation, 결과 보고에만 필요합니다.
|
|
1594
1599
|
|
|
@@ -2150,7 +2155,7 @@ function runMigrate(opts = {}) {
|
|
|
2150
2155
|
// Runtime scripts are package-owned. Migrate must refresh them so existing
|
|
2151
2156
|
// projects receive wake/meeting/OPS fixes without requiring a full init.
|
|
2152
2157
|
refreshScriptsForMigrate({ dryRun, backupDir });
|
|
2153
|
-
ensureWakeScheduler({ dryRun });
|
|
2158
|
+
ensureWakeScheduler({ dryRun, enabled: opts.enableWake === true });
|
|
2154
2159
|
migrateActivityHistory({ dryRun });
|
|
2155
2160
|
updateRuleRegistryLinks(flags.ruleRegistryLinks || [], { dryRun, backupDir });
|
|
2156
2161
|
|
|
@@ -2685,7 +2690,7 @@ function main() {
|
|
|
2685
2690
|
}
|
|
2686
2691
|
|
|
2687
2692
|
if (subcommand === 'migrate') {
|
|
2688
|
-
runMigrate({ dryRun: args.includes('--dry-run') });
|
|
2693
|
+
runMigrate({ dryRun: args.includes('--dry-run'), enableWake: isWakeEnabled });
|
|
2689
2694
|
return;
|
|
2690
2695
|
}
|
|
2691
2696
|
|
|
@@ -2721,7 +2726,7 @@ function main() {
|
|
|
2721
2726
|
gitUntrackLocalState();
|
|
2722
2727
|
installSkills();
|
|
2723
2728
|
installScripts();
|
|
2724
|
-
ensureWakeScheduler();
|
|
2729
|
+
ensureWakeScheduler({ enabled: isWakeEnabled });
|
|
2725
2730
|
installCommands();
|
|
2726
2731
|
installSessionHook();
|
|
2727
2732
|
installStatusline();
|
|
@@ -2782,7 +2787,7 @@ function main() {
|
|
|
2782
2787
|
console.log('');
|
|
2783
2788
|
|
|
2784
2789
|
if (subcommand === 'init' && subcommandArgs.includes('migrate')) {
|
|
2785
|
-
runMigrate({ dryRun: args.includes('--dry-run') });
|
|
2790
|
+
runMigrate({ dryRun: args.includes('--dry-run'), enableWake: isWakeEnabled });
|
|
2786
2791
|
}
|
|
2787
2792
|
}
|
|
2788
2793
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.49",
|
|
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"
|
|
@@ -31,6 +31,27 @@ command -v jq >/dev/null 2>&1 || {
|
|
|
31
31
|
exit 1
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
state_mtime() {
|
|
35
|
+
stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pick_active_mission_state() {
|
|
39
|
+
local best=""
|
|
40
|
+
local best_mtime=0
|
|
41
|
+
local state active mtime
|
|
42
|
+
[ -d "$DOCS" ] || return 0
|
|
43
|
+
while IFS= read -r -d '' state; do
|
|
44
|
+
active="$(jq -r '.active // false' "$state" 2>/dev/null || echo false)"
|
|
45
|
+
[ "$active" = "true" ] || continue
|
|
46
|
+
mtime="$(state_mtime "$state")"
|
|
47
|
+
if [ "${mtime:-0}" -ge "$best_mtime" ]; then
|
|
48
|
+
best="$state"
|
|
49
|
+
best_mtime="${mtime:-0}"
|
|
50
|
+
fi
|
|
51
|
+
done < <(find "$DOCS" -name mission-state.json -type f -print0)
|
|
52
|
+
[ -n "$best" ] && printf '%s\n' "$best"
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
if ! bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
|
|
35
56
|
'.company_state.state = "idle" |
|
|
36
57
|
.conductor.state = "blocked" |
|
|
@@ -69,17 +90,17 @@ fi
|
|
|
69
90
|
|
|
70
91
|
DOCS="$PROJECT_ROOT/.harness/documents"
|
|
71
92
|
if [ -d "$DOCS" ]; then
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
lifecycle="$(jq -r '.lifecycle // .status // "unknown"' "$
|
|
93
|
+
target_state="$(pick_active_mission_state)"
|
|
94
|
+
if [ -n "$target_state" ]; then
|
|
95
|
+
lifecycle="$(jq -r '.lifecycle // .status // "unknown"' "$target_state" 2>/dev/null || echo unknown)"
|
|
75
96
|
case "$lifecycle" in
|
|
76
|
-
closed|cancelled|superseded|complete|completed|blocked)
|
|
97
|
+
closed|cancelled|superseded|complete|completed|blocked) ;;
|
|
98
|
+
*)
|
|
99
|
+
tmp="$(mktemp)"
|
|
100
|
+
jq --arg r "$REASON" '.lifecycle = "blocked" | .active = false | .blocked_reason = $r | .blocked_at = (now | todate)' "$target_state" > "$tmp" && mv "$tmp" "$target_state"
|
|
101
|
+
;;
|
|
77
102
|
esac
|
|
78
|
-
|
|
79
|
-
tmp="$(mktemp)"
|
|
80
|
-
jq --arg r "$REASON" '.lifecycle = "blocked" | .active = false | .blocked_reason = $r | .blocked_at = (now | todate)' "$state" > "$tmp" && mv "$tmp" "$state"
|
|
81
|
-
fi
|
|
82
|
-
done
|
|
103
|
+
fi
|
|
83
104
|
fi
|
|
84
105
|
|
|
85
106
|
if command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/harness-activity-record.js" ]; then
|
|
@@ -25,6 +25,29 @@ command -v jq >/dev/null 2>&1 || {
|
|
|
25
25
|
exit 1
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
state_mtime() {
|
|
29
|
+
stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pick_transition_mission_state() {
|
|
33
|
+
local best=""
|
|
34
|
+
local best_mtime=0
|
|
35
|
+
local state active lifecycle mtime
|
|
36
|
+
[ -d "$DOCS" ] || return 0
|
|
37
|
+
while IFS= read -r -d '' state; do
|
|
38
|
+
active="$(jq -r '.active // false' "$state" 2>/dev/null || echo false)"
|
|
39
|
+
lifecycle="$(jq -r '.lifecycle // .status // "unknown"' "$state" 2>/dev/null || echo unknown)"
|
|
40
|
+
if [ "$active" = "true" ] || [ "$lifecycle" = "blocked" ]; then
|
|
41
|
+
mtime="$(state_mtime "$state")"
|
|
42
|
+
if [ "${mtime:-0}" -ge "$best_mtime" ]; then
|
|
43
|
+
best="$state"
|
|
44
|
+
best_mtime="${mtime:-0}"
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
done < <(find "$DOCS" -name mission-state.json -type f -print0)
|
|
48
|
+
[ -n "$best" ] && printf '%s\n' "$best"
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
if ! bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
|
|
29
52
|
'.company_state.state = "idle" |
|
|
30
53
|
.company_state.active_workers = 0 |
|
|
@@ -64,19 +87,17 @@ fi
|
|
|
64
87
|
|
|
65
88
|
DOCS="$PROJECT_ROOT/.harness/documents"
|
|
66
89
|
if [ -d "$DOCS" ]; then
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
lifecycle="$(jq -r '.lifecycle // .status // "unknown"' "$
|
|
90
|
+
target_state="$(pick_transition_mission_state)"
|
|
91
|
+
if [ -n "$target_state" ]; then
|
|
92
|
+
lifecycle="$(jq -r '.lifecycle // .status // "unknown"' "$target_state" 2>/dev/null || echo unknown)"
|
|
70
93
|
case "$lifecycle" in
|
|
71
|
-
closed|cancelled|superseded|complete|completed)
|
|
94
|
+
closed|cancelled|superseded|complete|completed) ;;
|
|
95
|
+
*)
|
|
96
|
+
tmp="$(mktemp)"
|
|
97
|
+
jq '.lifecycle = "complete" | .active = false | .completed_at = (now | todate) | del(.blocked_reason) | del(.blocked_at)' "$target_state" > "$tmp" && mv "$tmp" "$target_state"
|
|
98
|
+
;;
|
|
72
99
|
esac
|
|
73
|
-
|
|
74
|
-
# clear its blocker fields too — block -> complete must leave no stale state.
|
|
75
|
-
if [ "$active" = "true" ] || [ "$lifecycle" = "blocked" ]; then
|
|
76
|
-
tmp="$(mktemp)"
|
|
77
|
-
jq '.lifecycle = "complete" | .active = false | .completed_at = (now | todate) | del(.blocked_reason) | del(.blocked_at)' "$state" > "$tmp" && mv "$tmp" "$state"
|
|
78
|
-
fi
|
|
79
|
-
done
|
|
100
|
+
fi
|
|
80
101
|
fi
|
|
81
102
|
|
|
82
103
|
if command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/harness-activity-record.js" ]; then
|
|
@@ -130,4 +130,4 @@ echo "║ 실시간 (SSE) 시각화합니다. Ctrl+C 로 종료.
|
|
|
130
130
|
echo "╚══════════════════════════════════════════════════════════╝"
|
|
131
131
|
echo ""
|
|
132
132
|
|
|
133
|
-
HARNESS_ROOT="${HARNESS_ROOT}" npm run dev:dashboard -- -p "${PORT}"
|
|
133
|
+
HARNESS_ROOT="${HARNESS_ROOT}" npm run dev:dashboard -- -H 127.0.0.1 -p "${PORT}"
|