@tiphys/kernel 0.0.0 → 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.
Files changed (181) hide show
  1. package/AGENTS.md +611 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +278 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +14 -0
  11. package/dist/src/brief.d.ts +32 -0
  12. package/dist/src/brief.js +59 -0
  13. package/dist/src/checklists.d.ts +169 -0
  14. package/dist/src/checklists.js +310 -0
  15. package/dist/src/checks.d.ts +828 -0
  16. package/dist/src/checks.js +3314 -0
  17. package/dist/src/cli.d.ts +9 -0
  18. package/dist/src/cli.js +57 -0
  19. package/dist/src/commands/brief.d.ts +92 -0
  20. package/dist/src/commands/brief.js +320 -0
  21. package/dist/src/commands/checklist.d.ts +42 -0
  22. package/dist/src/commands/checklist.js +168 -0
  23. package/dist/src/commands/doctor.d.ts +35 -0
  24. package/dist/src/commands/doctor.js +605 -0
  25. package/dist/src/commands/gates.d.ts +9 -0
  26. package/dist/src/commands/gates.js +360 -0
  27. package/dist/src/commands/init.d.ts +30 -0
  28. package/dist/src/commands/init.js +128 -0
  29. package/dist/src/commands/lock.d.ts +1 -0
  30. package/dist/src/commands/lock.js +229 -0
  31. package/dist/src/commands/mode.d.ts +40 -0
  32. package/dist/src/commands/mode.js +134 -0
  33. package/dist/src/commands/plan.d.ts +20 -0
  34. package/dist/src/commands/plan.js +105 -0
  35. package/dist/src/commands/pool.d.ts +1 -0
  36. package/dist/src/commands/pool.js +128 -0
  37. package/dist/src/commands/spawn.d.ts +1 -0
  38. package/dist/src/commands/spawn.js +146 -0
  39. package/dist/src/commands/status.d.ts +19 -0
  40. package/dist/src/commands/status.js +120 -0
  41. package/dist/src/commands/teardown.d.ts +1 -0
  42. package/dist/src/commands/teardown.js +79 -0
  43. package/dist/src/commands/tuition.d.ts +21 -0
  44. package/dist/src/commands/tuition.js +218 -0
  45. package/dist/src/commands/validate.d.ts +78 -0
  46. package/dist/src/commands/validate.js +360 -0
  47. package/dist/src/commands/watch.d.ts +1 -0
  48. package/dist/src/commands/watch.js +172 -0
  49. package/dist/src/exec/env.d.ts +128 -0
  50. package/dist/src/exec/env.js +190 -0
  51. package/dist/src/fleet.d.ts +51 -0
  52. package/dist/src/fleet.js +80 -0
  53. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  54. package/dist/src/gates/adapters/http-json.js +283 -0
  55. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  56. package/dist/src/gates/adapters/migrations-command.js +373 -0
  57. package/dist/src/gates/citations.d.ts +408 -0
  58. package/dist/src/gates/citations.js +1163 -0
  59. package/dist/src/gates/coverage.d.ts +284 -0
  60. package/dist/src/gates/coverage.js +701 -0
  61. package/dist/src/gates/credentials.d.ts +74 -0
  62. package/dist/src/gates/credentials.js +533 -0
  63. package/dist/src/gates/deploy.d.ts +1 -0
  64. package/dist/src/gates/deploy.js +33 -0
  65. package/dist/src/gates/manifest.d.ts +99 -0
  66. package/dist/src/gates/manifest.js +208 -0
  67. package/dist/src/gates/migrations.d.ts +1 -0
  68. package/dist/src/gates/migrations.js +36 -0
  69. package/dist/src/gates/pin.d.ts +114 -0
  70. package/dist/src/gates/pin.js +154 -0
  71. package/dist/src/gates/red-witness.d.ts +22 -0
  72. package/dist/src/gates/red-witness.js +390 -0
  73. package/dist/src/gates/release.d.ts +283 -0
  74. package/dist/src/gates/release.js +820 -0
  75. package/dist/src/gates/result.d.ts +116 -0
  76. package/dist/src/gates/result.js +91 -0
  77. package/dist/src/gates/run.d.ts +566 -0
  78. package/dist/src/gates/run.js +1536 -0
  79. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  80. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  81. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  82. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  83. package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
  84. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  85. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  86. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  87. package/dist/src/gates/scope.d.ts +131 -0
  88. package/dist/src/gates/scope.js +1018 -0
  89. package/dist/src/gates/suite.d.ts +217 -0
  90. package/dist/src/gates/suite.js +927 -0
  91. package/dist/src/gates/validate.d.ts +121 -0
  92. package/dist/src/gates/validate.js +414 -0
  93. package/dist/src/hooks.d.ts +32 -0
  94. package/dist/src/hooks.js +62 -0
  95. package/dist/src/liveness.d.ts +321 -0
  96. package/dist/src/liveness.js +396 -0
  97. package/dist/src/lock.d.ts +178 -0
  98. package/dist/src/lock.js +500 -0
  99. package/dist/src/modes.d.ts +149 -0
  100. package/dist/src/modes.js +258 -0
  101. package/dist/src/path-identity.d.ts +2 -0
  102. package/dist/src/path-identity.js +10 -0
  103. package/dist/src/plan.d.ts +73 -0
  104. package/dist/src/plan.js +153 -0
  105. package/dist/src/pool.d.ts +130 -0
  106. package/dist/src/pool.js +721 -0
  107. package/dist/src/roles.d.ts +430 -0
  108. package/dist/src/roles.js +734 -0
  109. package/dist/src/spawn.d.ts +177 -0
  110. package/dist/src/spawn.js +332 -0
  111. package/dist/src/status.d.ts +91 -0
  112. package/dist/src/status.js +119 -0
  113. package/dist/src/task.d.ts +264 -0
  114. package/dist/src/task.js +305 -0
  115. package/dist/src/teardown.d.ts +32 -0
  116. package/dist/src/teardown.js +314 -0
  117. package/dist/src/tuition.d.ts +159 -0
  118. package/dist/src/tuition.js +311 -0
  119. package/dist/src/validate.d.ts +230 -0
  120. package/dist/src/validate.js +732 -0
  121. package/dist/src/version.d.ts +3 -0
  122. package/dist/src/version.js +38 -0
  123. package/dist/src/watcher.d.ts +275 -0
  124. package/dist/src/watcher.js +859 -0
  125. package/dist/src/witness/run.d.ts +274 -0
  126. package/dist/src/witness/run.js +1327 -0
  127. package/dist/src/witness/spec.d.ts +102 -0
  128. package/dist/src/witness/spec.js +253 -0
  129. package/dist/tsconfig.src.tsbuildinfo +1 -0
  130. package/gate-registry.yaml +390 -0
  131. package/gates.manifest.json +195 -0
  132. package/package.json +57 -3
  133. package/role-model-config.yaml +88 -0
  134. package/roles/README.md +128 -0
  135. package/roles/_shared-dispatch-contract.md +87 -0
  136. package/roles/adversarial-plan-reviewer.md +80 -0
  137. package/roles/clean-room-reviewer.md +140 -0
  138. package/roles/implementer.md +460 -0
  139. package/roles/investigator.md +138 -0
  140. package/roles/plan-writer.md +95 -0
  141. package/schemas/README.md +81 -0
  142. package/schemas/assurance-modes.schema.json +264 -0
  143. package/schemas/charter.schema.json +166 -0
  144. package/schemas/checklist.schema.json +114 -0
  145. package/schemas/decision-record.schema.json +88 -0
  146. package/schemas/final-report.schema.json +90 -0
  147. package/schemas/finding.schema.json +106 -0
  148. package/schemas/gate-registry.schema.json +260 -0
  149. package/schemas/mechanism-index.schema.json +94 -0
  150. package/schemas/plan.schema.json +300 -0
  151. package/schemas/report.schema.json +579 -0
  152. package/schemas/role-brief.schema.json +105 -0
  153. package/schemas/role-model-config.schema.json +90 -0
  154. package/schemas/status-line.schema.json +40 -0
  155. package/schemas/tuition.schema.json +191 -0
  156. package/schemas/verdict.schema.json +289 -0
  157. package/schemas/work-history.schema.json +183 -0
  158. package/templates/charter.example.yaml +54 -0
  159. package/templates/decision-record.example.yaml +27 -0
  160. package/templates/final-report.example.yaml +80 -0
  161. package/templates/plan.example.yaml +87 -0
  162. package/templates/report.example.yaml +236 -0
  163. package/templates/warnings.md +74 -0
  164. package/templates/work-history.example.yaml +185 -0
  165. package/tuition/README.md +76 -0
  166. package/tuition/T-001.yaml +48 -0
  167. package/tuition/T-002.yaml +51 -0
  168. package/tuition/T-003.yaml +100 -0
  169. package/tuition/T-004.yaml +52 -0
  170. package/tuition/T-005.yaml +72 -0
  171. package/tuition/T-006.yaml +81 -0
  172. package/tuition/T-007.yaml +56 -0
  173. package/tuition/T-008.yaml +111 -0
  174. package/tuition/T-009.yaml +50 -0
  175. package/tuition/T-015.yaml +36 -0
  176. package/tuition/T-016.yaml +36 -0
  177. package/tuition/T-017.yaml +46 -0
  178. package/tuition/T-018.yaml +84 -0
  179. package/tuition/T-021.yaml +40 -0
  180. package/tuition/T-022.yaml +36 -0
  181. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,360 @@
