@walwal-harness/cli 7.1.26 → 7.1.28
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 +8 -1
- package/apps/harness-dashboard/app/globals.css +165 -3
- package/apps/harness-dashboard/app/layout.tsx +1 -5
- package/apps/harness-dashboard/app/page.tsx +3 -7
- package/apps/harness-dashboard/components/Drawer.tsx +7 -7
- package/apps/harness-dashboard/components/MissionTimeline.tsx +4 -4
- package/apps/harness-dashboard/components/OrgTree.tsx +7 -7
- package/apps/harness-dashboard/components/Scene.tsx +1620 -487
- package/apps/harness-dashboard/lib/harness-state.ts +132 -10
- package/apps/harness-dashboard/lib/types.ts +39 -0
- package/apps/harness-dashboard/package-lock.json +1519 -53
- package/apps/harness-dashboard/package.json +2 -0
- package/assets/templates/AGENTS.md.template +8 -4
- package/assets/templates/HARNESS.md +6 -1
- package/assets/templates/todos-state.json.template +12 -0
- package/bin/init.js +43 -0
- package/package.json +1 -1
- package/scripts/harness-user-prompt-submit.sh +42 -0
- package/scripts/lib/harness-structured-log.sh +59 -0
|
@@ -18,12 +18,14 @@ import type {
|
|
|
18
18
|
FeatureSummary,
|
|
19
19
|
GoalCard,
|
|
20
20
|
HarnessSnapshot,
|
|
21
|
+
HarnessEvent,
|
|
21
22
|
HypothesisEntry,
|
|
22
23
|
IncidentEntry,
|
|
23
24
|
MeetingCadence,
|
|
24
25
|
MeetingRecord,
|
|
25
26
|
MeetingsState,
|
|
26
27
|
GotchaEntry,
|
|
28
|
+
ConventionEntry,
|
|
27
29
|
MissionDoc,
|
|
28
30
|
OperationsDashboard,
|
|
29
31
|
OpsServiceHealth,
|
|
@@ -37,6 +39,7 @@ import type {
|
|
|
37
39
|
TrackStatus,
|
|
38
40
|
WorkerDocEntry,
|
|
39
41
|
WorkerSnapshot,
|
|
42
|
+
CxxTodo,
|
|
40
43
|
} from "./types";
|
|
41
44
|
|
|
42
45
|
const SNAPSHOT_VERSION = "1.2.0";
|
|
@@ -224,6 +227,9 @@ function emptySnapshot(banner: ErrorBanner | null = null, rootDir?: string): Har
|
|
|
224
227
|
missions: [],
|
|
225
228
|
ownerHistory: [],
|
|
226
229
|
gotchas: [],
|
|
230
|
+
conventions: [],
|
|
231
|
+
todos: [],
|
|
232
|
+
events: [],
|
|
227
233
|
};
|
|
228
234
|
}
|
|
229
235
|
|
|
@@ -1161,7 +1167,15 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1161
1167
|
const reportAbs = path.join(missionPath, relDir, f);
|
|
1162
1168
|
const content = readMd(`${relDir}/${f}`) ?? "";
|
|
1163
1169
|
const statusMatch = content.match(/##\s*Status\s*\n+([A-Z_]+)/);
|
|
1164
|
-
|
|
1170
|
+
// Worker docs sometimes omit the "## Status" block but always carry a
|
|
1171
|
+
// docmeta frontmatter when the worker submitted its output. Treat the
|
|
1172
|
+
// presence of a docmeta block (or a non-empty body) as COMPLETE.
|
|
1173
|
+
const hasDocmeta = /^---[\s\S]*?docmeta:[\s\S]*?---/.test(content);
|
|
1174
|
+
const status: WorkerDocEntry["status"] = statusMatch
|
|
1175
|
+
? (statusMatch[1] as WorkerDocEntry["status"])
|
|
1176
|
+
: hasDocmeta
|
|
1177
|
+
? "COMPLETE"
|
|
1178
|
+
: "unknown";
|
|
1165
1179
|
const name = normalizeWorkerName(f);
|
|
1166
1180
|
const hired = hiredForMission(hiredWorkers, name, owner, rel);
|
|
1167
1181
|
let updatedAt: string | null = null;
|
|
@@ -1289,32 +1303,134 @@ function readOwnerHistory(rootDir: string, limit = 30): OwnerPromptEntry[] {
|
|
|
1289
1303
|
return entries.reverse().slice(0, limit);
|
|
1290
1304
|
}
|
|
1291
1305
|
|
|
1292
|
-
function
|
|
1293
|
-
|
|
1294
|
-
|
|
1306
|
+
function readKnowledgeDir<T extends GotchaEntry | ConventionEntry>(
|
|
1307
|
+
rootDir: string,
|
|
1308
|
+
relDir: string
|
|
1309
|
+
): T[] {
|
|
1310
|
+
const dir = path.join(rootDir, ".harness", relDir);
|
|
1311
|
+
if (!existsSync(dir)) return [];
|
|
1295
1312
|
let entries: Dirent[];
|
|
1296
|
-
try { entries = readdirSync(
|
|
1297
|
-
const results:
|
|
1313
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
1314
|
+
const results: T[] = [];
|
|
1298
1315
|
for (const entry of entries) {
|
|
1299
1316
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1300
1317
|
if (entry.name === "README.md") continue;
|
|
1301
1318
|
const id = entry.name.replace(/\.md$/, "");
|
|
1319
|
+
const abs = path.join(dir, entry.name);
|
|
1302
1320
|
let content: string;
|
|
1303
|
-
try { content = readFileSync(
|
|
1304
|
-
// Extract title from first H1 after optional frontmatter
|
|
1321
|
+
try { content = readFileSync(abs, "utf8"); } catch { continue; }
|
|
1305
1322
|
const body = content.replace(/^---[\s\S]*?---\n+/, "");
|
|
1306
1323
|
const h1 = body.match(/^#\s+(.+)/m);
|
|
1307
1324
|
const title = h1 ? h1[1].trim() : id;
|
|
1308
|
-
// Extract tags from docmeta frontmatter
|
|
1309
1325
|
const tagsMatch = content.match(/^\s*tags:\s*\[([^\]]*)\]/m);
|
|
1310
1326
|
const tags = tagsMatch
|
|
1311
1327
|
? tagsMatch[1].split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean)
|
|
1312
1328
|
: [];
|
|
1313
|
-
|
|
1329
|
+
let updatedAt: string | null = null;
|
|
1330
|
+
try { updatedAt = statSync(abs).mtime.toISOString(); } catch { /* ignore */ }
|
|
1331
|
+
results.push({
|
|
1332
|
+
id,
|
|
1333
|
+
title,
|
|
1334
|
+
content,
|
|
1335
|
+
tags,
|
|
1336
|
+
sourcePath: path.relative(rootDir, abs),
|
|
1337
|
+
updatedAt,
|
|
1338
|
+
} as T);
|
|
1314
1339
|
}
|
|
1315
1340
|
return results.sort((a, b) => a.id.localeCompare(b.id));
|
|
1316
1341
|
}
|
|
1317
1342
|
|
|
1343
|
+
function readGotchas(rootDir: string): GotchaEntry[] {
|
|
1344
|
+
return readKnowledgeDir<GotchaEntry>(rootDir, "gotchas");
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function readConventions(rootDir: string): ConventionEntry[] {
|
|
1348
|
+
return readKnowledgeDir<ConventionEntry>(rootDir, "conventions");
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function normalizeTodoStatus(raw: unknown): CxxTodo["status"] {
|
|
1352
|
+
const value = String(raw ?? "pending").toLowerCase();
|
|
1353
|
+
if (["pending", "active", "paused", "blocked", "done"].includes(value)) return value;
|
|
1354
|
+
return value;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function readTodos(rootDir: string): CxxTodo[] {
|
|
1358
|
+
const todoPath = path.join(rootDir, ".harness", "todos", "state.json");
|
|
1359
|
+
const result = readJsonSafe<{
|
|
1360
|
+
owners?: Record<string, Array<{
|
|
1361
|
+
id?: string;
|
|
1362
|
+
owner?: string;
|
|
1363
|
+
title?: string;
|
|
1364
|
+
status?: string;
|
|
1365
|
+
priority?: number;
|
|
1366
|
+
kind?: string;
|
|
1367
|
+
mission_path?: string | null;
|
|
1368
|
+
missionPath?: string | null;
|
|
1369
|
+
required_artifacts?: string[];
|
|
1370
|
+
requiredArtifacts?: string[];
|
|
1371
|
+
created_at?: string | null;
|
|
1372
|
+
createdAt?: string | null;
|
|
1373
|
+
updated_at?: string | null;
|
|
1374
|
+
updatedAt?: string | null;
|
|
1375
|
+
last_heartbeat_at?: string | null;
|
|
1376
|
+
lastHeartbeatAt?: string | null;
|
|
1377
|
+
blocked_reason?: string | null;
|
|
1378
|
+
blockedReason?: string | null;
|
|
1379
|
+
}>>;
|
|
1380
|
+
}>(todoPath);
|
|
1381
|
+
if (!result.ok) return [];
|
|
1382
|
+
const todos: CxxTodo[] = [];
|
|
1383
|
+
for (const [owner, list] of Object.entries(result.value.owners ?? {})) {
|
|
1384
|
+
for (const item of list ?? []) {
|
|
1385
|
+
const id = item.id ?? `${owner}-${todos.length + 1}`;
|
|
1386
|
+
todos.push({
|
|
1387
|
+
id,
|
|
1388
|
+
owner: item.owner ?? owner,
|
|
1389
|
+
title: item.title ?? id,
|
|
1390
|
+
status: normalizeTodoStatus(item.status),
|
|
1391
|
+
priority: item.priority ?? 0,
|
|
1392
|
+
kind: item.kind ?? "task",
|
|
1393
|
+
missionPath: item.mission_path ?? item.missionPath ?? null,
|
|
1394
|
+
requiredArtifacts: item.required_artifacts ?? item.requiredArtifacts ?? [],
|
|
1395
|
+
createdAt: item.created_at ?? item.createdAt ?? null,
|
|
1396
|
+
updatedAt: item.updated_at ?? item.updatedAt ?? null,
|
|
1397
|
+
lastHeartbeatAt: item.last_heartbeat_at ?? item.lastHeartbeatAt ?? null,
|
|
1398
|
+
blockedReason: item.blocked_reason ?? item.blockedReason ?? null,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return todos.sort((a, b) => (b.priority - a.priority) || ((b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")));
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function readJsonlTail<T extends Record<string, unknown>>(filePath: string, limit: number): T[] {
|
|
1406
|
+
if (!existsSync(filePath)) return [];
|
|
1407
|
+
let raw = "";
|
|
1408
|
+
try { raw = readFileSync(filePath, "utf8"); } catch { return []; }
|
|
1409
|
+
const rows: T[] = [];
|
|
1410
|
+
for (const line of raw.split("\n")) {
|
|
1411
|
+
const trimmed = line.trim();
|
|
1412
|
+
if (!trimmed) continue;
|
|
1413
|
+
try {
|
|
1414
|
+
rows.push(JSON.parse(trimmed) as T);
|
|
1415
|
+
} catch {
|
|
1416
|
+
// Ignore malformed legacy/manual lines in append-only logs.
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return rows.slice(-limit).reverse();
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function readEvents(rootDir: string, limit = 40): HarnessEvent[] {
|
|
1423
|
+
return readJsonlTail<Record<string, unknown>>(path.join(rootDir, ".harness", "events.jsonl"), limit).map((e) => ({
|
|
1424
|
+
ts: typeof e.ts === "string" ? e.ts : null,
|
|
1425
|
+
type: typeof e.type === "string" ? e.type : "event",
|
|
1426
|
+
owner: typeof e.owner === "string" ? e.owner : null,
|
|
1427
|
+
command: typeof e.command === "string" ? e.command : null,
|
|
1428
|
+
summary: typeof e.summary === "string" ? e.summary : null,
|
|
1429
|
+
title: typeof e.title === "string" ? e.title : null,
|
|
1430
|
+
status: typeof e.status === "string" ? e.status : null,
|
|
1431
|
+
}));
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1318
1434
|
export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
1319
1435
|
const harnessDir = path.join(rootDir, ".harness");
|
|
1320
1436
|
if (!existsSync(harnessDir)) {
|
|
@@ -1344,6 +1460,9 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
|
1344
1460
|
snapshot.missions = readMissions(rootDir, null);
|
|
1345
1461
|
snapshot.ownerHistory = readOwnerHistory(rootDir);
|
|
1346
1462
|
snapshot.gotchas = readGotchas(rootDir);
|
|
1463
|
+
snapshot.conventions = readConventions(rootDir);
|
|
1464
|
+
snapshot.todos = readTodos(rootDir);
|
|
1465
|
+
snapshot.events = readEvents(rootDir);
|
|
1347
1466
|
return snapshot;
|
|
1348
1467
|
}
|
|
1349
1468
|
|
|
@@ -1396,6 +1515,9 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
|
1396
1515
|
missions: readMissions(rootDir, progress),
|
|
1397
1516
|
ownerHistory: readOwnerHistory(rootDir),
|
|
1398
1517
|
gotchas: readGotchas(rootDir),
|
|
1518
|
+
conventions: readConventions(rootDir),
|
|
1519
|
+
todos: readTodos(rootDir),
|
|
1520
|
+
events: readEvents(rootDir),
|
|
1399
1521
|
};
|
|
1400
1522
|
}
|
|
1401
1523
|
|
|
@@ -292,6 +292,34 @@ export interface HarnessSnapshot {
|
|
|
292
292
|
missions: MissionDoc[];
|
|
293
293
|
ownerHistory: OwnerPromptEntry[];
|
|
294
294
|
gotchas: GotchaEntry[];
|
|
295
|
+
conventions: ConventionEntry[];
|
|
296
|
+
todos: CxxTodo[];
|
|
297
|
+
events: HarnessEvent[];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface CxxTodo {
|
|
301
|
+
id: string;
|
|
302
|
+
owner: "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops" | string;
|
|
303
|
+
title: string;
|
|
304
|
+
status: "pending" | "active" | "paused" | "blocked" | "done" | string;
|
|
305
|
+
priority: number;
|
|
306
|
+
kind: string;
|
|
307
|
+
missionPath?: string | null;
|
|
308
|
+
requiredArtifacts: string[];
|
|
309
|
+
createdAt: string | null;
|
|
310
|
+
updatedAt: string | null;
|
|
311
|
+
lastHeartbeatAt?: string | null;
|
|
312
|
+
blockedReason?: string | null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface HarnessEvent {
|
|
316
|
+
ts: string | null;
|
|
317
|
+
type: string;
|
|
318
|
+
owner?: string | null;
|
|
319
|
+
command?: string | null;
|
|
320
|
+
summary?: string | null;
|
|
321
|
+
title?: string | null;
|
|
322
|
+
status?: string | null;
|
|
295
323
|
}
|
|
296
324
|
|
|
297
325
|
// Real harness mission documents
|
|
@@ -335,6 +363,17 @@ export interface GotchaEntry {
|
|
|
335
363
|
title: string;
|
|
336
364
|
content: string;
|
|
337
365
|
tags: string[];
|
|
366
|
+
sourcePath?: string | null;
|
|
367
|
+
updatedAt?: string | null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export interface ConventionEntry {
|
|
371
|
+
id: string;
|
|
372
|
+
title: string;
|
|
373
|
+
content: string;
|
|
374
|
+
tags: string[];
|
|
375
|
+
sourcePath?: string | null;
|
|
376
|
+
updatedAt?: string | null;
|
|
338
377
|
}
|
|
339
378
|
|
|
340
379
|
export type OrgRole = "owner" | "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops";
|