@runbooks/supervise 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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/approval.d.ts +132 -0
- package/dist/approval.js +148 -0
- package/dist/approval.test.d.ts +1 -0
- package/dist/approval.test.js +190 -0
- package/dist/budgets.test.d.ts +1 -0
- package/dist/budgets.test.js +148 -0
- package/dist/contract.d.ts +38 -0
- package/dist/contract.js +77 -0
- package/dist/contract.test.d.ts +1 -0
- package/dist/contract.test.js +131 -0
- package/dist/deviations.d.ts +77 -0
- package/dist/deviations.js +81 -0
- package/dist/deviations.test.d.ts +1 -0
- package/dist/deviations.test.js +241 -0
- package/dist/emit.d.ts +97 -0
- package/dist/emit.js +184 -0
- package/dist/emit.test.d.ts +1 -0
- package/dist/emit.test.js +187 -0
- package/dist/enforce.d.ts +29 -0
- package/dist/enforce.js +100 -0
- package/dist/enforce.test.d.ts +1 -0
- package/dist/enforce.test.js +112 -0
- package/dist/expect.d.ts +67 -0
- package/dist/expect.js +187 -0
- package/dist/expect.test.d.ts +1 -0
- package/dist/expect.test.js +120 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +34 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +30 -0
- package/dist/observe.test.d.ts +1 -0
- package/dist/observe.test.js +246 -0
- package/dist/policy.d.ts +122 -0
- package/dist/policy.js +147 -0
- package/dist/policy.test.d.ts +1 -0
- package/dist/policy.test.js +124 -0
- package/dist/purity.test.d.ts +1 -0
- package/dist/purity.test.js +191 -0
- package/dist/report.d.ts +72 -0
- package/dist/report.js +67 -0
- package/dist/run.d.ts +216 -0
- package/dist/run.js +445 -0
- package/dist/run.test.d.ts +1 -0
- package/dist/run.test.js +198 -0
- package/package.json +42 -0
package/dist/report.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { countByClass, countByStep } from "./deviations.js";
|
|
2
|
+
import { isComplete } from "./run.js";
|
|
3
|
+
import { plural } from "@runbooks/schema";
|
|
4
|
+
export function report(state, events = [], options = {}) {
|
|
5
|
+
const passed = state.checks.filter((c) => c.outcome === "pass");
|
|
6
|
+
const failed = state.checks.filter((c) => c.outcome === "fail");
|
|
7
|
+
const unevaluable = state.checks.filter((c) => c.outcome === "unevaluable");
|
|
8
|
+
const adjudicated = state.checks.filter((c) => c.adjudicatedBy !== undefined);
|
|
9
|
+
const retries = Object.entries(state.retries).map(([edge, used]) => ({
|
|
10
|
+
edge,
|
|
11
|
+
used,
|
|
12
|
+
...(options.envelope?.budgets[edge] !== undefined ? { max: options.envelope.budgets[edge] } : {}),
|
|
13
|
+
}));
|
|
14
|
+
const elapsed = options.now !== undefined && state.startedAt !== undefined
|
|
15
|
+
? options.now - state.startedAt
|
|
16
|
+
: undefined;
|
|
17
|
+
const caveats = [
|
|
18
|
+
"The supervisor authorized calls and decided postconditions. It did not contain what a permitted call then did.",
|
|
19
|
+
];
|
|
20
|
+
if (unevaluable.length > 0) {
|
|
21
|
+
caveats.push(`${plural(unevaluable.length, "postcondition")} could not be decided. Undecided is not passed: nothing here says those steps succeeded.`);
|
|
22
|
+
}
|
|
23
|
+
if (adjudicated.length > 0) {
|
|
24
|
+
caveats.push(`${plural(adjudicated.length, "check")} ${adjudicated.length === 1 ? "was" : "were"} decided by a person, not verified by the supervisor.`);
|
|
25
|
+
}
|
|
26
|
+
if (state.wallClockSeconds === undefined) {
|
|
27
|
+
caveats.push("No wall-clock budget was set for this run, so nothing bounded it in time. An absent limit is not a generous one; it is none.");
|
|
28
|
+
}
|
|
29
|
+
if (state.outcome === "budget-exhausted") {
|
|
30
|
+
caveats.push("The run gave up on a retry budget. The step it was retrying decided nothing — this is not a report of a step that failed.");
|
|
31
|
+
}
|
|
32
|
+
if (!isComplete(state)) {
|
|
33
|
+
caveats.push("The run did not reach a terminal, so this is an account of an unfinished procedure.");
|
|
34
|
+
}
|
|
35
|
+
if (state.deviations.length > 0) {
|
|
36
|
+
caveats.push(`The run deviated from the procedure ${plural(state.deviations.length, "time")}. A procedure that provokes deviations is one whose graph may not match reality.`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
status: state.status,
|
|
40
|
+
...(state.outcome ? { outcome: state.outcome } : {}),
|
|
41
|
+
complete: isComplete(state),
|
|
42
|
+
stepsVisited: state.visited,
|
|
43
|
+
checks: {
|
|
44
|
+
passed: passed.length,
|
|
45
|
+
failed: failed.length,
|
|
46
|
+
unevaluable: unevaluable.length,
|
|
47
|
+
adjudicated: adjudicated.length,
|
|
48
|
+
},
|
|
49
|
+
deviations: {
|
|
50
|
+
count: state.deviations.length,
|
|
51
|
+
byClass: countByClass(state.deviations),
|
|
52
|
+
byStep: countByStep(state.deviations),
|
|
53
|
+
records: state.deviations,
|
|
54
|
+
},
|
|
55
|
+
budgets: {
|
|
56
|
+
retries,
|
|
57
|
+
wallClock: {
|
|
58
|
+
...(state.wallClockSeconds !== undefined ? { limitSeconds: state.wallClockSeconds } : {}),
|
|
59
|
+
...(elapsed !== undefined ? { elapsedSeconds: elapsed } : {}),
|
|
60
|
+
exhausted: state.outcome === "timed-out",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
...(state.awaiting ? { awaiting: state.awaiting } : {}),
|
|
64
|
+
detail: state.checks,
|
|
65
|
+
caveats,
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run state machine.
|
|
3
|
+
*
|
|
4
|
+
* Pure: every function takes state and returns new state plus the events it decided.
|
|
5
|
+
* Nothing here performs I/O, holds a credential, or reaches a network — an adapter sits
|
|
6
|
+
* in the call path, this decides (RUNBOOK.md 13.1). The same envelope and the same
|
|
7
|
+
* sequence of proposals must produce identical decisions, or a supervised run is not
|
|
8
|
+
* reproducible and an attestation about one means nothing.
|
|
9
|
+
*/
|
|
10
|
+
import type { PolicyEnvelope } from "./policy.js";
|
|
11
|
+
import type { Authorization } from "./policy.js";
|
|
12
|
+
import type { Grant, GateRefusal } from "./approval.js";
|
|
13
|
+
import type { DeviationRecord, EscalationPolicy } from "./deviations.js";
|
|
14
|
+
import type { Evaluation, Observation } from "./expect.js";
|
|
15
|
+
export type RunStatus = "running" | "awaiting-approval"
|
|
16
|
+
/**
|
|
17
|
+
* The postcondition could not be decided, so the run is stopped until a person says
|
|
18
|
+
* what happened. Distinct from every other status on purpose: "I could not tell" is
|
|
19
|
+
* neither a pass nor a failure, and collapsing it into either is the failure mode Q12
|
|
20
|
+
* exists to prevent.
|
|
21
|
+
*/
|
|
22
|
+
| "awaiting-adjudication" | "ended";
|
|
23
|
+
export type RunOutcome = "success" | "failed"
|
|
24
|
+
/** Stopped deliberately: a rollback, an operator, a refused gate. */
|
|
25
|
+
| "aborted"
|
|
26
|
+
/**
|
|
27
|
+
* A retry budget ran out. Distinct from a failure on purpose: the step did not decide
|
|
28
|
+
* anything, the run gave up on it — and a chronically exhausting step is a fact about
|
|
29
|
+
* the runbook, which a generic "failed" would hide.
|
|
30
|
+
*/
|
|
31
|
+
| "budget-exhausted"
|
|
32
|
+
/** The run outlasted the wall-clock budget the operator set for it. */
|
|
33
|
+
| "timed-out";
|
|
34
|
+
export interface RunEvent {
|
|
35
|
+
readonly kind: "started" | "entered" | "authorized" | "blocked" | "approval-requested" | "approved" | "deviation" | "escalated" | "budget-exhausted" | "ended" | "expectation-passed" | "expectation-failed" | "expectation-unevaluable" | "adjudicated";
|
|
36
|
+
/** Resolves to a spec anchor, so a blocked call can always be explained. */
|
|
37
|
+
readonly code: string;
|
|
38
|
+
readonly message: string;
|
|
39
|
+
readonly at?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface RunState {
|
|
42
|
+
readonly status: RunStatus;
|
|
43
|
+
readonly current?: string;
|
|
44
|
+
readonly visited: readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* The decision authorizing the current entry into the current step, if one has been
|
|
47
|
+
* made. Discarded when the run leaves the step — including when a retry returns to
|
|
48
|
+
* it, which is the realistic way one approval would otherwise cover many attempts.
|
|
49
|
+
*/
|
|
50
|
+
readonly grant?: Grant;
|
|
51
|
+
/**
|
|
52
|
+
* Why the run stopped, when it stopped because somebody said no.
|
|
53
|
+
*
|
|
54
|
+
* Present only on a refused gate. Its absence on an `aborted` run is what distinguishes
|
|
55
|
+
* an expiry — a question nobody answered — from a considered refusal, which is a
|
|
56
|
+
* different fact about the same outcome.
|
|
57
|
+
*/
|
|
58
|
+
readonly refusal?: GateRefusal;
|
|
59
|
+
/** Who is running this. A decision from this identity is not an approval. */
|
|
60
|
+
readonly agentIdentity: string;
|
|
61
|
+
/** Traversals per retry edge, keyed `from->to`. */
|
|
62
|
+
readonly retries: Readonly<Record<string, number>>;
|
|
63
|
+
/** Structured, aggregatable, and carrying no payload (§14). */
|
|
64
|
+
readonly deviations: readonly DeviationRecord[];
|
|
65
|
+
/** What the operator wants done about each class. Conservative unless they said otherwise. */
|
|
66
|
+
readonly escalation: EscalationPolicy;
|
|
67
|
+
readonly outcome?: RunOutcome;
|
|
68
|
+
readonly inputs: Readonly<Record<string, unknown>>;
|
|
69
|
+
/**
|
|
70
|
+
* Whether this run evaluates postconditions at all. R0 and R1 never do, so they never
|
|
71
|
+
* need one; R2 does, and the requirement attaches to the profile rather than to the
|
|
72
|
+
* document (Q12).
|
|
73
|
+
*/
|
|
74
|
+
readonly mustCheck: boolean;
|
|
75
|
+
/** When the run started, in seconds. Supplied by the adapter: the core has no clock. */
|
|
76
|
+
readonly startedAt?: number;
|
|
77
|
+
/**
|
|
78
|
+
* How long this run may take in total, if the operator set a limit.
|
|
79
|
+
*
|
|
80
|
+
* Deliberately not derived from the document. `duration` there is a coarse bucket
|
|
81
|
+
* written to set a reader's expectation (`<5m`, `5-30m`, `>30m`), and turning a
|
|
82
|
+
* reader's hint into a kill switch would stop runs for a reason nobody wrote down.
|
|
83
|
+
* A budget is the operator's decision; the envelope only suggests one.
|
|
84
|
+
*/
|
|
85
|
+
readonly wallClockSeconds?: number;
|
|
86
|
+
/** The step whose postcondition has been decided for the current entry into it. */
|
|
87
|
+
readonly decided?: string;
|
|
88
|
+
/** The postcondition waiting on a person, while one is. */
|
|
89
|
+
readonly awaiting?: {
|
|
90
|
+
readonly stepId: string;
|
|
91
|
+
readonly why: string;
|
|
92
|
+
};
|
|
93
|
+
/** Every postcondition decided so far, in order. The run report is built from this. */
|
|
94
|
+
readonly checks: readonly CheckRecord[];
|
|
95
|
+
}
|
|
96
|
+
export interface CheckRecord {
|
|
97
|
+
readonly stepId: string;
|
|
98
|
+
readonly outcome: "pass" | "fail" | "unevaluable";
|
|
99
|
+
readonly why: string;
|
|
100
|
+
/** Present when a person decided what the supervisor could not. */
|
|
101
|
+
readonly adjudicatedBy?: string;
|
|
102
|
+
}
|
|
103
|
+
export interface StartRefusal {
|
|
104
|
+
readonly code: string;
|
|
105
|
+
readonly message: string;
|
|
106
|
+
}
|
|
107
|
+
export type StartResult = {
|
|
108
|
+
readonly ok: true;
|
|
109
|
+
readonly state: RunState;
|
|
110
|
+
readonly events: readonly RunEvent[];
|
|
111
|
+
} | {
|
|
112
|
+
readonly ok: false;
|
|
113
|
+
readonly refusals: readonly StartRefusal[];
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Bind and validate inputs, then enter the graph.
|
|
117
|
+
*
|
|
118
|
+
* Binding happens before the first node rather than at first use. A missing required
|
|
119
|
+
* input aborts: interpolating an empty string does not make a command narrower, it makes
|
|
120
|
+
* it a different command, and `--group ""` against a production cluster is not a
|
|
121
|
+
* degraded version of `--group checkout`.
|
|
122
|
+
*/
|
|
123
|
+
export interface StartOptions {
|
|
124
|
+
readonly inputs?: Readonly<Record<string, unknown>>;
|
|
125
|
+
/** Defaults to a placeholder; an adapter supplies the real one so that a decision
|
|
126
|
+
* from the agent can be recognised and refused (§13.4). */
|
|
127
|
+
readonly agentIdentity?: string;
|
|
128
|
+
/** Set by an adapter running R2. Off, a run behaves as R1 did. */
|
|
129
|
+
readonly evaluatePostconditions?: boolean;
|
|
130
|
+
/** The clock, supplied rather than read: the core stays pure and reproducible. */
|
|
131
|
+
readonly now?: number;
|
|
132
|
+
/** The operator's escalation policy. Omitted means the conservative default. */
|
|
133
|
+
readonly escalation?: EscalationPolicy;
|
|
134
|
+
/** A whole-run limit, in seconds. Absent means there is none, and the report says so. */
|
|
135
|
+
readonly wallClockSeconds?: number;
|
|
136
|
+
}
|
|
137
|
+
export declare function startRun(envelope: PolicyEnvelope, options?: StartOptions | Readonly<Record<string, unknown>>): StartResult;
|
|
138
|
+
export interface Proposal {
|
|
139
|
+
readonly tool: string;
|
|
140
|
+
/** The clock, when the adapter has one. Absent means the wall clock is not checked. */
|
|
141
|
+
readonly now?: number;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Has the run outlasted its wall-clock budget?
|
|
145
|
+
*
|
|
146
|
+
* A procedure can be slow without looping, so this is counted separately from retries.
|
|
147
|
+
* With no budget set, or no clock supplied, the answer is no — an absent limit is not a
|
|
148
|
+
* zero one, and the report says plainly that nothing was bounding the run in time.
|
|
149
|
+
*/
|
|
150
|
+
export declare function outOfTime(state: RunState, now: number | undefined): boolean;
|
|
151
|
+
export interface Decided {
|
|
152
|
+
readonly state: RunState;
|
|
153
|
+
readonly authorization: Authorization;
|
|
154
|
+
readonly events: readonly RunEvent[];
|
|
155
|
+
}
|
|
156
|
+
/** Authorize a proposed invocation against the current step. */
|
|
157
|
+
export declare function propose(envelope: PolicyEnvelope, state: RunState, proposal: Proposal): Decided;
|
|
158
|
+
/**
|
|
159
|
+
* Attach a decision to the run.
|
|
160
|
+
*
|
|
161
|
+
* The grant is produced by `grant()` in ./approval.js, which is where self-approval is
|
|
162
|
+
* refused — the core never mints one itself, so there is no path by which a run
|
|
163
|
+
* approves itself.
|
|
164
|
+
*/
|
|
165
|
+
export declare function applyGrant(state: RunState, granted: Grant): Decided;
|
|
166
|
+
export interface Advanced {
|
|
167
|
+
readonly state: RunState;
|
|
168
|
+
readonly events: readonly RunEvent[];
|
|
169
|
+
}
|
|
170
|
+
/** Move to the next step, counting retry budgets and detecting terminals. */
|
|
171
|
+
export declare function advance(envelope: PolicyEnvelope, state: RunState, to: string, now?: number): Advanced;
|
|
172
|
+
/** A run that stopped anywhere but a terminal is incomplete, and says so (13.4). */
|
|
173
|
+
export declare function isComplete(state: RunState): boolean;
|
|
174
|
+
/** The result of deciding a step's postcondition. */
|
|
175
|
+
export interface Observed {
|
|
176
|
+
readonly state: RunState;
|
|
177
|
+
readonly events: readonly RunEvent[];
|
|
178
|
+
readonly evaluation: Evaluation;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Decide whether the current step did what the procedure said it would, and route.
|
|
182
|
+
*
|
|
183
|
+
* This is the behaviour that separates R2 from R1. The route on failure is the
|
|
184
|
+
* document's `on_fail`, taken by the supervisor: at no point does the agent's own
|
|
185
|
+
* account of what happened choose the next node. Where the postcondition cannot be
|
|
186
|
+
* decided the run stops and waits for a person — it does not proceed, and it does not
|
|
187
|
+
* fail.
|
|
188
|
+
*/
|
|
189
|
+
export declare function observe(envelope: PolicyEnvelope, state: RunState, observation: Observation, now?: number): Observed;
|
|
190
|
+
export declare class SelfAdjudicationError extends Error {
|
|
191
|
+
constructor(identity: string);
|
|
192
|
+
}
|
|
193
|
+
export interface Adjudication {
|
|
194
|
+
readonly verdict: "pass" | "fail";
|
|
195
|
+
readonly decidedBy: string;
|
|
196
|
+
readonly note?: string;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A person decides what the supervisor could not, and the run resumes on that decision.
|
|
200
|
+
*
|
|
201
|
+
* The decision is recorded as theirs. A run report that showed an adjudicated check as
|
|
202
|
+
* simply "passed" would be claiming the supervisor verified something it did not.
|
|
203
|
+
*/
|
|
204
|
+
export declare function adjudicate(envelope: PolicyEnvelope, state: RunState, decision: Adjudication): {
|
|
205
|
+
state: RunState;
|
|
206
|
+
events: readonly RunEvent[];
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Close out a run that stopped somewhere other than a terminal.
|
|
210
|
+
*
|
|
211
|
+
* §13.4: an incomplete run says so. An adapter that loses its client, or an operator who
|
|
212
|
+
* walks away, leaves a run whose outcome is genuinely unknown — and "unknown" recorded
|
|
213
|
+
* as success is the one reading that would be a lie. Reaching an escalate node is not
|
|
214
|
+
* this case: an escalation the runbook anticipated is where the procedure meant to end.
|
|
215
|
+
*/
|
|
216
|
+
export declare function conclude(envelope: PolicyEnvelope, state: RunState): Advanced;
|