@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.
- package/AGENTS.md +611 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +278 -0
- package/checklists/clean-room.yaml +325 -0
- package/checklists/env-failure-diagnosis.yaml +68 -0
- package/checklists/flake-playbook.yaml +68 -0
- package/checklists/hazard-review.yaml +144 -0
- package/checklists/plan-review.yaml +103 -0
- package/dist/bin/tiphys.d.ts +2 -0
- package/dist/bin/tiphys.js +14 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +59 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +828 -0
- package/dist/src/checks.js +3314 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +57 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +320 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/doctor.d.ts +35 -0
- package/dist/src/commands/doctor.js +605 -0
- package/dist/src/commands/gates.d.ts +9 -0
- package/dist/src/commands/gates.js +360 -0
- package/dist/src/commands/init.d.ts +30 -0
- package/dist/src/commands/init.js +128 -0
- package/dist/src/commands/lock.d.ts +1 -0
- package/dist/src/commands/lock.js +229 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -0
- package/dist/src/commands/plan.d.ts +20 -0
- package/dist/src/commands/plan.js +105 -0
- package/dist/src/commands/pool.d.ts +1 -0
- package/dist/src/commands/pool.js +128 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +146 -0
- package/dist/src/commands/status.d.ts +19 -0
- package/dist/src/commands/status.js +120 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +79 -0
- package/dist/src/commands/tuition.d.ts +21 -0
- package/dist/src/commands/tuition.js +218 -0
- package/dist/src/commands/validate.d.ts +78 -0
- package/dist/src/commands/validate.js +360 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/exec/env.d.ts +128 -0
- package/dist/src/exec/env.js +190 -0
- package/dist/src/fleet.d.ts +51 -0
- package/dist/src/fleet.js +80 -0
- package/dist/src/gates/adapters/http-json.d.ts +5 -0
- package/dist/src/gates/adapters/http-json.js +283 -0
- package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
- package/dist/src/gates/adapters/migrations-command.js +373 -0
- package/dist/src/gates/citations.d.ts +408 -0
- package/dist/src/gates/citations.js +1163 -0
- package/dist/src/gates/coverage.d.ts +284 -0
- package/dist/src/gates/coverage.js +701 -0
- package/dist/src/gates/credentials.d.ts +74 -0
- package/dist/src/gates/credentials.js +533 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -0
- package/dist/src/gates/migrations.d.ts +1 -0
- package/dist/src/gates/migrations.js +36 -0
- package/dist/src/gates/pin.d.ts +114 -0
- package/dist/src/gates/pin.js +154 -0
- package/dist/src/gates/red-witness.d.ts +22 -0
- package/dist/src/gates/red-witness.js +390 -0
- package/dist/src/gates/release.d.ts +283 -0
- package/dist/src/gates/release.js +820 -0
- package/dist/src/gates/result.d.ts +116 -0
- package/dist/src/gates/result.js +91 -0
- package/dist/src/gates/run.d.ts +566 -0
- package/dist/src/gates/run.js +1536 -0
- package/dist/src/gates/schemas/citation-config.schema.json +59 -0
- package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
- package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
- package/dist/src/gates/schemas/gate-result.schema.json +160 -0
- package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
- package/dist/src/gates/schemas/release-record.schema.json +119 -0
- package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
- package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
- package/dist/src/gates/scope.d.ts +131 -0
- package/dist/src/gates/scope.js +1018 -0
- package/dist/src/gates/suite.d.ts +217 -0
- package/dist/src/gates/suite.js +927 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +32 -0
- package/dist/src/hooks.js +62 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +178 -0
- package/dist/src/lock.js +500 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +2 -0
- package/dist/src/path-identity.js +10 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +130 -0
- package/dist/src/pool.js +721 -0
- package/dist/src/roles.d.ts +430 -0
- package/dist/src/roles.js +734 -0
- package/dist/src/spawn.d.ts +177 -0
- package/dist/src/spawn.js +332 -0
- package/dist/src/status.d.ts +91 -0
- package/dist/src/status.js +119 -0
- package/dist/src/task.d.ts +264 -0
- package/dist/src/task.js +305 -0
- package/dist/src/teardown.d.ts +32 -0
- package/dist/src/teardown.js +314 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +230 -0
- package/dist/src/validate.js +732 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.js +38 -0
- package/dist/src/watcher.d.ts +275 -0
- package/dist/src/watcher.js +859 -0
- package/dist/src/witness/run.d.ts +274 -0
- package/dist/src/witness/run.js +1327 -0
- package/dist/src/witness/spec.d.ts +102 -0
- package/dist/src/witness/spec.js +253 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +390 -0
- package/gates.manifest.json +195 -0
- package/package.json +57 -3
- package/role-model-config.yaml +88 -0
- package/roles/README.md +128 -0
- package/roles/_shared-dispatch-contract.md +87 -0
- package/roles/adversarial-plan-reviewer.md +80 -0
- package/roles/clean-room-reviewer.md +140 -0
- package/roles/implementer.md +460 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +81 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +166 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/decision-record.schema.json +88 -0
- package/schemas/final-report.schema.json +90 -0
- package/schemas/finding.schema.json +106 -0
- package/schemas/gate-registry.schema.json +260 -0
- package/schemas/mechanism-index.schema.json +94 -0
- package/schemas/plan.schema.json +300 -0
- package/schemas/report.schema.json +579 -0
- package/schemas/role-brief.schema.json +105 -0
- package/schemas/role-model-config.schema.json +90 -0
- package/schemas/status-line.schema.json +40 -0
- package/schemas/tuition.schema.json +191 -0
- package/schemas/verdict.schema.json +289 -0
- package/schemas/work-history.schema.json +183 -0
- package/templates/charter.example.yaml +54 -0
- package/templates/decision-record.example.yaml +27 -0
- package/templates/final-report.example.yaml +80 -0
- package/templates/plan.example.yaml +87 -0
- package/templates/report.example.yaml +236 -0
- package/templates/warnings.md +74 -0
- package/templates/work-history.example.yaml +185 -0
- package/tuition/README.md +76 -0
- package/tuition/T-001.yaml +48 -0
- package/tuition/T-002.yaml +51 -0
- package/tuition/T-003.yaml +100 -0
- package/tuition/T-004.yaml +52 -0
- package/tuition/T-005.yaml +72 -0
- package/tuition/T-006.yaml +81 -0
- package/tuition/T-007.yaml +56 -0
- package/tuition/T-008.yaml +111 -0
- package/tuition/T-009.yaml +50 -0
- package/tuition/T-015.yaml +36 -0
- package/tuition/T-016.yaml +36 -0
- package/tuition/T-017.yaml +46 -0
- package/tuition/T-018.yaml +84 -0
- package/tuition/T-021.yaml +40 -0
- package/tuition/T-022.yaml +36 -0
- 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>;
|