@tiphys/kernel 0.1.0 → 0.2.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.
Files changed (94) hide show
  1. package/AGENTS.md +56 -4
  2. package/assurance-modes.yaml +23 -2
  3. package/dist/bin/tiphys.js +86 -8
  4. package/dist/src/adapters/load.d.ts +202 -0
  5. package/dist/src/adapters/load.js +440 -0
  6. package/dist/src/brief.js +27 -20
  7. package/dist/src/checks.d.ts +720 -9
  8. package/dist/src/checks.js +1874 -163
  9. package/dist/src/cli.js +11 -0
  10. package/dist/src/commands/brief.js +27 -4
  11. package/dist/src/commands/cutover.d.ts +35 -0
  12. package/dist/src/commands/cutover.js +448 -0
  13. package/dist/src/commands/doctor.d.ts +229 -0
  14. package/dist/src/commands/doctor.js +968 -27
  15. package/dist/src/commands/init.d.ts +3 -3
  16. package/dist/src/commands/init.js +57 -8
  17. package/dist/src/commands/lock.d.ts +33 -0
  18. package/dist/src/commands/lock.js +117 -6
  19. package/dist/src/commands/next.d.ts +130 -0
  20. package/dist/src/commands/next.js +597 -0
  21. package/dist/src/commands/pool.js +12 -1
  22. package/dist/src/commands/resume.d.ts +1 -0
  23. package/dist/src/commands/resume.js +88 -0
  24. package/dist/src/commands/spawn.js +51 -2
  25. package/dist/src/commands/status.d.ts +6 -4
  26. package/dist/src/commands/status.js +6 -4
  27. package/dist/src/commands/sync.d.ts +47 -0
  28. package/dist/src/commands/sync.js +341 -0
  29. package/dist/src/commands/teardown.js +10 -2
  30. package/dist/src/commands/validate.js +70 -0
  31. package/dist/src/cutover.d.ts +584 -0
  32. package/dist/src/cutover.js +1444 -0
  33. package/dist/src/exclusion.d.ts +389 -0
  34. package/dist/src/exclusion.js +843 -0
  35. package/dist/src/exec/env.d.ts +152 -2
  36. package/dist/src/exec/env.js +146 -2
  37. package/dist/src/fleet.d.ts +172 -0
  38. package/dist/src/fleet.js +219 -1
  39. package/dist/src/gates/citations.js +7 -1
  40. package/dist/src/gates/coverage.d.ts +113 -22
  41. package/dist/src/gates/coverage.js +166 -31
  42. package/dist/src/gates/credentials.d.ts +159 -0
  43. package/dist/src/gates/credentials.js +221 -2
  44. package/dist/src/gates/gate-classes.d.ts +56 -0
  45. package/dist/src/gates/gate-classes.js +633 -0
  46. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  47. package/dist/src/gates/merge-preconditions.js +932 -0
  48. package/dist/src/gates/red-witness.js +105 -13
  49. package/dist/src/gates/run.d.ts +49 -1
  50. package/dist/src/gates/run.js +83 -5
  51. package/dist/src/gates/schemas/phase-declaration.schema.json +45 -0
  52. package/dist/src/gates/suite.js +48 -7
  53. package/dist/src/hooks.d.ts +55 -3
  54. package/dist/src/hooks.js +69 -6
  55. package/dist/src/index.d.ts +31 -0
  56. package/dist/src/index.js +30 -0
  57. package/dist/src/lock.d.ts +82 -4
  58. package/dist/src/lock.js +314 -22
  59. package/dist/src/model-resolution.d.ts +159 -0
  60. package/dist/src/model-resolution.js +307 -0
  61. package/dist/src/path-identity.d.ts +32 -0
  62. package/dist/src/path-identity.js +38 -0
  63. package/dist/src/pool.d.ts +197 -1
  64. package/dist/src/pool.js +289 -22
  65. package/dist/src/roles.d.ts +31 -0
  66. package/dist/src/roles.js +42 -0
  67. package/dist/src/spawn.d.ts +307 -2
  68. package/dist/src/spawn.js +690 -19
  69. package/dist/src/status.d.ts +27 -2
  70. package/dist/src/status.js +34 -5
  71. package/dist/src/task.d.ts +295 -55
  72. package/dist/src/task.js +125 -123
  73. package/dist/src/teardown.d.ts +7 -0
  74. package/dist/src/teardown.js +120 -12
  75. package/dist/src/validate.d.ts +44 -11
  76. package/dist/src/validate.js +44 -34
  77. package/dist/src/watcher.js +1 -11
  78. package/dist/src/witness/run.d.ts +32 -7
  79. package/dist/src/witness/run.js +76 -30
  80. package/dist/src/witness/spec.d.ts +168 -0
  81. package/dist/src/witness/spec.js +240 -18
  82. package/dist/tsconfig.src.tsbuildinfo +1 -1
  83. package/gate-registry.yaml +136 -0
  84. package/gates.manifest.json +63 -1
  85. package/package.json +18 -3
  86. package/roles/implementer.md +3 -0
  87. package/schemas/README.md +1 -0
  88. package/schemas/assurance-modes.schema.json +1 -1
  89. package/schemas/charter.schema.json +19 -0
  90. package/schemas/cutover-state.schema.json +64 -0
  91. package/schemas/executor-record.schema.json +36 -0
  92. package/schemas/model-resolution.schema.json +362 -0
  93. package/schemas/verdict.schema.json +9 -3
  94. package/schemas/write-bypass.schema.json +69 -0
