@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,92 @@
1
+ // The `fleet/v1` lane — the fleet's interchange contract. Every fleet repo commits a
2
+ // `fleet.manifest.yml` in this vocabulary (bamr87/wtd's FLEET-SPEC; `wtd fleet adopt`
3
+ // derives it, the hub and its consoles read it), so a lane designed in one tool and
4
+ // inventoried in another is the same record. `parseFleetManifest` reads any repo's manifest
5
+ // tolerantly; the blueprint → lanes direction lives with GitFactory's compiler, not here.
6
+ // Pure; manifest text is untrusted data.
7
+ import { parse as parseYaml } from 'yaml';
8
+ export const LANE_KINDS = [
9
+ 'content',
10
+ 'triage',
11
+ 'review',
12
+ 'maintenance',
13
+ 'analysis',
14
+ 'orchestrator',
15
+ 'fanout',
16
+ 'mention',
17
+ 'other',
18
+ ];
19
+ export const LANE_HARNESSES = ['claude-code-action', 'claude-cli', 'wtd-fleet', 'engine', 'none'];
20
+ // ── manifest → lanes ────────────────────────────────────────────────────────
21
+ const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
22
+ const strList = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
23
+ function laneFrom(v) {
24
+ if (!isRecord(v) || typeof v.id !== 'string' || !v.id.trim())
25
+ return null;
26
+ const kind = LANE_KINDS.includes(v.kind) ? v.kind : 'other';
27
+ const harness = LANE_HARNESSES.includes(v.harness)
28
+ ? v.harness
29
+ : 'none';
30
+ const triggers = [];
31
+ for (const t of Array.isArray(v.triggers) ? v.triggers : []) {
32
+ if (!isRecord(t))
33
+ continue;
34
+ if (t.kind === 'schedule' && typeof t.cron === 'string')
35
+ triggers.push({ kind: 'schedule', cron: t.cron });
36
+ else if (t.kind === 'dispatch')
37
+ triggers.push({ kind: 'dispatch' });
38
+ else if (t.kind === 'event')
39
+ triggers.push({ kind: 'event', events: strList(t.events) });
40
+ }
41
+ const g = isRecord(v.guardrails) ? v.guardrails : {};
42
+ const guardrails = { never_merges: g.never_merges !== false };
43
+ if (typeof g.opens_pull_requests === 'boolean')
44
+ guardrails.opens_pull_requests = g.opens_pull_requests;
45
+ if (Array.isArray(g.writable_paths))
46
+ guardrails.writable_paths = strList(g.writable_paths);
47
+ if (typeof g.max_writes_per_run === 'number')
48
+ guardrails.max_writes_per_run = g.max_writes_per_run;
49
+ return {
50
+ id: v.id.trim(),
51
+ kind,
52
+ harness,
53
+ implementation: typeof v.implementation === 'string' ? v.implementation : '',
54
+ description: typeof v.description === 'string' ? v.description : v.id.trim(),
55
+ triggers,
56
+ switch: typeof v.switch === 'string' && v.switch.trim() ? v.switch.trim() : null,
57
+ uses_tokens: strList(v.uses_tokens).sort(),
58
+ guardrails,
59
+ ...(Array.isArray(v.state_paths) ? { state_paths: strList(v.state_paths) } : {}),
60
+ };
61
+ }
62
+ /** Parse a `fleet.manifest.yml`. Never throws: junk yields an empty manifest with `skipped`. */
63
+ export function parseFleetManifest(text) {
64
+ let doc = null;
65
+ try {
66
+ doc = parseYaml(text);
67
+ }
68
+ catch {
69
+ doc = null;
70
+ }
71
+ const root = isRecord(doc) ? doc : {};
72
+ const rawLanes = Array.isArray(root.lanes) ? root.lanes : [];
73
+ const lanes = rawLanes.map(laneFrom).filter((l) => !!l);
74
+ const provenance = root.provenance === 'declared' || root.provenance === 'derived' ? root.provenance : 'unknown';
75
+ return {
76
+ spec_version: typeof root.spec_version === 'string' ? root.spec_version : '',
77
+ repo: typeof root.repo === 'string' ? root.repo : '',
78
+ provenance,
79
+ summary: typeof root.summary === 'string' ? root.summary : '',
80
+ lanes,
81
+ skipped: rawLanes.length - lanes.length,
82
+ };
83
+ }
84
+ /** The lane whose implementation is this workflow path, or whose id is its basename. */
85
+ export function laneForPath(manifest, path) {
86
+ if (!manifest)
87
+ return null;
88
+ const base = path.split('/').pop()?.replace(/\.ya?ml$/i, '') ?? path;
89
+ return (manifest.lanes.find((l) => l.implementation === path) ??
90
+ manifest.lanes.find((l) => l.id === base) ??
91
+ null);
92
+ }
@@ -0,0 +1,12 @@
1
+ import type { FleetLane } from './lanes.js';
2
+ export interface ManifestMeta {
3
+ repo: string;
4
+ summary: string;
5
+ provenance?: 'declared' | 'derived';
6
+ /** Comment lines (without `# `) written above the document. */
7
+ header?: string[];
8
+ }
9
+ /** A `fleet.manifest.yml` document for a set of lanes (lanes + the token contract), as text. */
10
+ export declare function toFleetManifestYaml(lanes: FleetLane[], meta: ManifestMeta): string;
11
+ /** A small, deterministic YAML emitter for the manifest (block style, quoted where needed). */
12
+ export declare function yamlDump(value: unknown, indent?: number): string;
@@ -0,0 +1,69 @@
1
+ // The fleet/v1 manifest as text: a `fleet.manifest.yml` document for a set of lanes, and the
2
+ // small deterministic YAML emitter behind it. Kept apart from lanes.ts so a reader that only
3
+ // parses does not carry the emitter.
4
+ /** A `fleet.manifest.yml` document for a set of lanes (lanes + the token contract), as text. */
5
+ export function toFleetManifestYaml(lanes, meta) {
6
+ const { repo } = meta;
7
+ const tokens = [...new Set(lanes.flatMap((l) => l.uses_tokens))].sort();
8
+ const doc = {
9
+ spec_version: 'fleet/v1',
10
+ repo,
11
+ provenance: meta.provenance ?? 'derived',
12
+ summary: meta.summary,
13
+ lanes,
14
+ tokens: tokens.map((name) => ({
15
+ name,
16
+ scope: 'fleet',
17
+ required: name === 'CLAUDE_CODE_OAUTH_TOKEN',
18
+ purpose: TOKEN_PURPOSE[name] ?? 'Declared by a machine on the floor.',
19
+ used_by: lanes.filter((l) => l.uses_tokens.includes(name)).map((l) => l.id),
20
+ })),
21
+ };
22
+ const header = meta.header ?? ["fleet.manifest.yml — this repository's AI fleet, in the shared 'fleet/v1' vocabulary."];
23
+ return `${header.map((h) => `# ${h}\n`).join('')}${yamlDump(doc)}`;
24
+ }
25
+ const TOKEN_PURPOSE = {
26
+ CLAUDE_CODE_OAUTH_TOKEN: 'Preferred Claude auth (house convention: OAuth first). Produced by `claude setup-token`.',
27
+ ANTHROPIC_API_KEY: 'Fallback Claude auth, used only when the OAuth token is absent.',
28
+ FLEET_TOKEN: 'Fine-grained PAT for cross-repo writes.',
29
+ };
30
+ /** A small, deterministic YAML emitter for the manifest (block style, quoted where needed). */
31
+ export function yamlDump(value, indent = 0) {
32
+ const pad = ' '.repeat(indent);
33
+ if (Array.isArray(value)) {
34
+ if (!value.length)
35
+ return `${pad}[]\n`;
36
+ return value
37
+ .map((item) => {
38
+ if (item && typeof item === 'object') {
39
+ const body = yamlDump(item, indent + 1).replace(/^\s*/, '');
40
+ return `${pad}- ${body}`;
41
+ }
42
+ return `${pad}- ${scalar(item)}\n`;
43
+ })
44
+ .join('');
45
+ }
46
+ if (value && typeof value === 'object') {
47
+ return Object.entries(value)
48
+ .map(([k, v]) => {
49
+ if (v && typeof v === 'object') {
50
+ if (Array.isArray(v) && !v.length)
51
+ return `${pad}${k}: []\n`;
52
+ return `${pad}${k}:\n${yamlDump(v, indent + 1)}`;
53
+ }
54
+ return `${pad}${k}: ${scalar(v)}\n`;
55
+ })
56
+ .join('');
57
+ }
58
+ return `${pad}${scalar(value)}\n`;
59
+ }
60
+ function scalar(v) {
61
+ if (v === null || v === undefined)
62
+ return 'null';
63
+ if (typeof v === 'boolean' || typeof v === 'number')
64
+ return String(v);
65
+ const s = String(v);
66
+ return /^[A-Za-z0-9_./@-][A-Za-z0-9_./@ -]*$/.test(s) && !/^(true|false|null|yes|no)$/i.test(s)
67
+ ? s
68
+ : JSON.stringify(s);
69
+ }
@@ -0,0 +1,32 @@
1
+ import type { FactoryRun } from '../github/types.js';
2
+ import type { MetricsRollup } from '../fleet/types.js';
3
+ import type { ActionsUsage, FleetTriage, HarnessSignals } from './health.js';
4
+ /** The slice of a Fleet Ops metrics row the adapter needs (`FleetWorkflowMetrics` fits). */
5
+ export interface LiveWorkflowMetrics {
6
+ repo: string;
7
+ displayPath: string;
8
+ name: string;
9
+ runs: number;
10
+ avgMin: number;
11
+ success: number;
12
+ }
13
+ /** The slice of a scanned repo the adapter needs (`RepoSnapshot` fits). */
14
+ export interface LiveRepo {
15
+ slug: string;
16
+ status: 'idle' | 'loading' | 'ready' | 'error';
17
+ runs: FactoryRun[];
18
+ }
19
+ export interface LiveScan {
20
+ /** Fleet-wide rollup (Fleet Ops' `fleetMetrics` rollup). */
21
+ rollup: MetricsRollup | null;
22
+ workflows: LiveWorkflowMetrics[];
23
+ repos: LiveRepo[];
24
+ /** When the scan finished; becomes every signal's `generated_at`. */
25
+ scannedAt: Date;
26
+ }
27
+ /** The hub's `actions_usage.yml` totals + per-workflow rows, from a live scan. */
28
+ export declare function liveActionsUsage(scan: LiveScan): ActionsUsage | null;
29
+ /** The hub's `fleet_triage.yml` totals the scorecard reads: standing red workflows and red repos. */
30
+ export declare function liveFleetTriage(scan: LiveScan): FleetTriage | null;
31
+ /** All four signals from a live scan; the hub-only loops are honestly absent. */
32
+ export declare function liveSignals(scan: LiveScan): HarnessSignals;
@@ -0,0 +1,74 @@
1
+ // Live signals (specs/014-harness-parity, FR-3): the hub's four signal shapes, built from a
2
+ // Fleet Ops scan instead of the hub's committed files, so the same `harnessHealth()` runs
3
+ // over a live fleet. Only `actions_usage` and `fleet_triage` can be derived from a scan;
4
+ // `issue_pipeline` and `token_rotation` are the hub's own loops and report as missing —
5
+ // exactly what the hub's generator would say if those files were absent. Pure.
6
+ import { stampUtc } from './health.js';
7
+ const round1 = (x) => Math.round(x * 10) / 10;
8
+ /** The hub's `actions_usage.yml` totals + per-workflow rows, from a live scan. */
9
+ export function liveActionsUsage(scan) {
10
+ if (!scan.rollup)
11
+ return null;
12
+ const r = scan.rollup;
13
+ const workflows = scan.workflows.map((w) => ({
14
+ repo: w.repo.split('/').pop() ?? w.repo,
15
+ workflow: w.name,
16
+ path: w.displayPath,
17
+ avg_min: round1(w.avgMin),
18
+ runs: w.runs,
19
+ success: w.success,
20
+ external: false,
21
+ }));
22
+ return {
23
+ generated_at: stampUtc(scan.scannedAt),
24
+ totals: {
25
+ success_rate_pct: round1(r.successRatePct),
26
+ effectiveness_pct: round1(r.effectivenessPct),
27
+ total_min: round1(r.totalMin),
28
+ waste_min: round1(r.wasteMin),
29
+ waste_hours: round1(r.wasteMin / 60),
30
+ },
31
+ workflows,
32
+ };
33
+ }
34
+ /** Newest run per workflow path, per repo. */
35
+ function latestByPath(runs) {
36
+ const m = new Map();
37
+ for (const run of runs) {
38
+ const cur = m.get(run.path);
39
+ if (!cur || run.createdAt > cur.createdAt)
40
+ m.set(run.path, run);
41
+ }
42
+ return m;
43
+ }
44
+ /** The hub's `fleet_triage.yml` totals the scorecard reads: standing red workflows and red repos. */
45
+ export function liveFleetTriage(scan) {
46
+ const ready = scan.repos.filter((r) => r.status === 'ready');
47
+ if (!ready.length)
48
+ return null;
49
+ let failing = 0;
50
+ let reposRed = 0;
51
+ for (const repo of ready) {
52
+ let red = 0;
53
+ for (const run of latestByPath(repo.runs).values()) {
54
+ if (run.status === 'completed' && run.conclusion === 'failure')
55
+ red += 1;
56
+ }
57
+ failing += red;
58
+ if (red > 0)
59
+ reposRed += 1;
60
+ }
61
+ return {
62
+ generated_at: stampUtc(scan.scannedAt),
63
+ totals: { failing_workflows: failing, repos_red: reposRed },
64
+ };
65
+ }
66
+ /** All four signals from a live scan; the hub-only loops are honestly absent. */
67
+ export function liveSignals(scan) {
68
+ return {
69
+ actions_usage: liveActionsUsage(scan),
70
+ fleet_triage: liveFleetTriage(scan),
71
+ issue_pipeline: null,
72
+ token_rotation: null,
73
+ };
74
+ }
@@ -0,0 +1,15 @@
1
+ export * from './github/types.js';
2
+ export * from './github/telemetry.js';
3
+ export * from './fleet/types.js';
4
+ export * from './fleet/manifest.js';
5
+ export * from './fleet/parse.js';
6
+ export * from './fleet/facts.js';
7
+ export * from './fleet/audit.js';
8
+ export * from './fleet/metrics.js';
9
+ export * from './fleet/import.js';
10
+ export * from './harness/lanes.js';
11
+ export * from './harness/manifest-yaml.js';
12
+ export * from './harness/health.js';
13
+ export * from './harness/signals.js';
14
+ export * from './harness/hub-paths.js';
15
+ export * from './harness/hubread.js';
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ // @bamr87/fleet-engines — the fleet's pure engines, versioned once and consumed by dependency:
2
+ // the fleet/v1 manifest, workflow facts, the audit rulebook, metrics, the harness scorecard
3
+ // and trip wires, and the GithubClient contract every console implements over its own fetch.
4
+ // No I/O of its own: everything that reads GitHub takes a client you provide.
5
+ export * from './github/types.js';
6
+ export * from './github/telemetry.js';
7
+ export * from './fleet/types.js';
8
+ export * from './fleet/manifest.js';
9
+ export * from './fleet/parse.js';
10
+ export * from './fleet/facts.js';
11
+ export * from './fleet/audit.js';
12
+ export * from './fleet/metrics.js';
13
+ export * from './fleet/import.js';
14
+ export * from './harness/lanes.js';
15
+ export * from './harness/manifest-yaml.js';
16
+ export * from './harness/health.js';
17
+ export * from './harness/signals.js';
18
+ export * from './harness/hub-paths.js';
19
+ export * from './harness/hubread.js';
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@bamr87/fleet-engines",
3
+ "version": "0.1.0",
4
+ "description": "The fleet's pure engines, versioned once: fleet/v1 manifest parse + emit, workflow facts, the audit rulebook, metrics, the harness scorecard and trip wires, and the GithubClient contract every console implements over its own fetch. Consumed by dependency by GitFactory and zer0-CMS.",
5
+ "license": "MIT",
6
+ "author": "bamr87",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/bamr87/bamr87.git",
10
+ "directory": "templates/fleet-engines"
11
+ },
12
+ "homepage": "https://github.com/bamr87/bamr87/tree/main/templates/fleet-engines",
13
+ "keywords": [
14
+ "fleet",
15
+ "github-actions",
16
+ "harness",
17
+ "audit",
18
+ "fleet-manifest"
19
+ ],
20
+ "type": "module",
21
+ "main": "dist/index.js",
22
+ "types": "dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist/",
31
+ "README.md",
32
+ "VERSION"
33
+ ],
34
+ "sideEffects": false,
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "engines": {
39
+ "node": ">=22"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.build.json",
43
+ "prepack": "npm run build",
44
+ "test": "vitest run",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit"
46
+ },
47
+ "dependencies": {
48
+ "yaml": ">=2.9.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": ">=22",
52
+ "typescript": ">=5.9",
53
+ "vitest": ">=3"
54
+ }
55
+ }