1
+ import { resolve } from "node:path";
2
+ import { EX_USAGE } from "../cli.js";
3
+ import { loadManifest, schemaDocumentPaths, } from "../gates/manifest.js";
4
+ import { EXIT_GATE_ERROR, exitCodeForStatus, makeGateResult, renderGateResult, } from "../gates/result.js";
5
+ import { runGates } from "../gates/run.js";
6
+ import { loadSchema } from "../gates/validate.js";
7
+ import { readRegularFileIfPresent, refuseOpenForWrite, runStep, singleLine, } from "../task.js";
8
+ import { writeFileSync } from "node:fs";
9
+ /**
10
+ * tiphys gates (kernel plan M2, M2-P1 step 7).
11
+ *
12
+ * gates run run a manifest's gates and write an evidence bundle
13
+ * gates self-check the `manifest-self-check` gate itself
14
+ *
15
+ * `self-check` lives here rather than in a script because it is the ONE gate
16
+ * the initial manifest carries, and M2R-012's point is that the first CI run
17
+ * must measure something real rather than report a green bundle over an
18
+ * empty gate set. It validates both shipped schema documents against the
19
+ * closed keyword set and the manifest against its own schema, so a schema
20
+ * document that grows an unimplemented keyword, or a manifest that drifts
21
+ * from its shape, turns the bundle red on the run that introduced it.
22
+ */
23
+ const USAGE = "usage: tiphys gates <run (--manifest <file> | --registry <file> [--mode <mode>]) " +
24
+ "--evidence <dir> [--base <ref>] [--head <ref>] [--phase <id>] [--only <id>] | " +
25
+ "self-check --manifest <file> --result <file> --evidence <dir>>";
26
+ function usageError(message) {
27
+ if (message !== undefined) {
28
+ process.stderr.write(`tiphys gates: ${message}\n`);
29
+ }
30
+ process.stderr.write(`${USAGE}\n`);
31
+ return EX_USAGE;
32
+ }
33
+ const VALUE_FLAGS = [
34
+ "--manifest",
35
+ "--registry",
36
+ "--mode",
37
+ "--evidence",
38
+ "--result",
39
+ "--base",
40
+ "--head",
41
+ "--phase",
42
+ ];
43
+ function parseFlags(args) {
44
+ const flags = { only: [] };
45
+ for (let i = 0; i < args.length; i += 1) {
46
+ const flag = args[i];
47
+ const value = args[i + 1];
48
+ if (flag === "--only") {
49
+ if (value === undefined || value.startsWith("--")) {
50
+ return undefined;
51
+ }
52
+ flags.only.push(value);
53
+ i += 1;
54
+ continue;
55
+ }
56
+ if (flag === undefined || !VALUE_FLAGS.includes(flag)) {
57
+ return undefined;
58
+ }
59
+ if (value === undefined || value.startsWith("--")) {
60
+ return undefined;
61
+ }
62
+ if (flag === "--manifest") {
63
+ flags.manifest = value;
64
+ }
65
+ else if (flag === "--registry") {
66
+ flags.registry = value;
67
+ }
68
+ else if (flag === "--mode") {
69
+ flags.mode = value;
70
+ }
71
+ else if (flag === "--evidence") {
72
+ flags.evidence = value;
73
+ }
74
+ else if (flag === "--result") {
75
+ flags.result = value;
76
+ }
77
+ else if (flag === "--base") {
78
+ flags.base = value;
79
+ }
80
+ else if (flag === "--head") {
81
+ flags.head = value;
82
+ }
83
+ else {
84
+ flags.phase = value;
85
+ }
86
+ i += 1;
87
+ }
88
+ return flags;
89
+ }
90
+ /**
91
+ * One gate's `detail`, made safe to print as ONE line of this stream.
92
+ *
93
+ * `singleLine` folds newlines, which was the claim the original comment on
94
+ * the print loop made ("cannot forge additional `gates:` lines"). A clean-
95
+ * room hazard reviewer measured that claim as true for `\n` and silently
96
+ * narrower than it reads: `"a\rb".trim()` only trims the ends, so a bare
97
+ * carriage return survives into the printed line and can cosmetically
98
+ * overwrite its start on a real terminal. A gate's `detail` is already-
99
+ * trusted manifest content rather than an external input, so that is
100
+ * defense in depth, not a live exploit; it is fixed here because a comment
101
+ * that claims more than it delivers is the shape this repository keeps
102
+ * paying for. Every C0 control character and DEL becomes a visible escape,
103
+ * so nothing in a detail can move the cursor and nothing is silently
104
+ * dropped either.
105
+ */
106
+ function printableDetail(detail) {
107
+ let printable = "";
108
+ for (const character of singleLine(detail)) {
109
+ const code = character.codePointAt(0) ?? 0;
110
+ if (code < 0x20 || code === 0x7f) {
111
+ printable += `\\x${code.toString(16).padStart(2, "0")}`;
112
+ continue;
113
+ }
114
+ printable += character;
115
+ }
116
+ return printable;
117
+ }
118
+ function cmdRun(args) {
119
+ const flags = parseFlags(args);
120
+ if (flags === undefined) {
121
+ return usageError();
122
+ }
123
+ if (flags.manifest !== undefined && flags.registry !== undefined) {
124
+ // Two source documents is not a stronger run, it is an ambiguous one, and
125
+ // an ambiguous run's summary would name a document that governed half of
126
+ // it. Refuse rather than pick (M2-C-3, fail closed).
127
+ return usageError("--manifest and --registry are mutually exclusive; pass one");
128
+ }
129
+ if (flags.mode !== undefined && flags.registry === undefined) {
130
+ // A mode with nothing to select from is silently ignored otherwise, and a
131
+ // caller who believed the run was mode-scoped would read a wider bundle as
132
+ // a narrower one.
133
+ return usageError("--mode selects registry entries and requires --registry");
134
+ }
135
+ const source = flags.manifest ?? flags.registry;
136
+ if (source === undefined || flags.evidence === undefined) {
137
+ return usageError("run requires --manifest or --registry, and --evidence");
138
+ }
139
+ if (flags.result !== undefined) {
140
+ return usageError("--result is a gate flag, not a runner flag");
141
+ }
142
+ const outcome = runGates({
143
+ manifestPath: source,
144
+ registry: flags.registry !== undefined,
145
+ mode: flags.mode,
146
+ evidenceDir: resolve(flags.evidence),
147
+ base: flags.base,
148
+ head: flags.head,
149
+ phase: flags.phase,
150
+ only: flags.only,
151
+ });
152
+ // CR-861: THE RUN IDENTIFIES ITSELF, on every outcome, before anything else
153
+ // it has to say. `summary.json` carried a runId and nothing emitted one, so
154
+ // a caller could not tell whether the summary it read was its own. That is
155
+ // what "a bundle is attributable" has to mean to be true, and it is the
156
+ // property the record-level runId decline rests on: the caller compares the
157
+ // id printed here with `summary.json`'s, and a mismatch means the bundle is
158
+ // someone else's. Printed to stdout even when the run fails, so the id is
159
+ // available to a consumer that captures only one stream.
160
+ process.stdout.write(`gates: run ${outcome.runId}\n`);
161
+ if (outcome.summary === undefined) {
162
+ process.stderr.write(`tiphys gates run: ${outcome.reason ?? "failed"}\n`);
163
+ return outcome.exitCode;
164
+ }
165
+ const counts = outcome.summary.counts;
166
+ // The registry can declare a gate this runner cannot execute (D-11: R-043
167
+ // and R-044 are verified by a clean-room checklist probe, not by a script).
168
+ // Printing them is what makes "the report accounts for EVERY gate the mode
169
+ // selected" checkable from the run's own output: executed rows plus these.
170
+ const declared = outcome.summary.declaredByChecklist ?? [];
171
+ if (declared.length > 0) {
172
+ process.stdout.write(`gates: ${String(declared.length)} registry gate(s) declared verified-by ` +
173
+ `clean-room-checklist and NOT executed by this runner: ` +
174
+ `${declared.map((entry) => `${entry.id} (probe ${entry.probe})`).join(", ")}\n`);
175
+ }
176
+ if (outcome.summary.registry === true) {
177
+ process.stdout.write(`gates: registry ${outcome.summary.manifest} mode ${String(outcome.summary.mode)}\n`);
178
+ }
179
+ process.stdout.write(`gates: declared ${String(counts.declared)} applicable ${String(counts.applicable)} ` +
180
+ `verdict ${String(counts.verdict)} ` +
181
+ `green ${String(counts.green)} red ${String(counts.red)} ` +
182
+ `not-applicable ${String(counts["not-applicable"])} error ${String(counts.error)} ` +
183
+ `vacuous ${String(counts.vacuous)}\n`);
184
+ // M3-P11 criterion 1: STDOUT NAMES THE PATH.
185
+ //
186
+ // The runner separates "the command could not run" from "the precondition
187
+ // is unmet" and puts the reason in each gate's `detail`, but until this
188
+ // change `detail` never left the evidence directory: this function printed
189
+ // bundle counts and one aggregate reason naming gate IDS, so an operator
190
+ // reading the terminal saw `1 gate(s) reported error: manifest-self-check`
191
+ // and had to open `summary.json` to learn that the cause was a missing
192
+ // `bin/tiphys.ts`. A verdict a reader has to go and look up is one step
193
+ // better than the skip-that-was-a-crash, not two.
194
+ //
195
+ // EVERY ROW, GREEN INCLUDED. Fix round 1, finding C-1, and the reason the
196
+ // rule is now "every row" rather than "every row that looks interesting".
197
+ //
198
+ // As first written this loop skipped green rows, on the stated ground that
199
+ // a green detail is a count the summary line above already carries. That
200
+ // is an ASSUMPTION ABOUT WHAT A GREEN VERDICT CAN CONTAIN, and the scope
201
+ // gate falsified it in the same pull request: M3-P11 change B relaxed a
202
+ // HARD refusal (a head-side declaration addition was impossible) into a
203
+ // VISIBLE one (it is allowed, and NAMED for a reviewer to sign off), which
204
+ // makes the printed line the entire remaining safeguard. A scope gate
205
+ // carrying nothing but an amendment is GREEN, so the note reached stdout
206
+ // only when the gate ALSO had something else to refuse: visible exactly
207
+ // where the gate already says no, invisible where it is the only refusal
208
+ // there is. The evidence directory holds it in `summary.json` and the
209
+ // gate's captured `stdout.txt`, and no workflow in this repository uploads
210
+ // an artifact, so neither leaves the runner.
211
+ //
212
+ // The mechanism, not the instance: a compensating control is worth what it
213
+ // is READ at, so nothing may decide on a gate's behalf that its own
214
+ // sentence is not worth relaying. Matching a marker string here would fix
215
+ // one gate and leave the next author to rediscover this; relaying every
216
+ // row costs one line per gate and closes the class.
217
+ //
218
+ // Bounded by the gate count, and every printed line goes through
219
+ // `printableDetail` so one gate's detail cannot forge additional `gates:`
220
+ // lines in this stream, by newline OR by carriage return.
221
+ for (const row of outcome.summary.gates) {
222
+ const detail = printableDetail(row.detail);
223
+ process.stdout.write(detail === ""
224
+ ? `gates: ${row.id}: ${row.status}\n`
225
+ : `gates: ${row.id}: ${row.status}: ${detail}\n`);
226
+ }
227
+ const stream = outcome.exitCode === 0 ? process.stdout : process.stderr;
228
+ stream.write(`gates: ${outcome.reason ?? ""}\n`);
229
+ return outcome.exitCode;
230
+ }
231
+ /** Write a gate's own record, then exit with the code its status maps to. */
232
+ function emit(path, fields) {
233
+ const result = makeGateResult(fields);
234
+ const refusal = refuseOpenForWrite(path);
235
+ if (refusal !== undefined) {
236
+ process.stderr.write(`tiphys gates self-check: ${refusal}\n`);
237
+ return EXIT_GATE_ERROR;
238
+ }
239
+ const written = runStep(`writing ${path}`, () => writeFileSync(path, renderGateResult(result)));
240
+ if (!written.ok) {
241
+ process.stderr.write(`tiphys gates self-check: ${written.reason}\n`);
242
+ return EXIT_GATE_ERROR;
243
+ }
244
+ const status = result.status;
245
+ process.stdout.write(`${result.gate}: ${status} (${String(result.units)} ${result.unitLabel})\n`);
246
+ if (result.detail !== "") {
247
+ process.stdout.write(`${result.detail}\n`);
248
+ }
249
+ return exitCodeForStatus(status);
250
+ }
251
+ function cmdSelfCheck(args) {
252
+ const flags = parseFlags(args);
253
+ if (flags === undefined) {
254
+ return usageError();
255
+ }
256
+ if (flags.manifest === undefined || flags.result === undefined) {
257
+ return usageError("self-check requires --manifest and --result");
258
+ }
259
+ const startedAt = new Date().toISOString();
260
+ const base = {
261
+ gate: "manifest-self-check",
262
+ unitLabel: "schema documents validated",
263
+ startedAt,
264
+ evidence: [],
265
+ };
266
+ // Every shipped schema document, loaded through the closed keyword check.
267
+ // A keyword this validator does not implement is a LOAD failure, which is
268
+ // this gate's red: the document would otherwise be validating less than it
269
+ // appears to.
270
+ let validated = 0;
271
+ for (const path of schemaDocumentPaths()) {
272
+ const read = readRegularFileIfPresent(path);
273
+ if (read.kind !== "read") {
274
+ return emit(flags.result, {
275
+ ...base,
276
+ status: "error",
277
+ units: validated,
278
+ endedAt: new Date().toISOString(),
279
+ detail: read.kind === "absent"
280
+ ? `schema document ${path} is missing from this installation`
281
+ : read.reason,
282
+ });
283
+ }
284
+ let parsed;
285
+ try {
286
+ parsed = JSON.parse(read.body);
287
+ }
288
+ catch (error) {
289
+ return emit(flags.result, {
290
+ ...base,
291
+ status: "red",
292
+ units: validated,
293
+ endedAt: new Date().toISOString(),
294
+ detail: `${path} does not parse as JSON: ${error.message}`,
295
+ });
296
+ }
297
+ const loaded = loadSchema(parsed, path);
298
+ if (!loaded.ok) {
299
+ return emit(flags.result, {
300
+ ...base,
301
+ status: "red",
302
+ units: validated,
303
+ endedAt: new Date().toISOString(),
304
+ detail: loaded.reason,
305
+ });
306
+ }
307
+ validated += 1;
308
+ }
309
+ const manifest = loadManifest(flags.manifest);
310
+ if (!manifest.ok) {
311
+ // Diagnostics mean the document WAS validated and found wanting: red.
312
+ // No diagnostics means validation could not happen at all (absent, not a
313
+ // regular file, unparseable): error, fail closed (M2-C-3).
314
+ const invalid = manifest.diagnostics.length > 0;
315
+ return emit(flags.result, {
316
+ ...base,
317
+ status: invalid ? "red" : "error",
318
+ units: validated,
319
+ endedAt: new Date().toISOString(),
320
+ detail: [manifest.reason, ...manifest.diagnostics].join("; "),
321
+ });
322
+ }
323
+ // CR-812. `units` used to be 3, counting the manifest as a "schema document
324
+ // validated". Section 1.4 fixes the unitLabel, `units` is the entire
325
+ // anti-vacuity device of M2-C-2, and the one gate this milestone ships was
326
+ // reporting a count that did not match its own declared unit. The manifest
327
+ // validation is real work and is reported in `detail`, where it belongs.
328
+ return emit(flags.result, {
329
+ ...base,
330
+ status: "green",
331
+ units: validated,
332
+ endedAt: new Date().toISOString(),
333
+ detail: `validated ${String(validated)} schema document(s) against the closed keyword set ` +
334
+ `(${schemaDocumentPaths().join(", ")}), and ${flags.manifest} against gate-manifest.schema.json`,
335
+ });
336
+ }
337
+ /**
338
+ * The outer backstop for CR-801. Node's uncaught-exception exit code is 1,
339
+ * which is this phase's own EXIT_RED, so a throw escaping anywhere under
340
+ * `gates` used to be indistinguishable to a consumer from a gate reporting
341
+ * red. `runGates` folds its own throws; this catches everything else the
342
+ * subcommand can reach, including the schema loads that `self-check`
343
+ * performs outside the runner.
344
+ */
345
+ export function cmdGates(args) {
346
+ try {
347
+ const [subcommand, ...rest] = args;
348
+ if (subcommand === "run") {
349
+ return cmdRun(rest);
350
+ }
351
+ if (subcommand === "self-check") {
352
+ return cmdSelfCheck(rest);
353
+ }
354
+ return usageError();
355
+ }
356
+ catch (error) {
357
+ process.stderr.write(`tiphys gates: ${singleLine(error.message ?? String(error))}\n`);
358
+ return EXIT_GATE_ERROR;
359
+ }
360
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * THE PUBLISHED KERNEL NAME (DR-0008, decided 2026-08-05: public npmjs under
3
+ * the `@tiphys` scope, `@tiphys/kernel` and `@tiphys/claude-code-plugin`).
4
+ *
5
+ * WHY THE NAME IS A CONSTANT AND THE VERSION IS DERIVED, since a reader will
6
+ * reasonably ask why the two halves of one pin are sourced differently. The
7
+ * version is the half that changes on every release, and a hardcoded one drifts
8
+ * silently the first time it is bumped somewhere else, which is exactly the
9
+ * failure the M1-P2 placeholder was left open against; so it is read from the
10
+ * running kernel's own `package.json` by the same walk `tiphys version` uses,
11
+ * and a fleet home is therefore pinned to the kernel that initialized it. The
12
+ * name is the half DR-0008 fixed permanently, because a published npm name
13
+ * cannot be taken back, which is why the plan marks that decision costly.
14
+ */
15
+ export declare const KERNEL_PACKAGE_NAME = "@tiphys/kernel";
16
+ /**
17
+ * The documented deterministic machine identity for fleet-scoped commits
18
+ * (kernel plan v1, M1-P2 step 2; EXT-F-02 reviewer Option B). It is set as
19
+ * command-scoped GIT_AUTHOR_* and GIT_COMMITTER_* environment variables on
20
+ * the bootstrap commit invocation only; init never reads or requires user
21
+ * git identity and never touches user or global git configuration.
22
+ */
23
+ export declare const MACHINE_IDENTITY_NAME = "Tiphys Fleet";
24
+ export declare const MACHINE_IDENTITY_EMAIL = "fleet@tiphys.invalid";
25
+ /**
26
+ * tiphys init <dir>: create a fleet home in an empty or absent directory
27
+ * (kernel plan v1, M1-P2 step 2). Substrate-neutral: pure filesystem and
28
+ * git (DR-0007).
29
+ */
30
+ export declare function cmdInit(args: string[]): number;
@@ -0,0 +1,128 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync, } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { EX_USAGE } from "../cli.js";
5
+ import { FLEET_DIRS, FLEET_IGNORED } from "../fleet.js";
6
+ import { readOwnVersion } from "../version.js";
7
+ /**
8
+ * THE PUBLISHED KERNEL NAME (DR-0008, decided 2026-08-05: public npmjs under
9
+ * the `@tiphys` scope, `@tiphys/kernel` and `@tiphys/claude-code-plugin`).
10
+ *
11
+ * WHY THE NAME IS A CONSTANT AND THE VERSION IS DERIVED, since a reader will
12
+ * reasonably ask why the two halves of one pin are sourced differently. The
13
+ * version is the half that changes on every release, and a hardcoded one drifts
14
+ * silently the first time it is bumped somewhere else, which is exactly the
15
+ * failure the M1-P2 placeholder was left open against; so it is read from the
16
+ * running kernel's own `package.json` by the same walk `tiphys version` uses,
17
+ * and a fleet home is therefore pinned to the kernel that initialized it. The
18
+ * name is the half DR-0008 fixed permanently, because a published npm name
19
+ * cannot be taken back, which is why the plan marks that decision costly.
20
+ */
21
+ export const KERNEL_PACKAGE_NAME = "@tiphys/kernel";
22
+ /**
23
+ * The documented deterministic machine identity for fleet-scoped commits
24
+ * (kernel plan v1, M1-P2 step 2; EXT-F-02 reviewer Option B). It is set as
25
+ * command-scoped GIT_AUTHOR_* and GIT_COMMITTER_* environment variables on
26
+ * the bootstrap commit invocation only; init never reads or requires user
27
+ * git identity and never touches user or global git configuration.
28
+ */
29
+ export const MACHINE_IDENTITY_NAME = "Tiphys Fleet";
30
+ export const MACHINE_IDENTITY_EMAIL = "fleet@tiphys.invalid";
31
+ /**
32
+ * Durable directories receive a .gitkeep so the bootstrap commit tracks
33
+ * them: git cannot track an empty directory, and without a tracked entry
34
+ * the durable layout (SC-002) would not survive a clone of the fleet repo.
35
+ * The ignored ephemera (state/, worktrees/, projects/) get no keep file:
36
+ * they are recreated locally and are deliberately not repository content.
37
+ */
38
+ const DURABLE_KEEP_DIRS = ["charter", "decisions", "tasks"];
39
+ function runGit(cwd, args, extraEnv) {
40
+ const result = spawnSync("git", ["-C", cwd, ...args], {
41
+ encoding: "utf8",
42
+ env: extraEnv === undefined ? process.env : { ...process.env, ...extraEnv },
43
+ });
44
+ return { status: result.status, stderr: result.stderr ?? "" };
45
+ }
46
+ /**
47
+ * tiphys init <dir>: create a fleet home in an empty or absent directory
48
+ * (kernel plan v1, M1-P2 step 2). Substrate-neutral: pure filesystem and
49
+ * git (DR-0007).
50
+ */
51
+ export function cmdInit(args) {
52
+ const [dir, ...extra] = args;
53
+ if (dir === undefined || extra.length > 0) {
54
+ process.stderr.write("usage: tiphys init <dir>\n");
55
+ return EX_USAGE;
56
+ }
57
+ const root = resolve(dir);
58
+ if (existsSync(root)) {
59
+ if (!statSync(root).isDirectory()) {
60
+ process.stderr.write(`tiphys init: ${root} exists and is not a directory\n`);
61
+ return 1;
62
+ }
63
+ const entries = readdirSync(root);
64
+ if (entries.length > 0) {
65
+ const fleetMarkers = new Set([...FLEET_DIRS, "backlog.md", ".git"]);
66
+ const looksInitialized = entries.some((entry) => fleetMarkers.has(entry));
67
+ if (looksInitialized) {
68
+ process.stderr.write(`tiphys init: ${root} is already initialized\n`);
69
+ }
70
+ else {
71
+ process.stderr.write(`tiphys init: ${root} is not empty and not a fleet home, refusing\n`);
72
+ }
73
+ return 1;
74
+ }
75
+ }
76
+ else {
77
+ mkdirSync(root, { recursive: true });
78
+ }
79
+ for (const name of FLEET_DIRS) {
80
+ mkdirSync(join(root, name), { recursive: true });
81
+ }
82
+ for (const name of DURABLE_KEEP_DIRS) {
83
+ writeFileSync(join(root, name, ".gitkeep"), "");
84
+ }
85
+ writeFileSync(join(root, "backlog.md"), "# Backlog\n");
86
+ /* THE FLEET-HOME KERNEL PIN (kernel plan M3, M3-P10 step 3), replacing the
87
+ M1-P2 placeholder whose own text said the pin "is added at M3 first
88
+ publish" (kernel plan v1, M1-P2 step 2). The pin is EXACT, with no range
89
+ prefix: blueprint section 3 makes the npm spine's pin the upgrade
90
+ mechanism, so a caret here would mean a fleet home silently changed kernel
91
+ between two `npm install` runs, which is the opposite of what the pin is
92
+ for. `readOwnVersion()` is the same reader `tiphys version` uses, so the
93
+ fleet home is pinned to the kernel that initialized it rather than to a
94
+ number typed here. */
95
+ const fleetPackageJson = {
96
+ name: "tiphys-fleet-home",
97
+ version: "0.0.0",
98
+ private: true,
99
+ description: "Tiphys fleet home. The kernel dependency below is an exact pin; changing it is how this fleet upgrades (blueprint section 3).",
100
+ dependencies: {
101
+ [KERNEL_PACKAGE_NAME]: readOwnVersion(),
102
+ },
103
+ };
104
+ writeFileSync(join(root, "package.json"), `${JSON.stringify(fleetPackageJson, null, 2)}\n`);
105
+ writeFileSync(join(root, ".gitignore"), `${FLEET_IGNORED.join("\n")}\n`);
106
+ const steps = [
107
+ { args: ["init", "--initial-branch=main"] },
108
+ { args: ["add", "-A"] },
109
+ {
110
+ args: ["commit", "-m", "tiphys init: fleet home bootstrap"],
111
+ env: {
112
+ GIT_AUTHOR_NAME: MACHINE_IDENTITY_NAME,
113
+ GIT_AUTHOR_EMAIL: MACHINE_IDENTITY_EMAIL,
114
+ GIT_COMMITTER_NAME: MACHINE_IDENTITY_NAME,
115
+ GIT_COMMITTER_EMAIL: MACHINE_IDENTITY_EMAIL,
116
+ },
117
+ },
118
+ ];
119
+ for (const step of steps) {
120
+ const result = runGit(root, step.args, step.env);
121
+ if (result.status !== 0) {
122
+ process.stderr.write(`tiphys init: git ${step.args[0]} failed with exit ${String(result.status)}; ${root} is left partially initialized, remove it and re-run init\n${result.stderr}`);
123
+ return 1;
124
+ }
125
+ }
126
+ process.stdout.write(`initialized fleet home at ${root}\n`);
127
+ return 0;
128
+ }
@@ -0,0 +1 @@
1
+ export declare function cmdLock(args: string[]): Promise<number>;