@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.
- package/README.md +43 -0
- package/VERSION +31 -0
- package/dist/fleet/audit.d.ts +26 -0
- package/dist/fleet/audit.js +467 -0
- package/dist/fleet/facts.d.ts +25 -0
- package/dist/fleet/facts.js +617 -0
- package/dist/fleet/import.d.ts +17 -0
- package/dist/fleet/import.js +74 -0
- package/dist/fleet/manifest.d.ts +23 -0
- package/dist/fleet/manifest.js +118 -0
- package/dist/fleet/metrics.d.ts +45 -0
- package/dist/fleet/metrics.js +284 -0
- package/dist/fleet/parse.d.ts +15 -0
- package/dist/fleet/parse.js +298 -0
- package/dist/fleet/types.d.ts +250 -0
- package/dist/fleet/types.js +7 -0
- package/dist/github/telemetry.d.ts +40 -0
- package/dist/github/telemetry.js +131 -0
- package/dist/github/types.d.ts +240 -0
- package/dist/github/types.js +36 -0
- package/dist/harness/health.d.ts +135 -0
- package/dist/harness/health.js +431 -0
- package/dist/harness/hub-paths.d.ts +45 -0
- package/dist/harness/hub-paths.js +35 -0
- package/dist/harness/hubread.d.ts +7 -0
- package/dist/harness/hubread.js +101 -0
- package/dist/harness/lanes.d.ts +45 -0
- package/dist/harness/lanes.js +92 -0
- package/dist/harness/manifest-yaml.d.ts +12 -0
- package/dist/harness/manifest-yaml.js +69 -0
- package/dist/harness/signals.d.ts +32 -0
- package/dist/harness/signals.js +74 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +19 -0
- package/package.json +55 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Pure telemetry mapping + stats (seed 04).
|
|
2
|
+
// NO network I/O: this consumes already-fetched FactoryRun[] (from the transport client's
|
|
3
|
+
// listFactoryRuns) and derives LED states + dashboard/per-line statistics. Keeping it pure
|
|
4
|
+
// keeps it deterministic and trivially testable — same rules as the compiler and the deploy
|
|
5
|
+
// planner (CLAUDE.md golden rule #3).
|
|
6
|
+
//
|
|
7
|
+
// Everything here parses fixed ISO strings only; no Date.now()/new Date() (argless) so the
|
|
8
|
+
// same input always yields the same output.
|
|
9
|
+
/** Conclusions that light a machine/line LED red (ARCHITECTURE §6): a run that failed to succeed. */
|
|
10
|
+
const BAD_CONCLUSIONS = new Set([
|
|
11
|
+
'failure',
|
|
12
|
+
'timed_out',
|
|
13
|
+
'startup_failure',
|
|
14
|
+
'stale',
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Conclusions counted as a hard failure in aggregate stats. Narrower than {@link BAD_CONCLUSIONS}:
|
|
18
|
+
* `stale` glows red on a LED but is not a run-the-numbers failure (it never really executed).
|
|
19
|
+
*/
|
|
20
|
+
const FAILURE_CONCLUSIONS = new Set([
|
|
21
|
+
'failure',
|
|
22
|
+
'timed_out',
|
|
23
|
+
'startup_failure',
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Map a single run (or its absence) to a LED colour.
|
|
27
|
+
* - no run yet → `idle`
|
|
28
|
+
* - still queued/in progress → `warn`
|
|
29
|
+
* - completed successfully → `on`
|
|
30
|
+
* - completed with a failing conclusion → `bad`
|
|
31
|
+
* - completed but cancelled/skipped/neutral/action_required/unknown → `idle`
|
|
32
|
+
*/
|
|
33
|
+
export function ledForRun(run) {
|
|
34
|
+
if (!run)
|
|
35
|
+
return 'idle';
|
|
36
|
+
if (run.status !== 'completed')
|
|
37
|
+
return 'warn';
|
|
38
|
+
if (run.conclusion === 'success')
|
|
39
|
+
return 'on';
|
|
40
|
+
if (BAD_CONCLUSIONS.has(run.conclusion))
|
|
41
|
+
return 'bad';
|
|
42
|
+
return 'idle';
|
|
43
|
+
}
|
|
44
|
+
/** True when `a` is newer than `b`: later createdAt, tie-broken by larger runId. */
|
|
45
|
+
function isNewer(a, b) {
|
|
46
|
+
if (a.createdAt !== b.createdAt)
|
|
47
|
+
return a.createdAt > b.createdAt;
|
|
48
|
+
return a.runId > b.runId;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The newest run per assembly-line slug. "Newest" = max ISO `createdAt` (lexical compare is
|
|
52
|
+
* correct for well-formed UTC timestamps), tie-broken by the larger `runId`. Deterministic.
|
|
53
|
+
*/
|
|
54
|
+
export function latestBySlug(runs) {
|
|
55
|
+
const latest = new Map();
|
|
56
|
+
for (const run of runs) {
|
|
57
|
+
const current = latest.get(run.slug);
|
|
58
|
+
if (!current || isNewer(run, current))
|
|
59
|
+
latest.set(run.slug, run);
|
|
60
|
+
}
|
|
61
|
+
return latest;
|
|
62
|
+
}
|
|
63
|
+
/** The LED colour for each line's latest run. */
|
|
64
|
+
export function statusBySlug(runs) {
|
|
65
|
+
const statuses = new Map();
|
|
66
|
+
for (const [slug, run] of latestBySlug(runs))
|
|
67
|
+
statuses.set(slug, ledForRun(run));
|
|
68
|
+
return statuses;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Wall-clock seconds a run took: `updatedAt - (runStartedAt ?? createdAt)`, clamped to `>= 0`
|
|
72
|
+
* (a run whose start stamp lands after its update stamp reports 0 rather than a negative). Parses
|
|
73
|
+
* fixed ISO strings only.
|
|
74
|
+
*/
|
|
75
|
+
function durationSec(run) {
|
|
76
|
+
const start = Date.parse(run.runStartedAt ?? run.createdAt);
|
|
77
|
+
const end = Date.parse(run.updatedAt);
|
|
78
|
+
return Math.max(0, (end - start) / 1000);
|
|
79
|
+
}
|
|
80
|
+
/** The p50 (median) of a set of numbers, or null if empty. Even counts average the two middles. */
|
|
81
|
+
function median(values) {
|
|
82
|
+
if (values.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
85
|
+
const mid = Math.floor(sorted.length / 2);
|
|
86
|
+
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
87
|
+
}
|
|
88
|
+
/** Success rate (0..1, or null with no completed runs) + p50 duration over a run group's completed runs. */
|
|
89
|
+
function rateAndP50(runs) {
|
|
90
|
+
const completed = runs.filter((r) => r.status === 'completed');
|
|
91
|
+
const success = completed.filter((r) => r.conclusion === 'success').length;
|
|
92
|
+
return {
|
|
93
|
+
successRate: completed.length ? success / completed.length : null,
|
|
94
|
+
p50DurationSec: median(completed.map(durationSec)),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Aggregate stats across all runs. Pure; counts derive from the run `status`/`conclusion` fields. */
|
|
98
|
+
export function dashboardStats(runs) {
|
|
99
|
+
const completedRuns = runs.filter((r) => r.status === 'completed');
|
|
100
|
+
const success = completedRuns.filter((r) => r.conclusion === 'success').length;
|
|
101
|
+
const failure = completedRuns.filter((r) => FAILURE_CONCLUSIONS.has(r.conclusion)).length;
|
|
102
|
+
const inProgress = runs.filter((r) => r.status !== 'completed').length;
|
|
103
|
+
const { successRate, p50DurationSec } = rateAndP50(runs);
|
|
104
|
+
return {
|
|
105
|
+
total: runs.length,
|
|
106
|
+
completed: completedRuns.length,
|
|
107
|
+
success,
|
|
108
|
+
failure,
|
|
109
|
+
inProgress,
|
|
110
|
+
successRate,
|
|
111
|
+
p50DurationSec,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** One {@link LineStat} per slug, sorted by slug ascending. Pure and deterministic. */
|
|
115
|
+
export function statsByLine(runs) {
|
|
116
|
+
const groups = new Map();
|
|
117
|
+
for (const run of runs) {
|
|
118
|
+
const group = groups.get(run.slug);
|
|
119
|
+
if (group)
|
|
120
|
+
group.push(run);
|
|
121
|
+
else
|
|
122
|
+
groups.set(run.slug, [run]);
|
|
123
|
+
}
|
|
124
|
+
const rows = [];
|
|
125
|
+
for (const [slug, groupRuns] of groups) {
|
|
126
|
+
const { successRate, p50DurationSec } = rateAndP50(groupRuns);
|
|
127
|
+
rows.push({ slug, total: groupRuns.length, successRate, p50DurationSec });
|
|
128
|
+
}
|
|
129
|
+
rows.sort((a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0));
|
|
130
|
+
return rows;
|
|
131
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/** An `owner/repo` pair. */
|
|
2
|
+
export interface RepoRef {
|
|
3
|
+
owner: string;
|
|
4
|
+
repo: string;
|
|
5
|
+
}
|
|
6
|
+
/** Parse `"owner/repo"` (tolerating a full GitHub URL or surrounding whitespace). */
|
|
7
|
+
export declare function parseRepo(input: string): RepoRef | null;
|
|
8
|
+
/** `owner/repo` string form. */
|
|
9
|
+
export declare function repoSlug(r: RepoRef): string;
|
|
10
|
+
/** Per-permission preflight state. `unknown` = we could not determine it. */
|
|
11
|
+
export type PermState = 'ok' | 'missing' | 'unknown';
|
|
12
|
+
/**
|
|
13
|
+
* Result of checking a fine-grained PAT against a specific repo. The app needs, at a
|
|
14
|
+
* minimum, Contents (RW) + Workflows (RW) to deploy; the rest gate optional features.
|
|
15
|
+
*/
|
|
16
|
+
export interface Preflight {
|
|
17
|
+
repo: RepoRef;
|
|
18
|
+
contents: PermState;
|
|
19
|
+
workflows: PermState;
|
|
20
|
+
issues: PermState;
|
|
21
|
+
pullRequests: PermState;
|
|
22
|
+
actions: PermState;
|
|
23
|
+
secrets: PermState;
|
|
24
|
+
/** True when the minimum-to-deploy permissions (contents + workflows) are present. */
|
|
25
|
+
canDeploy: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** A file read from the repo. `sha` is the blob sha required to update or delete it. */
|
|
28
|
+
export interface RepoFile {
|
|
29
|
+
path: string;
|
|
30
|
+
/** Decoded UTF-8 text. */
|
|
31
|
+
content: string;
|
|
32
|
+
sha: string;
|
|
33
|
+
}
|
|
34
|
+
/** One entry in a directory listing. */
|
|
35
|
+
export interface RepoDirEntry {
|
|
36
|
+
path: string;
|
|
37
|
+
name: string;
|
|
38
|
+
sha: string;
|
|
39
|
+
type: 'file' | 'dir';
|
|
40
|
+
}
|
|
41
|
+
/** GitHub Actions repo public key for sealed-box secret encryption. */
|
|
42
|
+
export interface ActionsPublicKey {
|
|
43
|
+
key: string;
|
|
44
|
+
keyId: string;
|
|
45
|
+
}
|
|
46
|
+
/** Where a deploy commit lands. `commit` = default branch; `pr` = new branch + PR. */
|
|
47
|
+
export type DeployMode = 'commit' | 'pr';
|
|
48
|
+
/** A single file the deploy will create or update. */
|
|
49
|
+
export interface DeployWrite {
|
|
50
|
+
path: string;
|
|
51
|
+
content: string;
|
|
52
|
+
/** Blob sha of the existing file, when updating; omitted when creating. */
|
|
53
|
+
sha?: string;
|
|
54
|
+
reason: 'blueprint' | 'workflow' | 'prompt';
|
|
55
|
+
}
|
|
56
|
+
/** A file the deploy will delete (an assembly line that no longer exists). */
|
|
57
|
+
export interface DeployDelete {
|
|
58
|
+
path: string;
|
|
59
|
+
sha: string;
|
|
60
|
+
reason: 'stale-workflow';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The full set of changes a deploy will make, computed by diffing freshly compiled
|
|
64
|
+
* output against what is currently in the repo. Rendered as a review diff before commit.
|
|
65
|
+
*/
|
|
66
|
+
export interface DeployPlan {
|
|
67
|
+
writes: DeployWrite[];
|
|
68
|
+
deletes: DeployDelete[];
|
|
69
|
+
/** Non-fatal notes, e.g. a referenced prompt file that is missing from the repo. */
|
|
70
|
+
warnings: string[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Drift for one deployed workflow: the blueprint hash embedded in its generated header
|
|
74
|
+
* versus the hash of the current blueprint's freshly compiled output.
|
|
75
|
+
*/
|
|
76
|
+
export interface DriftItem {
|
|
77
|
+
path: string;
|
|
78
|
+
/** Hash parsed from the deployed file's header, or null if unparseable/absent. */
|
|
79
|
+
deployedHash: string | null;
|
|
80
|
+
compiledHash: string;
|
|
81
|
+
drifted: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** Machine/line LED semantics (ARCHITECTURE §6): success / running / failed / never-ran. */
|
|
84
|
+
export type LedState = 'on' | 'warn' | 'bad' | 'idle';
|
|
85
|
+
/** A conclusion string from the Actions API, or null while a run is still in progress. */
|
|
86
|
+
export type RunConclusion = 'success' | 'failure' | 'cancelled' | 'timed_out' | 'skipped' | 'action_required' | 'neutral' | 'stale' | 'startup_failure' | null;
|
|
87
|
+
/** One GitHub Actions run of a `factory--*` workflow. */
|
|
88
|
+
export interface FactoryRun {
|
|
89
|
+
/** Workflow file path, e.g. `.github/workflows/factory--issue-triage-line.yml`. */
|
|
90
|
+
path: string;
|
|
91
|
+
/** Slug parsed from the path, e.g. `issue-triage-line`. */
|
|
92
|
+
slug: string;
|
|
93
|
+
runId: number;
|
|
94
|
+
runNumber: number;
|
|
95
|
+
status: 'queued' | 'in_progress' | 'completed' | (string & {});
|
|
96
|
+
conclusion: RunConclusion;
|
|
97
|
+
event: string;
|
|
98
|
+
htmlUrl: string;
|
|
99
|
+
/** ISO timestamps; runStartedAt is null before the run actually starts. */
|
|
100
|
+
runStartedAt: string | null;
|
|
101
|
+
updatedAt: string;
|
|
102
|
+
createdAt: string;
|
|
103
|
+
/** 1 for a first attempt; >1 means the run was re-run (the rework signal). */
|
|
104
|
+
runAttempt: number;
|
|
105
|
+
}
|
|
106
|
+
/** One registered workflow from the Actions workflows API (id needed to enable/disable). */
|
|
107
|
+
export interface RepoWorkflow {
|
|
108
|
+
id: number;
|
|
109
|
+
name: string;
|
|
110
|
+
/** `.github/workflows/<file>.yml`, or `dynamic/…` for GitHub-injected workflows. */
|
|
111
|
+
path: string;
|
|
112
|
+
/** `active` | `disabled_manually` | `disabled_inactivity` | … */
|
|
113
|
+
state: string;
|
|
114
|
+
htmlUrl: string;
|
|
115
|
+
}
|
|
116
|
+
/** Result of a conditional (ETag) runs poll. A 304 sets `notModified` and empty `runs`. */
|
|
117
|
+
export interface RunsPoll {
|
|
118
|
+
runs: FactoryRun[];
|
|
119
|
+
etag: string | null;
|
|
120
|
+
notModified: boolean;
|
|
121
|
+
/** `x-ratelimit-remaining` header value when present, for polite backoff. */
|
|
122
|
+
rateRemaining: number | null;
|
|
123
|
+
}
|
|
124
|
+
/** One step of a run job (drill-down detail). */
|
|
125
|
+
export interface RunJobStep {
|
|
126
|
+
number: number;
|
|
127
|
+
name: string;
|
|
128
|
+
status: string;
|
|
129
|
+
conclusion: RunConclusion;
|
|
130
|
+
}
|
|
131
|
+
/** One job of a workflow run — the Monitor tab's drill-down unit. */
|
|
132
|
+
export interface RunJob {
|
|
133
|
+
jobId: number;
|
|
134
|
+
name: string;
|
|
135
|
+
status: 'queued' | 'in_progress' | 'completed' | (string & {});
|
|
136
|
+
conclusion: RunConclusion;
|
|
137
|
+
startedAt: string | null;
|
|
138
|
+
completedAt: string | null;
|
|
139
|
+
htmlUrl: string;
|
|
140
|
+
steps: RunJobStep[];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Transport surface. Implemented by a thin fetch-based client (no Octokit dep — keeps
|
|
144
|
+
* the static bundle lean and trivially mockable via `globalThis.fetch`). Every method
|
|
145
|
+
* rejects with a {@link GithubError} on non-2xx.
|
|
146
|
+
*/
|
|
147
|
+
export interface GithubClient {
|
|
148
|
+
/** Authenticated login (from GET /user at connect time). */
|
|
149
|
+
readonly login: string;
|
|
150
|
+
/** Best-effort permission check for a repo. Never throws for permission gaps; maps them to `missing`. */
|
|
151
|
+
preflight(repo: RepoRef): Promise<Preflight>;
|
|
152
|
+
/** Read a file, or `null` if it does not exist (404). Throws on other errors. */
|
|
153
|
+
getFile(repo: RepoRef, path: string, ref?: string): Promise<RepoFile | null>;
|
|
154
|
+
/** List a directory, or `[]` if it does not exist. */
|
|
155
|
+
listDir(repo: RepoRef, path: string, ref?: string): Promise<RepoDirEntry[]>;
|
|
156
|
+
/** Create or update a file (Contents API). Pass `sha` to update, `branch` to target a branch. */
|
|
157
|
+
putFile(repo: RepoRef, path: string, content: string, message: string, opts?: {
|
|
158
|
+
sha?: string;
|
|
159
|
+
branch?: string;
|
|
160
|
+
}): Promise<void>;
|
|
161
|
+
/** Delete a file (Contents API). */
|
|
162
|
+
deleteFile(repo: RepoRef, path: string, sha: string, message: string, opts?: {
|
|
163
|
+
branch?: string;
|
|
164
|
+
}): Promise<void>;
|
|
165
|
+
/** Default branch name (e.g. `main`). */
|
|
166
|
+
getDefaultBranch(repo: RepoRef): Promise<string>;
|
|
167
|
+
/** Create a new branch `newBranch` pointing at the tip of `fromBranch`. */
|
|
168
|
+
createBranch(repo: RepoRef, newBranch: string, fromBranch: string): Promise<void>;
|
|
169
|
+
/** Open a pull request; returns the html_url. */
|
|
170
|
+
createPullRequest(repo: RepoRef, args: {
|
|
171
|
+
title: string;
|
|
172
|
+
head: string;
|
|
173
|
+
base: string;
|
|
174
|
+
body?: string;
|
|
175
|
+
}): Promise<string>;
|
|
176
|
+
/** Repo public key for sealed-box secret encryption. */
|
|
177
|
+
getActionsPublicKey(repo: RepoRef): Promise<ActionsPublicKey>;
|
|
178
|
+
/** Whether an Actions secret with this name exists (metadata only; value never returned). */
|
|
179
|
+
hasSecret(repo: RepoRef, name: string): Promise<boolean>;
|
|
180
|
+
/** Create/update an Actions secret with an already sealed-box-encrypted value. */
|
|
181
|
+
putSecret(repo: RepoRef, name: string, encryptedValue: string, keyId: string): Promise<void>;
|
|
182
|
+
/**
|
|
183
|
+
* List recent runs of `factory--*` workflows, newest first. Pass a prior `etag` for a
|
|
184
|
+
* conditional request — a 304 returns `{ notModified: true, runs: [] }` and costs no
|
|
185
|
+
* rate quota. Never throws for an empty/absent Actions history.
|
|
186
|
+
*/
|
|
187
|
+
listFactoryRuns(repo: RepoRef, opts?: {
|
|
188
|
+
etag?: string | null;
|
|
189
|
+
perPage?: number;
|
|
190
|
+
pathPrefix?: string;
|
|
191
|
+
}): Promise<RunsPoll>;
|
|
192
|
+
/** Jobs (with steps) for one run — the Monitor drill-down. */
|
|
193
|
+
listRunJobs(repo: RepoRef, runId: number): Promise<RunJob[]>;
|
|
194
|
+
/** Fire a workflow_dispatch for a workflow file (e.g. `factory--x.yml`). Defaults to the default branch. */
|
|
195
|
+
dispatchWorkflow(repo: RepoRef, workflowFile: string, ref?: string): Promise<void>;
|
|
196
|
+
/** Cancel an in-progress run. */
|
|
197
|
+
cancelRun(repo: RepoRef, runId: number): Promise<void>;
|
|
198
|
+
/**
|
|
199
|
+
* Registered workflows for a repo (id, path, `state` — the disable/enable handle).
|
|
200
|
+
* Never throws for a repo without Actions — returns `[]` on 403/404.
|
|
201
|
+
*/
|
|
202
|
+
listRepoWorkflows(repo: RepoRef): Promise<RepoWorkflow[]>;
|
|
203
|
+
/** Re-run a completed run (all jobs) — the "clear the jam" maintain action. */
|
|
204
|
+
rerunRun(repo: RepoRef, runId: number): Promise<void>;
|
|
205
|
+
/** Enable or disable a workflow by id — powering a machine on/off from the cockpit. */
|
|
206
|
+
setWorkflowEnabled(repo: RepoRef, workflowId: number, enabled: boolean): Promise<void>;
|
|
207
|
+
/** Open an issue (the audit's "file a work order" action); returns the html_url. */
|
|
208
|
+
createIssue(repo: RepoRef, args: {
|
|
209
|
+
title: string;
|
|
210
|
+
body: string;
|
|
211
|
+
labels?: string[];
|
|
212
|
+
}): Promise<string>;
|
|
213
|
+
/**
|
|
214
|
+
* List repository Actions variables (the enablement-gate kill switches). Never throws for
|
|
215
|
+
* an empty/absent list or missing permission — returns `[]`. Values are readable (unlike
|
|
216
|
+
* secrets), which is exactly what lets the app show a line as armed or idle.
|
|
217
|
+
*/
|
|
218
|
+
/**
|
|
219
|
+
* Repo Actions variables, or `null` when they could not be read (no permission,
|
|
220
|
+
* no Actions). `null` is NOT the same as `[]`: an empty list means the repo really
|
|
221
|
+
* has no variables, so a kill switch is genuinely off, while `null` means we do not
|
|
222
|
+
* know — and a cockpit must not draw a running loop as stopped.
|
|
223
|
+
*/
|
|
224
|
+
listVariables(repo: RepoRef): Promise<RepoVariable[] | null>;
|
|
225
|
+
/** Read one Actions variable, or `null` if unset. */
|
|
226
|
+
getVariable(repo: RepoRef, name: string): Promise<RepoVariable | null>;
|
|
227
|
+
/** Create or update an Actions variable (PATCH if it exists, else POST). */
|
|
228
|
+
setVariable(repo: RepoRef, name: string, value: string): Promise<void>;
|
|
229
|
+
}
|
|
230
|
+
/** A repository Actions variable — an enablement-gate kill switch. Values are plaintext. */
|
|
231
|
+
export interface RepoVariable {
|
|
232
|
+
name: string;
|
|
233
|
+
value: string;
|
|
234
|
+
}
|
|
235
|
+
/** Error carrying the HTTP status so callers can branch on 401/403/404/422. */
|
|
236
|
+
export declare class GithubError extends Error {
|
|
237
|
+
readonly status: number;
|
|
238
|
+
readonly url: string;
|
|
239
|
+
constructor(message: string, status: number, url: string);
|
|
240
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Shared contract for the GitHub I/O layer (seed 03).
|
|
2
|
+
// This file is the seam between the transport client, the crypto helper, and the
|
|
3
|
+
// deploy planner — each is implemented against these types. No I/O here; pure types
|
|
4
|
+
// + a couple of tiny pure helpers so the modules agree on shapes.
|
|
5
|
+
//
|
|
6
|
+
// Security (CLAUDE.md golden rule #7): the PAT lives in sessionStorage at most and is
|
|
7
|
+
// held in a closure by the client; it never appears in these types, in the blueprint,
|
|
8
|
+
// or in any persisted state. The Claude auth secret (CLAUDE_CODE_OAUTH_TOKEN or the
|
|
9
|
+
// ANTHROPIC_API_KEY fallback) plaintext is passed straight into sealSecret and never stored.
|
|
10
|
+
/** Parse `"owner/repo"` (tolerating a full GitHub URL or surrounding whitespace). */
|
|
11
|
+
export function parseRepo(input) {
|
|
12
|
+
const cleaned = input
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/^https?:\/\/github\.com\//i, '')
|
|
15
|
+
.replace(/\.git$/i, '')
|
|
16
|
+
.replace(/\/$/, '');
|
|
17
|
+
const m = /^([A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)\/([A-Za-z0-9._-]+)$/.exec(cleaned);
|
|
18
|
+
if (!m)
|
|
19
|
+
return null;
|
|
20
|
+
return { owner: m[1], repo: m[2] };
|
|
21
|
+
}
|
|
22
|
+
/** `owner/repo` string form. */
|
|
23
|
+
export function repoSlug(r) {
|
|
24
|
+
return `${r.owner}/${r.repo}`;
|
|
25
|
+
}
|
|
26
|
+
/** Error carrying the HTTP status so callers can branch on 401/403/404/422. */
|
|
27
|
+
export class GithubError extends Error {
|
|
28
|
+
status;
|
|
29
|
+
url;
|
|
30
|
+
constructor(message, status, url) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.status = status;
|
|
33
|
+
this.url = url;
|
|
34
|
+
this.name = 'GithubError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export interface UsageWorkflow {
|
|
2
|
+
repo?: string;
|
|
3
|
+
workflow?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
avg_min?: number;
|
|
6
|
+
runs?: number;
|
|
7
|
+
success?: number;
|
|
8
|
+
external?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ActionsUsage {
|
|
11
|
+
generated_at?: string;
|
|
12
|
+
totals?: {
|
|
13
|
+
success_rate_pct?: number;
|
|
14
|
+
effectiveness_pct?: number;
|
|
15
|
+
total_min?: number;
|
|
16
|
+
waste_min?: number;
|
|
17
|
+
waste_hours?: number;
|
|
18
|
+
};
|
|
19
|
+
workflows?: UsageWorkflow[];
|
|
20
|
+
}
|
|
21
|
+
export interface FleetTriage {
|
|
22
|
+
generated_at?: string;
|
|
23
|
+
totals?: {
|
|
24
|
+
failing_workflows?: number;
|
|
25
|
+
repos_red?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface IssuePipeline {
|
|
29
|
+
generated_at?: string;
|
|
30
|
+
totals?: {
|
|
31
|
+
pipeline_prs?: number;
|
|
32
|
+
stages?: Record<string, number>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface RotationToken {
|
|
36
|
+
name?: string;
|
|
37
|
+
oldest_age_days?: number;
|
|
38
|
+
max_age_days?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface TokenRotation {
|
|
41
|
+
generated_at?: string;
|
|
42
|
+
tokens?: RotationToken[];
|
|
43
|
+
}
|
|
44
|
+
/** The four signals. `null`/`undefined` (or `{}`) means "missing" — the hub's `{}`. */
|
|
45
|
+
export interface HarnessSignals {
|
|
46
|
+
actions_usage?: ActionsUsage | null;
|
|
47
|
+
fleet_triage?: FleetTriage | null;
|
|
48
|
+
issue_pipeline?: IssuePipeline | null;
|
|
49
|
+
token_rotation?: TokenRotation | null;
|
|
50
|
+
}
|
|
51
|
+
export declare const SIGNAL_NAMES: readonly ['actions_usage', 'fleet_triage', 'issue_pipeline', 'token_rotation'];
|
|
52
|
+
export type SignalName = (typeof SIGNAL_NAMES)[number];
|
|
53
|
+
export interface HarnessConfig {
|
|
54
|
+
scorecard: {
|
|
55
|
+
completion_rate_min_pct: number;
|
|
56
|
+
effectiveness_min_pct: number;
|
|
57
|
+
};
|
|
58
|
+
trip_wires: {
|
|
59
|
+
stale_data_days: number;
|
|
60
|
+
pass_rate_floor_pct: number;
|
|
61
|
+
waste_ceiling_pct: number;
|
|
62
|
+
cost_spike_multiplier: number;
|
|
63
|
+
cost_spike_min_runs: number;
|
|
64
|
+
cost_spike_min_avg_min: number;
|
|
65
|
+
standing_failures_max: number;
|
|
66
|
+
credential_grace_days: number;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** The hub's defaults (harness.py `DEFAULT_SCORECARD` / `DEFAULT_TRIP_WIRES`). */
|
|
70
|
+
export declare const DEFAULT_CONFIG: HarnessConfig;
|
|
71
|
+
/** A finite number, or null. Strings that are numbers are accepted (YAML round-trips them). */
|
|
72
|
+
export declare function num(v: unknown): number | null;
|
|
73
|
+
/**
|
|
74
|
+
* Read the `harness:` block out of a parsed `fleet.yml` (or the block itself), with the
|
|
75
|
+
* hub's defaults for every absent key — an absent block degrades to sane behaviour.
|
|
76
|
+
*/
|
|
77
|
+
export declare function parseHarnessConfig(fleetYml: unknown): HarnessConfig;
|
|
78
|
+
/** The generators stamp `%Y-%m-%d %H:%M UTC`; ISO and bare dates are tolerated (harness.py). */
|
|
79
|
+
export declare function parseGeneratedAt(value: unknown): Date | null;
|
|
80
|
+
export declare function sourceAgeDays(data: unknown, now: Date): number | null;
|
|
81
|
+
export declare function median(values: number[]): number | null;
|
|
82
|
+
export type Direction = 'up' | 'down' | 'steady';
|
|
83
|
+
export type MetricStatus = 'ok' | 'warn' | 'unknown';
|
|
84
|
+
export interface Metric {
|
|
85
|
+
value: number | null;
|
|
86
|
+
direction: Direction;
|
|
87
|
+
threshold?: number;
|
|
88
|
+
status?: MetricStatus;
|
|
89
|
+
}
|
|
90
|
+
export declare const SCORECARD_KEYS: readonly ['completion_rate_pct', 'effectiveness_pct', 'waste_hours', 'cost_min_per_verified_run', 'standing_failures', 'repos_red', 'escalations_open', 'agent_prs_open', 'oldest_credential_age_days'];
|
|
91
|
+
export type ScorecardKey = (typeof SCORECARD_KEYS)[number];
|
|
92
|
+
export type Scorecard = Record<ScorecardKey, Metric>;
|
|
93
|
+
/** The playbook's health scorecard, from the signals the fleet already keeps. */
|
|
94
|
+
export declare function buildScorecard(cfg: HarnessConfig, usage: ActionsUsage | null | undefined, triage: FleetTriage | null | undefined, pipeline: IssuePipeline | null | undefined, rotation: TokenRotation | null | undefined): Scorecard;
|
|
95
|
+
export type WireId = 'stale-data' | 'pass-rate-floor' | 'waste-ceiling' | 'cost-spike' | 'standing-failures' | 'credential-overdue';
|
|
96
|
+
export declare const WIRE_IDS: WireId[];
|
|
97
|
+
export interface TripWire {
|
|
98
|
+
id: WireId;
|
|
99
|
+
tripped: boolean;
|
|
100
|
+
summary: string;
|
|
101
|
+
detail?: Record<string, unknown>[];
|
|
102
|
+
}
|
|
103
|
+
/** Every wire is reported, tripped or not — a quiet panel and a lost panel must not look the same. */
|
|
104
|
+
export declare function evaluateTripWires(cfg: HarnessConfig, usage: ActionsUsage | null | undefined, triage: FleetTriage | null | undefined, pipeline: IssuePipeline | null | undefined, rotation: TokenRotation | null | undefined, now: Date): TripWire[];
|
|
105
|
+
/** Python's str() for an int-typed value the summaries interpolate (`None` when absent). */
|
|
106
|
+
export declare function fmt(v: number | null): string;
|
|
107
|
+
/**
|
|
108
|
+
* Python's str() for a float-typed value: an integral float prints with `.0` (`3.0`, `90.0`).
|
|
109
|
+
* The hub's rates, percentages, medians, and the multiplier/floor thresholds are floats there
|
|
110
|
+
* (`round(x, 1)` results and `3.0` / `5.0` in fleet.yml), so their summaries carry the decimal.
|
|
111
|
+
*/
|
|
112
|
+
export declare function fmtFloat(v: number | null): string;
|
|
113
|
+
export interface HarnessHealth {
|
|
114
|
+
generated_at: string;
|
|
115
|
+
sources: Record<SignalName, {
|
|
116
|
+
present: boolean;
|
|
117
|
+
age_days: number | null;
|
|
118
|
+
}>;
|
|
119
|
+
scorecard: Scorecard;
|
|
120
|
+
trip_wires: TripWire[];
|
|
121
|
+
tripped_count: number;
|
|
122
|
+
note: string;
|
|
123
|
+
}
|
|
124
|
+
export declare 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.";
|
|
125
|
+
/** `%Y-%m-%d %H:%M UTC`, the hub's stamp. */
|
|
126
|
+
export declare function stampUtc(now: Date): string;
|
|
127
|
+
/** The hub's `run()`: the committed `harness_health.yml`, as data. */
|
|
128
|
+
export declare function harnessHealth(signals: HarnessSignals, cfg?: HarnessConfig, now?: Date): HarnessHealth;
|
|
129
|
+
/** Keep only the fields the engine reads (untrusted YAML never travels further). */
|
|
130
|
+
export declare function toActionsUsage(v: unknown): ActionsUsage | null;
|
|
131
|
+
export declare function toFleetTriage(v: unknown): FleetTriage | null;
|
|
132
|
+
export declare function toIssuePipeline(v: unknown): IssuePipeline | null;
|
|
133
|
+
export declare function toTokenRotation(v: unknown): TokenRotation | null;
|
|
134
|
+
/** The hub's committed `harness_health.yml` as data, or null when the document is not one. */
|
|
135
|
+
export declare function toHarnessHealth(v: unknown): HarnessHealth | null;
|