@walwal-harness/cli 7.1.27 → 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 +26 -1
- package/apps/harness-dashboard/app/api/stream/route.ts +4 -0
- package/apps/harness-dashboard/app/globals.css +138 -8
- package/apps/harness-dashboard/app/layout.tsx +1 -5
- package/apps/harness-dashboard/app/page.tsx +3 -7
- package/apps/harness-dashboard/components/Scene.tsx +1788 -599
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +84 -0
- package/apps/harness-dashboard/lib/harness-state.ts +132 -16
- package/apps/harness-dashboard/lib/types.ts +15 -0
- package/apps/harness-dashboard/package-lock.json +1519 -53
- package/apps/harness-dashboard/package.json +2 -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
|
@@ -248,6 +248,45 @@ describe("readHarnessState", () => {
|
|
|
248
248
|
expect(missions.find((mission) => mission.missionId === "goal-1-auth-system")?.type).toBe("goal");
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
it("reads explicit mission lifecycle state and marks the active child mission", () => {
|
|
252
|
+
const harnessDir = path.join(dir, ".harness");
|
|
253
|
+
const goalDir = path.join(harnessDir, "documents", "goal-1-auth-system");
|
|
254
|
+
const submission1Dir = path.join(goalDir, "submission-1-add-sso");
|
|
255
|
+
const submission2Dir = path.join(goalDir, "submission-2-visualization");
|
|
256
|
+
mkdirSync(submission1Dir, { recursive: true });
|
|
257
|
+
mkdirSync(submission2Dir, { recursive: true });
|
|
258
|
+
writeFileSync(path.join(goalDir, "ceo.md"), "# Auth system\n");
|
|
259
|
+
writeFileSync(path.join(submission1Dir, "ceo.md"), "# Add SSO\n");
|
|
260
|
+
writeFileSync(path.join(submission2Dir, "ceo.md"), "# Visualization\n");
|
|
261
|
+
writeFileSync(
|
|
262
|
+
path.join(submission1Dir, "mission-state.json"),
|
|
263
|
+
JSON.stringify({ lifecycle: "closed", active: false })
|
|
264
|
+
);
|
|
265
|
+
writeFileSync(
|
|
266
|
+
path.join(submission2Dir, "mission-state.json"),
|
|
267
|
+
JSON.stringify({ lifecycle: "active", active: true })
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const missions = readHarnessState(dir).missions;
|
|
271
|
+
const s1 = missions.find((mission) => mission.missionId.endsWith("submission-1-add-sso"))!;
|
|
272
|
+
const s2 = missions.find((mission) => mission.missionId.endsWith("submission-2-visualization"))!;
|
|
273
|
+
expect(s1.lifecycle).toBe("closed");
|
|
274
|
+
expect(s1.active).toBe(false);
|
|
275
|
+
expect(s2.lifecycle).toBe("active");
|
|
276
|
+
expect(s2.active).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("flags CXX reports that lack hired worker evidence", () => {
|
|
280
|
+
const harnessDir = path.join(dir, ".harness");
|
|
281
|
+
const missionDir = path.join(harnessDir, "documents", "goal-1-dashboard");
|
|
282
|
+
mkdirSync(missionDir, { recursive: true });
|
|
283
|
+
writeFileSync(path.join(missionDir, "ceo.md"), "# Dashboard\n");
|
|
284
|
+
writeFileSync(path.join(missionDir, "cto.md"), "# CTO\n\nImplemented directly.\n");
|
|
285
|
+
|
|
286
|
+
const mission = readHarnessState(dir).missions[0];
|
|
287
|
+
expect(mission.protocolViolations).toContain("cto:missing-worker-evidence");
|
|
288
|
+
});
|
|
289
|
+
|
|
251
290
|
it("positions only hired HR-Resource workers and marks active workers", () => {
|
|
252
291
|
const harnessDir = path.join(dir, ".harness");
|
|
253
292
|
const missionDir = path.join(harnessDir, "documents", "goal-1-dashboard");
|
|
@@ -297,4 +336,49 @@ describe("readHarnessState", () => {
|
|
|
297
336
|
sourcePath: ".harness/shared/HR-Resource/react-ui-worker/SKILL.md",
|
|
298
337
|
});
|
|
299
338
|
});
|
|
339
|
+
|
|
340
|
+
it("does not revive recently touched workers after runtime completion", () => {
|
|
341
|
+
const harnessDir = path.join(dir, ".harness");
|
|
342
|
+
const missionDir = path.join(harnessDir, "documents", "goal-1-dashboard");
|
|
343
|
+
mkdirSync(path.join(missionDir, "cto", "workers"), { recursive: true });
|
|
344
|
+
mkdirSync(path.join(harnessDir, "shared", "HR-Resource", "react-ui-worker"), { recursive: true });
|
|
345
|
+
writeFileSync(path.join(missionDir, "ceo.md"), "# Dashboard\n");
|
|
346
|
+
writeFileSync(
|
|
347
|
+
path.join(missionDir, "cto", "workers", "react-ui-worker.md"),
|
|
348
|
+
"## Status\nIN_PROGRESS\n"
|
|
349
|
+
);
|
|
350
|
+
writeFileSync(
|
|
351
|
+
path.join(harnessDir, "shared", "HR-Resource", "react-ui-worker", "SKILL.md"),
|
|
352
|
+
"# React UI Worker\n"
|
|
353
|
+
);
|
|
354
|
+
writeFileSync(
|
|
355
|
+
path.join(harnessDir, "shared", "hr-roster.json"),
|
|
356
|
+
JSON.stringify({
|
|
357
|
+
hired: [
|
|
358
|
+
{
|
|
359
|
+
worker: "react-ui-worker",
|
|
360
|
+
owner: "cto",
|
|
361
|
+
skillPath: ".harness/shared/HR-Resource/react-ui-worker/SKILL.md",
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
})
|
|
365
|
+
);
|
|
366
|
+
writeFileSync(
|
|
367
|
+
path.join(harnessDir, "progress.json"),
|
|
368
|
+
JSON.stringify({
|
|
369
|
+
current_agent: null,
|
|
370
|
+
next_agent: "none",
|
|
371
|
+
agent_status: "completed",
|
|
372
|
+
company_state: {
|
|
373
|
+
workers: [{ agent: "react-ui-worker", status: "running", feature: "F1" }],
|
|
374
|
+
},
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const workers = readHarnessState(dir).missions[0].workers;
|
|
379
|
+
expect(workers[0]).toMatchObject({
|
|
380
|
+
name: "react-ui-worker",
|
|
381
|
+
active: false,
|
|
382
|
+
});
|
|
383
|
+
});
|
|
300
384
|
});
|
|
@@ -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
|
};
|
|
@@ -791,6 +793,44 @@ interface HiredWorkerEntry {
|
|
|
791
793
|
capability: string | null;
|
|
792
794
|
}
|
|
793
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
|
+
|
|
794
834
|
function deriveWorkerProgress(worker: RawWorker): number | null {
|
|
795
835
|
const status = (worker?.status ?? worker?.spawn_status ?? "").toLowerCase();
|
|
796
836
|
if (status === "blocked" || status === "failed") return 0.35;
|
|
@@ -938,8 +978,20 @@ function workerDisplayName(content: string, hired: HiredWorkerEntry | undefined,
|
|
|
938
978
|
return fallback;
|
|
939
979
|
}
|
|
940
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
|
+
|
|
941
992
|
function activeWorkerNames(progress: RawProgress | null): Set<string> {
|
|
942
993
|
const active = new Set<string>();
|
|
994
|
+
if (isHarnessRuntimeIdle(progress)) return active;
|
|
943
995
|
const workers = [
|
|
944
996
|
...(progress?.company_state?.workers ?? []),
|
|
945
997
|
...(progress?.company_state?.last_dispatch ?? []),
|
|
@@ -1109,6 +1161,7 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1109
1161
|
if (!existsSync(docsDir)) return [];
|
|
1110
1162
|
const hiredWorkers = readHiredWorkers(rootDir);
|
|
1111
1163
|
const activeWorkers = activeWorkerNames(progress);
|
|
1164
|
+
const runtimeIdle = isHarnessRuntimeIdle(progress);
|
|
1112
1165
|
|
|
1113
1166
|
const missionDirs: Array<{ rel: string; abs: string }> = [];
|
|
1114
1167
|
const cxxDocNames = new Set(["ceo.md", "cto.md", "cqo.md", "coo.md", "cdo.md", "ops.md"]);
|
|
@@ -1143,7 +1196,7 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1143
1196
|
return "unknown";
|
|
1144
1197
|
};
|
|
1145
1198
|
|
|
1146
|
-
|
|
1199
|
+
let missions = missionDirs
|
|
1147
1200
|
.map(({ rel, abs: missionPath }) => {
|
|
1148
1201
|
let mtime: Date;
|
|
1149
1202
|
try { mtime = statSync(missionPath).mtime; } catch { mtime = new Date(0); }
|
|
@@ -1165,7 +1218,15 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1165
1218
|
const reportAbs = path.join(missionPath, relDir, f);
|
|
1166
1219
|
const content = readMd(`${relDir}/${f}`) ?? "";
|
|
1167
1220
|
const statusMatch = content.match(/##\s*Status\s*\n+([A-Z_]+)/);
|
|
1168
|
-
|
|
1221
|
+
// Worker docs sometimes omit the "## Status" block but always carry a
|
|
1222
|
+
// docmeta frontmatter when the worker submitted its output. Treat the
|
|
1223
|
+
// presence of a docmeta block (or a non-empty body) as COMPLETE.
|
|
1224
|
+
const hasDocmeta = /^---[\s\S]*?docmeta:[\s\S]*?---/.test(content);
|
|
1225
|
+
const status: WorkerDocEntry["status"] = statusMatch
|
|
1226
|
+
? (statusMatch[1] as WorkerDocEntry["status"])
|
|
1227
|
+
: hasDocmeta
|
|
1228
|
+
? "COMPLETE"
|
|
1229
|
+
: "unknown";
|
|
1169
1230
|
const name = normalizeWorkerName(f);
|
|
1170
1231
|
const hired = hiredForMission(hiredWorkers, name, owner, rel);
|
|
1171
1232
|
let updatedAt: string | null = null;
|
|
@@ -1182,7 +1243,7 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1182
1243
|
status,
|
|
1183
1244
|
owner: hired?.owner ?? owner,
|
|
1184
1245
|
hired: !!hired,
|
|
1185
|
-
active: activeWorkers.has(name) || (recentlyTouched && status !== "COMPLETE"),
|
|
1246
|
+
active: !runtimeIdle && (activeWorkers.has(name) || (recentlyTouched && status !== "COMPLETE")),
|
|
1186
1247
|
sourcePath: hired?.sourcePath ?? null,
|
|
1187
1248
|
reportPath: path.relative(rootDir, reportAbs),
|
|
1188
1249
|
updatedAt,
|
|
@@ -1206,10 +1267,10 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1206
1267
|
name: hired.name,
|
|
1207
1268
|
displayName: workerDisplayName("", hired, hired.name),
|
|
1208
1269
|
content: "",
|
|
1209
|
-
status: activeWorkers.has(hired.name) ? "IN_PROGRESS" : "unknown",
|
|
1270
|
+
status: !runtimeIdle && activeWorkers.has(hired.name) ? "IN_PROGRESS" : "unknown",
|
|
1210
1271
|
owner: hired.owner,
|
|
1211
1272
|
hired: true,
|
|
1212
|
-
active: activeWorkers.has(hired.name),
|
|
1273
|
+
active: !runtimeIdle && activeWorkers.has(hired.name),
|
|
1213
1274
|
sourcePath: hired.sourcePath,
|
|
1214
1275
|
reportPath: null,
|
|
1215
1276
|
updatedAt: null,
|
|
@@ -1220,6 +1281,23 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1220
1281
|
|
|
1221
1282
|
const cxxRoles = ["ceo", "cto", "cqo", "coo", "cdo", "ops"] as const;
|
|
1222
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
|
+
}
|
|
1223
1301
|
|
|
1224
1302
|
const id = rel;
|
|
1225
1303
|
const label = rel.split("/").pop() ?? rel;
|
|
@@ -1227,6 +1305,9 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1227
1305
|
missionId: id,
|
|
1228
1306
|
ts: mtime.toISOString(),
|
|
1229
1307
|
type: typeFromId(id),
|
|
1308
|
+
lifecycle: state.lifecycle,
|
|
1309
|
+
active: state.active ?? false,
|
|
1310
|
+
protocolViolations,
|
|
1230
1311
|
label,
|
|
1231
1312
|
ceo: readMd("ceo.md"),
|
|
1232
1313
|
cto: readMd("cto.md"),
|
|
@@ -1239,8 +1320,22 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1239
1320
|
cxxPresent: [...cxxPresent],
|
|
1240
1321
|
} satisfies MissionDoc;
|
|
1241
1322
|
})
|
|
1242
|
-
.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())
|
|
1243
|
-
|
|
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);
|
|
1244
1339
|
|
|
1245
1340
|
// When the CEO updates a goal directory in-place for a submission or hot-fix
|
|
1246
1341
|
// (instead of creating a submission-* or hotfix-* subdirectory), the most
|
|
@@ -1293,32 +1388,51 @@ function readOwnerHistory(rootDir: string, limit = 30): OwnerPromptEntry[] {
|
|
|
1293
1388
|
return entries.reverse().slice(0, limit);
|
|
1294
1389
|
}
|
|
1295
1390
|
|
|
1296
|
-
function
|
|
1297
|
-
|
|
1298
|
-
|
|
1391
|
+
function readKnowledgeDir<T extends GotchaEntry | ConventionEntry>(
|
|
1392
|
+
rootDir: string,
|
|
1393
|
+
relDir: string
|
|
1394
|
+
): T[] {
|
|
1395
|
+
const dir = path.join(rootDir, ".harness", relDir);
|
|
1396
|
+
if (!existsSync(dir)) return [];
|
|
1299
1397
|
let entries: Dirent[];
|
|
1300
|
-
try { entries = readdirSync(
|
|
1301
|
-
const results:
|
|
1398
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
1399
|
+
const results: T[] = [];
|
|
1302
1400
|
for (const entry of entries) {
|
|
1303
1401
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1304
1402
|
if (entry.name === "README.md") continue;
|
|
1305
1403
|
const id = entry.name.replace(/\.md$/, "");
|
|
1404
|
+
const abs = path.join(dir, entry.name);
|
|
1306
1405
|
let content: string;
|
|
1307
|
-
try { content = readFileSync(
|
|
1308
|
-
// Extract title from first H1 after optional frontmatter
|
|
1406
|
+
try { content = readFileSync(abs, "utf8"); } catch { continue; }
|
|
1309
1407
|
const body = content.replace(/^---[\s\S]*?---\n+/, "");
|
|
1310
1408
|
const h1 = body.match(/^#\s+(.+)/m);
|
|
1311
1409
|
const title = h1 ? h1[1].trim() : id;
|
|
1312
|
-
// Extract tags from docmeta frontmatter
|
|
1313
1410
|
const tagsMatch = content.match(/^\s*tags:\s*\[([^\]]*)\]/m);
|
|
1314
1411
|
const tags = tagsMatch
|
|
1315
1412
|
? tagsMatch[1].split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean)
|
|
1316
1413
|
: [];
|
|
1317
|
-
|
|
1414
|
+
let updatedAt: string | null = null;
|
|
1415
|
+
try { updatedAt = statSync(abs).mtime.toISOString(); } catch { /* ignore */ }
|
|
1416
|
+
results.push({
|
|
1417
|
+
id,
|
|
1418
|
+
title,
|
|
1419
|
+
content,
|
|
1420
|
+
tags,
|
|
1421
|
+
sourcePath: path.relative(rootDir, abs),
|
|
1422
|
+
updatedAt,
|
|
1423
|
+
} as T);
|
|
1318
1424
|
}
|
|
1319
1425
|
return results.sort((a, b) => a.id.localeCompare(b.id));
|
|
1320
1426
|
}
|
|
1321
1427
|
|
|
1428
|
+
function readGotchas(rootDir: string): GotchaEntry[] {
|
|
1429
|
+
return readKnowledgeDir<GotchaEntry>(rootDir, "gotchas");
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function readConventions(rootDir: string): ConventionEntry[] {
|
|
1433
|
+
return readKnowledgeDir<ConventionEntry>(rootDir, "conventions");
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1322
1436
|
function normalizeTodoStatus(raw: unknown): CxxTodo["status"] {
|
|
1323
1437
|
const value = String(raw ?? "pending").toLowerCase();
|
|
1324
1438
|
if (["pending", "active", "paused", "blocked", "done"].includes(value)) return value;
|
|
@@ -1431,6 +1545,7 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
|
1431
1545
|
snapshot.missions = readMissions(rootDir, null);
|
|
1432
1546
|
snapshot.ownerHistory = readOwnerHistory(rootDir);
|
|
1433
1547
|
snapshot.gotchas = readGotchas(rootDir);
|
|
1548
|
+
snapshot.conventions = readConventions(rootDir);
|
|
1434
1549
|
snapshot.todos = readTodos(rootDir);
|
|
1435
1550
|
snapshot.events = readEvents(rootDir);
|
|
1436
1551
|
return snapshot;
|
|
@@ -1485,6 +1600,7 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
|
1485
1600
|
missions: readMissions(rootDir, progress),
|
|
1486
1601
|
ownerHistory: readOwnerHistory(rootDir),
|
|
1487
1602
|
gotchas: readGotchas(rootDir),
|
|
1603
|
+
conventions: readConventions(rootDir),
|
|
1488
1604
|
todos: readTodos(rootDir),
|
|
1489
1605
|
events: readEvents(rootDir),
|
|
1490
1606
|
};
|
|
@@ -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
|
}
|
|
@@ -339,6 +340,9 @@ export interface MissionDoc {
|
|
|
339
340
|
missionId: string;
|
|
340
341
|
ts: string;
|
|
341
342
|
type: "goal" | "submission" | "hotfix" | "feature" | "unknown";
|
|
343
|
+
lifecycle: "active" | "closed" | "cancelled" | "superseded" | "complete" | "blocked" | "unknown";
|
|
344
|
+
active: boolean;
|
|
345
|
+
protocolViolations: string[];
|
|
342
346
|
label: string;
|
|
343
347
|
ceo: string | null;
|
|
344
348
|
cto: string | null;
|
|
@@ -362,6 +366,17 @@ export interface GotchaEntry {
|
|
|
362
366
|
title: string;
|
|
363
367
|
content: string;
|
|
364
368
|
tags: string[];
|
|
369
|
+
sourcePath?: string | null;
|
|
370
|
+
updatedAt?: string | null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface ConventionEntry {
|
|
374
|
+
id: string;
|
|
375
|
+
title: string;
|
|
376
|
+
content: string;
|
|
377
|
+
tags: string[];
|
|
378
|
+
sourcePath?: string | null;
|
|
379
|
+
updatedAt?: string | null;
|
|
365
380
|
}
|
|
366
381
|
|
|
367
382
|
export type OrgRole = "owner" | "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops";
|