@@ -96,6 +96,76 @@ export const TYPE_TABLE = new Map([
96
96
  `mechanism-rule-evidence-resolves` are registered for type `tuition` and
97
97
  nothing runs them until the type resolves. */
98
98
  ["tuition", "tuition.schema.json"],
99
+ /* M4-P25 criterion 4. The fleet's `cutover.json`. It carries NO `kind`
100
+ field, so this row extends `--type` and NOT `resolveAutoType`; `role-brief`
101
+ above is the standing precedent for that half of M3R-001, and the half the
102
+ rule is actually about, a schema that ships without a `--type` row and
103
+ forces a later implementer to edit an undeclared file, is satisfied here.
104
+ The document is validated programmatically on every write as well
105
+ (src/cutover.ts:253), which is what criterion 4 means by validated by the
106
+ schema rather than by the command. */
107
+ ["cutover-state", "cutover-state.schema.json"],
108
+ /* M4-P3 criterion 4. tasks/<id>/executor.json, the launch record an
109
+ ExecutorAdapter writes.
110
+
111
+ THIS ROW IS THE WHOLE REGISTRATION, AND `resolveAutoType` IS NOT A SECOND
112
+ PLACE TO EDIT: that function answers `--type auto` by reading the
113
+ instance's `kind` and looking it up in THIS MAP, so a row added here is
114
+ the auto resolver's source of truth as well. M3R-001's requirement that a
115
+ shipped schema not arrive without its `--type` row is satisfied by the
116
+ row; nothing else in this module holds a type list.
117
+
118
+ WHAT THE ROW CANNOT DO, stated here rather than left to be discovered.
119
+ An executor record carries no `kind` field, because M4-P3's criterion 6
120
+ fixes the record's fields and `kind` is not among them, so `--type auto`
121
+ on a real executor.json is a USAGE error naming the missing kind. That is
122
+ the `role-brief` situation above reached by a different route: there the
123
+ document cannot be decoded before the type is known, here the decoded
124
+ document does not say what it is. Both are types a caller must NAME. The
125
+ property is asserted by a registered test rather than trusted, so a later
126
+ phase that gives the record a `kind` reddens this comment instead of
127
+ leaving it stale. */
128
+ ["executor-record", "executor-record.schema.json"],
129
+ /* M4-P7 criterion 1. tasks/<id>/model-resolution.json, the record a harness
130
+ adapter writes at TURN END saying which tier was requested, which the
131
+ charter resolved, and which model family served the turn.
132
+
133
+ ONE ROW SERVES BOTH REGISTRATIONS, which is the property the brief for
134
+ this phase asks to be got right and the row above states the mechanism
135
+ for: `resolveAutoType` answers `--type auto` by reading the instance's
136
+ `kind` and looking it up in THIS MAP. The difference from the executor
137
+ record is that a model-resolution record DOES carry a `kind`, required and
138
+ `const`, so `--type auto` resolves a real record rather than raising the
139
+ usage error the row above documents. `test/model-resolution.test.ts`
140
+ asserts both halves against the shipped CLI rather than against this
141
+ comment.
142
+
143
+ THE DERIVED CHECKS COME WITH THE ROW AND ARE THE REASON IT MATTERS. The
144
+ schema is Kind A and reaches presence; `model-resolution-subject-echo` in
145
+ src/checks.ts is Kind B and reaches agreement between sibling fields, and
146
+ nothing runs it until the type resolves here. */
147
+ ["model-resolution", "model-resolution.schema.json"],
148
+ /* M4-P9 step 4. <fleet>/write-bypass.json, the infrastructure-hotfix bypass
149
+ the project-write block reads.
150
+
151
+ THE ROW IS WHAT MAKES THE DECLARATION CHECKABLE BEFORE IT IS TRUSTED. The
152
+ hook reads the document defensively and refuses on anything it cannot use,
153
+ which is the right behaviour at decision time and a terrible way to find
154
+ out you mistyped an expiry: the orchestrator learns at the moment its own
155
+ hotfix is refused. With this row an author runs `tiphys validate --type
156
+ write-bypass <file>` and is told which pointer is wrong, before the
157
+ document is in front of a hook.
158
+
159
+ A bypass declaration DOES carry a `kind`, required and `const`, so one row
160
+ serves `--type` and `resolveAutoType` together (M3R-001), unlike the
161
+ executor record above.
162
+
163
+ WHAT THE ROW CANNOT DO, stated rather than left to be discovered: the
164
+ schema reaches the SHAPE and never the WINDOW. Whether `expiresAt` is in
165
+ the future is a comparison against a clock, and `decideWrite` takes `now`
166
+ as a parameter precisely so that comparison is testable at the boundary
167
+ rather than hidden inside a validator. */
168
+ ["write-bypass", "write-bypass.schema.json"],
99
169
  ]);
