@walwal-harness/cli 7.1.27 → 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.
@@ -25,6 +25,7 @@ import type {
25
25
  MeetingRecord,
26
26
  MeetingsState,
27
27
  GotchaEntry,
28
+ ConventionEntry,
28
29
  MissionDoc,
29
30
  OperationsDashboard,
30
31
  OpsServiceHealth,
@@ -226,6 +227,7 @@ function emptySnapshot(banner: ErrorBanner | null = null, rootDir?: string): Har
226
227
  missions: [],
227
228
  ownerHistory: [],
228
229
  gotchas: [],
230
+ conventions: [],
229
231
  todos: [],
230
232
  events: [],
231
233
  };
@@ -1165,7 +1167,15 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
1165
1167
  const reportAbs = path.join(missionPath, relDir, f);
1166
1168
  const content = readMd(`${relDir}/${f}`) ?? "";
1167
1169
  const statusMatch = content.match(/##\s*Status\s*\n+([A-Z_]+)/);
1168
- 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";
1169
1179
  const name = normalizeWorkerName(f);
1170
1180
  const hired = hiredForMission(hiredWorkers, name, owner, rel);
1171
1181
  let updatedAt: string | null = null;
@@ -1293,32 +1303,51 @@ function readOwnerHistory(rootDir: string, limit = 30): OwnerPromptEntry[] {
1293
1303
  return entries.reverse().slice(0, limit);
1294
1304
  }
1295
1305
 
1296
- function readGotchas(rootDir: string): GotchaEntry[] {
1297
- const gotchasDir = path.join(rootDir, ".harness", "gotchas");
1298
- 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 [];
1299
1312
  let entries: Dirent[];
1300
- try { entries = readdirSync(gotchasDir, { withFileTypes: true }); } catch { return []; }
1301
- const results: GotchaEntry[] = [];
1313
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return []; }
1314
+ const results: T[] = [];
1302
1315
  for (const entry of entries) {
1303
1316
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
1304
1317
  if (entry.name === "README.md") continue;
1305
1318
  const id = entry.name.replace(/\.md$/, "");
1319
+ const abs = path.join(dir, entry.name);
1306
1320
  let content: string;
1307
- try { content = readFileSync(path.join(gotchasDir, entry.name), "utf8"); } catch { continue; }
1308
- // Extract title from first H1 after optional frontmatter
1321
+ try { content = readFileSync(abs, "utf8"); } catch { continue; }
1309
1322
  const body = content.replace(/^---[\s\S]*?---\n+/, "");
1310
1323
  const h1 = body.match(/^#\s+(.+)/m);
1311
1324
  const title = h1 ? h1[1].trim() : id;
1312
- // Extract tags from docmeta frontmatter
1313
1325
  const tagsMatch = content.match(/^\s*tags:\s*\[([^\]]*)\]/m);
1314
1326
  const tags = tagsMatch
1315
1327
  ? tagsMatch[1].split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean)
1316
1328
  : [];
1317
- 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);
1318
1339
  }
1319
1340
  return results.sort((a, b) => a.id.localeCompare(b.id));
1320
1341
  }
1321
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
+
1322
1351
  function normalizeTodoStatus(raw: unknown): CxxTodo["status"] {
1323
1352
  const value = String(raw ?? "pending").toLowerCase();
1324
1353
  if (["pending", "active", "paused", "blocked", "done"].includes(value)) return value;
@@ -1431,6 +1460,7 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
1431
1460
  snapshot.missions = readMissions(rootDir, null);
1432
1461
  snapshot.ownerHistory = readOwnerHistory(rootDir);
1433
1462
  snapshot.gotchas = readGotchas(rootDir);
1463
+ snapshot.conventions = readConventions(rootDir);
1434
1464
  snapshot.todos = readTodos(rootDir);
1435
1465
  snapshot.events = readEvents(rootDir);
1436
1466
  return snapshot;
@@ -1485,6 +1515,7 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
1485
1515
  missions: readMissions(rootDir, progress),
1486
1516
  ownerHistory: readOwnerHistory(rootDir),
1487
1517
  gotchas: readGotchas(rootDir),
1518
+ conventions: readConventions(rootDir),
1488
1519
  todos: readTodos(rootDir),
1489
1520
  events: readEvents(rootDir),
1490
1521
  };
@@ -292,6 +292,7 @@ export interface HarnessSnapshot {
292
292
  missions: MissionDoc[];
293
293
  ownerHistory: OwnerPromptEntry[];
294
294
  gotchas: GotchaEntry[];
295
+ conventions: ConventionEntry[];
295
296
  todos: CxxTodo[];
296
297
  events: HarnessEvent[];
297
298
  }
@@ -362,6 +363,17 @@ export interface GotchaEntry {
362
363
  title: string;
363
364
  content: string;
364
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;
365
377
  }
366
378
 
367
379
  export type OrgRole = "owner" | "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops";