@walwal-harness/cli 7.1.40 → 7.1.42
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/HR-Resource/cdo/SKILL.md +9 -1
- package/HR-Resource/ceo/SKILL.md +33 -2
- package/HR-Resource/coo/SKILL.md +9 -1
- package/HR-Resource/cqo/SKILL.md +9 -1
- package/HR-Resource/cto/SKILL.md +10 -2
- package/HR-Resource/ops/SKILL.md +12 -4
- package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
- package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
- package/apps/harness-dashboard/app/api/metrics/route.ts +17 -0
- package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
- package/apps/harness-dashboard/app/brick.css +482 -0
- package/apps/harness-dashboard/app/layout.tsx +13 -0
- package/apps/harness-dashboard/app/page.tsx +2 -2
- package/apps/harness-dashboard/components/Scene.tsx +690 -36
- package/apps/harness-dashboard/components/brick/BrickDashboard.tsx +149 -0
- package/apps/harness-dashboard/components/brick/reports.tsx +291 -0
- package/apps/harness-dashboard/components/brick/tweaks.tsx +738 -0
- package/apps/harness-dashboard/components/brick/ui.tsx +463 -0
- package/apps/harness-dashboard/components/brick/views-graph.tsx +305 -0
- package/apps/harness-dashboard/components/brick/views-grid.tsx +543 -0
- package/apps/harness-dashboard/components/brick/views-timeline.tsx +238 -0
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
- package/apps/harness-dashboard/lib/brick/adapter.ts +423 -0
- package/apps/harness-dashboard/lib/brick/contract.ts +247 -0
- package/apps/harness-dashboard/lib/harness-state.ts +107 -796
- package/apps/harness-dashboard/lib/metrics/sampler.ts +545 -0
- package/apps/harness-dashboard/lib/types.ts +22 -222
- package/apps/harness-dashboard/playwright.config.ts +26 -7
- package/assets/templates/AGENTS-ko.md.template +1 -1
- package/assets/templates/AGENTS.md.template +12 -2
- package/assets/templates/HARNESS.md +2 -0
- package/assets/templates/company-pipeline-manifest.json +5 -14
- package/assets/templates/worker-report.md.template +38 -0
- package/bin/init.js +2 -1
- package/commands/goal.md +4 -2
- package/commands/hot-fix.md +1 -1
- package/commands/submission.md +1 -1
- package/package.json +1 -1
- package/scripts/conductor-tick.sh +12 -0
- package/scripts/harness-agenda.sh +105 -0
- package/scripts/harness-company-block.sh +89 -0
- package/scripts/harness-company-complete.sh +17 -8
- package/scripts/harness-company-cycle.sh +54 -0
- package/scripts/harness-progress-set.sh +8 -2
- package/scripts/harness-stop.sh +76 -0
- package/scripts/harness-wake-control.sh +151 -0
- package/scripts/harness-wake.sh +5 -0
- package/scripts/harness-worker-dispatch.sh +31 -7
- package/apps/harness-dashboard/components/Drawer.tsx +0 -146
- package/apps/harness-dashboard/components/Header.tsx +0 -40
- package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
- package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
- package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
- package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
- package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
- package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
- package/apps/harness-dashboard/lib/furniture.ts +0 -165
- package/apps/harness-dashboard/lib/i18n.ts +0 -26
- package/apps/harness-dashboard/lib/iso.ts +0 -150
- package/apps/harness-dashboard/lib/markdown.tsx +0 -306
- package/apps/harness-dashboard/lib/path-planning.ts +0 -72
- package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
|
@@ -1,46 +1,26 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync, readFileSync, type Dirent } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readJsonSafe } from "./safe-json";
|
|
4
|
-
import { AGENT_ROSTER, ROOM_LABELS } from "./agent-roster";
|
|
5
|
-
import { deriveMinifigState, pickTalkingPreview } from "./state-mapping";
|
|
6
4
|
import type {
|
|
7
5
|
AgentId,
|
|
8
|
-
AgentState,
|
|
9
|
-
ArchiveEntry,
|
|
10
|
-
ArchiveStat,
|
|
11
|
-
ContractSnapshot,
|
|
12
|
-
CurrentMeeting,
|
|
13
|
-
Dept,
|
|
14
6
|
ErrorBanner,
|
|
15
|
-
EnvFileSummary,
|
|
16
|
-
EscalationEntry,
|
|
17
7
|
EvalScores,
|
|
18
|
-
FeatureSummary,
|
|
19
|
-
GoalCard,
|
|
20
8
|
HarnessSnapshot,
|
|
21
9
|
HarnessEvent,
|
|
22
|
-
HypothesisEntry,
|
|
23
10
|
IncidentEntry,
|
|
24
11
|
MeetingCadence,
|
|
25
|
-
MeetingRecord,
|
|
26
|
-
MeetingsState,
|
|
27
12
|
GotchaEntry,
|
|
28
13
|
ConventionEntry,
|
|
29
14
|
MissionDoc,
|
|
30
|
-
OperationsDashboard,
|
|
31
|
-
OpsServiceHealth,
|
|
32
15
|
OwnerPromptEntry,
|
|
33
|
-
ParallelTrack,
|
|
34
16
|
Pipeline,
|
|
35
17
|
RoomId,
|
|
36
|
-
RoomMetrics,
|
|
37
|
-
RoomState,
|
|
38
18
|
RuntimeSnapshot,
|
|
39
19
|
TrackStatus,
|
|
40
20
|
WorkerDocEntry,
|
|
41
|
-
WorkerSnapshot,
|
|
42
21
|
CxxTodo,
|
|
43
22
|
ActivitySample,
|
|
23
|
+
HarnessFileEntry,
|
|
44
24
|
} from "./types";
|
|
45
25
|
|
|
46
26
|
const SNAPSHOT_VERSION = "1.2.0";
|
|
@@ -122,6 +102,10 @@ interface RawProgress {
|
|
|
122
102
|
// Real-harness conductor track shape (CLAUDE.md §parallel-tracks). Used when
|
|
123
103
|
// top-level `parallel_tracks` is absent.
|
|
124
104
|
conductor?: {
|
|
105
|
+
state?: string | null;
|
|
106
|
+
current_action?: string | null;
|
|
107
|
+
stop_chain_count?: number | null;
|
|
108
|
+
last_stop_chain_at?: string | null;
|
|
125
109
|
tracks?: Array<{
|
|
126
110
|
id: string;
|
|
127
111
|
owner?: string;
|
|
@@ -145,6 +129,7 @@ interface RawProgress {
|
|
|
145
129
|
feature_list?: { total?: number; passed?: number; failed?: number };
|
|
146
130
|
};
|
|
147
131
|
company_state?: {
|
|
132
|
+
state?: string | null;
|
|
148
133
|
active_workers?: number;
|
|
149
134
|
workers?: Array<{
|
|
150
135
|
name?: string;
|
|
@@ -193,52 +178,13 @@ interface RawProgress {
|
|
|
193
178
|
};
|
|
194
179
|
}
|
|
195
180
|
|
|
196
|
-
function emptyContract(): ContractSnapshot {
|
|
197
|
-
return {
|
|
198
|
-
sprint_number: null,
|
|
199
|
-
pipeline: null,
|
|
200
|
-
api_version: null,
|
|
201
|
-
feature_total: 0,
|
|
202
|
-
feature_passed: 0,
|
|
203
|
-
feature_failed: 0,
|
|
204
|
-
contract_signed: { be: false, fe: false },
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
181
|
function emptySnapshot(banner: ErrorBanner | null = null, rootDir?: string): HarnessSnapshot {
|
|
209
|
-
const agents: AgentState[] = AGENT_ROSTER.map((entry) => ({
|
|
210
|
-
id: entry.id,
|
|
211
|
-
name: entry.name,
|
|
212
|
-
dept: entry.dept,
|
|
213
|
-
room: entry.room,
|
|
214
|
-
homeRoom: entry.room,
|
|
215
|
-
minifigState: "idle",
|
|
216
|
-
lastActivity: null,
|
|
217
|
-
}));
|
|
218
|
-
const rooms: RoomState[] = (Object.keys(ROOM_LABELS) as RoomId[]).map((id) => ({
|
|
219
|
-
id,
|
|
220
|
-
label_ko: ROOM_LABELS[id].ko,
|
|
221
|
-
label_en: ROOM_LABELS[id].en,
|
|
222
|
-
dept: ROOM_LABELS[id].dept,
|
|
223
|
-
}));
|
|
224
182
|
return {
|
|
225
183
|
version: SNAPSHOT_VERSION,
|
|
226
184
|
ts: new Date().toISOString(),
|
|
227
185
|
projectName: rootDir ? path.basename(rootDir) : "(unknown)",
|
|
228
186
|
projectPath: rootDir ?? "",
|
|
229
|
-
agents,
|
|
230
|
-
rooms,
|
|
231
|
-
goal: null,
|
|
232
|
-
archive: { sprintCount: 0, recent: [], all: [] },
|
|
233
|
-
meetings: { active: [], cadence: "normal", next_scheduled: null, current: null },
|
|
234
|
-
tracks: [],
|
|
235
|
-
incidents: [],
|
|
236
|
-
hypothesis: [],
|
|
237
|
-
escalations: [],
|
|
238
|
-
contract: emptyContract(),
|
|
239
|
-
evalScores: null,
|
|
240
187
|
errorBanner: banner,
|
|
241
|
-
dashboard: { workers: [], features: [], recentMeetings: [], opsHealth: [], envFiles: [] },
|
|
242
188
|
runtime: {
|
|
243
189
|
currentAgent: null,
|
|
244
190
|
agentStatus: "unknown",
|
|
@@ -246,6 +192,7 @@ function emptySnapshot(banner: ErrorBanner | null = null, rootDir?: string): Har
|
|
|
246
192
|
updatedAt: null,
|
|
247
193
|
ownerPrompt: null,
|
|
248
194
|
},
|
|
195
|
+
incidents: [],
|
|
249
196
|
missions: [],
|
|
250
197
|
ownerHistory: [],
|
|
251
198
|
gotchas: [],
|
|
@@ -253,6 +200,7 @@ function emptySnapshot(banner: ErrorBanner | null = null, rootDir?: string): Har
|
|
|
253
200
|
todos: [],
|
|
254
201
|
events: [],
|
|
255
202
|
activitySamples: [],
|
|
203
|
+
files: [],
|
|
256
204
|
};
|
|
257
205
|
}
|
|
258
206
|
|
|
@@ -278,279 +226,6 @@ function truncateText(text: string, max = 150): string {
|
|
|
278
226
|
return `${cleaned.slice(0, max - 1)}…`;
|
|
279
227
|
}
|
|
280
228
|
|
|
281
|
-
function titleFromMarkdown(text: string | null): string | null {
|
|
282
|
-
if (!text) return null;
|
|
283
|
-
const h1 = text.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
284
|
-
if (h1) return h1;
|
|
285
|
-
const mission = text.match(/(?:mission|goal|title)\s*[:=]\s*(.+)$/im)?.[1]?.trim();
|
|
286
|
-
return mission || null;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function buildGoal(rootDir: string, progress: RawProgress | null): GoalCard | null {
|
|
290
|
-
const list = progress?.goals?.list ?? [];
|
|
291
|
-
const activeId = progress?.goals?.active_id;
|
|
292
|
-
const goalFile = readTextSafe(path.join(rootDir, ".harness", "actions", "goals.md"));
|
|
293
|
-
const mission = (progress?.goals as { mission?: string; title?: string; description?: string } | undefined)?.mission;
|
|
294
|
-
const topLevelTitle = (progress?.goals as { title?: string } | undefined)?.title;
|
|
295
|
-
if (!activeId && !mission && !topLevelTitle && !goalFile) return null;
|
|
296
|
-
if (!activeId) {
|
|
297
|
-
const title = mission ?? topLevelTitle ?? titleFromMarkdown(goalFile) ?? "Active autonomous company goal";
|
|
298
|
-
return {
|
|
299
|
-
id: "goal",
|
|
300
|
-
title,
|
|
301
|
-
description_truncated: truncateText(
|
|
302
|
-
(progress?.goals as { description?: string } | undefined)?.description ?? goalFile ?? "",
|
|
303
|
-
GOAL_DESC_TRUNCATE,
|
|
304
|
-
),
|
|
305
|
-
adherence: progress?.goals?.current_adherence ?? null,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
const found = list.find((g) => g.id === activeId);
|
|
309
|
-
if (!found) {
|
|
310
|
-
const title = mission ?? topLevelTitle ?? titleFromMarkdown(goalFile) ?? activeId;
|
|
311
|
-
return {
|
|
312
|
-
id: activeId,
|
|
313
|
-
title,
|
|
314
|
-
description_truncated: truncateText(
|
|
315
|
-
(progress?.goals as { description?: string } | undefined)?.description ?? goalFile ?? "",
|
|
316
|
-
GOAL_DESC_TRUNCATE,
|
|
317
|
-
),
|
|
318
|
-
adherence: progress?.goals?.current_adherence ?? null,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
const desc = found.description ?? "";
|
|
322
|
-
const truncated =
|
|
323
|
-
desc.length > GOAL_DESC_TRUNCATE
|
|
324
|
-
? desc.slice(0, GOAL_DESC_TRUNCATE) + "…"
|
|
325
|
-
: desc;
|
|
326
|
-
return {
|
|
327
|
-
id: found.id,
|
|
328
|
-
title: found.title ?? found.id,
|
|
329
|
-
description_truncated: truncated,
|
|
330
|
-
adherence: progress?.goals?.current_adherence ?? null,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function readArchiveVerdict(sprintDir: string): "PASS" | "FAIL" | "unknown" {
|
|
335
|
-
// Sprint verdict can live in a handful of well-known spots. Try them in
|
|
336
|
-
// order; first hit wins. Returns "unknown" if nothing definitive is found.
|
|
337
|
-
const verdictJson = path.join(sprintDir, "verdict.json");
|
|
338
|
-
if (existsSync(verdictJson)) {
|
|
339
|
-
const r = readJsonSafe<{ result?: string; verdict?: string }>(verdictJson);
|
|
340
|
-
if (r.ok) {
|
|
341
|
-
const v = (r.value.result ?? r.value.verdict ?? "").toUpperCase();
|
|
342
|
-
if (v === "PASS" || v === "FAIL") return v;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
// CQO sprint summary is the canonical source if verdict.json is absent.
|
|
346
|
-
try {
|
|
347
|
-
const candidates = readdirSync(sprintDir).filter(
|
|
348
|
-
(n) => n.startsWith("cqo-audit-") || n === "evaluation-summary.md"
|
|
349
|
-
);
|
|
350
|
-
for (const name of candidates) {
|
|
351
|
-
const text = readFileSync(path.join(sprintDir, name), "utf-8");
|
|
352
|
-
if (/sprint[_\s-]*verdict\s*[:=]\s*PASS/i.test(text)) return "PASS";
|
|
353
|
-
if (/sprint[_\s-]*verdict\s*[:=]\s*FAIL/i.test(text)) return "FAIL";
|
|
354
|
-
}
|
|
355
|
-
} catch {
|
|
356
|
-
// ignore
|
|
357
|
-
}
|
|
358
|
-
return "unknown";
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function buildArchive(harnessRoot: string): ArchiveStat {
|
|
362
|
-
const archiveDir = path.join(harnessRoot, ".harness", "archive");
|
|
363
|
-
if (!existsSync(archiveDir)) {
|
|
364
|
-
return { sprintCount: 0, recent: [], all: [] };
|
|
365
|
-
}
|
|
366
|
-
let entries: string[] = [];
|
|
367
|
-
try {
|
|
368
|
-
entries = readdirSync(archiveDir).filter((name) => name.startsWith("sprint-"));
|
|
369
|
-
} catch {
|
|
370
|
-
return { sprintCount: 0, recent: [], all: [] };
|
|
371
|
-
}
|
|
372
|
-
const all: ArchiveEntry[] = entries.map((name) => {
|
|
373
|
-
const sprintDir = path.join(archiveDir, name);
|
|
374
|
-
let ts: string | null = null;
|
|
375
|
-
try {
|
|
376
|
-
ts = statSync(sprintDir).mtime.toISOString();
|
|
377
|
-
} catch {
|
|
378
|
-
ts = null;
|
|
379
|
-
}
|
|
380
|
-
const result = readArchiveVerdict(sprintDir);
|
|
381
|
-
return { dir: name, label: name, result, ts };
|
|
382
|
-
});
|
|
383
|
-
all.sort((a, b) => (b.ts ?? "").localeCompare(a.ts ?? ""));
|
|
384
|
-
return { sprintCount: all.length, recent: all.slice(0, 3), all };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function passAxis(pass: unknown): string | null {
|
|
388
|
-
if (typeof pass === "string") return pass;
|
|
389
|
-
if (pass && typeof pass === "object") {
|
|
390
|
-
const p = pass as { axis?: string; by?: string; status?: string };
|
|
391
|
-
if (p.status && p.status.toUpperCase() !== "PASS") return null;
|
|
392
|
-
return p.axis ?? p.by ?? null;
|
|
393
|
-
}
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function buildFeatureSummaries(rootDir: string): FeatureSummary[] {
|
|
398
|
-
const featurePath = path.join(rootDir, ".harness", "actions", "feature-list.json");
|
|
399
|
-
const queuePath = path.join(rootDir, ".harness", "actions", "feature-queue.json");
|
|
400
|
-
const featureResult = readJsonSafe<{
|
|
401
|
-
features?: Array<{
|
|
402
|
-
id?: string;
|
|
403
|
-
title?: string;
|
|
404
|
-
name?: string;
|
|
405
|
-
description?: string;
|
|
406
|
-
status?: string;
|
|
407
|
-
passes?: unknown[];
|
|
408
|
-
}>;
|
|
409
|
-
}>(featurePath);
|
|
410
|
-
const queueResult = readJsonSafe<{
|
|
411
|
-
queue?: {
|
|
412
|
-
ready?: string[];
|
|
413
|
-
blocked?: Record<string, unknown>;
|
|
414
|
-
in_progress?: Record<string, unknown>;
|
|
415
|
-
passed?: string[];
|
|
416
|
-
failed?: string[];
|
|
417
|
-
};
|
|
418
|
-
}>(queuePath);
|
|
419
|
-
const queue = queueResult.ok ? queueResult.value.queue : undefined;
|
|
420
|
-
const statusById = new Map<string, FeatureSummary["status"]>();
|
|
421
|
-
for (const id of queue?.ready ?? []) statusById.set(id, "ready");
|
|
422
|
-
for (const id of Object.keys(queue?.blocked ?? {})) statusById.set(id, "blocked");
|
|
423
|
-
for (const id of Object.keys(queue?.in_progress ?? {})) statusById.set(id, "in_progress");
|
|
424
|
-
for (const id of queue?.passed ?? []) statusById.set(id, "passed");
|
|
425
|
-
for (const id of queue?.failed ?? []) statusById.set(id, "failed");
|
|
426
|
-
|
|
427
|
-
if (!featureResult.ok) {
|
|
428
|
-
return [...statusById.entries()].map(([id, status]) => ({
|
|
429
|
-
id,
|
|
430
|
-
title: id,
|
|
431
|
-
status,
|
|
432
|
-
passes: [],
|
|
433
|
-
}));
|
|
434
|
-
}
|
|
435
|
-
return (featureResult.value.features ?? [])
|
|
436
|
-
.filter((f) => !!f.id)
|
|
437
|
-
.map((f) => {
|
|
438
|
-
const passes = (f.passes ?? []).map(passAxis).filter((x): x is string => !!x);
|
|
439
|
-
const explicitStatus = (f.status ?? "").toLowerCase();
|
|
440
|
-
const status =
|
|
441
|
-
explicitStatus === "pass" || explicitStatus === "passed" || explicitStatus === "done"
|
|
442
|
-
? "passed"
|
|
443
|
-
: explicitStatus === "fail" || explicitStatus === "failed"
|
|
444
|
-
? "failed"
|
|
445
|
-
: statusById.get(f.id!) ?? (passes.length > 0 ? "passed" : "unknown");
|
|
446
|
-
return {
|
|
447
|
-
id: f.id!,
|
|
448
|
-
title: f.title ?? f.name ?? f.description ?? f.id!,
|
|
449
|
-
status,
|
|
450
|
-
passes,
|
|
451
|
-
};
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function buildContract(rootDir: string, progress: RawProgress | null, features: FeatureSummary[]): ContractSnapshot {
|
|
456
|
-
const total = progress?.contracts?.feature_list?.total ?? features.length;
|
|
457
|
-
const passed =
|
|
458
|
-
progress?.contracts?.feature_list?.passed ??
|
|
459
|
-
features.filter((f) => f.status === "passed" || f.passes.length > 0).length;
|
|
460
|
-
const failed =
|
|
461
|
-
progress?.contracts?.feature_list?.failed ??
|
|
462
|
-
features.filter((f) => f.status === "failed").length;
|
|
463
|
-
return {
|
|
464
|
-
sprint_number: progress?.sprint?.number ?? null,
|
|
465
|
-
pipeline: (progress?.pipeline ?? null) as Pipeline,
|
|
466
|
-
api_version: progress?.contracts?.api?.version ?? null,
|
|
467
|
-
feature_total: total,
|
|
468
|
-
feature_passed: passed,
|
|
469
|
-
feature_failed: failed,
|
|
470
|
-
contract_signed: {
|
|
471
|
-
be: !!progress?.cto?.contract_signed?.be,
|
|
472
|
-
fe: !!progress?.cto?.contract_signed?.fe,
|
|
473
|
-
},
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Map a conductor-track owner (real-harness schema) to a dashboard room.
|
|
478
|
-
// Owners are agent IDs / dept names; we collapse them to the room their
|
|
479
|
-
// minifig sits in.
|
|
480
|
-
function ownerToRoom(owner: string | undefined): RoomId {
|
|
481
|
-
if (!owner) return "cto-team";
|
|
482
|
-
const o = owner.toLowerCase();
|
|
483
|
-
if (o === "planner" || o === "coo" || o.includes("documentation")) return "coo";
|
|
484
|
-
if (o === "cto" || o.startsWith("generator-") || o === "conductor") return "cto-team";
|
|
485
|
-
if (o === "cqo" || o.startsWith("evaluator-")) return "cqo-team";
|
|
486
|
-
if (o === "service-ops" || o === "ops") return "service-ops";
|
|
487
|
-
if (o === "meeting-manager") return "meeting";
|
|
488
|
-
if (o === "dispatcher" || o === "ceo") return "ceo";
|
|
489
|
-
return "cto-team";
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function ownerToDept(owner: string | undefined): ParallelTrack["to_dept"] {
|
|
493
|
-
if (!owner) return "Multi";
|
|
494
|
-
const o = owner.toLowerCase();
|
|
495
|
-
if (o === "planner" || o === "coo") return "Planner";
|
|
496
|
-
if (o === "cto" || o.startsWith("generator-")) return "CTO";
|
|
497
|
-
if (o === "cqo" || o.startsWith("evaluator-")) return "CQO";
|
|
498
|
-
if (o === "service-ops" || o === "ops") return "Operations";
|
|
499
|
-
if (o === "meeting-manager") return "Meeting";
|
|
500
|
-
if (o === "dispatcher" || o === "ceo") return "CEO";
|
|
501
|
-
return "Multi";
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Real-harness track status values use a different vocabulary. Normalize so
|
|
505
|
-
// the visualization and tests stay on the same enum.
|
|
506
|
-
function normalizeTrackStatus(raw: string | undefined): TrackStatus {
|
|
507
|
-
const allowed: TrackStatus[] = ["dispatched", "in_progress", "joined", "blocked"];
|
|
508
|
-
if (raw && allowed.includes(raw as TrackStatus)) return raw as TrackStatus;
|
|
509
|
-
switch ((raw ?? "").toLowerCase()) {
|
|
510
|
-
case "running":
|
|
511
|
-
case "active":
|
|
512
|
-
return "in_progress";
|
|
513
|
-
case "completed":
|
|
514
|
-
case "done":
|
|
515
|
-
case "succeeded":
|
|
516
|
-
return "joined";
|
|
517
|
-
case "blocked":
|
|
518
|
-
case "failed":
|
|
519
|
-
case "error":
|
|
520
|
-
return "blocked";
|
|
521
|
-
case "pending":
|
|
522
|
-
case "queued":
|
|
523
|
-
case "scheduled":
|
|
524
|
-
default:
|
|
525
|
-
return "dispatched";
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function buildTracks(progress: RawProgress | null): ParallelTrack[] {
|
|
530
|
-
// Prefer the explicit dashboard schema if the harness opts in; otherwise
|
|
531
|
-
// fall back to the real-harness `conductor.tracks[]` shape.
|
|
532
|
-
const explicit = progress?.parallel_tracks;
|
|
533
|
-
if (explicit && explicit.length > 0) {
|
|
534
|
-
return explicit.map((t) => ({
|
|
535
|
-
id: t.id,
|
|
536
|
-
from_meeting: t.from_meeting ?? "meeting",
|
|
537
|
-
to_dept: (t.to_dept ?? "Multi") as ParallelTrack["to_dept"],
|
|
538
|
-
to_room: t.to_room ?? "cto-team",
|
|
539
|
-
status: normalizeTrackStatus(t.status as string | undefined),
|
|
540
|
-
label: t.label,
|
|
541
|
-
}));
|
|
542
|
-
}
|
|
543
|
-
const conductorTracks = progress?.conductor?.tracks ?? [];
|
|
544
|
-
return conductorTracks.map((t) => ({
|
|
545
|
-
id: t.id,
|
|
546
|
-
from_meeting: progress?.conductor?.fork_meeting_id ?? "meeting",
|
|
547
|
-
to_dept: ownerToDept(t.owner),
|
|
548
|
-
to_room: ownerToRoom(t.owner),
|
|
549
|
-
status: normalizeTrackStatus(t.status),
|
|
550
|
-
label: t.action_type ?? t.deliverable,
|
|
551
|
-
}));
|
|
552
|
-
}
|
|
553
|
-
|
|
554
229
|
function buildIncidents(progress: RawProgress | null): IncidentEntry[] {
|
|
555
230
|
const list = progress?.service_ops?.incident?.open ?? [];
|
|
556
231
|
return list.map((it) => {
|
|
@@ -569,241 +244,6 @@ function buildIncidents(progress: RawProgress | null): IncidentEntry[] {
|
|
|
569
244
|
});
|
|
570
245
|
}
|
|
571
246
|
|
|
572
|
-
function buildHypothesis(progress: RawProgress | null): HypothesisEntry[] {
|
|
573
|
-
const list = progress?.hypothesis?.active ?? [];
|
|
574
|
-
return list.map((h) => {
|
|
575
|
-
const v = (h.verdict ?? "pending").toLowerCase();
|
|
576
|
-
const verdict =
|
|
577
|
-
v === "valid" || v === "invalid" ? (v as HypothesisEntry["verdict"]) : "pending";
|
|
578
|
-
return {
|
|
579
|
-
id: h.id,
|
|
580
|
-
brief: h.brief ?? h.id,
|
|
581
|
-
verdict,
|
|
582
|
-
ts: h.ts ?? null,
|
|
583
|
-
};
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function buildEscalations(progress: RawProgress | null): EscalationEntry[] {
|
|
588
|
-
return (progress?.escalations?.open ?? []).map((e) => ({
|
|
589
|
-
id: e.id,
|
|
590
|
-
reason: e.reason ?? "other",
|
|
591
|
-
message: e.message,
|
|
592
|
-
ts: e.ts ?? null,
|
|
593
|
-
}));
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function buildCurrentMeeting(progress: RawProgress | null): CurrentMeeting | null {
|
|
597
|
-
const m = progress?.meetings;
|
|
598
|
-
if (!m) return null;
|
|
599
|
-
// Prefer explicit `current` (dashboard schema), then real-harness
|
|
600
|
-
// `requested_type` (currently being convened), then `last_type` (most-recent).
|
|
601
|
-
const rawType =
|
|
602
|
-
(m.current && m.current.type) ||
|
|
603
|
-
m.requested_type ||
|
|
604
|
-
m.last_type ||
|
|
605
|
-
null;
|
|
606
|
-
if (!rawType) return null;
|
|
607
|
-
const allowed: CurrentMeeting["type"][] = [
|
|
608
|
-
"standup",
|
|
609
|
-
"sprint-review",
|
|
610
|
-
"spec-review",
|
|
611
|
-
"incident-war-room",
|
|
612
|
-
"all-hands",
|
|
613
|
-
"followup-review",
|
|
614
|
-
];
|
|
615
|
-
const type = allowed.includes(rawType as CurrentMeeting["type"])
|
|
616
|
-
? (rawType as CurrentMeeting["type"])
|
|
617
|
-
: "standup";
|
|
618
|
-
// Topic falls back to the decision summary when the harness hasn't written
|
|
619
|
-
// a free-text topic yet.
|
|
620
|
-
const decisionTopic = m.decision?.owner
|
|
621
|
-
? `${m.decision.owner}${m.decision.action_type ? ` · ${m.decision.action_type}` : ""}`
|
|
622
|
-
: undefined;
|
|
623
|
-
return {
|
|
624
|
-
type,
|
|
625
|
-
topic: m.current?.topic ?? m.requested_reason ?? m.last_reason ?? decisionTopic,
|
|
626
|
-
convened_at: m.current?.convened_at ?? null,
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function deriveCadence(raw: string | undefined): MeetingCadence {
|
|
631
|
-
if (raw === "light" || raw === "normal" || raw === "heavy") return raw;
|
|
632
|
-
return "normal";
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function deptToRoom(dept: string): RoomId | null {
|
|
636
|
-
const map: Record<string, RoomId> = {
|
|
637
|
-
CEO: "ceo",
|
|
638
|
-
Meeting: "meeting",
|
|
639
|
-
Planner: "coo",
|
|
640
|
-
COO: "coo",
|
|
641
|
-
CTO: "cto-team",
|
|
642
|
-
CQO: "cqo-team",
|
|
643
|
-
Operations: "service-ops",
|
|
644
|
-
};
|
|
645
|
-
return map[dept] ?? null;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function buildRooms(
|
|
649
|
-
progress: RawProgress | null,
|
|
650
|
-
meetingActive: AgentId[],
|
|
651
|
-
meetings: MeetingsState,
|
|
652
|
-
tracks: ParallelTrack[],
|
|
653
|
-
incidents: IncidentEntry[],
|
|
654
|
-
hypothesisList: HypothesisEntry[],
|
|
655
|
-
contract: ContractSnapshot,
|
|
656
|
-
evalScores: EvalScores | null,
|
|
657
|
-
): RoomState[] {
|
|
658
|
-
const cto = progress?.cto;
|
|
659
|
-
const cqo = progress?.cqo;
|
|
660
|
-
const ops = progress?.service_ops;
|
|
661
|
-
const sprintNumber = progress?.sprint?.number;
|
|
662
|
-
|
|
663
|
-
const passRate =
|
|
664
|
-
contract.feature_total > 0
|
|
665
|
-
? contract.feature_passed / contract.feature_total
|
|
666
|
-
: null;
|
|
667
|
-
|
|
668
|
-
const tracksByRoom = new Map<RoomId, number>();
|
|
669
|
-
for (const t of tracks) {
|
|
670
|
-
tracksByRoom.set(t.to_room, (tracksByRoom.get(t.to_room) ?? 0) + 1);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const incidentByDept = new Map<string, number>();
|
|
674
|
-
for (const it of incidents) {
|
|
675
|
-
incidentByDept.set(it.dept, (incidentByDept.get(it.dept) ?? 0) + 1);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const metrics: Partial<Record<RoomId, RoomMetrics>> = {
|
|
679
|
-
ceo: {
|
|
680
|
-
sprint_number: sprintNumber,
|
|
681
|
-
},
|
|
682
|
-
meeting: {
|
|
683
|
-
cadence: meetings.cadence,
|
|
684
|
-
next_scheduled: meetings.next_scheduled,
|
|
685
|
-
active_tracks: tracks.length,
|
|
686
|
-
},
|
|
687
|
-
coo: {
|
|
688
|
-
sprint_number: sprintNumber,
|
|
689
|
-
active_hypothesis: hypothesisList.length,
|
|
690
|
-
},
|
|
691
|
-
"cto-team": {
|
|
692
|
-
sprint_number: sprintNumber,
|
|
693
|
-
last_review: cto?.last_review ?? null,
|
|
694
|
-
open_arch_risks: cto?.open_arch_risks ?? 0,
|
|
695
|
-
active_workers: progress?.company_state?.active_workers ?? 0,
|
|
696
|
-
contract_signed: {
|
|
697
|
-
be: !!cto?.contract_signed?.be,
|
|
698
|
-
fe: !!cto?.contract_signed?.fe,
|
|
699
|
-
},
|
|
700
|
-
pass_rate: passRate,
|
|
701
|
-
},
|
|
702
|
-
"cqo-team": {
|
|
703
|
-
sprint_number: sprintNumber,
|
|
704
|
-
last_audit: cqo?.last_audit ?? null,
|
|
705
|
-
sprint_verdict: cqo?.sprint_verdict ?? "pending",
|
|
706
|
-
open_regressions: cqo?.open_regressions ?? 0,
|
|
707
|
-
pass_rate: passRate,
|
|
708
|
-
eval_scores: evalScores,
|
|
709
|
-
},
|
|
710
|
-
"service-ops": {
|
|
711
|
-
sprint_number: sprintNumber,
|
|
712
|
-
last_check: ops?.monitor?.last_check ?? null,
|
|
713
|
-
open_alerts: ops?.monitor?.alerts_this_sprint ?? 0,
|
|
714
|
-
open_incidents: incidents.length,
|
|
715
|
-
},
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
return (Object.keys(ROOM_LABELS) as RoomId[]).map((id) => {
|
|
719
|
-
const base: RoomState = {
|
|
720
|
-
id,
|
|
721
|
-
label_ko: ROOM_LABELS[id].ko,
|
|
722
|
-
label_en: ROOM_LABELS[id].en,
|
|
723
|
-
dept: ROOM_LABELS[id].dept,
|
|
724
|
-
};
|
|
725
|
-
if (metrics[id]) base.metrics = metrics[id];
|
|
726
|
-
if (id === "meeting") {
|
|
727
|
-
const seats = 7;
|
|
728
|
-
const occupants = meetingActive.slice(0, seats);
|
|
729
|
-
const overflow = Math.max(0, meetingActive.length - seats);
|
|
730
|
-
base.seatLayout = { seats, occupants, overflow };
|
|
731
|
-
}
|
|
732
|
-
return base;
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function buildAgents(
|
|
737
|
-
progress: RawProgress | null,
|
|
738
|
-
meetingActive: AgentId[],
|
|
739
|
-
incidents: IncidentEntry[],
|
|
740
|
-
): AgentState[] {
|
|
741
|
-
const meetingSet = new Set<AgentId>(meetingActive);
|
|
742
|
-
const incidentDepts = new Set(incidents.map((i) => i.dept));
|
|
743
|
-
return AGENT_ROSTER.map((entry) => {
|
|
744
|
-
const minifigState = deriveMinifigState(entry.id, progress ?? null, {
|
|
745
|
-
incidentDepts,
|
|
746
|
-
dept: entry.dept,
|
|
747
|
-
});
|
|
748
|
-
const inMeeting = meetingSet.has(entry.id);
|
|
749
|
-
const room = inMeeting ? "meeting" : entry.room;
|
|
750
|
-
const lastActivity =
|
|
751
|
-
progress?.current_agent === entry.id
|
|
752
|
-
? `current_agent (${progress.agent_status ?? "unknown"})`
|
|
753
|
-
: progress?.next_agent === entry.id
|
|
754
|
-
? "next_agent queued"
|
|
755
|
-
: null;
|
|
756
|
-
const matchedIncident = incidents.find((i) => i.dept === entry.dept);
|
|
757
|
-
return {
|
|
758
|
-
id: entry.id,
|
|
759
|
-
name: entry.name,
|
|
760
|
-
dept: entry.dept,
|
|
761
|
-
room,
|
|
762
|
-
homeRoom: entry.room,
|
|
763
|
-
minifigState,
|
|
764
|
-
lastActivity,
|
|
765
|
-
talkingPreview: minifigState === "talking" ? pickTalkingPreview(entry.id) : undefined,
|
|
766
|
-
alertReason:
|
|
767
|
-
minifigState === "red-alert"
|
|
768
|
-
? matchedIncident?.message ??
|
|
769
|
-
progress?.failure?.message ??
|
|
770
|
-
"incident"
|
|
771
|
-
: undefined,
|
|
772
|
-
};
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function readFeatureTitles(rootDir: string): Map<string, string> {
|
|
777
|
-
const map = new Map<string, string>();
|
|
778
|
-
const candidates = [
|
|
779
|
-
path.join(rootDir, ".harness", "actions", "feature-list.json"),
|
|
780
|
-
path.join(rootDir, ".harness", "actions", "feature-queue.json"),
|
|
781
|
-
];
|
|
782
|
-
for (const file of candidates) {
|
|
783
|
-
const result = readJsonSafe<{
|
|
784
|
-
features?: Array<{ id?: string; title?: string; summary?: string }>;
|
|
785
|
-
queue?: {
|
|
786
|
-
ready?: Array<{ id?: string; title?: string; summary?: string }>;
|
|
787
|
-
in_progress?: Array<{ id?: string; title?: string; summary?: string }>;
|
|
788
|
-
passed?: Array<{ id?: string; title?: string; summary?: string }>;
|
|
789
|
-
failed?: Array<{ id?: string; title?: string; summary?: string }>;
|
|
790
|
-
};
|
|
791
|
-
}>(file);
|
|
792
|
-
if (!result.ok) continue;
|
|
793
|
-
for (const f of result.value.features ?? []) {
|
|
794
|
-
if (f.id) map.set(f.id, f.title ?? f.summary ?? f.id);
|
|
795
|
-
}
|
|
796
|
-
const q = result.value.queue;
|
|
797
|
-
for (const list of [q?.ready, q?.in_progress, q?.passed, q?.failed]) {
|
|
798
|
-
if (!Array.isArray(list)) continue;
|
|
799
|
-
for (const f of list) {
|
|
800
|
-
if (f.id) map.set(f.id, f.title ?? f.summary ?? f.id);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
return map;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
247
|
interface RawWorker {
|
|
808
248
|
name?: string;
|
|
809
249
|
team?: number | string;
|
|
@@ -838,6 +278,7 @@ function normalizeMissionLifecycle(raw: unknown): MissionLifecycle {
|
|
|
838
278
|
const value = String(raw ?? "").toLowerCase().replace(/[_\s-]+/g, "-");
|
|
839
279
|
if (
|
|
840
280
|
value === "active" ||
|
|
281
|
+
value === "operating" ||
|
|
841
282
|
value === "closed" ||
|
|
842
283
|
value === "cancelled" ||
|
|
843
284
|
value === "superseded" ||
|
|
@@ -848,6 +289,7 @@ function normalizeMissionLifecycle(raw: unknown): MissionLifecycle {
|
|
|
848
289
|
}
|
|
849
290
|
if (value === "done" || value === "completed") return "complete";
|
|
850
291
|
if (value === "canceled") return "cancelled";
|
|
292
|
+
if (value === "monitoring" || value === "perpetual") return "operating";
|
|
851
293
|
return "unknown";
|
|
852
294
|
}
|
|
853
295
|
|
|
@@ -909,59 +351,6 @@ function normalizeRawWorkers(value: RawWorker[] | Record<string, RawWorker> | nu
|
|
|
909
351
|
});
|
|
910
352
|
}
|
|
911
353
|
|
|
912
|
-
function buildWorkers(rootDir: string, progress: RawProgress | null): WorkerSnapshot[] {
|
|
913
|
-
const titles = readFeatureTitles(rootDir);
|
|
914
|
-
let workers = normalizeRawWorkers(progress?.company_state?.workers);
|
|
915
|
-
if (workers.length === 0) {
|
|
916
|
-
const queue = readJsonSafe<{
|
|
917
|
-
teams?: Record<string, {
|
|
918
|
-
status?: string;
|
|
919
|
-
feature?: string | null;
|
|
920
|
-
pid?: number | null;
|
|
921
|
-
agent?: string;
|
|
922
|
-
phase?: string;
|
|
923
|
-
prompt?: string | null;
|
|
924
|
-
log?: string | null;
|
|
925
|
-
spawn_status?: string;
|
|
926
|
-
}>;
|
|
927
|
-
}>(path.join(rootDir, ".harness", "actions", "feature-queue.json"));
|
|
928
|
-
if (queue.ok) {
|
|
929
|
-
workers = Object.entries(queue.value.teams ?? {})
|
|
930
|
-
.filter(([, t]) => t.status === "busy" && !!t.feature)
|
|
931
|
-
.map(([team, t]) => ({ ...t, team, feature: t.feature ?? undefined }));
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
if (workers.length === 0) {
|
|
935
|
-
workers = progress?.company_state?.last_dispatch ?? [];
|
|
936
|
-
}
|
|
937
|
-
return workers.map((w, idx) => {
|
|
938
|
-
const feature = w.feature ?? `worker-${idx + 1}`;
|
|
939
|
-
const promptText = w.prompt ? readTextSafe(path.join(rootDir, w.prompt)) : null;
|
|
940
|
-
const logText = w.log ? readTextSafe(path.join(rootDir, w.log)) : null;
|
|
941
|
-
const eta = w.eta ? ` ETA ${w.eta}` : "";
|
|
942
|
-
const summarySource = firstLine(logText) || firstLine(promptText) || `${w.agent ?? w.name ?? "worker"} ${w.status ?? "assigned"}${eta}`;
|
|
943
|
-
const material = w.prompt ?? w.log ?? null;
|
|
944
|
-
const rawStatus = (w.status ?? w.spawn_status ?? "unknown").toLowerCase();
|
|
945
|
-
const status = ["spawned", "recorded", "running", "idle", "blocked"].includes(rawStatus)
|
|
946
|
-
? (rawStatus as WorkerSnapshot["status"])
|
|
947
|
-
: "unknown";
|
|
948
|
-
return {
|
|
949
|
-
team: w.team ?? idx + 1,
|
|
950
|
-
feature,
|
|
951
|
-
title: titles.get(feature) ?? feature,
|
|
952
|
-
agent: w.agent ?? "unknown",
|
|
953
|
-
phase: w.phase,
|
|
954
|
-
status,
|
|
955
|
-
pid: w.pid ?? null,
|
|
956
|
-
prompt: w.prompt ?? null,
|
|
957
|
-
log: w.log ?? null,
|
|
958
|
-
progress: deriveWorkerProgress(w),
|
|
959
|
-
summary: truncateText(summarySource, 140),
|
|
960
|
-
next_material: material,
|
|
961
|
-
};
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
|
|
965
354
|
function normalizeWorkerName(value: string | null | undefined): string {
|
|
966
355
|
return (value ?? "")
|
|
967
356
|
.toLowerCase()
|
|
@@ -1077,137 +466,6 @@ function activeWorkerNames(progress: RawProgress | null): Set<string> {
|
|
|
1077
466
|
return active;
|
|
1078
467
|
}
|
|
1079
468
|
|
|
1080
|
-
function meetingTitleFromMarkdown(text: string, fallback: string): string {
|
|
1081
|
-
const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
1082
|
-
return heading || fallback;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function meetingVerdict(text: string): string | null {
|
|
1086
|
-
const explicit = text.match(/verdict:\s*([^\n]+)/i)?.[1]?.trim();
|
|
1087
|
-
if (explicit) return explicit;
|
|
1088
|
-
if (/incident/i.test(text)) return "incident";
|
|
1089
|
-
if (/PASS|passed|working|continue autonomous/i.test(text)) return "working";
|
|
1090
|
-
return null;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
function buildRecentMeetings(rootDir: string): MeetingRecord[] {
|
|
1094
|
-
const meetingsDir = path.join(rootDir, ".harness", "actions", "meetings");
|
|
1095
|
-
if (!existsSync(meetingsDir)) return [];
|
|
1096
|
-
const records: MeetingRecord[] = [];
|
|
1097
|
-
for (const dir of readdirSync(meetingsDir)) {
|
|
1098
|
-
const fullDir = path.join(meetingsDir, dir);
|
|
1099
|
-
try {
|
|
1100
|
-
if (!statSync(fullDir).isDirectory()) continue;
|
|
1101
|
-
} catch {
|
|
1102
|
-
continue;
|
|
1103
|
-
}
|
|
1104
|
-
const md = readdirSync(fullDir).find((name) => name.startsWith("meeting-") && name.endsWith(".md"));
|
|
1105
|
-
if (!md) continue;
|
|
1106
|
-
const fullPath = path.join(fullDir, md);
|
|
1107
|
-
const text = readTextSafe(fullPath) ?? "";
|
|
1108
|
-
let ts: string | null = null;
|
|
1109
|
-
try {
|
|
1110
|
-
ts = statSync(fullPath).mtime.toISOString();
|
|
1111
|
-
} catch {
|
|
1112
|
-
ts = null;
|
|
1113
|
-
}
|
|
1114
|
-
const summary =
|
|
1115
|
-
text.match(/## Decision\s+([\s\S]*?)(?=\n## |\n# |$)/i)?.[1] ??
|
|
1116
|
-
text.match(/## Summary\s+([\s\S]*?)(?=\n## |\n# |$)/i)?.[1] ??
|
|
1117
|
-
text.split("\n").slice(0, 12).join(" ");
|
|
1118
|
-
records.push({
|
|
1119
|
-
id: dir,
|
|
1120
|
-
path: path.relative(rootDir, fullPath),
|
|
1121
|
-
ts,
|
|
1122
|
-
title: meetingTitleFromMarkdown(text, dir),
|
|
1123
|
-
verdict: meetingVerdict(text),
|
|
1124
|
-
summary: truncateText(summary, 220),
|
|
1125
|
-
content: text,
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
records.sort((a, b) => (b.ts ?? "").localeCompare(a.ts ?? ""));
|
|
1129
|
-
return records.slice(0, 6);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
function buildOpsHealth(progress: RawProgress | null): OpsServiceHealth[] {
|
|
1133
|
-
return (progress?.service_ops?.health ?? []).map((h) => {
|
|
1134
|
-
const statusRaw = (h.status ?? "unknown").toLowerCase();
|
|
1135
|
-
const status = statusRaw === "ok" || statusRaw === "degraded" || statusRaw === "down"
|
|
1136
|
-
? statusRaw
|
|
1137
|
-
: "unknown";
|
|
1138
|
-
return {
|
|
1139
|
-
name: h.name ?? "service",
|
|
1140
|
-
host: h.host ?? "127.0.0.1",
|
|
1141
|
-
port: h.port ?? 0,
|
|
1142
|
-
status,
|
|
1143
|
-
port_state: h.port_state ?? null,
|
|
1144
|
-
health_status: h.health_status ?? null,
|
|
1145
|
-
health_path: h.health_path ?? null,
|
|
1146
|
-
recent_errors: h.log?.recent_errors ?? null,
|
|
1147
|
-
};
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
function categorizeEnvKey(key: string): EnvFileSummary["keys"][number]["category"] {
|
|
1152
|
-
if (/TOKEN|SECRET|KEY|PASSWORD|PASS|COOKIE|JWT/i.test(key)) return "secret";
|
|
1153
|
-
if (/URL|HOST|ORIGIN|ENDPOINT/i.test(key)) return "endpoint";
|
|
1154
|
-
if (/PORT/i.test(key)) return "port";
|
|
1155
|
-
if (/MODE|ENV|NODE_ENV|LIVE|PAPER/i.test(key)) return "mode";
|
|
1156
|
-
return "other";
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
function maskEnvValue(value: string, category: EnvFileSummary["keys"][number]["category"]): string {
|
|
1160
|
-
if (!value) return "(empty)";
|
|
1161
|
-
if (category === "secret") return value.length <= 4 ? "••••" : `${value.slice(0, 2)}••••${value.slice(-2)}`;
|
|
1162
|
-
if (value.length <= 18) return value;
|
|
1163
|
-
return `${value.slice(0, 10)}…${value.slice(-5)}`;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function buildEnvFiles(rootDir: string): EnvFileSummary[] {
|
|
1167
|
-
const names = [".env", ".env.local", ".env.production", ".env.development", ".env.example"];
|
|
1168
|
-
return names
|
|
1169
|
-
.map((name) => {
|
|
1170
|
-
const fullPath = path.join(rootDir, name);
|
|
1171
|
-
if (!existsSync(fullPath)) return null;
|
|
1172
|
-
const text = readTextSafe(fullPath) ?? "";
|
|
1173
|
-
const keys = text
|
|
1174
|
-
.split("\n")
|
|
1175
|
-
.map((line) => line.trim())
|
|
1176
|
-
.filter((line) => line && !line.startsWith("#") && line.includes("="))
|
|
1177
|
-
.map((line) => {
|
|
1178
|
-
const idx = line.indexOf("=");
|
|
1179
|
-
const key = line.slice(0, idx).trim();
|
|
1180
|
-
const value = line.slice(idx + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
1181
|
-
const category = categorizeEnvKey(key);
|
|
1182
|
-
return { key, category, masked: maskEnvValue(value, category) };
|
|
1183
|
-
});
|
|
1184
|
-
let updated_at: string | null = null;
|
|
1185
|
-
try {
|
|
1186
|
-
updated_at = statSync(fullPath).mtime.toISOString();
|
|
1187
|
-
} catch {
|
|
1188
|
-
updated_at = null;
|
|
1189
|
-
}
|
|
1190
|
-
return {
|
|
1191
|
-
path: name,
|
|
1192
|
-
updated_at,
|
|
1193
|
-
key_count: keys.length,
|
|
1194
|
-
keys: keys.slice(0, 12),
|
|
1195
|
-
};
|
|
1196
|
-
})
|
|
1197
|
-
.filter((x): x is EnvFileSummary => x !== null);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
function buildDashboard(rootDir: string, progress: RawProgress | null): OperationsDashboard {
|
|
1201
|
-
const features = buildFeatureSummaries(rootDir);
|
|
1202
|
-
return {
|
|
1203
|
-
workers: buildWorkers(rootDir, progress),
|
|
1204
|
-
features,
|
|
1205
|
-
recentMeetings: buildRecentMeetings(rootDir),
|
|
1206
|
-
opsHealth: buildOpsHealth(progress),
|
|
1207
|
-
envFiles: buildEnvFiles(rootDir),
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
469
|
function buildRuntime(progress: RawProgress | null): RuntimeSnapshot {
|
|
1212
470
|
const ownerPrompt = progress?.owner_prompt
|
|
1213
471
|
? {
|
|
@@ -1222,6 +480,11 @@ function buildRuntime(progress: RawProgress | null): RuntimeSnapshot {
|
|
|
1222
480
|
agentStatus: progress?.agent_status ?? "unknown",
|
|
1223
481
|
nextAgent: progress?.next_agent ?? null,
|
|
1224
482
|
updatedAt: progress?.updated_at ?? null,
|
|
483
|
+
conductorState: progress?.conductor?.state ?? null,
|
|
484
|
+
currentAction: progress?.conductor?.current_action ?? null,
|
|
485
|
+
companyState: progress?.company_state?.state ?? null,
|
|
486
|
+
activeWorkers: progress?.company_state?.active_workers ?? 0,
|
|
487
|
+
lastDispatchAt: progress?.company_state?.last_dispatch_at ?? null,
|
|
1225
488
|
ownerPrompt,
|
|
1226
489
|
};
|
|
1227
490
|
}
|
|
@@ -1295,14 +558,13 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1295
558
|
updatedAt = mtime.toISOString();
|
|
1296
559
|
} catch { /* ignore */ }
|
|
1297
560
|
const statusMatch = content.match(/##\s*Status\s*\n+([A-Z_]+)/);
|
|
1298
|
-
const hasDocmeta = /^---[\s\S]*?docmeta:[\s\S]*?---/.test(content);
|
|
1299
561
|
// Runtime state is the source of truth for live activity. A
|
|
1300
562
|
// recently touched docmeta-only draft is not enough to prove that a
|
|
1301
|
-
// detached worker session is still running
|
|
563
|
+
// detached worker session is still running, nor that it has
|
|
564
|
+
// completed — leave it "unknown" unless the doc has an explicit
|
|
565
|
+
// ## Status section.
|
|
1302
566
|
const status: WorkerDocEntry["status"] = statusMatch
|
|
1303
567
|
? (statusMatch[1] as WorkerDocEntry["status"])
|
|
1304
|
-
: hasDocmeta
|
|
1305
|
-
? "COMPLETE"
|
|
1306
568
|
: "unknown";
|
|
1307
569
|
const entry: WorkerDocEntry = {
|
|
1308
570
|
name,
|
|
@@ -1367,6 +629,16 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1367
629
|
}
|
|
1368
630
|
}
|
|
1369
631
|
|
|
632
|
+
// Perpetual (operating) goals carry an agenda.json the CXX co-write.
|
|
633
|
+
const agenda = readJsonSafe<{
|
|
634
|
+
items?: Array<{ status?: string }>;
|
|
635
|
+
cycles?: number;
|
|
636
|
+
}>(path.join(missionPath, "agenda.json"));
|
|
637
|
+
const agendaOpen = agenda.ok
|
|
638
|
+
? (agenda.value.items ?? []).filter((it) => (it.status ?? "") !== "done").length
|
|
639
|
+
: undefined;
|
|
640
|
+
const operatingCycles = agenda.ok ? agenda.value.cycles ?? undefined : undefined;
|
|
641
|
+
|
|
1370
642
|
const id = rel;
|
|
1371
643
|
const label = rel.split("/").pop() ?? rel;
|
|
1372
644
|
return {
|
|
@@ -1386,6 +658,8 @@ function readMissions(rootDir: string, progress: RawProgress | null = null, limi
|
|
|
1386
658
|
ops: readMd("ops.md"),
|
|
1387
659
|
workers,
|
|
1388
660
|
cxxPresent: [...cxxPresent],
|
|
661
|
+
agendaOpen,
|
|
662
|
+
operatingCycles,
|
|
1389
663
|
} satisfies MissionDoc;
|
|
1390
664
|
})
|
|
1391
665
|
.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
|
|
@@ -1614,6 +888,80 @@ function readActivitySamples(rootDir: string): ActivitySample[] {
|
|
|
1614
888
|
return rows.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
1615
889
|
}
|
|
1616
890
|
|
|
891
|
+
function fileCategory(relPath: string): HarnessFileEntry["category"] {
|
|
892
|
+
if (relPath.startsWith(".harness/documents/")) return "documents";
|
|
893
|
+
if (relPath.startsWith(".harness/todos/")) return "todos";
|
|
894
|
+
if (relPath.startsWith(".harness/activity/")) return "activity";
|
|
895
|
+
if (relPath.startsWith(".harness/gotchas/") || relPath.startsWith(".harness/conventions/")) return "knowledge";
|
|
896
|
+
if (relPath.startsWith(".harness/runtime/")) return "runtime";
|
|
897
|
+
if (relPath === ".harness/progress.json" || relPath === ".harness/config.json") return "config";
|
|
898
|
+
return "other";
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function shouldSkipHarnessDir(relPath: string): boolean {
|
|
902
|
+
return (
|
|
903
|
+
relPath === ".harness/dashboard" ||
|
|
904
|
+
relPath.startsWith(".harness/dashboard/") ||
|
|
905
|
+
relPath === ".harness/shared/HR-Resource" ||
|
|
906
|
+
relPath.startsWith(".harness/shared/HR-Resource/")
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function readHarnessFiles(rootDir: string, limit = 180): HarnessFileEntry[] {
|
|
911
|
+
const harnessDir = path.join(rootDir, ".harness");
|
|
912
|
+
if (!existsSync(harnessDir)) return [];
|
|
913
|
+
|
|
914
|
+
const entries: HarnessFileEntry[] = [];
|
|
915
|
+
const visit = (absDir: string, relDir: string, depth: number) => {
|
|
916
|
+
if (entries.length >= limit || depth > 5 || shouldSkipHarnessDir(relDir)) return;
|
|
917
|
+
let dirents: Dirent[];
|
|
918
|
+
try {
|
|
919
|
+
dirents = readdirSync(absDir, { withFileTypes: true });
|
|
920
|
+
} catch {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
dirents.sort((a, b) => {
|
|
924
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
925
|
+
return a.name.localeCompare(b.name);
|
|
926
|
+
});
|
|
927
|
+
for (const entry of dirents) {
|
|
928
|
+
if (entries.length >= limit || entry.name === ".DS_Store") break;
|
|
929
|
+
const abs = path.join(absDir, entry.name);
|
|
930
|
+
const rel = `${relDir}/${entry.name}`;
|
|
931
|
+
if (shouldSkipHarnessDir(rel)) continue;
|
|
932
|
+
let stat = null as ReturnType<typeof statSync> | null;
|
|
933
|
+
try {
|
|
934
|
+
stat = statSync(abs);
|
|
935
|
+
} catch {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
entries.push({
|
|
939
|
+
path: rel,
|
|
940
|
+
name: entry.name,
|
|
941
|
+
kind: entry.isDirectory() ? "dir" : "file",
|
|
942
|
+
depth,
|
|
943
|
+
size: entry.isDirectory() ? null : stat.size,
|
|
944
|
+
updatedAt: stat.mtime.toISOString(),
|
|
945
|
+
category: fileCategory(rel),
|
|
946
|
+
});
|
|
947
|
+
if (entry.isDirectory()) visit(abs, rel, depth + 1);
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
entries.push({
|
|
952
|
+
path: ".harness",
|
|
953
|
+
name: ".harness",
|
|
954
|
+
kind: "dir",
|
|
955
|
+
depth: 0,
|
|
956
|
+
size: null,
|
|
957
|
+
updatedAt: null,
|
|
958
|
+
category: "runtime",
|
|
959
|
+
});
|
|
960
|
+
visit(harnessDir, ".harness", 1);
|
|
961
|
+
|
|
962
|
+
return entries;
|
|
963
|
+
}
|
|
964
|
+
|
|
1617
965
|
export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
1618
966
|
const harnessDir = path.join(rootDir, ".harness");
|
|
1619
967
|
if (!existsSync(harnessDir)) {
|
|
@@ -1639,7 +987,6 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
|
1639
987
|
message_en: "progress.json is corrupt — check the file.",
|
|
1640
988
|
};
|
|
1641
989
|
const snapshot = emptySnapshot(banner, rootDir);
|
|
1642
|
-
snapshot.archive = buildArchive(rootDir);
|
|
1643
990
|
snapshot.missions = readMissions(rootDir, null);
|
|
1644
991
|
snapshot.ownerHistory = readOwnerHistory(rootDir);
|
|
1645
992
|
snapshot.gotchas = readGotchas(rootDir);
|
|
@@ -1647,55 +994,21 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
|
1647
994
|
snapshot.todos = readTodos(rootDir);
|
|
1648
995
|
snapshot.events = readEvents(rootDir);
|
|
1649
996
|
snapshot.activitySamples = readActivitySamples(rootDir);
|
|
997
|
+
snapshot.files = readHarnessFiles(rootDir);
|
|
1650
998
|
return snapshot;
|
|
1651
999
|
}
|
|
1652
1000
|
|
|
1653
1001
|
const progress = result.value;
|
|
1654
|
-
const meetingActive = progress.meetings?.active ?? [];
|
|
1655
|
-
const meetings: MeetingsState = {
|
|
1656
|
-
active: meetingActive,
|
|
1657
|
-
cadence: deriveCadence(progress.meetings?.cadence as string | undefined),
|
|
1658
|
-
next_scheduled: progress.meetings?.next_scheduled ?? null,
|
|
1659
|
-
current: buildCurrentMeeting(progress),
|
|
1660
|
-
};
|
|
1661
|
-
|
|
1662
|
-
const tracks = buildTracks(progress);
|
|
1663
1002
|
const incidents = buildIncidents(progress);
|
|
1664
|
-
const hypothesisList = buildHypothesis(progress);
|
|
1665
|
-
const escalations = buildEscalations(progress);
|
|
1666
|
-
const features = buildFeatureSummaries(rootDir);
|
|
1667
|
-
const contract = buildContract(rootDir, progress, features);
|
|
1668
|
-
const evalScores = progress.cqo?.last_scores ?? null;
|
|
1669
|
-
const dashboard = buildDashboard(rootDir, progress);
|
|
1670
1003
|
|
|
1671
1004
|
return {
|
|
1672
1005
|
version: SNAPSHOT_VERSION,
|
|
1673
1006
|
ts: new Date().toISOString(),
|
|
1674
1007
|
projectName: path.basename(rootDir),
|
|
1675
1008
|
projectPath: rootDir,
|
|
1676
|
-
agents: buildAgents(progress, meetingActive, incidents),
|
|
1677
|
-
rooms: buildRooms(
|
|
1678
|
-
progress,
|
|
1679
|
-
meetingActive,
|
|
1680
|
-
meetings,
|
|
1681
|
-
tracks,
|
|
1682
|
-
incidents,
|
|
1683
|
-
hypothesisList,
|
|
1684
|
-
contract,
|
|
1685
|
-
evalScores,
|
|
1686
|
-
),
|
|
1687
|
-
goal: buildGoal(rootDir, progress),
|
|
1688
|
-
archive: buildArchive(rootDir),
|
|
1689
|
-
meetings,
|
|
1690
|
-
tracks,
|
|
1691
|
-
incidents,
|
|
1692
|
-
hypothesis: hypothesisList,
|
|
1693
|
-
escalations,
|
|
1694
|
-
contract,
|
|
1695
|
-
evalScores,
|
|
1696
1009
|
errorBanner: null,
|
|
1697
|
-
dashboard,
|
|
1698
1010
|
runtime: buildRuntime(progress),
|
|
1011
|
+
incidents,
|
|
1699
1012
|
missions: readMissions(rootDir, progress),
|
|
1700
1013
|
ownerHistory: readOwnerHistory(rootDir),
|
|
1701
1014
|
gotchas: readGotchas(rootDir),
|
|
@@ -1703,8 +1016,6 @@ export function readHarnessState(rootDir: string): HarnessSnapshot {
|
|
|
1703
1016
|
todos: readTodos(rootDir),
|
|
1704
1017
|
events: readEvents(rootDir),
|
|
1705
1018
|
activitySamples: readActivitySamples(rootDir),
|
|
1019
|
+
files: readHarnessFiles(rootDir),
|
|
1706
1020
|
};
|
|
1707
1021
|
}
|
|
1708
|
-
|
|
1709
|
-
// Exported for tests — silences the unused warning when only the type is used.
|
|
1710
|
-
export type { Dept };
|