100
170
  /**
101
171
  * COMPANION SCHEMAS, declared beside the type table (M3-P4 step 3).
@@ -0,0 +1,584 @@
1
+ /**
2
+ * Cutover state and rollback (kernel plan M4, M4-P26; D-19's second limb at
3
+ * delivery/plan/kernel-plan-v1.md:394).
4
+ *
5
+ * WHAT THIS MODULE IS. The freeze point is not one event. It is FIVE
6
+ * independently flippable switches, taken from the list the current delivery
7
+ * process retains at
8
+ * delivery/decisions/DR-0025-controlled-pre-m4-local-pilot.md:45 and read
9
+ * against DR-0036's authority condition. Five switches rather than one event
10
+ * is what makes rollback PARTIAL rather than all-or-nothing, and it is why
11
+ * `credentials-and-refs`, the one switch with owner latency, is separable
12
+ * from the four that are not.
13
+ *
14
+ * THE SPLIT THAT KEEPS THIS HONEST, and it runs through every function here.
15
+ * Rollback of the FILES is cheap: everything under the retirement roots is
16
+ * git-tracked and revertible from history. Rollback of AUTHORITY is the
17
+ * expensive half: the branch-protection ruleset is owner-configured, the
18
+ * orchestrator cannot change it, and
19
+ * delivery/decisions/DR-0036-the-harness-adapter-leads-m4-and-the-kernel-is-the-second-subject.md:15
20
+ * prices the revert at "whatever phases ran under it". Nothing in this module
21
+ * flips authority. It PREPARES the request an owner acts on, and it refuses
22
+ * loudly rather than pretending the preparation is the act.
23
+ *
24
+ * WHAT THIS MODULE DELIBERATELY DOES NOT DO. It does not print
25
+ * `tiphys cutover status`, does not write switches to `kernel`, and does not
26
+ * ship a state schema. Those are M4-P25's acceptance criteria and M4-P25's
27
+ * files. This phase was dispatched ahead of its stated dependency because the
28
+ * conflict pre-pass holds `src/commands/cutover.ts` for exactly one unit at a
29
+ * time, so the state MODEL is created here and the status SURFACE is left
30
+ * where the plan put it. The boundary is recorded in
31
+ * delivery/work-history/m4-p26.md:1 rather than left to be discovered.
32
+ *
33
+ * PLAN CONSTRAINTS. C-1: current state comes from `cutover.json` and from
34
+ * meta.json plus the turn-end file, never from the tail of a log. C-2: no pid,
35
+ * no signal, no /proc and no process probing appears here; a live worktree is
36
+ * a DIRECTORY and an open task is a FILE STATE. C-3: nothing is backgrounded.
37
+ */
38
+ import type { Fleet } from "./fleet.ts";
39
+ import { type GuardResult } from "./task.ts";
40
+ import type { SchemaDocument } from "./validate.ts";
41
+ /**
42
+ * The five retained items of DR-0025, in the order section 4.3 of the M4
43
+ * plan tabulates them. This list is CLOSED. A sixth name is a plan revision,
44
+ * not a code edit, because every rollback trigger enumerates it.
45
+ */
46
+ export declare const CUTOVER_SWITCHES: readonly ["planning-and-scope", "review-and-arbitration", "credentials-and-refs", "salvage-and-recovery", "closeout"];
47
+ export type CutoverSwitchName = (typeof CUTOVER_SWITCHES)[number];
48
+ /** Which process holds the authority a switch governs. */
49
+ export type SwitchState = "current" | "kernel";
50
+ export declare const SWITCH_STATES: readonly SwitchState[];
51
+ /**
52
+ * The switch with owner latency. Named as a constant because three separate
53
+ * places have to treat it differently and a repeated string literal is how
54
+ * one of them ends up not doing so.
55
+ */
56
+ export declare const OWNER_LATENCY_SWITCH: CutoverSwitchName;
57
+ /**
58
+ * One switch's record. `restoreTo` is the value the switch held BEFORE the
59
+ * flip. It is what makes freeze-point restore a matter of reading a recorded
60
+ * value rather than reconstructing an intent, and a record without it cannot
61
+ * be rolled back at all, which is why every read here refuses one.
62
+ */
63
+ export interface SwitchRecord {
64
+ state: SwitchState;
65
+ flippedAt: string;
66
+ flippedBy: string;
67
+ reason: string;
68
+ restoreTo: SwitchState;
69
+ }
70
+ export interface CutoverState {
71
+ switches: Record<CutoverSwitchName, SwitchRecord>;
72
+ }
73
+ /**
74
+ * `cutover.json` sits at the FLEET ROOT and not under `state/`, because
75
+ * `state/` is gitignored (src/fleet.ts:29) and trigger 2 step 1 identifies a
76
+ * flip with `git log` over this exact path. A switch history that is not
77
+ * committed is not a history.
78
+ */
79
+ export declare function cutoverStatePath(fleet: Fleet): string;
80
+ /**
81
+ * The document as it was parsed, keys and all. It is carried beside the typed
82
+ * state because the typed view is NARROWER than the file: `CutoverState` names
83
+ * `switches` and nothing else, so a writer that serialises the typed view
84
+ * DELETES every other key the file held. A rollback is a change to the
85
+ * switches, not a rewrite of the document, and this is what lets the two be
86
+ * different sizes.
87
+ */
88
+ export type CutoverDocument = Record<string, unknown>;
89
+ export type CutoverRead = {
90
+ kind: "read";
91
+ state: CutoverState;
92
+ document: CutoverDocument;
93
+ } | {
94
+ kind: "absent";
95
+ } | {
96
+ kind: "refused";
97
+ reason: string;
98
+ };
99
+ /**
100
+ * Validate a parsed document into a CutoverState, returning every reason at
101
+ * once rather than the first.
102
+ *
103
+ * FAIL CLOSED ON `restoreTo`. A record missing it, or carrying it as a word
104
+ * outside the two-value vocabulary, is REFUSED rather than defaulted to
105
+ * `current`. Defaulting would make rollback silently guess, and a guess that
106
+ * happens to be right most of the time is the guard that cannot go red.
107
+ */
108
+ export declare function validateCutoverDocument(document: unknown): string[];
109
+ /** Read and validate the fleet's cutover state. */
110
+ export declare function readCutoverState(fleet: Fleet): CutoverRead;
111
+ export declare function renderCutoverState(state: CutoverState | CutoverDocument): string;
112
+ /**
113
+ * Put a new switch table into the document the file actually held, leaving
114
+ * every other key exactly as it was read.
115
+ *
116
+ * WHY THIS EXISTS AS A NAMED FUNCTION. `validateCutoverDocument` refuses an
117
+ * unknown SWITCH NAME because the five are a closed list, and that made it
118
+ * look as though the whole document were closed. It is not: M4-P25 owns the
119
+ * schema and may add top-level keys, so a rollback that serialises only what
120
+ * this module's interface names would silently delete them. Refusing them
121
+ * instead would be this phase deciding M4-P25's schema, which is not its to
122
+ * decide, so the document is CARRIED rather than narrowed or refused.
123
+ */
124
+ export declare function withSwitches(document: CutoverDocument, switches: Record<CutoverSwitchName, SwitchRecord>): CutoverDocument;
125
+ /**
126
+ * Publish a whole cutover state ATOMICALLY: serialise everything, write it to
127
+ * a sibling temporary file, fsync that file, then rename over the
128
+ * destination. Nothing partial is ever visible at the destination path,
129
+ * because rename(2) within one directory is atomic.
130
+ *
131
+ * THE MECHANISM THIS EXISTS AGAINST is not "a crash". It is WRITING STATE
132
+ * PER ITEM WHILE ITERATING, which leaves a file that is internally valid and
133
+ * factually wrong: three switches saying `kernel` and two saying `current`
134
+ * describes a process that has no owner for three of its five authorities.
135
+ * Every caller in this module builds the complete next state first and calls
136
+ * this once.
137
+ */
138
+ export declare function publishCutoverState(path: string, state: CutoverState | CutoverDocument): void;
139
+ export interface SwitchChange {
140
+ name: CutoverSwitchName;
141
+ from: SwitchState;
142
+ to: SwitchState;
143
+ }
144
+ export interface RollbackPlan {
145
+ changes: SwitchChange[];
146
+ next: CutoverState;
147
+ }
148
+ /**
149
+ * Observer called once per switch as the next state is assembled IN MEMORY,
150
+ * before anything reaches disk. It exists so a caller can report progress,
151
+ * and it is also the seam criterion 2's red witness uses: an observer that
152
+ * throws on the third call aborts the assembly, and the destination file must
153
+ * still hold the five values it held before.
154
+ */
155
+ export type SwitchObserver = (change: SwitchChange, index: number) => void;
156
+ export interface RollbackOptions {
157
+ now: string;
158
+ by: string;
159
+ reason: string;
160
+ onSwitch?: SwitchObserver;
161
+ }
162
+ export type RollbackTrigger = "drain-reversal" | "freeze-point-restore" | "retirement-unmet";
163
+ export declare const ROLLBACK_TRIGGERS: readonly RollbackTrigger[];
164
+ /**
165
+ * The target value a trigger moves a switch to.
166
+ *
167
+ * Trigger 1 (drain reversal) hands every authority back to the current
168
+ * process, so the target is `current` for all five: in-flight work has to be
169
+ * handled by the process that started it.
170
+ *
171
+ * Trigger 2 (freeze-point restore) reads each switch's own `restoreTo`. That
172
+ * is the difference between the two, and it is why M4-P25 criterion 4 refuses
173
+ * a write that omits the field.
174
+ *
175
+ * ROLLBACK IS MONOTONE AND THE CLAMP BELOW IS WHAT MAKES THE MODULE HEADER
176
+ * TRUE. `restoreTo` is the value a switch held BEFORE its last flip, and
177
+ * `planRollback` sets it to the value the switch is leaving. So a switch that
178
+ * has already been rolled back to `current` carries `restoreTo: "kernel"`, and
179
+ * returning that value would move the switch FORWARD on the second run of the
180
+ * same command. The failure arm of trigger 1 step 1 exits nonzero after the
181
+ * local write has already happened, and the natural response to that is to run
182
+ * the command again, so the second run is not a hypothetical. A rollback never
183
+ * hands authority to the kernel: `current` is a floor, and a switch already at
184
+ * it is a no-op rather than a flip.
185
+ */
186
+ export declare function targetFor(trigger: RollbackTrigger, record: SwitchRecord): SwitchState;
187
+ /**
188
+ * Assemble the complete next state. Throws rather than returning a partial
189
+ * plan, and touches no file at all: publishing is a separate call.
190
+ */
191
+ export declare function planRollback(state: CutoverState, trigger: RollbackTrigger, options: RollbackOptions): RollbackPlan;
192
+ export type RollbackOutcome = {
193
+ ok: true;
194
+ changes: SwitchChange[];
195
+ next: CutoverState;
196
+ } | {
197
+ ok: false;
198
+ reason: string;
199
+ };
200
+ /**
201
+ * Plan and publish in one call: validate the whole input, assemble the whole
202
+ * next state, then write once.
203
+ *
204
+ * ORDER IS THE PROPERTY. Every refusal reachable from a bad input happens
205
+ * before `publishCutoverState` is called, so a refusal leaves the file
206
+ * byte-identical. That is asserted directly rather than trusted: see
207
+ * test/cutover.test.ts.
208
+ */
209
+ export declare function applyRollback(fleet: Fleet, trigger: RollbackTrigger, options: RollbackOptions): RollbackOutcome;
210
+ /**
211
+ * `unexaminable` is a THIRD kind and not an error channel. A drain predicate
212
+ * that cannot decide an entry has not found it clean, and folding the
213
+ * undecidable into "nothing here" is how a guard reads quiet at full speed.
214
+ */
215
+ export type InFlightKind = "worktree" | "task" | "unexaminable";
216
+ export interface InFlightItem {
217
+ kind: InFlightKind;
218
+ id: string;
219
+ detail: string;
220
+ }
221
+ /**
222
+ * WHAT DRAIN COUNTS, and the omission is the decision.
223
+ *
224
+ * In-flight work only: live worktrees and open tasks with no turn-end. Pushed
225
+ * unmerged BRANCHES are deliberately NOT counted (M4-D-15, decided at
226
+ * delivery/plan/kernel-plan-m4.md:3279). This container cannot delete a remote
227
+ * ref and the delete dry run exits 0 either way, and the probes generalised
228
+ * that further: a dry run does not probe push authorization at all
229
+ * (delivery/verification/m4-prototype-probes.md:165). A drain predicate over
230
+ * branches therefore blocks cutover on an owner action with no local
231
+ * pre-check, which is a predicate that can never read clean.
232
+ *
233
+ * C-2 is load-bearing here. A live worktree is a DIRECTORY on disk and an
234
+ * open task is meta.json's status plus the absence of the turn-end file. No
235
+ * process is probed, no pid is read, and nothing asks whether an agent is
236
+ * still breathing.
237
+ */
238
+ export declare function inFlightItems(fleet: Fleet): InFlightItem[];
239
+ /**
240
+ * The named list of things no local command can see. It is printed with every
241
+ * drain report and is NOT abbreviated when the network is unreachable: a
242
+ * command that silently degrades to a shorter answer is indistinguishable
243
+ * from one reporting a quiet system, which is the shape standing warning 6
244
+ * records for watchers.
245
+ */
246
+ export declare const CANNOT_SEE: readonly string[];
247
+ export interface CommandResult {
248
+ status: number;
249
+ stdout: string;
250
+ stderr: string;
251
+ }
252
+ export declare function runGit(cwd: string, args: string[]): CommandResult;
253
+ /**
254
+ * Commit and push the fleet's rolled-back state.
255
+ *
256
+ * THE FAILURE ARM IS WRITTEN FIRST, and that ordering is the point rather
257
+ * than a style. A rollback that changes `cutover.json` locally and does not
258
+ * land it leaves every other environment believing the switches are still
259
+ * `kernel`, and a push whose failure is swallowed is indistinguishable from a
260
+ * push that worked. Every arm below returns a reason; none returns silence.
261
+ *
262
+ * A fleet with no remote is a REFUSAL, not a skip. The caller may pass
263
+ * `allowNoRemote` to turn it into a reported, non-fatal condition, and when it
264
+ * does the reason is still printed, so a reader can never mistake the local
265
+ * write for a published one.
266
+ *
267
+ * THE COMMIT CARRIES THE NAMED PATHS AND NOTHING ELSE. Staging everything at
268
+ * the fleet root stages whatever is dirty anywhere under it, and the
269
+ * precondition of trigger 1 is that IN-FLIGHT WORK EXISTS: a half-written
270
+ * note, a torn meta.json, an operator scratch file. Committing all of it under
271
+ * the message "cutover rollback" publishes somebody else's unfinished work
272
+ * under this command's name, which is a write whose scope is wider than the
273
+ * sentence describing it. Three checks make the scope exactly the named set:
274
+ * the index must be empty before, the staging names paths, and the staged set
275
+ * is compared with the requested one afterwards.
276
+ */
277
+ export type SyncOutcome = {
278
+ ok: true;
279
+ pushed: true;
280
+ head: string;
281
+ staged: string[];
282
+ } | {
283
+ ok: true;
284
+ pushed: false;
285
+ reason: string;
286
+ staged: string[];
287
+ } | {
288
+ ok: false;
289
+ reason: string;
290
+ };
291
+ export declare function syncFleetState(fleetRoot: string, options: {
292
+ allowNoRemote?: boolean;
293
+ message: string;
294
+ paths: string[];
295
+ }): SyncOutcome;
296
+ /**
297
+ * Refuse to touch a tree that holds uncommitted work.
298
+ *
299
+ * ANY `git checkout --` in such a tree is destructive, INCLUDING when it names
300
+ * a single path, and especially the path being edited. This repository has
301
+ * paid for that twice; the rule is standing warning 8 in the agent-rules file.
302
+ * The guard is a precondition of the restore and not a warning printed beside
303
+ * it, because a warning is advice and this is the only thing standing between
304
+ * a rollback and four rounds of somebody's uncommitted work.
305
+ */
306
+ export declare function refuseIfTreeDirty(repoRoot: string): GuardResult;
307
+ export type RestoreFilesOutcome = {
308
+ ok: true;
309
+ roots: string[];
310
+ removed: string[];
311
+ } | {
312
+ ok: false;
313
+ reason: string;
314
+ };
315
+ /**
316
+ * Restore the named retirement roots from a pre-freeze sha.
317
+ *
318
+ * The dirty check runs FIRST and returns before any version-control
319
+ * invocation that can write. That order is the whole guarantee, and the test
320
+ * asserts the dirty file is byte-identical afterwards rather than asserting
321
+ * the exit code alone.
322
+ *
323
+ * "RESTORE" IS A CLAIM ABOUT THE WHOLE ROOT, AND CHECKING A TREE OUT OVER A
324
+ * PATH IS NOT ONE. `checkout <sha> -- <root>` writes what the sha held and
325
+ * removes NOTHING, so every file added under the root after the freeze
326
+ * survives the restore untouched. The result is a hybrid tree that the caller
327
+ * prints `RESTORED` over: a verdict wider than the operation that produced it.
328
+ * The post-freeze additions are therefore enumerated and removed, and then the
329
+ * root is COMPARED with the sha. The comparison is the verdict; without it the
330
+ * success arm is an assumption, and a success arm that cannot fail is the
331
+ * guard that cannot go red.
332
+ */
333
+ export declare function restoreRetirementRoots(repoRoot: string, sha: string, roots: string[]): RestoreFilesOutcome;
334
+ /**
335
+ * The property that makes trigger 2 step 3 unrehearsable, in one sentence,
336
+ * exported so the document, the command and the rehearsal script all print
337
+ * the SAME sentence and cannot drift apart.
338
+ */
339
+ export declare const UNREHEARSABLE_REASON: string;
340
+ export type RestoreRequestOutcome = {
341
+ ok: true;
342
+ text: string;
343
+ fields: number;
344
+ } | {
345
+ ok: false;
346
+ reasons: string[];
347
+ };
348
+ /**
349
+ * Generate the owner request for trigger 2 step 3.
350
+ *
351
+ * TWO STRUCTURALLY DIFFERENT REFUSALS, and the second is the one a field
352
+ * check is green on. An ABSENT key is caught by asking whether the key is
353
+ * there. A key that is PRESENT and EMPTY passes that question and produces a
354
+ * request with a blank where a pre-flip value belongs, which the owner would
355
+ * have to fill in from memory. Both are refused, and the refusal names the
356
+ * field, because an owner request with a hole in it is worse than no request:
357
+ * it looks complete.
358
+ */
359
+ export declare function generateRestoreRequest(document: unknown): RestoreRequestOutcome;
360
+ export type Disposition = "PORT" | "DELETE" | "KEEP";
361
+ export interface RetirementRow {
362
+ id: string;
363
+ disposition: Disposition;
364
+ /** For PORT: the kernel artifact the rule moves to, relative to the repo. */
365
+ destination?: string;
366
+ /**
367
+ * For PORT: a command that was RED under the OLD rule. Retirement is
368
+ * refused until it is red under the NEW artifact too. This is what turns
369
+ * "PORT, verify not weaker" from a judgment into a command.
370
+ */
371
+ negativeWitness?: string[];
372
+ /**
373
+ * The exit status the row RECORDED for its negative witness, when it
374
+ * records one. Present, it is required to match: see `evaluatePortRow`.
375
+ */
376
+ expectedWitnessExit?: number;
377
+ }
378
+ export type PortVerdict = "ported" | "unported";
379
+ export interface PortResult {
380
+ id: string;
381
+ verdict: PortVerdict;
382
+ reason: string;
383
+ }
384
+ /**
385
+ * Decide one PORT row.
386
+ *
387
+ * `ported` needs BOTH halves and the second is the one that matters. A verdict
388
+ * derived only from the destination file existing is the vacuous version: a
389
+ * file can exist and say nothing. The negative witness was RED under the old
390
+ * rule, so a witness that exits 0 under the new artifact means the new
391
+ * artifact does not catch what the old one caught, which is precisely
392
+ * "WEAKER". Exit 0 from the witness is therefore `unported`, not a pass.
393
+ *
394
+ * `ported` IS REACHED FROM A POSITIVE TEST AND NEVER FROM A FALLTHROUGH, and
395
+ * that is what the first three checks below are. `readRetirementInventory`
396
+ * casts whatever the fixture's `rows` array holds, so a row may be a string, a
397
+ * number, or an object whose `disposition` is misspelt; the earlier form asked
398
+ * only whether the disposition was NOT the word `PORT`, so every one of those
399
+ * returned `ported`, which is a verdict of "this retirement is complete" over
400
+ * a row nobody could read. The vocabulary is closed, so a value outside it is
401
+ * `unported` and names itself.
402
+ */
403
+ export declare const DISPOSITIONS: readonly Disposition[];
404
+ export declare function evaluatePortRow(row: RetirementRow, repoRoot: string): PortResult;
405
+ export type InventoryRead = {
406
+ kind: "read";
407
+ rows: RetirementRow[];
408
+ } | {
409
+ kind: "absent";
410
+ } | {
411
+ kind: "refused";
412
+ reason: string;
413
+ };
414
+ /**
415
+ * Read an M4-P23 retirement inventory. The path is a parameter because that
416
+ * phase runs concurrently with this one and its file does not exist on this
417
+ * branch; the rehearsal supplies a fixture.
418
+ */
419
+ export declare function readRetirementInventory(path: string): InventoryRead;
420
+ /** The shipped schema document's basename, in the package's `schemas/`. */
421
+ export declare const CUTOVER_STATE_SCHEMA_FILENAME = "cutover-state.schema.json";
422
+ /**
423
+ * Absolute path of the shipped cutover-state schema.
424
+ *
425
+ * `packageRoot()` walks UP and TESTS rather than counting `..`, because the
426
+ * depth differs between running from `src/` and running from `dist/src/`
427
+ * (src/modes.ts:39 states the same reason for the same walk).
428
+ */
429
+ export declare function cutoverStateSchemaPath(): string;
430
+ /** The shipped schema document, read once. */
431
+ export declare function cutoverStateSchema(): SchemaDocument;
432
+ /**
433
+ * Validate a whole cutover document against the SHIPPED SCHEMA.
434
+ *
435
+ * WHY THIS IS NOT `validateCutoverDocument`, and the two are kept apart on
436
+ * purpose. That function is this module's READ guard and answers "can this
437
+ * file be interpreted". This one is the WRITE guard and answers "is this
438
+ * document one the package is prepared to ship", and criterion 4 says the
439
+ * required-field rule is validated BY THE SCHEMA rather than by a command.
440
+ * A check implemented only in a command is one the schema cannot enforce for
441
+ * a later writer, and a later writer is precisely who rollback depends on:
442
+ * M4-P26's `targetFor` READS the recorded `restoreTo` (src/cutover.ts:352)
443
+ * rather than remembering an intent, so a record written without one is a
444
+ * switch that can never be rolled back.
445
+ */
446
+ export declare function cutoverSchemaDiagnostics(document: unknown): string[];
447
+ /**
448
+ * The captured pre-freeze state, relative to the REPOSITORY root. It carries
449
+ * the branch-protection ruleset as it was before the first flip, so the
450
+ * restore request M4-P26 generates has an input rather than a memory. T-025
451
+ * is why: the one step that cannot be rehearsed is the one that failed, and
452
+ * its INPUT can be captured in advance even when its EXECUTION cannot.
453
+ */
454
+ export declare const PRE_FREEZE_RULESET_PATH = "delivery/plan/cutover/pre-freeze-ruleset.json";
455
+ export type PreFreezeVerdict =
456
+ /** No switch reads `kernel`, so nothing has been frozen and nothing is owed. */
457
+ {
458
+ kind: "not-required";
459
+ } | {
460
+ kind: "satisfied";
461
+ capturedAt: string;
462
+ } | {
463
+ kind: "refused";
464
+ arm: "absent" | "stale" | "unreadable";
465
+ reason: string;
466
+ };
467
+ /**
468
+ * THE CAPTURE TIME IS READ FROM THE DOCUMENT, NOT FROM ITS MTIME, and that is
469
+ * a correction rather than a preference.
470
+ *
471
+ * The criterion says the capture must not be OLDER than the most recent switch
472
+ * write. An mtime does not survive the journey: `git clone` and `git checkout`
473
+ * set every working-tree mtime to the moment of the checkout, so a capture
474
+ * taken weeks before a flip reads as newer than the flip on any fresh clone,
475
+ * and the guard reports satisfied on exactly the machine a reviewer uses.
476
+ * Measured, and the measurement is in the work history. So the document
477
+ * records `captured-at` and the comparison is content to content.
478
+ *
479
+ * FAIL CLOSED ON AN UNREADABLE CAPTURE. A document with no `captured-at`, or
480
+ * one that does not parse as an instant, is `unreadable` and refuses. Treating
481
+ * it as satisfied would make a malformed capture indistinguishable from a good
482
+ * one, which is the guard that cannot go red.
483
+ */
484
+ export declare function preFreezeGuard(repoRoot: string, state: CutoverState): PreFreezeVerdict;
485
+ export type BranchCount = {
486
+ kind: "counted";
487
+ count: number;
488
+ branches: string[];
489
+ } | {
490
+ kind: "unexaminable";
491
+ reason: string;
492
+ };
493
+ /**
494
+ * Count the PUSHED, UNMERGED branches, and print them nowhere near the drain
495
+ * verdict.
496
+ *
497
+ * THIS NUMBER DOES NOT FEED THE PREDICATE, AND THAT IS THE DECISION (M4-D-15,
498
+ * delivery/plan/kernel-plan-m4.md:3279). Remote ref deletion is refused in
499
+ * this container, and `git push --dry-run` does not probe push authorization
500
+ * at all, in either direction, for any ref namespace
501
+ * (delivery/verification/m4-prototype-probes.md:165). A drain defined as "no
502
+ * unmerged branches" therefore waits forever on an owner action that has no
503
+ * local pre-check, which is a predicate that can never read clean. It is
504
+ * reported because an operator wants to see it, and it is reported on its own
505
+ * line so that no reader can mistake it for a reason the cutover is blocked.
506
+ *
507
+ * `unexaminable` is a THIRD answer for the same reason it is one in
508
+ * `inFlightItems`: a repository with no `origin/<default>` cannot be asked
509
+ * this question, and printing `0` there would be an answer nobody measured.
510
+ */
511
+ export declare function unmergedBranchCount(repoRoot: string, upstream?: string): BranchCount;
512
+ /** The M4-P23 inventory, relative to the REPOSITORY root. */
513
+ export declare const RETIREMENT_INVENTORY_PATH = "delivery/plan/cutover/retirement-inventory.json";
514
+ /**
515
+ * The first tokens a retirement row's command may start a segment with.
516
+ *
517
+ * MIRRORED FROM `scripts/check-retirement-inventory.mjs`, DELIBERATELY, AND
518
+ * THE DRIFT IS ASSERTED BY A TEST rather than by this comment. The script is
519
+ * this project's own predicate and is KEPT rather than shipped (DR-0029), so
520
+ * the kernel cannot import it; but the two lists screening the same rows must
521
+ * not diverge, so `test/cutover.test.ts` reads the script's
522
+ * `ALLOWED_FIRST_TOKENS` and requires this set to be no wider.
523
+ *
524
+ * WHAT THE LIST BUYS AND WHAT IT DOES NOT. Every tool on it is one with no
525
+ * option for writing a file, so a row cannot modify the tree this command is
526
+ * auditing. It is a TOOL allowlist, not a sandbox: the child still runs with
527
+ * this process's privileges and can read anything this process can read.
528
+ */
529
+ export declare const RETIREMENT_COMMAND_TOKENS: ReadonlySet<string>;
530
+ /**
531
+ * Screen one inventory command. The rows are DATA FROM A FILE, so the command
532
+ * is screened before anything spawns it, and the check is on the EXECUTABLE
533
+ * POSITION of every segment rather than on the whole string: a whole-string
534
+ * denylist refuses `grep -c 'npm ci' gate-registry.yaml`, which runs no npm at
535
+ * all and merely searches for those characters.
536
+ */
537
+ export declare function screenRetirementCommand(command: unknown): string[];
538
+ export type RowAdaptation = {
539
+ kind: "row";
540
+ row: RetirementRow;
541
+ } | {
542
+ kind: "refused";
543
+ result: PortResult;
544
+ };
545
+ /**
546
+ * Turn one row of the SHIPPED inventory into the shape `evaluatePortRow`
547
+ * takes.
548
+ *
549
+ * THIS ADAPTER EXISTS BECAUSE THE TWO SHAPES REALLY ARE DIFFERENT, and the
550
+ * difference is silent in the dangerous direction. `RetirementRow` declares
551
+ * `negativeWitness` as an argv ARRAY (src/cutover.ts:1128), and the M4-P23
552
+ * inventory writes `negative-witness` as an OBJECT carrying a SHELL STRING
553
+ * (delivery/plan/cutover/retirement-inventory.json:1). Handing the shipped
554
+ * document straight to `evaluatePortRow` therefore returns `unported` with
555
+ * "PORT row carries no negative-witness command" for every row in it: a
556
+ * verdict that looks like a finding about the kernel and is a finding about a
557
+ * key spelling. The adapter is named, tested and refuses rather than
558
+ * defaulting, so the mismatch cannot come back as a silent all-red.
559
+ */
560
+ export declare function retirementRowFromDocument(raw: unknown): RowAdaptation;
561
+ export interface RetirementReport {
562
+ results: PortResult[];
563
+ unported: number;
564
+ }
565
+ export type RetirementRead = {
566
+ kind: "read";
567
+ report: RetirementReport;
568
+ } | {
569
+ kind: "refused";
570
+ reason: string;
571
+ };
572
+ /**
573
+ * Evaluate every PORT row of the inventory.
574
+ *
575
+ * THE VACUOUS VERDICT IS THE ONE THIS GUARDS AGAINST. `ported` is not "the
576
+ * named kernel artifact exists"; a file can exist and say nothing. Both halves
577
+ * are required and the second is the one that matters: the row's negative
578
+ * witness was RED against a subject that does not carry the rule, so a witness
579
+ * exiting 0 means the probe discriminates nothing and the row is `unported`.
580
+ * That derivation is M4-P26's `evaluatePortRow` and is REUSED here rather than
581
+ * reimplemented; this function supplies the reading, the adaptation and the
582
+ * screen.
583
+ */
584
+ export declare function evaluateRetirementInventory(inventoryPath: string, repoRoot: string): RetirementRead;