@tiphys/kernel 0.0.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 (207) hide show
  1. package/AGENTS.md +663 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +299 -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 +92 -0
  11. package/dist/src/adapters/load.d.ts +202 -0
  12. package/dist/src/adapters/load.js +440 -0
  13. package/dist/src/brief.d.ts +32 -0
  14. package/dist/src/brief.js +66 -0
  15. package/dist/src/checklists.d.ts +169 -0
  16. package/dist/src/checklists.js +310 -0
  17. package/dist/src/checks.d.ts +1539 -0
  18. package/dist/src/checks.js +5025 -0
  19. package/dist/src/cli.d.ts +9 -0
  20. package/dist/src/cli.js +68 -0
  21. package/dist/src/commands/brief.d.ts +92 -0
  22. package/dist/src/commands/brief.js +343 -0
  23. package/dist/src/commands/checklist.d.ts +42 -0
  24. package/dist/src/commands/checklist.js +168 -0
  25. package/dist/src/commands/cutover.d.ts +35 -0
  26. package/dist/src/commands/cutover.js +448 -0
  27. package/dist/src/commands/doctor.d.ts +264 -0
  28. package/dist/src/commands/doctor.js +1546 -0
  29. package/dist/src/commands/gates.d.ts +9 -0
  30. package/dist/src/commands/gates.js +360 -0
  31. package/dist/src/commands/init.d.ts +30 -0
  32. package/dist/src/commands/init.js +177 -0
  33. package/dist/src/commands/lock.d.ts +34 -0
  34. package/dist/src/commands/lock.js +340 -0
  35. package/dist/src/commands/mode.d.ts +40 -0
  36. package/dist/src/commands/mode.js +134 -0
  37. package/dist/src/commands/next.d.ts +130 -0
  38. package/dist/src/commands/next.js +597 -0
  39. package/dist/src/commands/plan.d.ts +20 -0
  40. package/dist/src/commands/plan.js +105 -0
  41. package/dist/src/commands/pool.d.ts +1 -0
  42. package/dist/src/commands/pool.js +139 -0
  43. package/dist/src/commands/resume.d.ts +1 -0
  44. package/dist/src/commands/resume.js +88 -0
  45. package/dist/src/commands/spawn.d.ts +1 -0
  46. package/dist/src/commands/spawn.js +195 -0
  47. package/dist/src/commands/status.d.ts +21 -0
  48. package/dist/src/commands/status.js +122 -0
  49. package/dist/src/commands/sync.d.ts +47 -0
  50. package/dist/src/commands/sync.js +341 -0
  51. package/dist/src/commands/teardown.d.ts +1 -0
  52. package/dist/src/commands/teardown.js +87 -0
  53. package/dist/src/commands/tuition.d.ts +21 -0
  54. package/dist/src/commands/tuition.js +218 -0
  55. package/dist/src/commands/validate.d.ts +78 -0
  56. package/dist/src/commands/validate.js +430 -0
  57. package/dist/src/commands/watch.d.ts +1 -0
  58. package/dist/src/commands/watch.js +172 -0
  59. package/dist/src/cutover.d.ts +584 -0
  60. package/dist/src/cutover.js +1444 -0
  61. package/dist/src/exclusion.d.ts +389 -0
  62. package/dist/src/exclusion.js +843 -0
  63. package/dist/src/exec/env.d.ts +278 -0
  64. package/dist/src/exec/env.js +334 -0
  65. package/dist/src/fleet.d.ts +223 -0
  66. package/dist/src/fleet.js +298 -0
  67. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  68. package/dist/src/gates/adapters/http-json.js +283 -0
  69. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  70. package/dist/src/gates/adapters/migrations-command.js +373 -0
  71. package/dist/src/gates/citations.d.ts +408 -0
  72. package/dist/src/gates/citations.js +1169 -0
  73. package/dist/src/gates/coverage.d.ts +375 -0
  74. package/dist/src/gates/coverage.js +836 -0
  75. package/dist/src/gates/credentials.d.ts +233 -0
  76. package/dist/src/gates/credentials.js +752 -0
  77. package/dist/src/gates/deploy.d.ts +1 -0
  78. package/dist/src/gates/deploy.js +33 -0
  79. package/dist/src/gates/gate-classes.d.ts +56 -0
  80. package/dist/src/gates/gate-classes.js +633 -0
  81. package/dist/src/gates/manifest.d.ts +99 -0
  82. package/dist/src/gates/manifest.js +208 -0
  83. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  84. package/dist/src/gates/merge-preconditions.js +932 -0
  85. package/dist/src/gates/migrations.d.ts +1 -0
  86. package/dist/src/gates/migrations.js +36 -0
  87. package/dist/src/gates/pin.d.ts +114 -0
  88. package/dist/src/gates/pin.js +154 -0
  89. package/dist/src/gates/red-witness.d.ts +22 -0
  90. package/dist/src/gates/red-witness.js +482 -0
  91. package/dist/src/gates/release.d.ts +283 -0
  92. package/dist/src/gates/release.js +820 -0
  93. package/dist/src/gates/result.d.ts +116 -0
  94. package/dist/src/gates/result.js +91 -0
  95. package/dist/src/gates/run.d.ts +614 -0
  96. package/dist/src/gates/run.js +1614 -0
  97. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  98. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  99. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  100. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  101. package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
  102. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  103. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  104. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  105. package/dist/src/gates/scope.d.ts +131 -0
  106. package/dist/src/gates/scope.js +1018 -0
  107. package/dist/src/gates/suite.d.ts +217 -0
  108. package/dist/src/gates/suite.js +968 -0
  109. package/dist/src/gates/validate.d.ts +121 -0
  110. package/dist/src/gates/validate.js +414 -0
  111. package/dist/src/hooks.d.ts +84 -0
  112. package/dist/src/hooks.js +125 -0
  113. package/dist/src/index.d.ts +31 -0
  114. package/dist/src/index.js +30 -0
  115. package/dist/src/liveness.d.ts +321 -0
  116. package/dist/src/liveness.js +396 -0
  117. package/dist/src/lock.d.ts +256 -0
  118. package/dist/src/lock.js +792 -0
  119. package/dist/src/model-resolution.d.ts +159 -0
  120. package/dist/src/model-resolution.js +307 -0
  121. package/dist/src/modes.d.ts +149 -0
  122. package/dist/src/modes.js +258 -0
  123. package/dist/src/path-identity.d.ts +34 -0
  124. package/dist/src/path-identity.js +48 -0
  125. package/dist/src/plan.d.ts +73 -0
  126. package/dist/src/plan.js +153 -0
  127. package/dist/src/pool.d.ts +326 -0
  128. package/dist/src/pool.js +988 -0
  129. package/dist/src/roles.d.ts +461 -0
  130. package/dist/src/roles.js +776 -0
  131. package/dist/src/spawn.d.ts +482 -0
  132. package/dist/src/spawn.js +1003 -0
  133. package/dist/src/status.d.ts +116 -0
  134. package/dist/src/status.js +148 -0
  135. package/dist/src/task.d.ts +504 -0
  136. package/dist/src/task.js +307 -0
  137. package/dist/src/teardown.d.ts +39 -0
  138. package/dist/src/teardown.js +422 -0
  139. package/dist/src/tuition.d.ts +159 -0
  140. package/dist/src/tuition.js +311 -0
  141. package/dist/src/validate.d.ts +263 -0
  142. package/dist/src/validate.js +742 -0
  143. package/dist/src/version.d.ts +3 -0
  144. package/dist/src/version.js +38 -0
  145. package/dist/src/watcher.d.ts +275 -0
  146. package/dist/src/watcher.js +849 -0
  147. package/dist/src/witness/run.d.ts +299 -0
  148. package/dist/src/witness/run.js +1373 -0
  149. package/dist/src/witness/spec.d.ts +270 -0
  150. package/dist/src/witness/spec.js +475 -0
  151. package/dist/tsconfig.src.tsbuildinfo +1 -0
  152. package/gate-registry.yaml +526 -0
  153. package/gates.manifest.json +257 -0
  154. package/package.json +72 -3
  155. package/role-model-config.yaml +88 -0
  156. package/roles/README.md +128 -0
  157. package/roles/_shared-dispatch-contract.md +87 -0
  158. package/roles/adversarial-plan-reviewer.md +80 -0
  159. package/roles/clean-room-reviewer.md +140 -0
  160. package/roles/implementer.md +463 -0
  161. package/roles/investigator.md +138 -0
  162. package/roles/plan-writer.md +95 -0
  163. package/schemas/README.md +82 -0
  164. package/schemas/assurance-modes.schema.json +264 -0
  165. package/schemas/charter.schema.json +185 -0
  166. package/schemas/checklist.schema.json +114 -0
  167. package/schemas/cutover-state.schema.json +64 -0
  168. package/schemas/decision-record.schema.json +88 -0
  169. package/schemas/executor-record.schema.json +36 -0
  170. package/schemas/final-report.schema.json +90 -0
  171. package/schemas/finding.schema.json +106 -0
  172. package/schemas/gate-registry.schema.json +260 -0
  173. package/schemas/mechanism-index.schema.json +94 -0
  174. package/schemas/model-resolution.schema.json +362 -0
  175. package/schemas/plan.schema.json +300 -0
  176. package/schemas/report.schema.json +579 -0
  177. package/schemas/role-brief.schema.json +105 -0
  178. package/schemas/role-model-config.schema.json +90 -0
  179. package/schemas/status-line.schema.json +40 -0
  180. package/schemas/tuition.schema.json +191 -0
  181. package/schemas/verdict.schema.json +295 -0
  182. package/schemas/work-history.schema.json +183 -0
  183. package/schemas/write-bypass.schema.json +69 -0
  184. package/templates/charter.example.yaml +54 -0
  185. package/templates/decision-record.example.yaml +27 -0
  186. package/templates/final-report.example.yaml +80 -0
  187. package/templates/plan.example.yaml +87 -0
  188. package/templates/report.example.yaml +236 -0
  189. package/templates/warnings.md +74 -0
  190. package/templates/work-history.example.yaml +185 -0
  191. package/tuition/README.md +76 -0
  192. package/tuition/T-001.yaml +48 -0
  193. package/tuition/T-002.yaml +51 -0
  194. package/tuition/T-003.yaml +100 -0
  195. package/tuition/T-004.yaml +52 -0
  196. package/tuition/T-005.yaml +72 -0
  197. package/tuition/T-006.yaml +81 -0
  198. package/tuition/T-007.yaml +56 -0
  199. package/tuition/T-008.yaml +111 -0
  200. package/tuition/T-009.yaml +50 -0
  201. package/tuition/T-015.yaml +36 -0
  202. package/tuition/T-016.yaml +36 -0
  203. package/tuition/T-017.yaml +46 -0
  204. package/tuition/T-018.yaml +84 -0
  205. package/tuition/T-021.yaml +40 -0
  206. package/tuition/T-022.yaml +36 -0
  207. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The outer backstop for CR-801. Node's uncaught-exception exit code is 1,
