@walwal-harness/cli 7.1.39 → 7.1.41
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/HR-Resource/cdo/SKILL.md +9 -1
- package/HR-Resource/ceo/SKILL.md +33 -2
- package/HR-Resource/coo/SKILL.md +9 -1
- package/HR-Resource/cqo/SKILL.md +9 -1
- package/HR-Resource/cto/SKILL.md +10 -2
- package/HR-Resource/ops/SKILL.md +20 -2
- package/HR-Resource/ops-prompt-inspector/SKILL.md +105 -0
- package/HR-Resource/ops-prompt-inspector/agents/installation-agent.json +7 -0
- package/HR-Resource/ops-prompt-inspector/assets/PromptInspector.tsx +599 -0
- package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
- package/HR-Resource/ops-prompt-inspector/references/handler-templates.md +255 -0
- package/HR-Resource/ops-prompt-inspector/scripts/discover_apis.py +509 -0
- package/HR-Resource/ops-prompt-inspector/scripts/install.sh +32 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +381 -0
- package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
- package/apps/harness-dashboard/components/Scene.tsx +568 -35
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
- package/apps/harness-dashboard/lib/harness-state.ts +107 -796
- package/apps/harness-dashboard/lib/types.ts +22 -222
- package/apps/harness-dashboard/playwright.config.ts +26 -7
- package/assets/templates/AGENTS-ko.md.template +1 -1
- package/assets/templates/AGENTS.md.template +12 -2
- package/assets/templates/HARNESS.md +2 -0
- package/assets/templates/company-pipeline-manifest.json +5 -14
- package/assets/templates/worker-report.md.template +38 -0
- package/bin/init.js +4 -3
- package/commands/goal.md +4 -2
- package/commands/hot-fix.md +1 -1
- package/commands/submission.md +1 -1
- package/package.json +1 -1
- package/scripts/conductor-tick.sh +12 -0
- package/scripts/harness-agenda.sh +105 -0
- package/scripts/harness-company-block.sh +89 -0
- package/scripts/harness-company-complete.sh +17 -8
- package/scripts/harness-company-cycle.sh +54 -0
- package/scripts/harness-progress-set.sh +8 -2
- package/scripts/harness-stop.sh +76 -0
- package/scripts/harness-wake-control.sh +151 -0
- package/scripts/harness-wake.sh +5 -0
- package/apps/harness-dashboard/components/Drawer.tsx +0 -146
- package/apps/harness-dashboard/components/Header.tsx +0 -40
- package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
- package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
- package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
- package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
- package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
- package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
- package/apps/harness-dashboard/lib/furniture.ts +0 -165
- package/apps/harness-dashboard/lib/i18n.ts +0 -26
- package/apps/harness-dashboard/lib/iso.ts +0 -150
- package/apps/harness-dashboard/lib/markdown.tsx +0 -306
- package/apps/harness-dashboard/lib/path-planning.ts +0 -72
- package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox end-to-end test for the two harness usage modes.
|
|
3
|
+
*
|
|
4
|
+
* Creates two real sample projects under ~/tmp via the actual installer
|
|
5
|
+
* (bin/init.js), then drives the INSTALLED hooks/scripts and simulates a
|
|
6
|
+
* faithful v7 CEO/CXX document flow. After each milestone it computes the
|
|
7
|
+
* dashboard snapshot through the dashboard's own readHarnessState() and asserts
|
|
8
|
+
* the values the live Scene.tsx renders, plus the Stop-hook autonomy behavior.
|
|
9
|
+
*
|
|
10
|
+
* Gated behind WALWAL_E2E=1 so it stays out of the fast unit suite and only
|
|
11
|
+
* runs (and writes to ~/tmp) when explicitly requested:
|
|
12
|
+
* WALWAL_E2E=1 npx vitest run sandbox-e2e
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
appendFileSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
lstatSync,
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { readHarnessState } from "../harness-state";
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = path.dirname(__filename);
|
|
32
|
+
const repoRoot = path.resolve(__dirname, "../../../../");
|
|
33
|
+
|
|
34
|
+
const gate = process.env.WALWAL_E2E === "1" ? describe : describe.skip;
|
|
35
|
+
|
|
36
|
+
const NOTES = `\n\n## Implementation Notes\n\n### Design Decisions\n- None\n\n### Deviations\n- None\n\n### Tradeoffs\n- None\n\n### Open Questions\n- None\n`;
|
|
37
|
+
|
|
38
|
+
function init(sbx: string) {
|
|
39
|
+
rmSync(sbx, { recursive: true, force: true });
|
|
40
|
+
mkdirSync(sbx, { recursive: true });
|
|
41
|
+
execFileSync("node", [path.join(repoRoot, "bin", "init.js"), "init", "--force", "--project-root", sbx], {
|
|
42
|
+
cwd: repoRoot,
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
stdio: "pipe",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Run an installed hook script, feeding hook JSON on stdin. Returns stdout. */
|
|
49
|
+
function runHook(sbx: string, script: string, input: Record<string, unknown>): string {
|
|
50
|
+
try {
|
|
51
|
+
return execFileSync("bash", [path.join(sbx, "scripts", script)], {
|
|
52
|
+
input: JSON.stringify(input),
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
55
|
+
});
|
|
56
|
+
} catch (e: unknown) {
|
|
57
|
+
// hooks exit 0 normally; capture stdout even on nonzero just in case
|
|
58
|
+
const err = e as { stdout?: string };
|
|
59
|
+
return err.stdout ?? "";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Run an installed runtime script with positional args. */
|
|
64
|
+
function runScript(sbx: string, script: string, args: string[] = []): string {
|
|
65
|
+
return execFileSync("bash", [path.join(sbx, "scripts", script), ...args], {
|
|
66
|
+
cwd: sbx,
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function progress(sbx: string): Record<string, any> {
|
|
73
|
+
return JSON.parse(readFileSync(path.join(sbx, ".harness", "progress.json"), "utf8"));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function snap(sbx: string, label: string) {
|
|
77
|
+
const s = readHarnessState(sbx);
|
|
78
|
+
// leave an inspectable snapshot next to the sandbox for the Owner
|
|
79
|
+
writeFileSync(path.join(sbx, ".harness", `snapshot-${label}.json`), JSON.stringify(s, null, 2));
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeDoc(sbx: string, rel: string, body: string) {
|
|
84
|
+
const p = path.join(sbx, ".harness", "documents", rel);
|
|
85
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
86
|
+
writeFileSync(p, body);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function writeMissionState(sbx: string, missionRel: string, lifecycle: string, active: boolean) {
|
|
90
|
+
const p = path.join(sbx, ".harness", "documents", missionRel, "mission-state.json");
|
|
91
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
92
|
+
writeFileSync(p, JSON.stringify({ lifecycle, active }, null, 2) + "\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Simulate the UserPromptSubmit hook for an Owner command. */
|
|
96
|
+
function ownerCommand(sbx: string, prompt: string) {
|
|
97
|
+
return runHook(sbx, "harness-user-prompt-submit.sh", {
|
|
98
|
+
cwd: sbx,
|
|
99
|
+
prompt,
|
|
100
|
+
hook_event_name: "UserPromptSubmit",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Simulate the Stop hook one tick. Returns {blocked, raw}. */
|
|
105
|
+
function stopTick(sbx: string): { blocked: boolean; raw: string } {
|
|
106
|
+
const raw = runHook(sbx, "harness-stop.sh", {
|
|
107
|
+
cwd: sbx,
|
|
108
|
+
hook_event_name: "Stop",
|
|
109
|
+
stop_hook_active: false,
|
|
110
|
+
});
|
|
111
|
+
return { blocked: raw.includes('"decision"') && raw.includes('"block"'), raw };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const SBX_ROOT = path.join(os.homedir(), "tmp");
|
|
115
|
+
|
|
116
|
+
gate("walwal-harness sandbox e2e — two usage modes", () => {
|
|
117
|
+
beforeAll(() => {
|
|
118
|
+
mkdirSync(SBX_ROOT, { recursive: true });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("install contract: init produces commands, CXX skills, runtime scripts, CLAUDE.md symlink", () => {
|
|
122
|
+
const sbx = path.join(SBX_ROOT, "walwal-sandbox-install");
|
|
123
|
+
init(sbx);
|
|
124
|
+
for (const f of ["goal.md", "submission.md", "hot-fix.md"]) {
|
|
125
|
+
expect(existsSync(path.join(sbx, ".claude", "commands", f))).toBe(true);
|
|
126
|
+
}
|
|
127
|
+
for (const role of ["ceo", "coo", "cdo", "cto", "cqo", "ops"]) {
|
|
128
|
+
expect(existsSync(path.join(sbx, ".claude", "skills", `harness-${role}`, "SKILL.md"))).toBe(true);
|
|
129
|
+
}
|
|
130
|
+
// the two completion/blocked transition scripts must be installed
|
|
131
|
+
expect(existsSync(path.join(sbx, "scripts", "harness-company-complete.sh"))).toBe(true);
|
|
132
|
+
expect(existsSync(path.join(sbx, "scripts", "harness-company-block.sh"))).toBe(true);
|
|
133
|
+
expect(existsSync(path.join(sbx, ".harness", "shared", "HR-Resource"))).toBe(true);
|
|
134
|
+
// CLAUDE.md is a symlink -> AGENTS.md
|
|
135
|
+
expect(lstatSync(path.join(sbx, "CLAUDE.md")).isSymbolicLink()).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
139
|
+
// MODE 1 — AUTONOMY: Owner sets a broad goal, the company runs itself, and
|
|
140
|
+
// the loop must (a) keep going while work remains and (b) stop CLEANLY at
|
|
141
|
+
// genuine completion (dashboard goes idle). Also verifies the deterministic
|
|
142
|
+
// Stop-hook backstop when the model forgets the explicit completion call.
|
|
143
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
144
|
+
it("autonomy mode: loop runs, then completion idles runtime + dashboard", () => {
|
|
145
|
+
const sbx = path.join(SBX_ROOT, "walwal-sandbox-autonomy");
|
|
146
|
+
init(sbx);
|
|
147
|
+
const goalDir = "goal-1-growth-autopilot";
|
|
148
|
+
|
|
149
|
+
// (1) Owner issues a broad, self-directed goal.
|
|
150
|
+
ownerCommand(
|
|
151
|
+
sbx,
|
|
152
|
+
"/goal Run an autonomous growth program: discover problems in our funnel, propose and validate experiments, and expand the business plan without waiting on me for routine decisions."
|
|
153
|
+
);
|
|
154
|
+
let p = progress(sbx);
|
|
155
|
+
expect(p.current_agent).toBe("ceo");
|
|
156
|
+
expect(p.conductor.state).toBe("running");
|
|
157
|
+
expect(p.owner_prompt.status).toBe("routing");
|
|
158
|
+
let s = snap(sbx, "01-after-goal");
|
|
159
|
+
expect(s.runtime.currentAgent).toBe("ceo");
|
|
160
|
+
// not idle while running
|
|
161
|
+
expect(s.runtime.agentStatus).not.toBe("completed");
|
|
162
|
+
|
|
163
|
+
// (2) Autonomy at the start: Stop must BLOCK (keep going), not wait on Owner.
|
|
164
|
+
expect(stopTick(sbx).blocked).toBe(true);
|
|
165
|
+
|
|
166
|
+
// (3) Simulate the CEO/CXX document flow producing real artifacts.
|
|
167
|
+
writeDoc(sbx, `${goalDir}/ceo.md`, `# CEO — Growth Autopilot\n\nRouted COO for discovery + experiment validation.${NOTES}`);
|
|
168
|
+
writeMissionState(sbx, goalDir, "active", true);
|
|
169
|
+
// COO enters: dashboard should show the live handoff.
|
|
170
|
+
runScript(sbx, "harness-progress-set.sh", [".", '.current_agent="coo" | .agent_status="running"']);
|
|
171
|
+
writeDoc(sbx, `${goalDir}/coo.md`, `# COO — Discovery\n\nWorker Evidence Manifest: coo/workers/funnel-researcher.md${NOTES}`);
|
|
172
|
+
writeDoc(
|
|
173
|
+
sbx,
|
|
174
|
+
`${goalDir}/coo/workers/funnel-researcher.md`,
|
|
175
|
+
`# Funnel Researcher — Worker Report\n\n## Status\nCOMPLETE\n\nFound 3 drop-off points; proposed 2 experiments.${NOTES}`
|
|
176
|
+
);
|
|
177
|
+
// worker telemetry: mark a live worker
|
|
178
|
+
runScript(sbx, "harness-progress-set.sh", [
|
|
179
|
+
".",
|
|
180
|
+
'.company_state.workers = [{"name":"funnel-researcher","owner":"coo","status":"running","report_path":".harness/documents/' +
|
|
181
|
+
goalDir +
|
|
182
|
+
'/coo/workers/funnel-researcher.md"}] | .company_state.active_workers = 1 | .conductor.current_action="spawn:funnel-researcher"',
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
s = snap(sbx, "02-mid-work");
|
|
186
|
+
const m = s.missions.find((x) => x.missionId === goalDir);
|
|
187
|
+
expect(m).toBeTruthy();
|
|
188
|
+
expect(m!.active).toBe(true);
|
|
189
|
+
expect(m!.lifecycle).toBe("active");
|
|
190
|
+
expect(m!.cxxPresent).toEqual(expect.arrayContaining(["ceo", "coo"]));
|
|
191
|
+
const w = m!.workers.find((x) => x.name === "funnel-researcher");
|
|
192
|
+
expect(w).toBeTruthy();
|
|
193
|
+
expect(w!.owner).toBe("coo");
|
|
194
|
+
// (4) Autonomy mid-work: Stop must still BLOCK (active mission exists).
|
|
195
|
+
expect(stopTick(sbx).blocked).toBe(true);
|
|
196
|
+
|
|
197
|
+
// (5) Genuine completion: final report + terminal mission-state + the
|
|
198
|
+
// wired runtime transition (what the new CEO protocol mandates).
|
|
199
|
+
writeDoc(sbx, `${goalDir}/ceo.md`, `# CEO — Growth Autopilot\n\nFINAL OWNER REPORT: 2 experiments validated, plan expanded.${NOTES}`);
|
|
200
|
+
writeMissionState(sbx, goalDir, "complete", false);
|
|
201
|
+
runScript(sbx, "harness-company-complete.sh", [".", "goal-complete"]);
|
|
202
|
+
|
|
203
|
+
p = progress(sbx);
|
|
204
|
+
expect(p.conductor.state).toBe("completed");
|
|
205
|
+
expect(p.current_agent).toBeNull();
|
|
206
|
+
expect(p.agent_status).toBe("completed");
|
|
207
|
+
expect(p.owner_prompt.status).toBe("completed");
|
|
208
|
+
|
|
209
|
+
s = snap(sbx, "03-after-complete");
|
|
210
|
+
// dashboard idle gate resolves: runtime no longer "busy / stuck on ceo"
|
|
211
|
+
expect(s.runtime.currentAgent).toBeNull();
|
|
212
|
+
expect(s.runtime.agentStatus).toBe("completed");
|
|
213
|
+
const mc = s.missions.find((x) => x.missionId === goalDir)!;
|
|
214
|
+
expect(mc.lifecycle).toBe("complete");
|
|
215
|
+
expect(mc.active).toBe(false);
|
|
216
|
+
|
|
217
|
+
// (6) THE key autonomy assertion: after completion the Stop hook stops
|
|
218
|
+
// cleanly instead of nagging "continue".
|
|
219
|
+
expect(stopTick(sbx).blocked).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("autonomy backstop: terminal mission-state stops the loop even if the explicit completion call is skipped", () => {
|
|
223
|
+
const sbx = path.join(SBX_ROOT, "walwal-sandbox-autonomy-backstop");
|
|
224
|
+
init(sbx);
|
|
225
|
+
const goalDir = "goal-1-backstop";
|
|
226
|
+
ownerCommand(sbx, "/goal Backstop check.");
|
|
227
|
+
writeDoc(sbx, `${goalDir}/ceo.md`, `# CEO\n\nReport.${NOTES}`);
|
|
228
|
+
writeMissionState(sbx, goalDir, "active", true);
|
|
229
|
+
expect(progress(sbx).conductor.state).toBe("running");
|
|
230
|
+
|
|
231
|
+
// CEO marks the mission terminal (existing behavior) but FORGETS to fire
|
|
232
|
+
// harness-company-complete.sh — conductor.state stays "running".
|
|
233
|
+
writeMissionState(sbx, goalDir, "complete", false);
|
|
234
|
+
expect(progress(sbx).conductor.state).toBe("running");
|
|
235
|
+
|
|
236
|
+
// Stop hook backstop: no active mission remains -> auto-completes + stops.
|
|
237
|
+
const tick = stopTick(sbx);
|
|
238
|
+
expect(tick.blocked).toBe(false);
|
|
239
|
+
const p = progress(sbx);
|
|
240
|
+
expect(p.conductor.state).toBe("completed");
|
|
241
|
+
expect(p.current_agent).toBeNull();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
245
|
+
// STALE-BLOCKER CONTRACT — block -> complete must leave NO stale blocker state
|
|
246
|
+
// in ANY of the three layers block.sh writes: progress.json,
|
|
247
|
+
// .harness/todos/state.json, and per-mission mission-state.json. Guards the
|
|
248
|
+
// del()/status-reset fixes in harness-company-{block,complete}.sh.
|
|
249
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
250
|
+
it("block -> complete clears blocker state across progress, todos, and mission-state", () => {
|
|
251
|
+
const sbx = path.join(SBX_ROOT, "walwal-sandbox-block-complete");
|
|
252
|
+
init(sbx);
|
|
253
|
+
const goalDir = "goal-1-blocked-then-done";
|
|
254
|
+
const reason = 'needs "STRIPE" key';
|
|
255
|
+
const msPath = path.join(sbx, ".harness", "documents", goalDir, "mission-state.json");
|
|
256
|
+
const todosPath = path.join(sbx, ".harness", "todos", "state.json");
|
|
257
|
+
|
|
258
|
+
ownerCommand(sbx, "/goal Ship a feature that needs a third-party credential.");
|
|
259
|
+
writeDoc(sbx, `${goalDir}/ceo.md`, `# CEO\n\nRouted; awaiting external credential.${NOTES}`);
|
|
260
|
+
writeMissionState(sbx, goalDir, "active", true);
|
|
261
|
+
// a live todo that block.sh will flip to "blocked"
|
|
262
|
+
mkdirSync(path.dirname(todosPath), { recursive: true });
|
|
263
|
+
writeFileSync(todosPath, JSON.stringify({ owners: { coo: [{ id: "t1", title: "integrate API", status: "active" }] } }, null, 2));
|
|
264
|
+
|
|
265
|
+
// (1) BLOCK on external authority (quotes in the reason exercise quote-safety).
|
|
266
|
+
runScript(sbx, "harness-company-block.sh", [".", reason]);
|
|
267
|
+
let p = progress(sbx);
|
|
268
|
+
expect(p.conductor.state).toBe("blocked");
|
|
269
|
+
expect(p.owner_prompt.blocked_reason).toBe(reason);
|
|
270
|
+
let todos = JSON.parse(readFileSync(todosPath, "utf8"));
|
|
271
|
+
expect(todos.owners.coo[0].status).toBe("blocked");
|
|
272
|
+
expect(todos.owners.coo[0].blocked_reason).toBe(reason);
|
|
273
|
+
let ms = JSON.parse(readFileSync(msPath, "utf8"));
|
|
274
|
+
expect(ms.lifecycle).toBe("blocked");
|
|
275
|
+
expect(ms.blocked_reason).toBe(reason);
|
|
276
|
+
|
|
277
|
+
// (2) Authority provided → COMPLETE. No stale blocker may survive in any layer.
|
|
278
|
+
runScript(sbx, "harness-company-complete.sh", [".", "credential provided; shipped"]);
|
|
279
|
+
p = progress(sbx);
|
|
280
|
+
expect(p.conductor.state).toBe("completed");
|
|
281
|
+
expect("blocked_reason" in p.owner_prompt).toBe(false);
|
|
282
|
+
expect("blocked_at" in p.owner_prompt).toBe(false);
|
|
283
|
+
expect("blocked_at" in p.conductor).toBe(false);
|
|
284
|
+
|
|
285
|
+
todos = JSON.parse(readFileSync(todosPath, "utf8"));
|
|
286
|
+
expect(todos.owners.coo[0].status).toBe("done");
|
|
287
|
+
expect("blocked_reason" in todos.owners.coo[0]).toBe(false);
|
|
288
|
+
|
|
289
|
+
ms = JSON.parse(readFileSync(msPath, "utf8"));
|
|
290
|
+
expect(ms.lifecycle).toBe("complete");
|
|
291
|
+
expect(ms.active).toBe(false);
|
|
292
|
+
expect("blocked_reason" in ms).toBe(false);
|
|
293
|
+
expect("blocked_at" in ms).toBe(false);
|
|
294
|
+
|
|
295
|
+
// the dashboard must NOT paint a completed run as blocked
|
|
296
|
+
const m = snap(sbx, "block-complete-final").missions.find((x) => x.missionId === goalDir)!;
|
|
297
|
+
expect(m.lifecycle).toBe("complete");
|
|
298
|
+
expect(m.active).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
302
|
+
// WAKE CONTROL — the hourly launchd job that drives the perpetual loop across
|
|
303
|
+
// sessions is toggleable from the dashboard and keyed to HARNESS_BASE_PORT.
|
|
304
|
+
// (Read-only + dry-run only — never loads a real job into the tester's launchd.)
|
|
305
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
306
|
+
it("wake control: launchd job is keyed to HARNESS_BASE_PORT and dashboard-toggleable (dry-run)", () => {
|
|
307
|
+
const sbx = path.join(SBX_ROOT, "walwal-sandbox-wake");
|
|
308
|
+
init(sbx);
|
|
309
|
+
appendFileSync(path.join(sbx, ".env"), "\nHARNESS_BASE_PORT=51000\n");
|
|
310
|
+
|
|
311
|
+
const label = runScript(sbx, "harness-wake-control.sh", ["label", sbx]).trim();
|
|
312
|
+
expect(label).toBe("com.walwal.harness-wake.51000");
|
|
313
|
+
|
|
314
|
+
const status = JSON.parse(
|
|
315
|
+
runScript(sbx, "harness-wake-control.sh", ["status", sbx]).trim().split("\n").filter(Boolean).pop()!
|
|
316
|
+
);
|
|
317
|
+
expect(status.label).toBe("com.walwal.harness-wake.51000");
|
|
318
|
+
expect(status.base_port).toBe(51000);
|
|
319
|
+
expect(typeof status.enabled).toBe("boolean");
|
|
320
|
+
expect(typeof status.supported).toBe("boolean");
|
|
321
|
+
|
|
322
|
+
// dry-run on: writes the per-project plist preview WITHOUT touching launchd
|
|
323
|
+
runScript(sbx, "harness-wake-control.sh", ["on", sbx, "--dry-run"]);
|
|
324
|
+
const preview = path.join(sbx, ".harness", "runtime", "com.walwal.harness-wake.51000.plist.preview");
|
|
325
|
+
expect(existsSync(preview)).toBe(true);
|
|
326
|
+
const plist = readFileSync(preview, "utf8");
|
|
327
|
+
expect(plist).toContain("<string>com.walwal.harness-wake.51000</string>");
|
|
328
|
+
expect(plist).toContain("WALWAL_HARNESS_PROJECTS");
|
|
329
|
+
expect(plist).toContain(sbx);
|
|
330
|
+
|
|
331
|
+
const state = JSON.parse(readFileSync(path.join(sbx, ".harness", "runtime", "wake-control.json"), "utf8"));
|
|
332
|
+
expect(state.base_port).toBe(51000);
|
|
333
|
+
expect(state.label).toBe("com.walwal.harness-wake.51000");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
337
|
+
// MODE 1 (PERPETUAL) — a never-completing operating goal (crypto trading).
|
|
338
|
+
// The loop must: keep running while agenda items exist (forced CEO
|
|
339
|
+
// adjudication), force a status-briefing when the agenda is empty, yield to
|
|
340
|
+
// the hourly wake on an operating heartbeat, re-engage on a discovered loss,
|
|
341
|
+
// and NEVER reach a terminal lifecycle / never call company-complete.
|
|
342
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
343
|
+
it("perpetual operating mode: agenda-driven loop runs forever, never completes", () => {
|
|
344
|
+
const sbx = path.join(SBX_ROOT, "walwal-sandbox-operating");
|
|
345
|
+
init(sbx);
|
|
346
|
+
const goalDir = "goal-1-trading-autopilot";
|
|
347
|
+
const agenda = (...args: string[]) => runScript(sbx, "harness-agenda.sh", [".", goalDir, ...args]);
|
|
348
|
+
|
|
349
|
+
// (1) Owner issues a perpetual operating goal.
|
|
350
|
+
ownerCommand(
|
|
351
|
+
sbx,
|
|
352
|
+
"/goal Build a crypto auto-trading system and keep it profitable forever: COO continuously researches and backtests new strategies, CTO operates them stably, on loss apply a new strategy, monitor and run perpetually."
|
|
353
|
+
);
|
|
354
|
+
// CEO classifies it operating (lifecycle=operating, never terminal).
|
|
355
|
+
writeDoc(sbx, `${goalDir}/ceo.md`, `# CEO — Trading Autopilot (operating)\n\nClassified perpetual; standing agenda loop.${NOTES}`);
|
|
356
|
+
writeMissionState(sbx, goalDir, "operating", true);
|
|
357
|
+
|
|
358
|
+
let s = snap(sbx, "01-operating-start");
|
|
359
|
+
let m = s.missions.find((x) => x.missionId === goalDir)!;
|
|
360
|
+
expect(m.lifecycle).toBe("operating");
|
|
361
|
+
expect(m.active).toBe(true);
|
|
362
|
+
|
|
363
|
+
// (2) Empty agenda → Stop hook must FORCE a status-briefing (block), not stop, not complete.
|
|
364
|
+
runScript(sbx, "harness-progress-set.sh", [".", '.conductor.state="running" | .current_agent="ceo" | .agent_status="running"']);
|
|
365
|
+
let tick = stopTick(sbx);
|
|
366
|
+
expect(tick.blocked).toBe(true);
|
|
367
|
+
expect(tick.raw).toMatch(/현황 보고|status-briefing|안건 없음/);
|
|
368
|
+
|
|
369
|
+
// (3) Status-briefing surfaces a loss → OPS raises an agenda item.
|
|
370
|
+
const id1 = agenda("raise", "ops", "loss", "BTC strategy drawdown -8% / 24h", `${goalDir}/ops/workers/monitor.md`).trim();
|
|
371
|
+
expect(agenda("active-count").trim()).toBe("1");
|
|
372
|
+
s = snap(sbx, "02-agenda-raised");
|
|
373
|
+
m = s.missions.find((x) => x.missionId === goalDir)!;
|
|
374
|
+
expect(m.lifecycle).toBe("operating");
|
|
375
|
+
expect(m.agendaOpen).toBe(1);
|
|
376
|
+
|
|
377
|
+
// (4) Open agenda → Stop hook FORCES CEO adjudication (block), still never completes.
|
|
378
|
+
tick = stopTick(sbx);
|
|
379
|
+
expect(tick.blocked).toBe(true);
|
|
380
|
+
expect(tick.raw).toMatch(/활성 안건|agenda/);
|
|
381
|
+
|
|
382
|
+
// (5) CEO adjudicates → routes COO to research+backtest a new strategy; work done → close.
|
|
383
|
+
agenda("decide", id1, "COO: research + backtest a regime-aware strategy; CTO apply; CQO verify", "coo");
|
|
384
|
+
writeDoc(sbx, `${goalDir}/coo.md`, `# COO\n\nNew strategy backtested. Worker Evidence Manifest: coo/workers/strategy.md${NOTES}`);
|
|
385
|
+
writeDoc(sbx, `${goalDir}/coo/workers/strategy.md`, `# Strategy — Worker Report\n\n## Status\nCOMPLETE${NOTES}`);
|
|
386
|
+
agenda("close", id1);
|
|
387
|
+
expect(agenda("active-count").trim()).toBe("0");
|
|
388
|
+
|
|
389
|
+
// (6) Agenda empty again + briefing surfaces nothing → operating heartbeat, yield to wake.
|
|
390
|
+
runScript(sbx, "harness-company-cycle.sh", [".", goalDir]);
|
|
391
|
+
let p = progress(sbx);
|
|
392
|
+
expect(p.conductor.state).toBe("operating");
|
|
393
|
+
expect(p.agent_status).toBe("operating");
|
|
394
|
+
expect(p.operating.cycles).toBeGreaterThanOrEqual(1);
|
|
395
|
+
// Stop hook now yields CLEANLY (no block) — but the goal stays operating/active.
|
|
396
|
+
tick = stopTick(sbx);
|
|
397
|
+
expect(tick.blocked).toBe(false);
|
|
398
|
+
s = snap(sbx, "03-operating-heartbeat");
|
|
399
|
+
m = s.missions.find((x) => x.missionId === goalDir)!;
|
|
400
|
+
expect(m.lifecycle).toBe("operating");
|
|
401
|
+
expect(m.active).toBe(true); // STILL active — never completed
|
|
402
|
+
|
|
403
|
+
// (7) A new loss arrives later → re-engage the cycle (back to forced adjudication).
|
|
404
|
+
const id2 = agenda("raise", "ops", "loss", "New regime: strategy stale, -5%", `${goalDir}/ops/workers/monitor2.md`).trim();
|
|
405
|
+
expect(agenda("active-count").trim()).toBe("1");
|
|
406
|
+
tick = stopTick(sbx);
|
|
407
|
+
expect(tick.blocked).toBe(true); // operating loop resumes, never terminal
|
|
408
|
+
|
|
409
|
+
// INVARIANT: across the whole perpetual run the goal never reached a terminal
|
|
410
|
+
// lifecycle and company-complete was never appropriate.
|
|
411
|
+
const finalProgress = progress(sbx);
|
|
412
|
+
expect(["operating", "running"]).toContain(finalProgress.conductor.state);
|
|
413
|
+
const finalMission = snap(sbx, "04-operating-final").missions.find((x) => x.missionId === goalDir)!;
|
|
414
|
+
expect(["operating"]).toContain(finalMission.lifecycle);
|
|
415
|
+
expect(finalMission.active).toBe(true);
|
|
416
|
+
void id2;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
420
|
+
// MODE 2 — COMMAND-DRIVEN: Owner walks goal -> submission (additional dev)
|
|
421
|
+
// -> hot-fix (detail fix). The dashboard must show the goal tree with the
|
|
422
|
+
// submission + hotfix children, correct types/lifecycles, gotcha growth, and
|
|
423
|
+
// the owner command history.
|
|
424
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
425
|
+
it("command mode: goal -> submission -> hot-fix builds the mission tree the dashboard renders", () => {
|
|
426
|
+
const sbx = path.join(SBX_ROOT, "walwal-sandbox-command");
|
|
427
|
+
init(sbx);
|
|
428
|
+
const goalDir = "goal-1-checkout";
|
|
429
|
+
|
|
430
|
+
// --- /goal ---
|
|
431
|
+
ownerCommand(sbx, "/goal Build a checkout page.");
|
|
432
|
+
writeDoc(sbx, `${goalDir}/ceo.md`, `# CEO — Checkout\n\nFINAL: checkout shipped.${NOTES}`);
|
|
433
|
+
writeDoc(sbx, `${goalDir}/cto.md`, `# CTO\n\nWorker Evidence Manifest: cto/workers/fe-dev.md${NOTES}`);
|
|
434
|
+
writeDoc(sbx, `${goalDir}/cto/workers/fe-dev.md`, `# FE Dev — Worker Report\n\n## Status\nCOMPLETE${NOTES}`);
|
|
435
|
+
writeMissionState(sbx, goalDir, "complete", false);
|
|
436
|
+
runScript(sbx, "harness-company-complete.sh", [".", "goal-complete"]);
|
|
437
|
+
let s = snap(sbx, "01-after-goal");
|
|
438
|
+
expect(s.missions.some((m) => m.missionId === goalDir && m.type === "goal")).toBe(true);
|
|
439
|
+
expect(s.runtime.agentStatus).toBe("completed"); // idle after goal
|
|
440
|
+
|
|
441
|
+
// --- /submission (additional development under the goal) ---
|
|
442
|
+
ownerCommand(sbx, "/submission Add promo-code support to checkout.");
|
|
443
|
+
const subDir = `${goalDir}/submission-1-promo-codes`;
|
|
444
|
+
writeDoc(sbx, `${subDir}/ceo.md`, `# CEO — Promo Codes\n\nFINAL: promo codes shipped.${NOTES}`);
|
|
445
|
+
writeDoc(sbx, `${subDir}/cto.md`, `# CTO\n\nWorker Evidence Manifest: cto/workers/promo-dev.md${NOTES}`);
|
|
446
|
+
writeDoc(sbx, `${subDir}/cto/workers/promo-dev.md`, `# Promo Dev — Worker Report\n\n## Status\nCOMPLETE${NOTES}`);
|
|
447
|
+
writeMissionState(sbx, subDir, "complete", false);
|
|
448
|
+
runScript(sbx, "harness-company-complete.sh", [".", "submission-complete"]);
|
|
449
|
+
s = snap(sbx, "02-after-submission");
|
|
450
|
+
const sub = s.missions.find((m) => m.missionId === subDir);
|
|
451
|
+
expect(sub).toBeTruthy();
|
|
452
|
+
expect(sub!.type).toBe("submission");
|
|
453
|
+
|
|
454
|
+
// --- /hot-fix (detail fix under the goal) + mandatory gotcha ---
|
|
455
|
+
const gotchasBefore = s.gotchas.length;
|
|
456
|
+
ownerCommand(sbx, "/hot-fix Promo code validation throws on empty input.");
|
|
457
|
+
const hotDir = `${goalDir}/hotfix-1-empty-promo`;
|
|
458
|
+
writeDoc(sbx, `${hotDir}/ceo.md`, `# CEO — Hotfix\n\nFINAL: guard added.${NOTES}`);
|
|
459
|
+
writeDoc(sbx, `${hotDir}/cto.md`, `# CTO\n\nWorker Evidence Manifest: cto/workers/patch.md${NOTES}`);
|
|
460
|
+
writeDoc(sbx, `${hotDir}/cto/workers/patch.md`, `# Patch — Worker Report\n\n## Status\nCOMPLETE${NOTES}`);
|
|
461
|
+
writeDoc(sbx, `${hotDir}/cqo.md`, `# CQO\n\nVerdict: PASS. Worker Evidence Manifest: cqo/workers/regress.md${NOTES}`);
|
|
462
|
+
writeDoc(sbx, `${hotDir}/cqo/workers/regress.md`, `# Regression — Worker Report\n\n## Status\nCOMPLETE${NOTES}`);
|
|
463
|
+
// hot-fix mandatory durable lesson
|
|
464
|
+
writeFileSync(
|
|
465
|
+
path.join(sbx, ".harness", "gotchas", "promo-empty-input.md"),
|
|
466
|
+
`# Promo code empty-input guard\n\nAlways guard promo validation against empty input.\n`
|
|
467
|
+
);
|
|
468
|
+
writeMissionState(sbx, hotDir, "complete", false);
|
|
469
|
+
runScript(sbx, "harness-company-complete.sh", [".", "hotfix-complete"]);
|
|
470
|
+
s = snap(sbx, "03-after-hotfix");
|
|
471
|
+
const hot = s.missions.find((m) => m.missionId === hotDir);
|
|
472
|
+
expect(hot).toBeTruthy();
|
|
473
|
+
expect(hot!.type).toBe("hotfix");
|
|
474
|
+
expect(s.gotchas.length).toBe(gotchasBefore + 1);
|
|
475
|
+
|
|
476
|
+
// --- dashboard tree expectations (HistoryNavigator groups by goal) ---
|
|
477
|
+
const goalScope = s.missions.filter(
|
|
478
|
+
(m) => m.missionId === goalDir || m.missionId.startsWith(`${goalDir}/`)
|
|
479
|
+
);
|
|
480
|
+
const ids = goalScope.map((m) => m.missionId);
|
|
481
|
+
expect(ids).toEqual(expect.arrayContaining([goalDir, subDir, hotDir]));
|
|
482
|
+
|
|
483
|
+
// --- owner command history (CommandLog + Cadence read this) ---
|
|
484
|
+
const cmds = s.ownerHistory.map((e) => e.type);
|
|
485
|
+
expect(cmds).toEqual(expect.arrayContaining(["goal", "submission", "hot-fix"]));
|
|
486
|
+
|
|
487
|
+
// runtime ends idle (loop cleanly closed)
|
|
488
|
+
expect(s.runtime.agentStatus).toBe("completed");
|
|
489
|
+
});
|
|
490
|
+
});
|