@bamr87/fleet-engines 0.1.0

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.
@@ -0,0 +1,431 @@
1
+ // The six-layer harness health — a pure TypeScript port of the bamr87 hub's `dash-gen harness`
2
+ // (specs/014-harness-parity, FR-1; reference: bamr87/bamr87@d58a26c
3
+ // .github/scripts/dash-gen/harness.py). The hub computes a SCORECARD (nine metrics, two of
4
+ // them judged against declared thresholds) and six TRIP WIRES (aggregate-drift alarms) from
5
+ // four committed fleet signals, offline and deterministically; GitFactory runs the same rules
6
+ // over the hub's committed files (the hub as fleet source) or over a live Fleet Ops scan
7
+ // (`signals.ts`). Every input is optional: a missing or malformed signal nulls its metrics
8
+ // and trips `stale-data`, never throws — the wire that watches for staleness is exactly how
9
+ // a missing input becomes visible. No I/O, no globals (golden rule #3). All parsed values
10
+ // are untrusted data: numbers are coerced and bounded, strings pass through as text.
11
+ export const SIGNAL_NAMES = [
12
+ 'actions_usage',
13
+ 'fleet_triage',
14
+ 'issue_pipeline',
15
+ 'token_rotation',
16
+ ];
17
+ /** The hub's defaults (harness.py `DEFAULT_SCORECARD` / `DEFAULT_TRIP_WIRES`). */
18
+ export const DEFAULT_CONFIG = {
19
+ scorecard: { completion_rate_min_pct: 80, effectiveness_min_pct: 70 },
20
+ trip_wires: {
21
+ stale_data_days: 3,
22
+ pass_rate_floor_pct: 75,
23
+ waste_ceiling_pct: 30,
24
+ cost_spike_multiplier: 3.0,
25
+ cost_spike_min_runs: 3,
26
+ cost_spike_min_avg_min: 5.0,
27
+ standing_failures_max: 30,
28
+ credential_grace_days: 15,
29
+ },
30
+ };
31
+ /** The weekly rotation ledger gets slack the daily signals do not (harness.py, literal 10). */
32
+ const ROTATION_STALE_DAYS = 10;
33
+ const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
34
+ /** A finite number, or null. Strings that are numbers are accepted (YAML round-trips them). */
35
+ export function num(v) {
36
+ if (typeof v === 'number')
37
+ return Number.isFinite(v) ? v : null;
38
+ if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)))
39
+ return Number(v);
40
+ return null;
41
+ }
42
+ /**
43
+ * Read the `harness:` block out of a parsed `fleet.yml` (or the block itself), with the
44
+ * hub's defaults for every absent key — an absent block degrades to sane behaviour.
45
+ */
46
+ export function parseHarnessConfig(fleetYml) {
47
+ const root = isRecord(fleetYml) ? fleetYml : {};
48
+ const block = isRecord(root.harness) ? root.harness : root;
49
+ const sc = isRecord(block.scorecard) ? block.scorecard : {};
50
+ const tw = isRecord(block.trip_wires) ? block.trip_wires : {};
51
+ const pick = (src, defaults) => {
52
+ const out = { ...defaults };
53
+ for (const key of Object.keys(defaults)) {
54
+ const v = num(src[key]);
55
+ if (v !== null)
56
+ out[key] = v;
57
+ }
58
+ return out;
59
+ };
60
+ return {
61
+ scorecard: pick(sc, DEFAULT_CONFIG.scorecard),
62
+ trip_wires: pick(tw, DEFAULT_CONFIG.trip_wires),
63
+ };
64
+ }
65
+ // ── time ───────────────────────────────────────────────────────────────────
66
+ /** The generators stamp `%Y-%m-%d %H:%M UTC`; ISO and bare dates are tolerated (harness.py). */
67
+ export function parseGeneratedAt(value) {
68
+ if (typeof value !== 'string')
69
+ return null;
70
+ let m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}) UTC$/.exec(value);
71
+ if (m)
72
+ return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]));
73
+ m = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/.exec(value);
74
+ if (m) {
75
+ const t = Date.parse(value);
76
+ return Number.isNaN(t) ? null : new Date(t);
77
+ }
78
+ m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
79
+ if (m)
80
+ return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
81
+ return null;
82
+ }
83
+ /** Python's round-half-even at one decimal, as the hub's `round(x, 1)` produces. */
84
+ const round1 = (x) => roundHalfEven(x, 1);
85
+ const round2 = (x) => roundHalfEven(x, 2);
86
+ function roundHalfEven(x, digits) {
87
+ const f = 10 ** digits;
88
+ const y = x * f;
89
+ const floor = Math.floor(y);
90
+ const diff = y - floor;
91
+ let r;
92
+ if (Math.abs(diff - 0.5) < 1e-9)
93
+ r = floor % 2 === 0 ? floor : floor + 1;
94
+ else
95
+ r = Math.round(y);
96
+ return r / f;
97
+ }
98
+ export function sourceAgeDays(data, now) {
99
+ const ts = parseGeneratedAt(isRecord(data) ? data.generated_at : undefined);
100
+ if (!ts)
101
+ return null;
102
+ return round1((now.getTime() - ts.getTime()) / 86_400_000);
103
+ }
104
+ export function median(values) {
105
+ if (!values.length)
106
+ return null;
107
+ const ordered = [...values].sort((a, b) => a - b);
108
+ const mid = Math.floor(ordered.length / 2);
109
+ return ordered.length % 2 ? ordered[mid] : (ordered[mid - 1] + ordered[mid]) / 2;
110
+ }
111
+ export const SCORECARD_KEYS = [
112
+ 'completion_rate_pct',
113
+ 'effectiveness_pct',
114
+ 'waste_hours',
115
+ 'cost_min_per_verified_run',
116
+ 'standing_failures',
117
+ 'repos_red',
118
+ 'escalations_open',
119
+ 'agent_prs_open',
120
+ 'oldest_credential_age_days',
121
+ ];
122
+ const present = (v) => v !== null && v !== undefined;
123
+ /** The playbook's health scorecard, from the signals the fleet already keeps. */
124
+ export function buildScorecard(cfg, usage, triage, pipeline, rotation) {
125
+ const uTot = usage?.totals ?? {};
126
+ const tTot = triage?.totals ?? {};
127
+ const pTot = pipeline?.totals ?? {};
128
+ const stages = pTot.stages ?? {};
129
+ let verifiedRuns = null;
130
+ const workflows = Array.isArray(usage?.workflows) ? usage.workflows : null;
131
+ if (workflows && workflows.length) {
132
+ verifiedRuns = workflows.reduce((sum, w) => sum + Math.trunc(num(w.success) ?? 0), 0);
133
+ }
134
+ let costPerVerified = null;
135
+ const totalMin = num(uTot.total_min);
136
+ if (verifiedRuns && totalMin)
137
+ costPerVerified = round2(totalMin / verifiedRuns);
138
+ let oldestCredential = null;
139
+ for (const token of rotation?.tokens ?? []) {
140
+ const age = isRecord(token) ? num(token.oldest_age_days) : null;
141
+ if (age !== null)
142
+ oldestCredential = Math.max(oldestCredential ?? 0, age);
143
+ }
144
+ const sc = cfg.scorecard;
145
+ const metric = (value, direction, threshold, ok) => {
146
+ const entry = { value, direction };
147
+ if (threshold !== undefined) {
148
+ entry.threshold = threshold;
149
+ entry.status = value === null ? 'unknown' : ok ? 'ok' : 'warn';
150
+ }
151
+ return entry;
152
+ };
153
+ const completion = num(uTot.success_rate_pct);
154
+ const effectiveness = num(uTot.effectiveness_pct);
155
+ let escalations = null;
156
+ if (Object.keys(stages).length) {
157
+ escalations = Math.trunc(num(stages.blocked) ?? 0) + Math.trunc(num(stages.hold) ?? 0);
158
+ }
159
+ return {
160
+ completion_rate_pct: metric(completion, 'up', sc.completion_rate_min_pct, present(completion) && completion >= sc.completion_rate_min_pct),
161
+ effectiveness_pct: metric(effectiveness, 'up', sc.effectiveness_min_pct, present(effectiveness) && effectiveness >= sc.effectiveness_min_pct),
162
+ waste_hours: metric(num(uTot.waste_hours), 'down'),
163
+ cost_min_per_verified_run: metric(costPerVerified, 'down'),
164
+ standing_failures: metric(num(tTot.failing_workflows), 'down'),
165
+ repos_red: metric(num(tTot.repos_red), 'down'),
166
+ escalations_open: metric(escalations, 'down'),
167
+ agent_prs_open: metric(num(pTot.pipeline_prs), 'steady'),
168
+ oldest_credential_age_days: metric(oldestCredential, 'down'),
169
+ };
170
+ }
171
+ export const WIRE_IDS = [
172
+ 'stale-data',
173
+ 'pass-rate-floor',
174
+ 'waste-ceiling',
175
+ 'cost-spike',
176
+ 'standing-failures',
177
+ 'credential-overdue',
178
+ ];
179
+ /** Every wire is reported, tripped or not — a quiet panel and a lost panel must not look the same. */
180
+ export function evaluateTripWires(cfg, usage, triage, pipeline, rotation, now) {
181
+ const tw = cfg.trip_wires;
182
+ const wires = [];
183
+ const wire = (id, tripped, summary, detail) => {
184
+ const entry = { id, tripped: !!tripped, summary };
185
+ if (detail && detail.length)
186
+ entry.detail = detail;
187
+ wires.push(entry);
188
+ };
189
+ const has = (data) => isRecord(data) && Object.keys(data).length > 0;
190
+ // 1. stale-data — the observability layer watching itself.
191
+ const sources = [
192
+ ['actions_usage', usage],
193
+ ['fleet_triage', triage],
194
+ ['issue_pipeline', pipeline],
195
+ ['token_rotation', rotation],
196
+ ];
197
+ const stale = [];
198
+ for (const [name, data] of sources) {
199
+ const limit = name === 'token_rotation' ? ROTATION_STALE_DAYS : tw.stale_data_days;
200
+ const age = sourceAgeDays(data, now);
201
+ if (!has(data))
202
+ stale.push({ source: name, age_days: null, why: 'missing' });
203
+ else if (age === null)
204
+ stale.push({ source: name, age_days: null, why: 'no generated_at' });
205
+ else if (age > limit)
206
+ stale.push({ source: name, age_days: age, why: `older than ${limit}d` });
207
+ }
208
+ wire('stale-data', stale.length > 0, stale.length ? 'a committed fleet signal stopped refreshing' : 'all fleet signals fresh', stale);
209
+ const uTot = usage?.totals ?? {};
210
+ // 2. pass-rate-floor — fleet-wide quality regression.
211
+ const rate = num(uTot.success_rate_pct);
212
+ wire('pass-rate-floor', rate !== null && rate < tw.pass_rate_floor_pct, `fleet workflow success rate ${fmtFloat(rate)}% vs floor ${fmt(tw.pass_rate_floor_pct)}%`);
213
+ // 3. waste-ceiling — minutes ending in non-success, as a share of all minutes.
214
+ let wastePct = null;
215
+ const totalMin = num(uTot.total_min);
216
+ if (totalMin)
217
+ wastePct = round1((100 * (num(uTot.waste_min) ?? 0)) / totalMin);
218
+ wire('waste-ceiling', wastePct !== null && wastePct > tw.waste_ceiling_pct, `wasted minutes ${fmtFloat(wastePct)}% of total vs ceiling ${fmt(tw.waste_ceiling_pct)}%`);
219
+ // 4. cost-spike — a workflow whose average run dwarfs the fleet MEDIAN (never the mean:
220
+ // one runaway must not raise the baseline that would have flagged it).
221
+ const eligible = (Array.isArray(usage?.workflows) ? usage.workflows : []).filter((w) => isRecord(w) &&
222
+ !w.external &&
223
+ Math.trunc(num(w.runs) ?? 0) >= tw.cost_spike_min_runs &&
224
+ num(w.avg_min) !== null);
225
+ const med = median(eligible.map((w) => num(w.avg_min)));
226
+ let spikes = [];
227
+ if (med) {
228
+ // The absolute floor keeps a fleet of tiny workflows honest.
229
+ const threshold = Math.max(med * tw.cost_spike_multiplier, tw.cost_spike_min_avg_min);
230
+ spikes = eligible
231
+ .filter((w) => num(w.avg_min) >= threshold)
232
+ .sort((a, b) => num(b.avg_min) - num(a.avg_min))
233
+ .slice(0, 5);
234
+ }
235
+ wire('cost-spike', spikes.length > 0, med
236
+ ? `workflows averaging ≥${fmtFloat(tw.cost_spike_multiplier)}× the fleet median (${fmtFloat(med)} min)`
237
+ : 'no usable per-workflow cost data', spikes.map((w) => ({
238
+ repo: w.repo ?? null,
239
+ workflow: w.workflow ?? null,
240
+ path: w.path ?? null,
241
+ avg_min: w.avg_min ?? null,
242
+ runs: w.runs ?? null,
243
+ })));
244
+ // 5. standing-failures — the backlog the doctor drains is growing past its caps.
245
+ const failing = num(triage?.totals?.failing_workflows);
246
+ wire('standing-failures', failing !== null && failing > tw.standing_failures_max, `${fmt(failing)} standing red workflows vs max ${fmt(tw.standing_failures_max)}`);
247
+ // 6. credential-overdue — a credential aged past its own rotation policy plus grace.
248
+ const overdue = [];
249
+ for (const token of rotation?.tokens ?? []) {
250
+ if (!isRecord(token))
251
+ continue;
252
+ const age = num(token.oldest_age_days);
253
+ const maxAge = num(token.max_age_days);
254
+ if (age !== null && maxAge !== null && age > maxAge + tw.credential_grace_days) {
255
+ overdue.push({ name: token.name ?? null, oldest_age_days: age, max_age_days: maxAge });
256
+ }
257
+ }
258
+ wire('credential-overdue', overdue.length > 0, overdue.length
259
+ ? 'a fleet credential is past its rotation policy plus grace'
260
+ : 'credential ages within policy', overdue);
261
+ return wires;
262
+ }
263
+ /** Python's str() for an int-typed value the summaries interpolate (`None` when absent). */
264
+ export function fmt(v) {
265
+ return v === null ? 'None' : String(v);
266
+ }
267
+ /**
268
+ * Python's str() for a float-typed value: an integral float prints with `.0` (`3.0`, `90.0`).
269
+ * The hub's rates, percentages, medians, and the multiplier/floor thresholds are floats there
270
+ * (`round(x, 1)` results and `3.0` / `5.0` in fleet.yml), so their summaries carry the decimal.
271
+ */
272
+ export function fmtFloat(v) {
273
+ if (v === null)
274
+ return 'None';
275
+ return Number.isInteger(v) ? v.toFixed(1) : String(v);
276
+ }
277
+ export const HARNESS_NOTE = 'Six-layer harness health: scorecard + trip wires computed offline from the committed fleet signals (docs/HARNESS.md). Thresholds live in _data/fleet.yml `harness:`. A tripped wire is an attention item; the doctor and issue-pipeline loops own the fixes.';
278
+ /** `%Y-%m-%d %H:%M UTC`, the hub's stamp. */
279
+ export function stampUtc(now) {
280
+ const p = (n) => String(n).padStart(2, '0');
281
+ return `${now.getUTCFullYear()}-${p(now.getUTCMonth() + 1)}-${p(now.getUTCDate())} ${p(now.getUTCHours())}:${p(now.getUTCMinutes())} UTC`;
282
+ }
283
+ /** The hub's `run()`: the committed `harness_health.yml`, as data. */
284
+ export function harnessHealth(signals, cfg = DEFAULT_CONFIG, now = new Date()) {
285
+ const usage = signals.actions_usage ?? null;
286
+ const triage = signals.fleet_triage ?? null;
287
+ const pipeline = signals.issue_pipeline ?? null;
288
+ const rotation = signals.token_rotation ?? null;
289
+ const scorecard = buildScorecard(cfg, usage, triage, pipeline, rotation);
290
+ const wires = evaluateTripWires(cfg, usage, triage, pipeline, rotation, now);
291
+ const has = (d) => isRecord(d) && Object.keys(d).length > 0;
292
+ const sources = Object.fromEntries([
293
+ ['actions_usage', usage],
294
+ ['fleet_triage', triage],
295
+ ['issue_pipeline', pipeline],
296
+ ['token_rotation', rotation],
297
+ ].map(([name, data]) => [
298
+ name,
299
+ { present: has(data), age_days: sourceAgeDays(data, now) },
300
+ ]));
301
+ return {
302
+ generated_at: stampUtc(now),
303
+ sources,
304
+ scorecard,
305
+ trip_wires: wires,
306
+ tripped_count: wires.filter((w) => w.tripped).length,
307
+ note: HARNESS_NOTE,
308
+ };
309
+ }
310
+ // ── tolerant readers for the committed files ───────────────────────────────
311
+ /** Keep only the fields the engine reads (untrusted YAML never travels further). */
312
+ export function toActionsUsage(v) {
313
+ if (!isRecord(v))
314
+ return null;
315
+ const totals = isRecord(v.totals) ? v.totals : {};
316
+ const workflows = Array.isArray(v.workflows)
317
+ ? v.workflows.filter(isRecord).map((w) => ({
318
+ repo: typeof w.repo === 'string' ? w.repo : undefined,
319
+ workflow: typeof w.workflow === 'string' ? w.workflow : undefined,
320
+ path: typeof w.path === 'string' ? w.path : undefined,
321
+ avg_min: num(w.avg_min) ?? undefined,
322
+ runs: num(w.runs) ?? undefined,
323
+ success: num(w.success) ?? undefined,
324
+ external: w.external === true,
325
+ }))
326
+ : undefined;
327
+ return {
328
+ generated_at: typeof v.generated_at === 'string' ? v.generated_at : undefined,
329
+ totals: {
330
+ success_rate_pct: num(totals.success_rate_pct) ?? undefined,
331
+ effectiveness_pct: num(totals.effectiveness_pct) ?? undefined,
332
+ total_min: num(totals.total_min) ?? undefined,
333
+ waste_min: num(totals.waste_min) ?? undefined,
334
+ waste_hours: num(totals.waste_hours) ?? undefined,
335
+ },
336
+ ...(workflows ? { workflows } : {}),
337
+ };
338
+ }
339
+ export function toFleetTriage(v) {
340
+ if (!isRecord(v))
341
+ return null;
342
+ const totals = isRecord(v.totals) ? v.totals : {};
343
+ return {
344
+ generated_at: typeof v.generated_at === 'string' ? v.generated_at : undefined,
345
+ totals: {
346
+ failing_workflows: num(totals.failing_workflows) ?? undefined,
347
+ repos_red: num(totals.repos_red) ?? undefined,
348
+ },
349
+ };
350
+ }
351
+ export function toIssuePipeline(v) {
352
+ if (!isRecord(v))
353
+ return null;
354
+ const totals = isRecord(v.totals) ? v.totals : {};
355
+ const stages = {};
356
+ if (isRecord(totals.stages)) {
357
+ for (const [k, s] of Object.entries(totals.stages)) {
358
+ const n = num(s);
359
+ if (n !== null)
360
+ stages[k] = n;
361
+ }
362
+ }
363
+ return {
364
+ generated_at: typeof v.generated_at === 'string' ? v.generated_at : undefined,
365
+ totals: {
366
+ pipeline_prs: num(totals.pipeline_prs) ?? undefined,
367
+ ...(isRecord(totals.stages) ? { stages } : {}),
368
+ },
369
+ };
370
+ }
371
+ export function toTokenRotation(v) {
372
+ if (!isRecord(v))
373
+ return null;
374
+ const tokens = Array.isArray(v.tokens)
375
+ ? v.tokens.filter(isRecord).map((t) => ({
376
+ name: typeof t.name === 'string' ? t.name : undefined,
377
+ oldest_age_days: num(t.oldest_age_days) ?? undefined,
378
+ max_age_days: num(t.max_age_days) ?? undefined,
379
+ }))
380
+ : undefined;
381
+ return {
382
+ generated_at: typeof v.generated_at === 'string' ? v.generated_at : undefined,
383
+ ...(tokens ? { tokens } : {}),
384
+ };
385
+ }
386
+ // ── the committed harness_health.yml, read back ─────────────────────────────
387
+ /** The hub's committed `harness_health.yml` as data, or null when the document is not one. */
388
+ export function toHarnessHealth(v) {
389
+ if (!isRecord(v) || !isRecord(v.scorecard) || !Array.isArray(v.trip_wires))
390
+ return null;
391
+ const sc = v.scorecard;
392
+ const scorecard = Object.fromEntries(SCORECARD_KEYS.map((k) => {
393
+ const m = isRecord(sc[k]) ? sc[k] : {};
394
+ const metric = {
395
+ value: num(m.value),
396
+ direction: m.direction === 'up' || m.direction === 'down' ? m.direction : 'steady',
397
+ };
398
+ const threshold = num(m.threshold);
399
+ if (threshold !== null)
400
+ metric.threshold = threshold;
401
+ if (m.status === 'ok' || m.status === 'warn' || m.status === 'unknown')
402
+ metric.status = m.status;
403
+ return [k, metric];
404
+ }));
405
+ const wires = v.trip_wires.filter(isRecord).flatMap((w) => {
406
+ const id = WIRE_IDS.find((x) => x === w.id);
407
+ if (!id)
408
+ return [];
409
+ const wire = {
410
+ id,
411
+ tripped: w.tripped === true,
412
+ summary: typeof w.summary === 'string' ? w.summary : '',
413
+ };
414
+ if (Array.isArray(w.detail))
415
+ wire.detail = w.detail.filter(isRecord);
416
+ return [wire];
417
+ });
418
+ const src = isRecord(v.sources) ? v.sources : {};
419
+ const sources = Object.fromEntries(SIGNAL_NAMES.map((name) => {
420
+ const s = isRecord(src[name]) ? src[name] : {};
421
+ return [name, { present: s.present === true, age_days: num(s.age_days) }];
422
+ }));
423
+ return {
424
+ generated_at: typeof v.generated_at === 'string' ? v.generated_at : String(v.generated_at ?? ''),
425
+ sources,
426
+ scorecard,
427
+ trip_wires: wires,
428
+ tripped_count: num(v.tripped_count) ?? wires.filter((w) => w.tripped).length,
429
+ note: typeof v.note === 'string' ? v.note : '',
430
+ };
431
+ }
@@ -0,0 +1,45 @@
1
+ import type { HarnessConfig, HarnessHealth, HarnessSignals, SignalName } from './health.js';
2
+ import type { FleetManifest } from './lanes.js';
3
+ export declare const DEFAULT_HUB = "bamr87/bamr87";
4
+ /** What the hub commits and its consoles read. */
5
+ export declare const HUB_PATHS: {
6
+ readonly config: '_data/fleet.yml';
7
+ readonly health: '_data/harness_health.yml';
8
+ readonly manifest: 'fleet.manifest.yml';
9
+ readonly pulse: '.github/workflows/fleet-pulse.yml';
10
+ };
11
+ export declare const signalPath: (name: SignalName) => string;
12
+ /** The hub's five archify diagrams, as its /harness/ page lists them. */
13
+ export declare const HUB_DIAGRAMS: {
14
+ file: string;
15
+ title: string;
16
+ }[];
17
+ /** The hub's Pages URL for a path (`/harness/` by default); a user site drops the repo segment. */
18
+ export declare function hubPagesUrl(slug: string, path?: string): string | null;
19
+ export declare const hubDiagramUrl: (slug: string, file: string) => string | null;
20
+ export declare const hubFileUrl: (slug: string, path: string) => string;
21
+ /** Where a committed health file and an in-app recomputation disagree. */
22
+ export interface ParityDiff {
23
+ where: string;
24
+ committed: string;
25
+ recomputed: string;
26
+ }
27
+ /** One hub read: what was committed, the same engine re-run over the committed signals, the diff. */
28
+ export interface HubSnapshot {
29
+ slug: string;
30
+ fetchedAt: string;
31
+ /** The hub's `_data/fleet.yml` → `harness:` thresholds (its defaults when the file is absent). */
32
+ config: HarnessConfig;
33
+ /** The health file the hub committed, or null when it has not published one. */
34
+ committed: HarnessHealth | null;
35
+ /** The same engine run over the hub's committed signals, at the committed stamp. */
36
+ recomputed: HarnessHealth;
37
+ /** Where the committed file and the recomputation disagree (empty = parity). */
38
+ parity: ParityDiff[];
39
+ signals: HarnessSignals;
40
+ /** Which hub files were found, by path. */
41
+ found: Record<string, boolean>;
42
+ manifest: FleetManifest | null;
43
+ /** The fleet-pulse cron (the hub's daily loop), when its workflow file was readable. */
44
+ pulseCron: string | null;
45
+ }
@@ -0,0 +1,35 @@
1
+ // What the hub commits and a console reads — the paths, the Pages URLs, and the snapshot
2
+ // shape a hub read produces. Pure constants and types; the store that persists a hub slug
3
+ // stays in each consumer (GitFactory keeps its zustand store, an editor keeps its settings).
4
+ import { parseRepo } from '../github/types.js';
5
+ export const DEFAULT_HUB = 'bamr87/bamr87';
6
+ /** What the hub commits and its consoles read. */
7
+ export const HUB_PATHS = {
8
+ config: '_data/fleet.yml',
9
+ health: '_data/harness_health.yml',
10
+ manifest: 'fleet.manifest.yml',
11
+ pulse: '.github/workflows/fleet-pulse.yml',
12
+ };
13
+ export const signalPath = (name) => `_data/${name}.yml`;
14
+ /** The hub's five archify diagrams, as its /harness/ page lists them. */
15
+ export const HUB_DIAGRAMS = [
16
+ { file: 'harness-layers.architecture', title: 'The six layers, mapped onto the hub' },
17
+ { file: 'fleet-pulse.workflow', title: 'fleet-pulse.yml — the daily loop' },
18
+ { file: 'fleet-signals.dataflow', title: 'Fleet signals — from GitHub to the alarm panel' },
19
+ { file: 'issue-pipeline.lifecycle', title: 'issue-pipeline.yml — labels as state' },
20
+ {
21
+ file: 'repo-evolution.sequence',
22
+ title: 'repo-evolution.yml — a draft PR into a submodule’s own upstream',
23
+ },
24
+ ];
25
+ /** The hub's Pages URL for a path (`/harness/` by default); a user site drops the repo segment. */
26
+ export function hubPagesUrl(slug, path = '/harness/') {
27
+ const ref = parseRepo(slug);
28
+ if (!ref)
29
+ return null;
30
+ const site = `${ref.owner.toLowerCase()}.github.io`;
31
+ const base = ref.repo.toLowerCase() === site ? `https://${site}` : `https://${site}/${ref.repo}`;
32
+ return `${base}${path.startsWith('/') ? path : `/${path}`}`;
33
+ }
34
+ export const hubDiagramUrl = (slug, file) => hubPagesUrl(slug, `/diagrams/${file}.html`);
35
+ export const hubFileUrl = (slug, path) => `https://github.com/${slug}/blob/main/${path}`;
@@ -0,0 +1,7 @@
1
+ import { type GithubClient, type RepoRef } from '../github/types.js';
2
+ import { type HubSnapshot, type ParityDiff } from './hub-paths.js';
3
+ import { type HarnessHealth } from './health.js';
4
+ /** Where a committed health file and an in-app recomputation disagree. */
5
+ export declare function compareHealth(committed: HarnessHealth, recomputed: HarnessHealth): ParityDiff[];
6
+ /** Read a hub through a client: every file tolerant (missing or unreadable → absent), never throws for a file. */
7
+ export declare function readHub(client: GithubClient, ref: RepoRef, now?: Date): Promise<HubSnapshot>;
@@ -0,0 +1,101 @@
1
+ // The hub reader: eight tolerant reads through an injected GithubClient, the committed
2
+ // harness_health.yml read back, the same engine re-run over the committed signals, and a
3
+ // parity diff. I/O only through the client; no globals, no clock.
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { repoSlug } from '../github/types.js';
6
+ import { HUB_PATHS, signalPath } from './hub-paths.js';
7
+ import { DEFAULT_CONFIG, harnessHealth, parseGeneratedAt, parseHarnessConfig, SCORECARD_KEYS, SIGNAL_NAMES, toActionsUsage, toFleetTriage, toHarnessHealth, toIssuePipeline, toTokenRotation, } from './health.js';
8
+ import { parseFleetManifest } from './lanes.js';
9
+ /** Where a committed health file and an in-app recomputation disagree. */
10
+ export function compareHealth(committed, recomputed) {
11
+ const diffs = [];
12
+ const show = (v) => (v === null || v === undefined ? 'None' : String(v));
13
+ for (const k of SCORECARD_KEYS) {
14
+ const a = committed.scorecard[k];
15
+ const b = recomputed.scorecard[k];
16
+ if (show(a.value) !== show(b.value)) {
17
+ diffs.push({ where: `scorecard.${k}.value`, committed: show(a.value), recomputed: show(b.value) });
18
+ }
19
+ if ((a.status ?? 'unknown') !== (b.status ?? 'unknown')) {
20
+ diffs.push({
21
+ where: `scorecard.${k}.status`,
22
+ committed: a.status ?? 'unknown',
23
+ recomputed: b.status ?? 'unknown',
24
+ });
25
+ }
26
+ }
27
+ for (const w of recomputed.trip_wires) {
28
+ const c = committed.trip_wires.find((x) => x.id === w.id);
29
+ if (!c) {
30
+ diffs.push({ where: `trip_wires.${w.id}`, committed: 'absent', recomputed: w.tripped ? 'tripped' : 'armed' });
31
+ }
32
+ else if (c.tripped !== w.tripped) {
33
+ diffs.push({ where: `trip_wires.${w.id}.tripped`, committed: String(c.tripped), recomputed: String(w.tripped) });
34
+ }
35
+ else if (c.summary !== w.summary) {
36
+ diffs.push({ where: `trip_wires.${w.id}.summary`, committed: c.summary, recomputed: w.summary });
37
+ }
38
+ }
39
+ return diffs;
40
+ }
41
+ const CRON_RE = /cron:\s*(?:'([^']*)'|"([^"]*)"|([^#\n]+))/;
42
+ /** Read a hub through a client: every file tolerant (missing or unreadable → absent), never throws for a file. */
43
+ export async function readHub(client, ref, now = new Date()) {
44
+ const read = async (path) => {
45
+ try {
46
+ const f = await client.getFile(ref, path);
47
+ return f ? f.content : null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ };
53
+ const doc = (text) => {
54
+ if (text === null)
55
+ return null;
56
+ try {
57
+ return parseYaml(text);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ };
63
+ const paths = [
64
+ HUB_PATHS.config,
65
+ HUB_PATHS.health,
66
+ HUB_PATHS.manifest,
67
+ HUB_PATHS.pulse,
68
+ ...SIGNAL_NAMES.map(signalPath),
69
+ ];
70
+ const texts = await Promise.all(paths.map(read));
71
+ const byPath = new Map(paths.map((p, i) => [p, texts[i]]));
72
+ const text = (p) => byPath.get(p) ?? null;
73
+ const found = Object.fromEntries(paths.map((p) => [p, text(p) !== null]));
74
+ const configDoc = doc(text(HUB_PATHS.config));
75
+ const config = configDoc ? parseHarnessConfig(configDoc) : DEFAULT_CONFIG;
76
+ const signals = {
77
+ actions_usage: toActionsUsage(doc(text(signalPath('actions_usage')))),
78
+ fleet_triage: toFleetTriage(doc(text(signalPath('fleet_triage')))),
79
+ issue_pipeline: toIssuePipeline(doc(text(signalPath('issue_pipeline')))),
80
+ token_rotation: toTokenRotation(doc(text(signalPath('token_rotation')))),
81
+ };
82
+ const committed = toHarnessHealth(doc(text(HUB_PATHS.health)));
83
+ const stamp = committed ? parseGeneratedAt(committed.generated_at) : null;
84
+ const recomputed = harnessHealth(signals, config, stamp ?? now);
85
+ const manifestText = text(HUB_PATHS.manifest);
86
+ const manifest = manifestText ? parseFleetManifest(manifestText) : null;
87
+ const pulse = text(HUB_PATHS.pulse);
88
+ const cron = pulse ? CRON_RE.exec(pulse) : null;
89
+ return {
90
+ slug: repoSlug(ref),
91
+ fetchedAt: now.toISOString(),
92
+ config,
93
+ committed,
94
+ recomputed,
95
+ parity: committed ? compareHealth(committed, recomputed) : [],
96
+ signals,
97
+ found,
98
+ manifest,
99
+ pulseCron: cron ? (cron[1] ?? cron[2] ?? cron[3] ?? '').trim() || null : null,
100
+ };
101
+ }
@@ -0,0 +1,45 @@
1
+ export type LaneKind = 'content' | 'triage' | 'review' | 'maintenance' | 'analysis' | 'orchestrator' | 'fanout' | 'mention' | 'other';
2
+ export declare const LANE_KINDS: LaneKind[];
3
+ export type LaneHarness = 'claude-code-action' | 'claude-cli' | 'wtd-fleet' | 'engine' | 'none';
4
+ export declare const LANE_HARNESSES: LaneHarness[];
5
+ export type LaneTrigger = {
6
+ kind: 'schedule';
7
+ cron: string;
8
+ } | {
9
+ kind: 'dispatch';
10
+ } | {
11
+ kind: 'event';
12
+ events: string[];
13
+ };
14
+ export interface LaneGuardrails {
15
+ never_merges: boolean;
16
+ opens_pull_requests?: boolean;
17
+ writable_paths?: string[];
18
+ max_writes_per_run?: number;
19
+ }
20
+ export interface FleetLane {
21
+ id: string;
22
+ kind: LaneKind;
23
+ harness: LaneHarness;
24
+ implementation: string;
25
+ description: string;
26
+ triggers: LaneTrigger[];
27
+ /** The repo variable that switches the lane, or null. */
28
+ switch: string | null;
29
+ uses_tokens: string[];
30
+ guardrails: LaneGuardrails;
31
+ state_paths?: string[];
32
+ }
33
+ export interface FleetManifest {
34
+ spec_version: string;
35
+ repo: string;
36
+ provenance: 'declared' | 'derived' | 'unknown';
37
+ summary: string;
38
+ lanes: FleetLane[];
39
+ /** Lines the parser could not read as lanes (kept as counts, never as content). */
40
+ skipped: number;
41
+ }
42
+ /** Parse a `fleet.manifest.yml`. Never throws: junk yields an empty manifest with `skipped`. */
43
+ export declare function parseFleetManifest(text: string): FleetManifest;
44
+ /** The lane whose implementation is this workflow path, or whose id is its basename. */
45
+ export declare function laneForPath(manifest: FleetManifest | null, path: string): FleetLane | null;