3
+ * which is this phase's own EXIT_RED, so a throw escaping anywhere under
4
+ * `gates` used to be indistinguishable to a consumer from a gate reporting
5
+ * red. `runGates` folds its own throws; this catches everything else the
6
+ * subcommand can reach, including the schema loads that `self-check`
7
+ * performs outside the runner.
8
+ */
9
+ export declare function cmdGates(args: string[]): number;
@@ -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> [--shared-exclusion]: create a fleet home in an empty or
27
+ * absent directory (kernel plan v1, M1-P2 step 2). Substrate-neutral: pure
28
+ * filesystem and git (DR-0007).
29
+ */
30
+ export declare function cmdInit(args: string[]): number;
@@ -0,0 +1,177 @@
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 { DEFAULT_SHARED_REF, DEFAULT_SHARED_REMOTE, SHARED_EXCLUSION_FIELD, } from "../exclusion.js";
6
+ import { FLEET_DIRS, FLEET_IGNORED } from "../fleet.js";
7
+ import { DURABLE_STATUS_DIR } from "../status.js";
8
+ import { readOwnVersion } from "../version.js";
9
+ /**
10
+ * THE PUBLISHED KERNEL NAME (DR-0008, decided 2026-08-05: public npmjs under
11
+ * the `@tiphys` scope, `@tiphys/kernel` and `@tiphys/claude-code-plugin`).
12
+ *
13
+ * WHY THE NAME IS A CONSTANT AND THE VERSION IS DERIVED, since a reader will
14
+ * reasonably ask why the two halves of one pin are sourced differently. The
15
+ * version is the half that changes on every release, and a hardcoded one drifts
16
+ * silently the first time it is bumped somewhere else, which is exactly the
17
+ * failure the M1-P2 placeholder was left open against; so it is read from the
18
+ * running kernel's own `package.json` by the same walk `tiphys version` uses,
19
+ * and a fleet home is therefore pinned to the kernel that initialized it. The
20
+ * name is the half DR-0008 fixed permanently, because a published npm name
21
+ * cannot be taken back, which is why the plan marks that decision costly.
22
+ */
23
+ export const KERNEL_PACKAGE_NAME = "@tiphys/kernel";
24
+ /**
25
+ * The documented deterministic machine identity for fleet-scoped commits
26
+ * (kernel plan v1, M1-P2 step 2; EXT-F-02 reviewer Option B). It is set as
27
+ * command-scoped GIT_AUTHOR_* and GIT_COMMITTER_* environment variables on
28
+ * the bootstrap commit invocation only; init never reads or requires user
29
+ * git identity and never touches user or global git configuration.
30
+ */
31
+ export const MACHINE_IDENTITY_NAME = "Tiphys Fleet";
32
+ export const MACHINE_IDENTITY_EMAIL = "fleet@tiphys.invalid";
33
+ /**
34
+ * Durable directories receive a .gitkeep so the bootstrap commit tracks
35
+ * them: git cannot track an empty directory, and without a tracked entry
36
+ * the durable layout (SC-002) would not survive a clone of the fleet repo.
37
+ * The ignored ephemera (state/, worktrees/, projects/) get no keep file:
38
+ * they are recreated locally and are deliberately not repository content.
39
+ *
40
+ * `status` JOINED THIS LIST AT M4-P18 AND IT IS THE HALF OF M4-D-13 THAT IS
41
+ * VISIBLE IN THE LAYOUT. The status pointer used to live beside its stream
42
+ * under the ignored `state/` prefix, where the sentence that says where the
43
+ * pipeline stands could be neither committed nor pushed; the split moved it
44
+ * here. It is created at init rather than on first emit so that a fleet home
45
+ * carries the durable directory from its bootstrap commit, and so a CLONE of
46
+ * one carries it too: `tiphys resume` rebuilds the EPHEMERAL three and does
47
+ * not, and must not, fabricate durable content (src/commands/resume.ts).
48
+ */
49
+ const DURABLE_KEEP_DIRS = ["charter", "decisions", "tasks", DURABLE_STATUS_DIR];
50
+ function runGit(cwd, args, extraEnv) {
51
+ const result = spawnSync("git", ["-C", cwd, ...args], {
52
+ encoding: "utf8",
53
+ env: extraEnv === undefined ? process.env : { ...process.env, ...extraEnv },
54
+ });
55
+ return { status: result.status, stderr: result.stderr ?? "" };
56
+ }
57
+ /**
58
+ * THE OPT-IN FLAG FOR THE SHARED EXCLUSION REGISTER (M4-P21 criterion 1).
59
+ *
60
+ * The layer is DECLARED, never inferred, and the declaration lives in the
61
+ * fleet home's own `package.json` because that file is already the fleet's
62
+ * owner-controlled pin file. Without this flag `init` writes exactly the
63
+ * package.json it wrote before, with no `tiphys` section at all, so a fleet
64
+ * created today behaves exactly as it did and `src/exclusion.ts` returns
65
+ * before spawning anything. That default is the H-D carve-out in the plan's
66
+ * hazard table: a fleet that cannot reach a remote is not forced to switch
67
+ * the layer off globally, because it was never on.
68
+ */
69
+ const SHARED_EXCLUSION_FLAG = "--shared-exclusion";
70
+ /**
71
+ * tiphys init <dir> [--shared-exclusion]: create a fleet home in an empty or
72
+ * absent directory (kernel plan v1, M1-P2 step 2). Substrate-neutral: pure
73
+ * filesystem and git (DR-0007).
74
+ */
75
+ export function cmdInit(args) {
76
+ const rest = args.filter((arg) => arg !== SHARED_EXCLUSION_FLAG);
77
+ const sharedExclusion = args.length !== rest.length;
78
+ const [dir, ...extra] = rest;
79
+ if (dir === undefined || extra.length > 0) {
80
+ process.stderr.write(`usage: tiphys init <dir> [${SHARED_EXCLUSION_FLAG}]\n`);
81
+ return EX_USAGE;
82
+ }
83
+ const root = resolve(dir);
84
+ if (existsSync(root)) {
85
+ if (!statSync(root).isDirectory()) {
86
+ process.stderr.write(`tiphys init: ${root} exists and is not a directory\n`);
87
+ return 1;
88
+ }
89
+ const entries = readdirSync(root);
90
+ if (entries.length > 0) {
91
+ const fleetMarkers = new Set([...FLEET_DIRS, "backlog.md", ".git"]);
92
+ const looksInitialized = entries.some((entry) => fleetMarkers.has(entry));
93
+ if (looksInitialized) {
94
+ /* THE REMEDY TOKEN IS THE POINT OF THIS LINE (M4-P16 criterion 6).
95
+ The exit code was already 1 and already correct, so a reader who
96
+ got here learned only that init refused. A CLONE of a fleet home
97
+ lands here too, because the marker set contains `.git`, and that
98
+ reader's actual next step is `tiphys resume`, which rebuilds the
99
+ three gitignored directories the clone does not carry. Naming it
100
+ here is the difference between a refusal and an instruction. */
101
+ process.stderr.write(`tiphys init: ${root} is already initialized; run tiphys resume to rebuild the ephemeral directories a clone does not carry\n`);
102
+ }
103
+ else {
104
+ process.stderr.write(`tiphys init: ${root} is not empty and not a fleet home, refusing\n`);
105
+ }
106
+ return 1;
107
+ }
108
+ }
109
+ else {
110
+ mkdirSync(root, { recursive: true });
111
+ }
112
+ for (const name of [...FLEET_DIRS, DURABLE_STATUS_DIR]) {
113
+ mkdirSync(join(root, name), { recursive: true });
114
+ }
115
+ for (const name of DURABLE_KEEP_DIRS) {
116
+ writeFileSync(join(root, name, ".gitkeep"), "");
117
+ }
118
+ writeFileSync(join(root, "backlog.md"), "# Backlog\n");
119
+ /* THE FLEET-HOME KERNEL PIN (kernel plan M3, M3-P10 step 3), replacing the
120
+ M1-P2 placeholder whose own text said the pin "is added at M3 first
121
+ publish" (kernel plan v1, M1-P2 step 2). The pin is EXACT, with no range
122
+ prefix: blueprint section 3 makes the npm spine's pin the upgrade
123
+ mechanism, so a caret here would mean a fleet home silently changed kernel
124
+ between two `npm install` runs, which is the opposite of what the pin is
125
+ for. `readOwnVersion()` is the same reader `tiphys version` uses, so the
126
+ fleet home is pinned to the kernel that initialized it rather than to a
127
+ number typed here. */
128
+ const fleetPackageJson = {
129
+ name: "tiphys-fleet-home",
130
+ version: "0.0.0",
131
+ private: true,
132
+ description: "Tiphys fleet home. The kernel dependency below is an exact pin; changing it is how this fleet upgrades (blueprint section 3).",
133
+ dependencies: {
134
+ [KERNEL_PACKAGE_NAME]: readOwnVersion(),
135
+ },
136
+ };
137
+ if (sharedExclusion) {
138
+ /* The declared form is written out in full rather than as `true` so the
139
+ owner can see, and edit, the remote and the ref this fleet's shared
140
+ lease lives on. The ref is a BRANCH because only refs/heads/* is
141
+ pushable against the remote this kernel is built for (CLAUDE.md
142
+ standing warning 14, re-measured for M4-P21). */
143
+ fleetPackageJson["tiphys"] = {
144
+ sharedExclusion: {
145
+ remote: DEFAULT_SHARED_REMOTE,
146
+ ref: DEFAULT_SHARED_REF,
147
+ },
148
+ };
149
+ }
150
+ writeFileSync(join(root, "package.json"), `${JSON.stringify(fleetPackageJson, null, 2)}\n`);
151
+ writeFileSync(join(root, ".gitignore"), `${FLEET_IGNORED.join("\n")}\n`);
152
+ const steps = [
153
+ { args: ["init", "--initial-branch=main"] },
154
+ { args: ["add", "-A"] },
155
+ {
156
+ args: ["commit", "-m", "tiphys init: fleet home bootstrap"],
157
+ env: {
158
+ GIT_AUTHOR_NAME: MACHINE_IDENTITY_NAME,
159
+ GIT_AUTHOR_EMAIL: MACHINE_IDENTITY_EMAIL,
160
+ GIT_COMMITTER_NAME: MACHINE_IDENTITY_NAME,
161
+ GIT_COMMITTER_EMAIL: MACHINE_IDENTITY_EMAIL,
162
+ },
163
+ },
164
+ ];
165
+ for (const step of steps) {
166
+ const result = runGit(root, step.args, step.env);
167
+ if (result.status !== 0) {
168
+ 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}`);
169
+ return 1;
170
+ }
171
+ }
172
+ process.stdout.write(`initialized fleet home at ${root}\n`);
173
+ if (sharedExclusion) {
174
+ process.stdout.write(`declared ${SHARED_EXCLUSION_FIELD} on ${DEFAULT_SHARED_REMOTE} at ${DEFAULT_SHARED_REF}\n`);
175
+ }
176
+ return 0;
177
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * THE DECISION-CLOCK SEAM (M4-P21 criteria 4 and 5). When
3
+ * TIPHYS_LOCK_TEST_NOW_MS carries a finite number of milliseconds, every
4
+ * mutating subcommand decides against that instant instead of `Date.now()`,
5
+ * by feeding the EXISTING `nowMs` option the lease library already takes
6
+ * (src/lock.ts:568). It exists because the two clock-skew witnesses must
7
+ * move ONE environment's clock ten minutes without touching a system clock,
8
+ * which the plan requires by name, and because criterion 6 is about what the
9
+ * COMMAND prints, so the skew has to reach the command rather than only the
10
+ * library. Inert unless the variable is set, exactly like the hold point
11
+ * above, and a value that does not parse is a loud refusal rather than a
12
+ * silent fall back to the real clock: a seam that quietly ignores its input
13
+ * would make a skew witness green while measuring no skew at all.
14
+ *
15
+ * AND IT IS GATED, BECAUSE IT SHIPS. `judgeByCounter` (src/exclusion.ts:553)
16
+ * computes `unchangedForMs = nowMs - previous.firstSeenMs`, so the module's
17
+ * promised "two readings of ONE clock" become one reading of a clock the
18
+ * CALLER supplies. Measured in the published CLI before this gate existed:
19
+ * the honest challenger was refused with "fencing counter 1 has stood still
20
+ * for 0ms of the 900000ms this environment requires", and the SAME command
21
+ * with this one variable set an hour ahead took a live lease over instantly
22
+ * and still printed `signal=counter`. A captured witness of that run was
23
+ * indistinguishable from an honest one.
24
+ *
25
+ * TWO CHANGES, and they are different in kind. The ALLOWANCE
26
+ * (TIPHYS_ALLOW_TEST_CLOCK=1) makes reaching the seam a declared act rather
27
+ * than a side effect of one environment variable. The LABEL makes the run
28
+ * SAY SO: `injectedClockSuffix` below is appended to every verdict line the
29
+ * shared layer prints, so the output of a measurement can never be quoted as
30
+ * the output of a real judgement. An allowance alone would leave the second
31
+ * hole open, which is why both are here.
32
+ */
33
+ export declare const TEST_CLOCK_ALLOWANCE = "TIPHYS_ALLOW_TEST_CLOCK";
34
+ export declare function cmdLock(args: string[]): Promise<number>;