@walwal-harness/cli 7.1.28 → 7.1.29
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/README.md +25 -1
- package/apps/harness-dashboard/app/api/stream/route.ts +4 -0
- package/apps/harness-dashboard/components/Scene.tsx +225 -38
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +84 -0
- package/apps/harness-dashboard/lib/harness-state.ts +91 -6
- package/apps/harness-dashboard/lib/types.ts +3 -0
- package/assets/templates/AGENTS.md.template +6 -5
- package/bin/init.js +273 -41
- package/commands/goal.md +10 -8
- package/commands/hot-fix.md +12 -10
- package/commands/submission.md +12 -9
- package/package.json +1 -1
- package/scripts/harness-company-complete.sh +30 -0
|
@@ -793,6 +793,44 @@ interface HiredWorkerEntry {
|
|
|
793
793
|
capability: string | null;
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
+
type MissionLifecycle = MissionDoc["lifecycle"];
|
|
797
|
+
|
|
798
|
+
function normalizeMissionLifecycle(raw: unknown): MissionLifecycle {
|
|
799
|
+
const value = String(raw ?? "").toLowerCase().replace(/[_\s-]+/g, "-");
|
|
800
|
+
if (
|
|
801
|
+
value === "active" ||
|
|
802
|
+
value === "closed" ||
|
|
803
|
+
value === "cancelled" ||
|
|
804
|
+
value === "superseded" ||
|
|
805
|
+
value === "complete" ||
|
|
806
|
+
value === "blocked"
|
|
807
|
+
) {
|
|
808
|
+
return value;
|
|
809
|
+
}
|
|
810
|
+
if (value === "done" || value === "completed") return "complete";
|
|
811
|
+
if (value === "canceled") return "cancelled";
|
|
812
|
+
return "unknown";
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function readMissionState(missionPath: string): {
|
|
816
|
+
lifecycle: MissionLifecycle;
|
|
817
|
+
active: boolean | null;
|
|
818
|
+
} {
|
|
819
|
+
const statePath = path.join(missionPath, "mission-state.json");
|
|
820
|
+
const state = readJsonSafe<{
|
|
821
|
+
lifecycle?: unknown;
|
|
822
|
+
status?: unknown;
|
|
823
|
+
active?: boolean;
|
|
824
|
+
}>(statePath);
|
|
825
|
+
if (!state.ok) return { lifecycle: "unknown", active: null };
|
|
826
|
+
|
|
827
|
+
const lifecycle = normalizeMissionLifecycle(state.value.lifecycle ?? state.value.status);
|
|
828
|
+
return {
|
|
829
|
+
lifecycle,
|
|
830
|
+
active: typeof state.value.active === "boolean" ? state.value.active : null,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
796
834
|
function deriveWorkerProgress(worker: RawWorker): number | null {
|
|
797
835
|
const status = (worker?.status ?? worker?.spawn_status ?? "").toLowerCase();
|
|
798
836
|
if (status === "blocked" || status === "failed") return 0.35;
|
|
@@ -940,8 +978,20 @@ function workerDisplayName(content: string, hired: HiredWorkerEntry | undefined,
|
|
|
940
978
|
return fallback;
|
|
941
979
|
}
|
|
942
980
|
|
|
981
|
+
function isHarnessRuntimeIdle(progress: RawProgress | null): boolean {
|
|
982
|
+
const status = (progress?.agent_status ?? "").toLowerCase();
|
|
983
|
+
const current = progress?.current_agent ?? null;
|
|
984
|
+
const next = progress?.next_agent ?? null;
|
|
985
|
+
return (
|
|
986
|
+
status === "completed" ||
|
|
987
|
+
status === "complete" ||
|
|
988
|
+
(status === "idle" && current === null && (next === null || String(next) === "none"))
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
943
992
|
function activeWorkerNames(progress: RawProgress | null): Set<string> {
|
|
944
993
|
const active = new Set<string>();
|
|
994
|
+
if (isHarnessRuntimeIdle(progress)) return active;
|
|
945
995
|
const workers = [
|
|
946
996
|
...(progress?.company_state?.workers ?? []),
|
|
947
997
|
...(progress?.company_state?.last_dispatch ?? []),
|
|
@@ -1111,6 +1161,7 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1111
1161
|
if (!existsSync(docsDir)) return [];
|
|
1112
1162
|
const hiredWorkers = readHiredWorkers(rootDir);
|
|
1113
1163
|
const activeWorkers = activeWorkerNames(progress);
|
|
1164
|
+
const runtimeIdle = isHarnessRuntimeIdle(progress);
|
|
1114
1165
|
|
|
1115
1166
|
const missionDirs: Array<{ rel: string; abs: string }> = [];
|
|
1116
1167
|
const cxxDocNames = new Set(["ceo.md", "cto.md", "cqo.md", "coo.md", "cdo.md", "ops.md"]);
|
|
@@ -1145,7 +1196,7 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1145
1196
|
return "unknown";
|
|
1146
1197
|
};
|
|
1147
1198
|
|
|
1148
|
-
|
|
1199
|
+
let missions = missionDirs
|
|
1149
1200
|
.map(({ rel, abs: missionPath }) => {
|
|
1150
1201
|
let mtime: Date;
|
|
1151
1202
|
try { mtime = statSync(missionPath).mtime; } catch { mtime = new Date(0); }
|
|
@@ -1192,7 +1243,7 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1192
1243
|
status,
|
|
1193
1244
|
owner: hired?.owner ?? owner,
|
|
1194
1245
|
hired: !!hired,
|
|
1195
|
-
active: activeWorkers.has(name) || (recentlyTouched && status !== "COMPLETE"),
|
|
1246
|
+
active: !runtimeIdle && (activeWorkers.has(name) || (recentlyTouched && status !== "COMPLETE")),
|
|
1196
1247
|
sourcePath: hired?.sourcePath ?? null,
|
|
1197
1248
|
reportPath: path.relative(rootDir, reportAbs),
|
|
1198
1249
|
updatedAt,
|
|
@@ -1216,10 +1267,10 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1216
1267
|
name: hired.name,
|
|
1217
1268
|
displayName: workerDisplayName("", hired, hired.name),
|
|
1218
1269
|
content: "",
|
|
1219
|
-
status: activeWorkers.has(hired.name) ? "IN_PROGRESS" : "unknown",
|
|
1270
|
+
status: !runtimeIdle && activeWorkers.has(hired.name) ? "IN_PROGRESS" : "unknown",
|
|
1220
1271
|
owner: hired.owner,
|
|
1221
1272
|
hired: true,
|
|
1222
|
-
active: activeWorkers.has(hired.name),
|
|
1273
|
+
active: !runtimeIdle && activeWorkers.has(hired.name),
|
|
1223
1274
|
sourcePath: hired.sourcePath,
|
|
1224
1275
|
reportPath: null,
|
|
1225
1276
|
updatedAt: null,
|
|
@@ -1230,6 +1281,23 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1230
1281
|
|
|
1231
1282
|
const cxxRoles = ["ceo", "cto", "cqo", "coo", "cdo", "ops"] as const;
|
|
1232
1283
|
const cxxPresent = cxxRoles.filter(role => existsSync(path.join(missionPath, `${role}.md`)));
|
|
1284
|
+
const state = readMissionState(missionPath);
|
|
1285
|
+
const protocolViolations: string[] = [];
|
|
1286
|
+
for (const role of cxxPresent) {
|
|
1287
|
+
if (role === "ceo") continue;
|
|
1288
|
+
const roleWorkers = workers.filter((w) => w.owner === role);
|
|
1289
|
+
const roleDoc = readMd(`${role}.md`) ?? "";
|
|
1290
|
+
const hasManifest = /Worker Evidence Manifest/i.test(roleDoc);
|
|
1291
|
+
const mentionsWorkerReport = new RegExp(`${role}/workers/`, "i").test(roleDoc);
|
|
1292
|
+
if (roleWorkers.length === 0 || !hasManifest || !mentionsWorkerReport) {
|
|
1293
|
+
protocolViolations.push(`${role}:missing-worker-evidence`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
for (const w of workers) {
|
|
1297
|
+
if (w.owner === "unknown" || !w.hired) {
|
|
1298
|
+
protocolViolations.push(`${w.owner}:${w.name}:not-hired-or-unowned`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1233
1301
|
|
|
1234
1302
|
const id = rel;
|
|
1235
1303
|
const label = rel.split("/").pop() ?? rel;
|
|
@@ -1237,6 +1305,9 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1237
1305
|
missionId: id,
|
|
1238
1306
|
ts: mtime.toISOString(),
|
|
1239
1307
|
type: typeFromId(id),
|
|
1308
|
+
lifecycle: state.lifecycle,
|
|
1309
|
+
active: state.active ?? false,
|
|
1310
|
+
protocolViolations,
|
|
1240
1311
|
label,
|
|
1241
1312
|
ceo: readMd("ceo.md"),
|
|
1242
1313
|
cto: readMd("cto.md"),
|
|
@@ -1249,8 +1320,22 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1249
1320
|
cxxPresent: [...cxxPresent],
|
|
1250
1321
|
} satisfies MissionDoc;
|
|
1251
1322
|
})
|
|
1252
|
-
.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())
|
|
1253
|
-
|
|
1323
|
+
.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
|
|
1324
|
+
|
|
1325
|
+
const explicitActive = missions.some((m) => m.active);
|
|
1326
|
+
if (!explicitActive) {
|
|
1327
|
+
const latestOpen = missions.find(
|
|
1328
|
+
(m) => m.lifecycle === "active" || m.lifecycle === "unknown"
|
|
1329
|
+
);
|
|
1330
|
+
if (latestOpen) {
|
|
1331
|
+
missions = missions.map((m) => ({
|
|
1332
|
+
...m,
|
|
1333
|
+
active: m.missionId === latestOpen.missionId,
|
|
1334
|
+
}));
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
missions = missions.slice(0, limit);
|
|
1254
1339
|
|
|
1255
1340
|
// When the CEO updates a goal directory in-place for a submission or hot-fix
|
|
1256
1341
|
// (instead of creating a submission-* or hotfix-* subdirectory), the most
|
|
@@ -340,6 +340,9 @@ export interface MissionDoc {
|
|
|
340
340
|
missionId: string;
|
|
341
341
|
ts: string;
|
|
342
342
|
type: "goal" | "submission" | "hotfix" | "feature" | "unknown";
|
|
343
|
+
lifecycle: "active" | "closed" | "cancelled" | "superseded" | "complete" | "blocked" | "unknown";
|
|
344
|
+
active: boolean;
|
|
345
|
+
protocolViolations: string[];
|
|
343
346
|
label: string;
|
|
344
347
|
ceo: string | null;
|
|
345
348
|
cto: string | null;
|
|
@@ -88,7 +88,7 @@ The Owner is the final acceptance reviewer, not a tester, QA worker, debugger, o
|
|
|
88
88
|
4. CEO aggregates CXX outputs, routes to next CXX or reports to Owner.
|
|
89
89
|
5. If the mission uses a runnable local, preview, Docker, or production-like environment, OPS watches the runtime while CQO evaluator workers run Playwright/E2E/API/visual/performance/regression checks.
|
|
90
90
|
|
|
91
|
-
**Complete when:** `.harness/documents/goal-{index}-{name}/ceo.md` contains a final Owner report.
|
|
91
|
+
**Complete when:** `.harness/documents/goal-{index}-{name}/ceo.md` contains a final Owner report and `mission-state.json` is terminal (`complete`, `closed`, `cancelled`, or `superseded`) with `active:false`.
|
|
92
92
|
|
|
93
93
|
### Submission
|
|
94
94
|
|
|
@@ -98,7 +98,7 @@ The Owner is the final acceptance reviewer, not a tester, QA worker, debugger, o
|
|
|
98
98
|
4. Relevant CXX update `.harness/conventions/` when the new requirement changes durable project rules.
|
|
99
99
|
5. Workers report under `{owning-cxx}/workers/`; CXX summarize choices in `{cxx}.md`.
|
|
100
100
|
|
|
101
|
-
**Complete when:** the submission child directory has CEO/CXX records, required worker evidence,
|
|
101
|
+
**Complete when:** the submission child directory has CEO/CXX records, required worker evidence, any convention updates required by the new requirement, and terminal `mission-state.json`.
|
|
102
102
|
|
|
103
103
|
### Hot Fix
|
|
104
104
|
|
|
@@ -150,9 +150,10 @@ Note: A docmeta skip decision on harness documents (ceo.md, cto.md, cqo.md, work
|
|
|
150
150
|
11. **Lazy convention/gotcha loading** — CXX roles read `.harness/conventions/shared.md`, `.harness/conventions/{cxx}.md`, `.harness/gotchas/shared.md`, and `.harness/gotchas/{cxx}.md`, then follow only the related topic links listed in those CXX files. Workers read only the related links supplied by their owning CXX.
|
|
151
151
|
12. **Implementation Notes required** — `ceo.md`, every `{cxx}.md`, and every worker report must end with one English `## Implementation Notes` section containing `Design Decisions`, `Deviations`, `Tradeoffs`, and `Open Questions`. Use `None` for empty subsections. Do not create a separate sidecar notes file; the notes belong at the bottom of the same role or worker report that produced the decision/evidence.
|
|
152
152
|
13. **Structured runtime state** — If the harness must parse it, record it in JSON or JSONL. CXX todo queues, event history, heartbeat timestamps, preemption/resume state, and completion evidence belong in `.harness/todos/*.json*` or `.harness/events.jsonl`, not in free-form Markdown tables. Markdown remains for instructions, conventions, gotchas, skills, and human-readable mission narrative.
|
|
153
|
-
14. **
|
|
154
|
-
15. **
|
|
155
|
-
16. **
|
|
153
|
+
14. **Mission lifecycle is explicit** — Every goal, submission, and hot-fix directory must contain `mission-state.json` with `lifecycle` and `active`. Only one child mission under a goal may be active. Starting a newer submission/hot-fix closes, cancels, or supersedes the previous active child unless CEO records a deliberate TODO/resume plan.
|
|
154
|
+
15. **Owner is final acceptance only** — Owner is not a tester or QA substitute. CEO/CXX must not report "done, please check" until worker-backed verification proves the goal can be completed. Use unit tests, E2E, Playwright, test accounts, seeded data, build/run checks, logs, and CQO evidence before requesting Owner acceptance.
|
|
155
|
+
16. **OPS watches CQO verification** — For runnable products, CQO PASS is invalid unless OPS has monitored the verification runtime or documented why OPS watch is not applicable. Open OPS incidents, missing runtime mapping, required log gaps, service down, health mismatch, or an unmonitored tested service block Owner acceptance.
|
|
156
|
+
17. **Production incidents are company events** — After launch, user-impacting OPS signals route through CEO to CTO/CQO/OPS. CTO owns recovery, CQO owns regression confirmation, and OPS owns evidence plus close criteria.
|
|
156
157
|
|
|
157
158
|
---
|
|
158
159
|
|
package/bin/init.js
CHANGED
|
@@ -112,6 +112,22 @@ function chmodShellScripts(dir) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function readJsonFileSafe(filePath) {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readTextFileSafe(filePath) {
|
|
124
|
+
try {
|
|
125
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
115
131
|
function fileExists(p) {
|
|
116
132
|
return fs.existsSync(p);
|
|
117
133
|
}
|
|
@@ -451,6 +467,99 @@ function migrateExistingDocs() {
|
|
|
451
467
|
log(`Migration report: ${reportPath}`);
|
|
452
468
|
}
|
|
453
469
|
|
|
470
|
+
function missionTypeFromDirName(name) {
|
|
471
|
+
if (/^goal[-_]/i.test(name)) return 'goal';
|
|
472
|
+
if (/^submission[-_]/i.test(name)) return 'submission';
|
|
473
|
+
if (/^hot[-_]?fix[-_]/i.test(name) || /^hotfix[-_]/i.test(name)) return 'hotfix';
|
|
474
|
+
return 'unknown';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function isTerminalMissionLifecycle(value) {
|
|
478
|
+
return ['closed', 'cancelled', 'superseded', 'complete', 'blocked'].includes(String(value || '').toLowerCase());
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function collectMissionDirs(docsDir) {
|
|
482
|
+
const cxxDocs = new Set(['ceo.md', 'coo.md', 'cdo.md', 'cto.md', 'cqo.md', 'ops.md']);
|
|
483
|
+
const result = [];
|
|
484
|
+
const walk = (dir, rel = '') => {
|
|
485
|
+
if (!fs.existsSync(dir)) return;
|
|
486
|
+
let entries = [];
|
|
487
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
488
|
+
const hasMissionDoc = entries.some((entry) => entry.isFile() && cxxDocs.has(entry.name));
|
|
489
|
+
if (rel && hasMissionDoc) {
|
|
490
|
+
let mtime = 0;
|
|
491
|
+
try { mtime = fs.statSync(dir).mtimeMs; } catch {}
|
|
492
|
+
const name = path.basename(rel);
|
|
493
|
+
result.push({ abs: dir, rel, name, type: missionTypeFromDirName(name), mtime });
|
|
494
|
+
}
|
|
495
|
+
for (const entry of entries) {
|
|
496
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
497
|
+
if (['workers', 'coo', 'cdo', 'cto', 'cqo', 'ops'].includes(entry.name)) continue;
|
|
498
|
+
walk(path.join(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
walk(docsDir);
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function ensureMissionLifecycleStates(opts = {}) {
|
|
506
|
+
const dryRun = opts.dryRun || false;
|
|
507
|
+
const backupDir = opts.backupDir || null;
|
|
508
|
+
const docsDir = path.join(HARNESS_DIR, 'documents');
|
|
509
|
+
if (!fs.existsSync(docsDir)) return { missing: 0, written: 0 };
|
|
510
|
+
|
|
511
|
+
const missions = collectMissionDirs(docsDir);
|
|
512
|
+
if (!missions.length) return { missing: 0, written: 0 };
|
|
513
|
+
|
|
514
|
+
const byGoal = new Map();
|
|
515
|
+
for (const mission of missions) {
|
|
516
|
+
const goalKey = mission.rel.split('/')[0];
|
|
517
|
+
if (!byGoal.has(goalKey)) byGoal.set(goalKey, []);
|
|
518
|
+
byGoal.get(goalKey).push(mission);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const writes = [];
|
|
522
|
+
for (const group of byGoal.values()) {
|
|
523
|
+
const children = group.filter((m) => m.rel.includes('/') && (m.type === 'submission' || m.type === 'hotfix'));
|
|
524
|
+
children.sort((a, b) => b.mtime - a.mtime);
|
|
525
|
+
const existingActiveChild = children.find((m) => {
|
|
526
|
+
const state = readJsonFileSafe(path.join(m.abs, 'mission-state.json'));
|
|
527
|
+
return state?.active === true && !isTerminalMissionLifecycle(state.lifecycle || state.status);
|
|
528
|
+
});
|
|
529
|
+
const activeChild = existingActiveChild || children[0] || null;
|
|
530
|
+
|
|
531
|
+
for (const mission of group) {
|
|
532
|
+
const statePath = path.join(mission.abs, 'mission-state.json');
|
|
533
|
+
if (fs.existsSync(statePath)) continue;
|
|
534
|
+
const isChild = mission.rel.includes('/');
|
|
535
|
+
const isActive = isChild
|
|
536
|
+
? activeChild?.rel === mission.rel
|
|
537
|
+
: true;
|
|
538
|
+
writes.push({
|
|
539
|
+
path: statePath,
|
|
540
|
+
state: {
|
|
541
|
+
lifecycle: isActive ? 'active' : 'closed',
|
|
542
|
+
active: isActive,
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (dryRun) return { missing: writes.length, written: 0 };
|
|
549
|
+
|
|
550
|
+
for (const item of writes) {
|
|
551
|
+
if (backupDir) {
|
|
552
|
+
const rel = path.relative(HARNESS_DIR, item.path).replace(/[\\/]/g, '__');
|
|
553
|
+
if (fs.existsSync(item.path)) fs.writeFileSync(path.join(backupDir, rel), fs.readFileSync(item.path, 'utf8'));
|
|
554
|
+
}
|
|
555
|
+
fs.writeFileSync(item.path, JSON.stringify(item.state, null, 2) + '\n');
|
|
556
|
+
}
|
|
557
|
+
if (writes.length) {
|
|
558
|
+
log(`mission-state.json: ${writes.length}개 기존 mission에 lifecycle 보강`);
|
|
559
|
+
}
|
|
560
|
+
return { missing: writes.length, written: writes.length };
|
|
561
|
+
}
|
|
562
|
+
|
|
454
563
|
// ─────────────────────────────────────────
|
|
455
564
|
// 1. .harness/ scaffolding
|
|
456
565
|
// ─────────────────────────────────────────
|
|
@@ -585,6 +694,7 @@ function scaffoldHarness() {
|
|
|
585
694
|
if (!fileExists(p) || isForce) fs.writeFileSync(p, '');
|
|
586
695
|
}
|
|
587
696
|
ensureStructuredRuntimeFiles();
|
|
697
|
+
ensureMissionLifecycleStates();
|
|
588
698
|
|
|
589
699
|
// Migrate progress.json v1 → v2 (add mode + team_state fields)
|
|
590
700
|
const progressPath = path.join(HARNESS_DIR, 'progress.json');
|
|
@@ -1094,55 +1204,129 @@ function installAgentTeamsEnv() {
|
|
|
1094
1204
|
}
|
|
1095
1205
|
}
|
|
1096
1206
|
|
|
1097
|
-
|
|
1207
|
+
const HARNESS_HEADER_MARKER = '<!-- walwal-harness:managed-header -->';
|
|
1208
|
+
const HARNESS_SECTION_START = '<!-- walwal-harness:agents:start -->';
|
|
1209
|
+
const HARNESS_SECTION_END = '<!-- walwal-harness:agents:end -->';
|
|
1210
|
+
const PRESERVED_CLAUDE_START = '<!-- walwal-harness:preserved-claude:start -->';
|
|
1211
|
+
const PRESERVED_CLAUDE_END = '<!-- walwal-harness:preserved-claude:end -->';
|
|
1212
|
+
|
|
1213
|
+
function backupDocPath(filePath, backupDir, label) {
|
|
1214
|
+
if (!filePath || !fs.existsSync(filePath)) return;
|
|
1215
|
+
ensureDir(backupDir);
|
|
1216
|
+
const safe = label.replace(/[^A-Za-z0-9_.-]/g, '_');
|
|
1217
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1218
|
+
try {
|
|
1219
|
+
fs.copyFileSync(filePath, path.join(backupDir, `${safe}.${ts}.bak`));
|
|
1220
|
+
} catch {}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function renderHarnessAgentsSection() {
|
|
1224
|
+
const templateSrc = path.join(PKG_ROOT, 'assets', 'templates', 'AGENTS.md.template');
|
|
1225
|
+
let content = fs.existsSync(templateSrc)
|
|
1226
|
+
? fs.readFileSync(templateSrc, 'utf8')
|
|
1227
|
+
: '# walwal-harness\n\nUse `/goal`, `/submission`, and `/hot-fix` for CEO/CXX/worker flow.\n';
|
|
1228
|
+
content = content.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0]);
|
|
1229
|
+
content = content.replace(/\{\{DATE_ISO\}\}/g, new Date().toISOString());
|
|
1230
|
+
return [
|
|
1231
|
+
HARNESS_SECTION_START,
|
|
1232
|
+
'',
|
|
1233
|
+
content.trim(),
|
|
1234
|
+
'',
|
|
1235
|
+
HARNESS_SECTION_END,
|
|
1236
|
+
''
|
|
1237
|
+
].join('\n');
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function ensureTopHarnessHeader(body) {
|
|
1241
|
+
if (body.includes(HARNESS_HEADER_MARKER)) return body;
|
|
1242
|
+
const header = [
|
|
1243
|
+
HARNESS_HEADER_MARKER,
|
|
1244
|
+
'> This project is walwal-harness enabled. Harness operating rules live in the marked walwal-harness section below.',
|
|
1245
|
+
''
|
|
1246
|
+
].join('\n');
|
|
1247
|
+
return header + body.replace(/^\s+/, '');
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function upsertMarkedBlock(body, start, end, block) {
|
|
1251
|
+
const startIdx = body.indexOf(start);
|
|
1252
|
+
const endIdx = body.indexOf(end);
|
|
1253
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
1254
|
+
const afterEnd = endIdx + end.length;
|
|
1255
|
+
return body.slice(0, startIdx).replace(/\s*$/, '\n\n') + block.trim() + '\n' + body.slice(afterEnd).replace(/^\s*/, '\n');
|
|
1256
|
+
}
|
|
1257
|
+
const sep = body.trim() ? '\n\n' : '';
|
|
1258
|
+
return body.replace(/\s*$/, '') + sep + block.trim() + '\n';
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function normalizeDocBody(body) {
|
|
1262
|
+
return String(body || '').replace(/\r\n/g, '\n').trim();
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function ensureProjectAgentDocs(opts = {}) {
|
|
1266
|
+
const dryRun = opts.dryRun || false;
|
|
1267
|
+
const backupDir = opts.backupDir || path.join(HARNESS_DIR, 'archive', 'pre-harness-backup');
|
|
1098
1268
|
const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
|
|
1099
1269
|
const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
1100
1270
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1271
|
+
const agentsTarget = fs.existsSync(agentsMd) ? fs.realpathSync(agentsMd) : agentsMd;
|
|
1272
|
+
const existingAgents = readTextFileSafe(agentsMd) || '';
|
|
1273
|
+
const claudeExists = fs.existsSync(claudeMd) || fs.existsSync(claudeMd);
|
|
1274
|
+
const claudeIsSymlink = fs.existsSync(claudeMd) && fs.lstatSync(claudeMd).isSymbolicLink();
|
|
1275
|
+
const claudeTarget = claudeExists ? fs.realpathSync(claudeMd) : null;
|
|
1276
|
+
const claudeBody = claudeExists ? (readTextFileSafe(claudeMd) || '') : '';
|
|
1277
|
+
|
|
1278
|
+
let nextAgents = existingAgents;
|
|
1279
|
+
nextAgents = ensureTopHarnessHeader(nextAgents);
|
|
1280
|
+
nextAgents = upsertMarkedBlock(nextAgents, HARNESS_SECTION_START, HARNESS_SECTION_END, renderHarnessAgentsSection());
|
|
1281
|
+
|
|
1282
|
+
const normalizedClaude = normalizeDocBody(claudeBody);
|
|
1283
|
+
const claudePointsAtAgents = claudeIsSymlink && claudeTarget === agentsTarget;
|
|
1284
|
+
if (normalizedClaude && !claudePointsAtAgents) {
|
|
1285
|
+
const preservedBlock = [
|
|
1286
|
+
PRESERVED_CLAUDE_START,
|
|
1287
|
+
'',
|
|
1288
|
+
'## Preserved CLAUDE.md',
|
|
1289
|
+
'',
|
|
1290
|
+
normalizedClaude,
|
|
1291
|
+
'',
|
|
1292
|
+
PRESERVED_CLAUDE_END,
|
|
1293
|
+
''
|
|
1294
|
+
].join('\n');
|
|
1295
|
+
nextAgents = upsertMarkedBlock(nextAgents, PRESERVED_CLAUDE_START, PRESERVED_CLAUDE_END, preservedBlock);
|
|
1121
1296
|
}
|
|
1122
1297
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1298
|
+
if (dryRun) {
|
|
1299
|
+
log(' (dry-run) AGENTS.md: walwal-harness marked section merge 예정');
|
|
1300
|
+
if (!claudePointsAtAgents) log(' (dry-run) CLAUDE.md: content preserved then symlink → AGENTS.md 예정');
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (fs.existsSync(agentsTarget)) backupDocPath(agentsTarget, backupDir, 'AGENTS.md');
|
|
1305
|
+
if (claudeExists && claudeTarget && claudeTarget !== agentsTarget) {
|
|
1306
|
+
backupDocPath(claudeTarget, backupDir, 'CLAUDE.md');
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
ensureDir(path.dirname(agentsMd));
|
|
1310
|
+
fs.writeFileSync(agentsMd, nextAgents.replace(/\s*$/, '\n'));
|
|
1311
|
+
log(fs.existsSync(agentsTarget) ? 'AGENTS.md merged with walwal-harness marked section' : 'AGENTS.md created from walwal-harness template');
|
|
1312
|
+
|
|
1313
|
+
try {
|
|
1314
|
+
if (fs.existsSync(claudeMd) || fs.lstatSync(claudeMd)) fs.unlinkSync(claudeMd);
|
|
1315
|
+
} catch {}
|
|
1316
|
+
try {
|
|
1317
|
+
fs.symlinkSync('AGENTS.md', claudeMd);
|
|
1318
|
+
log('CLAUDE.md replaced with symlink → AGENTS.md');
|
|
1319
|
+
} catch (e) {
|
|
1320
|
+
log(`WARNING: Could not create CLAUDE.md symlink (${e.message})`);
|
|
1143
1321
|
}
|
|
1144
1322
|
}
|
|
1145
1323
|
|
|
1324
|
+
function setupAgentsMd() {
|
|
1325
|
+
const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
|
|
1326
|
+
const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
1327
|
+
ensureProjectAgentDocs({ backupDir: path.join(HARNESS_DIR, 'archive', 'pre-harness-backup') });
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1146
1330
|
// ─────────────────────────────────────────
|
|
1147
1331
|
// 5. Playwright MCP check
|
|
1148
1332
|
// ─────────────────────────────────────────
|
|
@@ -1300,6 +1484,8 @@ function detectMigrationNeeded() {
|
|
|
1300
1484
|
harnessMdStale: false,
|
|
1301
1485
|
agentsMissingOpsVerificationRules: false,
|
|
1302
1486
|
agentsMissingCodexAdapterRules: false,
|
|
1487
|
+
agentsMissingMissionLifecycleRules: false,
|
|
1488
|
+
missionLifecycleMissing: false,
|
|
1303
1489
|
rosterCodexPathsMissing: false,
|
|
1304
1490
|
resourceIndexCodexWordingMissing: false,
|
|
1305
1491
|
memoryMissingSystemEntries: [],
|
|
@@ -1427,8 +1613,19 @@ function detectMigrationNeeded() {
|
|
|
1427
1613
|
) {
|
|
1428
1614
|
flags.agentsMissingCodexAdapterRules = true;
|
|
1429
1615
|
}
|
|
1616
|
+
if (
|
|
1617
|
+
!agentsBody.includes('Mission Lifecycle Rules') &&
|
|
1618
|
+
!agentsBody.includes('mission-state.json')
|
|
1619
|
+
) {
|
|
1620
|
+
flags.agentsMissingMissionLifecycleRules = true;
|
|
1621
|
+
}
|
|
1430
1622
|
} catch {}
|
|
1431
1623
|
}
|
|
1624
|
+
const docsDir = path.join(HARNESS_DIR, 'documents');
|
|
1625
|
+
if (fs.existsSync(docsDir)) {
|
|
1626
|
+
flags.missionLifecycleMissing = collectMissionDirs(docsDir)
|
|
1627
|
+
.some((mission) => !fs.existsSync(path.join(mission.abs, 'mission-state.json')));
|
|
1628
|
+
}
|
|
1432
1629
|
if (fs.existsSync(rosterPath)) {
|
|
1433
1630
|
try {
|
|
1434
1631
|
const roster = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
|
|
@@ -1764,6 +1961,8 @@ function runMigrate(opts = {}) {
|
|
|
1764
1961
|
!flags.agentsMissingOpsVerificationRules &&
|
|
1765
1962
|
!flags.rosterCodexPathsMissing &&
|
|
1766
1963
|
!flags.resourceIndexCodexWordingMissing &&
|
|
1964
|
+
!flags.agentsMissingMissionLifecycleRules &&
|
|
1965
|
+
!flags.missionLifecycleMissing &&
|
|
1767
1966
|
(!flags.memoryMissingSystemEntries || flags.memoryMissingSystemEntries.length === 0) &&
|
|
1768
1967
|
gotchaMissingTotal === 0 &&
|
|
1769
1968
|
conventionMissingTotal === 0 &&
|
|
@@ -1785,6 +1984,14 @@ function runMigrate(opts = {}) {
|
|
|
1785
1984
|
const backupDir = path.join(HARNESS_DIR, 'archive', `migration-${ts}`);
|
|
1786
1985
|
if (!dryRun) ensureDir(backupDir);
|
|
1787
1986
|
ensureStructuredRuntimeFiles({ dryRun });
|
|
1987
|
+
ensureProjectAgentDocs({ dryRun, backupDir });
|
|
1988
|
+
flags.agentsMissingOpsVerificationRules = false;
|
|
1989
|
+
flags.agentsMissingCodexAdapterRules = false;
|
|
1990
|
+
flags.agentsMissingMissionLifecycleRules = false;
|
|
1991
|
+
const missionStateResult = ensureMissionLifecycleStates({ dryRun, backupDir });
|
|
1992
|
+
if (dryRun && missionStateResult.missing > 0) {
|
|
1993
|
+
log(` (dry-run) mission-state.json: ${missionStateResult.missing}개 기존 mission에 lifecycle 보강 예정`);
|
|
1994
|
+
}
|
|
1788
1995
|
|
|
1789
1996
|
// Runtime scripts are package-owned. Migrate must refresh them so existing
|
|
1790
1997
|
// projects receive wake/meeting/OPS fixes without requiring a full init.
|
|
@@ -2099,6 +2306,31 @@ function runMigrate(opts = {}) {
|
|
|
2099
2306
|
}
|
|
2100
2307
|
}
|
|
2101
2308
|
|
|
2309
|
+
if (flags.agentsMissingMissionLifecycleRules) {
|
|
2310
|
+
const agentsPath = path.join(PROJECT_ROOT, 'AGENTS.md');
|
|
2311
|
+
if (fs.existsSync(agentsPath)) {
|
|
2312
|
+
const original = fs.readFileSync(agentsPath, 'utf8');
|
|
2313
|
+
const block = [
|
|
2314
|
+
'',
|
|
2315
|
+
'---',
|
|
2316
|
+
'',
|
|
2317
|
+
'## walwal-harness Mission Lifecycle Rules — v7.1.28',
|
|
2318
|
+
'',
|
|
2319
|
+
'- Every goal, submission, and hot-fix directory must contain `mission-state.json` with `lifecycle` and `active`.',
|
|
2320
|
+
'- Only one child mission under a goal may be active. Starting a newer submission/hot-fix closes, cancels, or supersedes the previous active child unless CEO records a deliberate TODO/resume plan.',
|
|
2321
|
+
'- CXX completion requires a Worker Evidence Manifest plus worker report paths under `{owning-cxx}/workers/`. A CXX report without hired worker evidence is a protocol violation.',
|
|
2322
|
+
'- Worker execution should be shown as worker activity; CXX activity is coordination only.',
|
|
2323
|
+
''
|
|
2324
|
+
].join('\n');
|
|
2325
|
+
log(' AGENTS.md: mission lifecycle / CXX hire-only rules append');
|
|
2326
|
+
if (!dryRun) {
|
|
2327
|
+
fs.writeFileSync(path.join(backupDir, 'AGENTS.mission-lifecycle.md'), original);
|
|
2328
|
+
const sep = original.endsWith('\n') ? '' : '\n';
|
|
2329
|
+
fs.writeFileSync(agentsPath, original + sep + block);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2102
2334
|
// 7. Bundle version stamp — record which package version was last applied.
|
|
2103
2335
|
if (flags.bundleVersionStale) {
|
|
2104
2336
|
const { current, installed } = flags.bundleVersionStale;
|
package/commands/goal.md
CHANGED
|
@@ -14,14 +14,16 @@ Codex adapter:
|
|
|
14
14
|
|
|
15
15
|
Required flow:
|
|
16
16
|
1. Create or update a goal under `.harness/documents/goal-{goal_index}-{goal_name}/`.
|
|
17
|
-
2.
|
|
18
|
-
3. CEO
|
|
19
|
-
4. CEO
|
|
20
|
-
5.
|
|
21
|
-
6. CXX roles must
|
|
22
|
-
7.
|
|
23
|
-
8.
|
|
24
|
-
9.
|
|
17
|
+
2. Write `.harness/documents/goal-{goal_index}-{goal_name}/mission-state.json` with `{"lifecycle":"active","active":true}` before routing CXX.
|
|
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.
|
|
20
|
+
5. CEO asks COO, CDO, CTO, and CQO only the questions needed for this goal.
|
|
21
|
+
6. CXX roles must start in a fresh session context. Do not let the default model impersonate a missing worker.
|
|
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.
|
|
23
|
+
8. If a required CXX or worker skill is missing, CEO must invoke the installed `harness-hiring` skill before assigning the work.
|
|
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
|
+
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
|
+
11. Do not invoke internal roles through slash commands; commands are Owner entrypoints only.
|
|
25
27
|
|
|
26
28
|
Note: A goal is the company's objective. Submissions and hot-fixes that happen while pursuing it should be recorded under that goal directory.
|
|
27
29
|
|