@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.
@@ -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
- const status = (statusMatch?.[1] as WorkerDocEntry["status"]) ?? "unknown";
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 readGotchas(rootDir: string): GotchaEntry[] {
1293
- const gotchasDir = path.join(rootDir, ".harness", "gotchas");
1294
- if (!existsSync(gotchasDir)) return [];
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(gotchasDir, { withFileTypes: true }); } catch { return []; }
1297
- const results: GotchaEntry[] = [];
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(path.join(gotchasDir, entry.name), "utf8"); } catch { continue; }
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
- results.push({ id, title, content, tags });
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";