@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,229 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
|
+
import { EX_USAGE } from "../cli.js";
|
|
4
|
+
import { loadFleet } from "../fleet.js";
|
|
5
|
+
import { acquireLease, leaseStatus, observeLease, releaseLease, renewLease, } from "../lock.js";
|
|
6
|
+
/**
|
|
7
|
+
* tiphys lock <acquire [--take-over] [--duration <seconds>] | renew
|
|
8
|
+
* --holder <id> [--duration <seconds>] | release --holder <id> | status>
|
|
9
|
+
* (kernel plan v1, M1-P3 step 2). Runs in a fleet home (cwd). The holder
|
|
10
|
+
* identity for renew and release is the opaque holderId that acquire
|
|
11
|
+
* printed; it outlives any one command invocation (FM-022) and is never
|
|
12
|
+
* derived from the running program (plan constraint C-2).
|
|
13
|
+
*/
|
|
14
|
+
const USAGE = "usage: tiphys lock <acquire [--take-over] [--duration <seconds>] | " +
|
|
15
|
+
"renew --holder <id> [--duration <seconds>] | " +
|
|
16
|
+
"release --holder <id> | status>";
|
|
17
|
+
/**
|
|
18
|
+
* Deterministic race-witness hold point (the plan's test determinism
|
|
19
|
+
* rule: scripted interleaves over sleep-based timing). When
|
|
20
|
+
* TIPHYS_LOCK_TEST_HOLD names a barrier path, the mutating subcommands
|
|
21
|
+
* observe the lease and freeze their decision clock first, write
|
|
22
|
+
* <barrier>.observed as a ready marker, then wait for the barrier file
|
|
23
|
+
* to appear before deciding and applying through the one mutation
|
|
24
|
+
* primitive. A test can thereby interleave two real CLI invocations at
|
|
25
|
+
* the exact compare-and-swap point. Inert unless the variable is set.
|
|
26
|
+
*
|
|
27
|
+
* The seam is LOUD about not holding (D-3). It previously had two exits
|
|
28
|
+
* from its wait and only one of them meant "held": on the other the
|
|
29
|
+
* command walked into the mutation carrying a stale observation and
|
|
30
|
+
* told nobody, so a witness could score a compare-and-swap it never
|
|
31
|
+
* staged and still pass. Both exits are now closed with a throw, the
|
|
32
|
+
* bound is monotonic (process.hrtime.bigint, not Date.now, because a
|
|
33
|
+
* realtime bound mixed with monotonic test durations is precisely what
|
|
34
|
+
* made "it failed fast" look like proof the bound was not reached), and
|
|
35
|
+
* the reason the wait ended is written to <barrier>.released so the
|
|
36
|
+
* test can assert the interleave actually happened. This is a test
|
|
37
|
+
* integrity fix; it is NOT a fix for the unattributed U-2 flake, whose
|
|
38
|
+
* trigger remains unexplained.
|
|
39
|
+
*/
|
|
40
|
+
const HOLD_WAIT_LIMIT_MS = 30_000;
|
|
41
|
+
async function maybeHoldForTest(lockPath) {
|
|
42
|
+
const barrier = process.env.TIPHYS_LOCK_TEST_HOLD;
|
|
43
|
+
if (barrier === undefined || barrier === "") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (existsSync(barrier)) {
|
|
47
|
+
throw new Error(`lock test hold point: barrier ${barrier} already existed before the ` +
|
|
48
|
+
`hold; this interleave was never staged and the run is not evidence`);
|
|
49
|
+
}
|
|
50
|
+
const observed = observeLease(lockPath);
|
|
51
|
+
const nowMs = Date.now();
|
|
52
|
+
writeFileSync(`${barrier}.observed`, "");
|
|
53
|
+
const startNs = process.hrtime.bigint();
|
|
54
|
+
const limitNs = BigInt(HOLD_WAIT_LIMIT_MS) * 1000000n;
|
|
55
|
+
let held = false;
|
|
56
|
+
for (;;) {
|
|
57
|
+
if (existsSync(barrier)) {
|
|
58
|
+
held = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
if (process.hrtime.bigint() - startNs >= limitNs) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
await sleep(10);
|
|
65
|
+
}
|
|
66
|
+
const waitedMs = Number((process.hrtime.bigint() - startNs) / 1000000n);
|
|
67
|
+
if (!held) {
|
|
68
|
+
throw new Error(`lock test hold point: barrier ${barrier} never appeared within ` +
|
|
69
|
+
`${String(HOLD_WAIT_LIMIT_MS)}ms (waited ${String(waitedMs)}ms on the ` +
|
|
70
|
+
`monotonic clock); this interleave was never staged and the run is ` +
|
|
71
|
+
`not evidence`);
|
|
72
|
+
}
|
|
73
|
+
// Record that the hold really held, and why the wait ended, so the
|
|
74
|
+
// witness can assert the interleave rather than assume it.
|
|
75
|
+
writeFileSync(`${barrier}.released`, `held after ${String(waitedMs)}ms (monotonic), barrier observed\n`);
|
|
76
|
+
return { observed, nowMs };
|
|
77
|
+
}
|
|
78
|
+
function usageError(message) {
|
|
79
|
+
if (message !== undefined) {
|
|
80
|
+
process.stderr.write(`tiphys lock: ${message}\n`);
|
|
81
|
+
}
|
|
82
|
+
process.stderr.write(`${USAGE}\n`);
|
|
83
|
+
return EX_USAGE;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The one failure emitter for acquire, renew, and release (CR-204).
|
|
87
|
+
* When the library classified the failure as a stale mutation claim,
|
|
88
|
+
* the operator is pointed at the claim file rather than at a holder
|
|
89
|
+
* that may not exist; the remedy text lives here, so a CLI that
|
|
90
|
+
* ignored the classification would visibly lose it. Operational
|
|
91
|
+
* failures exit 1 (usage errors alone use EX_USAGE).
|
|
92
|
+
*/
|
|
93
|
+
function failure(outcome) {
|
|
94
|
+
// D-2: the claim file is the sole serializer of lock mutations, so
|
|
95
|
+
// deleting one that is actually live lets a second mutation enter the
|
|
96
|
+
// critical section and can produce two live holders. The remedy text
|
|
97
|
+
// therefore states that cost instead of inviting the deletion, and it
|
|
98
|
+
// names the safe order: make sure no tiphys process is running first.
|
|
99
|
+
const remedy = outcome.claimTimeout === true
|
|
100
|
+
? "; a crashed mutation can leave this file behind, but deleting it while a mutation is genuinely in flight can produce two lock holders, so confirm no tiphys process is running against this fleet before removing it"
|
|
101
|
+
: "";
|
|
102
|
+
process.stderr.write(`tiphys lock: ${outcome.reason}${remedy}\n`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
function parseFlags(rest) {
|
|
106
|
+
const parsed = {
|
|
107
|
+
takeover: false,
|
|
108
|
+
holder: undefined,
|
|
109
|
+
durationSeconds: undefined,
|
|
110
|
+
};
|
|
111
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
112
|
+
const flag = rest[i];
|
|
113
|
+
if (flag === "--take-over") {
|
|
114
|
+
parsed.takeover = true;
|
|
115
|
+
}
|
|
116
|
+
else if (flag === "--holder" && i + 1 < rest.length) {
|
|
117
|
+
parsed.holder = rest[i + 1];
|
|
118
|
+
i += 1;
|
|
119
|
+
}
|
|
120
|
+
else if (flag === "--duration" && i + 1 < rest.length) {
|
|
121
|
+
const seconds = Number(rest[i + 1]);
|
|
122
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
parsed.durationSeconds = seconds;
|
|
126
|
+
i += 1;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
export async function cmdLock(args) {
|
|
135
|
+
const [subcommand, ...rest] = args;
|
|
136
|
+
if (subcommand === undefined) {
|
|
137
|
+
return usageError();
|
|
138
|
+
}
|
|
139
|
+
const flags = parseFlags(rest);
|
|
140
|
+
if (flags === undefined) {
|
|
141
|
+
return usageError();
|
|
142
|
+
}
|
|
143
|
+
let lockPath;
|
|
144
|
+
try {
|
|
145
|
+
lockPath = loadFleet(process.cwd()).lockPath;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
process.stderr.write(`tiphys lock: ${error.message}\n`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
switch (subcommand) {
|
|
152
|
+
case "acquire": {
|
|
153
|
+
if (flags.holder !== undefined) {
|
|
154
|
+
return usageError("acquire does not take --holder");
|
|
155
|
+
}
|
|
156
|
+
const held = await maybeHoldForTest(lockPath);
|
|
157
|
+
const outcome = await acquireLease(lockPath, {
|
|
158
|
+
takeover: flags.takeover,
|
|
159
|
+
durationSeconds: flags.durationSeconds,
|
|
160
|
+
observed: held?.observed,
|
|
161
|
+
nowMs: held?.nowMs,
|
|
162
|
+
});
|
|
163
|
+
if (!outcome.ok) {
|
|
164
|
+
return failure(outcome);
|
|
165
|
+
}
|
|
166
|
+
const lease = outcome.lease;
|
|
167
|
+
if (lease === null) {
|
|
168
|
+
throw new Error("unreachable: acquire produced no lease");
|
|
169
|
+
}
|
|
170
|
+
process.stdout.write(`acquired ${lease.holderId} expires ${lease.expiresAt}\n`);
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
case "renew": {
|
|
174
|
+
if (flags.holder === undefined || flags.takeover) {
|
|
175
|
+
return usageError("renew requires --holder <id>");
|
|
176
|
+
}
|
|
177
|
+
const held = await maybeHoldForTest(lockPath);
|
|
178
|
+
const outcome = await renewLease(lockPath, flags.holder, {
|
|
179
|
+
durationSeconds: flags.durationSeconds,
|
|
180
|
+
observed: held?.observed,
|
|
181
|
+
nowMs: held?.nowMs,
|
|
182
|
+
});
|
|
183
|
+
if (!outcome.ok) {
|
|
184
|
+
return failure(outcome);
|
|
185
|
+
}
|
|
186
|
+
const lease = outcome.lease;
|
|
187
|
+
if (lease === null) {
|
|
188
|
+
throw new Error("unreachable: renew produced no lease");
|
|
189
|
+
}
|
|
190
|
+
process.stdout.write(`renewed ${lease.holderId} expires ${lease.expiresAt}\n`);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
case "release": {
|
|
194
|
+
if (flags.holder === undefined || flags.takeover || flags.durationSeconds !== undefined) {
|
|
195
|
+
return usageError("release requires --holder <id> and no other flags");
|
|
196
|
+
}
|
|
197
|
+
const held = await maybeHoldForTest(lockPath);
|
|
198
|
+
const outcome = await releaseLease(lockPath, flags.holder, {
|
|
199
|
+
observed: held?.observed,
|
|
200
|
+
});
|
|
201
|
+
if (!outcome.ok) {
|
|
202
|
+
return failure(outcome);
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write(`released ${flags.holder}\n`);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
case "status": {
|
|
208
|
+
if (flags.holder !== undefined || flags.takeover || flags.durationSeconds !== undefined) {
|
|
209
|
+
return usageError("status takes no flags");
|
|
210
|
+
}
|
|
211
|
+
const status = leaseStatus(lockPath);
|
|
212
|
+
if (status.state === "free") {
|
|
213
|
+
process.stdout.write("free\n");
|
|
214
|
+
}
|
|
215
|
+
else if (status.state === "corrupt") {
|
|
216
|
+
process.stdout.write(`corrupt ${status.detail}\n`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
process.stdout.write(`${status.state} holder ${status.lease.holderId} acquired ` +
|
|
220
|
+
`${status.lease.acquiredAt} expires ${status.lease.expiresAt}\n`);
|
|
221
|
+
}
|
|
222
|
+
// status always exits 0 so a human takeover decision is informed
|
|
223
|
+
// by reading, never blocked by an exit code.
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
return usageError(`unknown lock subcommand "${subcommand}"`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys mode show --mode <id> [--file <path>]` (kernel plan M3, M3-P3
|
|
3
|
+
* step 5; fix round 1 finding B-002).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS COMMAND EXISTS: a brief or a human has to be able to read what a
|
|
6
|
+
* declared mode requires without parsing YAML by hand. That is the whole
|
|
7
|
+
* scope. It shows; it does not resolve a project into a mode, does not
|
|
8
|
+
* enforce one, and does not run one.
|
|
9
|
+
*
|
|
10
|
+
* IT VALIDATES BEFORE IT SERVES, and that is the point of the fix round.
|
|
11
|
+
* As first shipped this command read the document and printed it, invoking
|
|
12
|
+
* neither the schema nor any registered check, so every guard the phase built
|
|
13
|
+
* was bypassed on the one path built for humans and briefs. The mechanism is
|
|
14
|
+
* A READER THAT DOES NOT VALIDATE WHAT IT READS, and the fix is not "detect
|
|
15
|
+
* one more bad state": it is that this command now runs exactly what
|
|
16
|
+
* `tiphys validate --type assurance-modes --context <dir>` runs, and REFUSES
|
|
17
|
+
* rather than printing when the document fails. A partial answer from an
|
|
18
|
+
* invalid document is worse than an error, because the reader cannot tell.
|
|
19
|
+
*
|
|
20
|
+
* THE CONTEXT IS THE DOCUMENT'S OWN DIRECTORY. The cross-document checks
|
|
21
|
+
* resolve `gate-registry.yaml` and `schemas/charter.schema.json` beside the
|
|
22
|
+
* document, which is where the npm package puts them and where this repository
|
|
23
|
+
* keeps them. That makes the two `requiresContext` checks run here rather than
|
|
24
|
+
* SKIP, which matters: a skip fails the run, and a command that always failed
|
|
25
|
+
* would be a command nobody uses.
|
|
26
|
+
*
|
|
27
|
+
* Exit codes:
|
|
28
|
+
* 0 the document is valid and the mode was found and printed
|
|
29
|
+
* 1 the document could not be read, is invalid, or no mode carries that id
|
|
30
|
+
* 64 usage error (BSD sysexits EX_USAGE)
|
|
31
|
+
*
|
|
32
|
+
* A MODE ID THAT IS NOT DECLARED IS 1, NOT 64, and the difference is not
|
|
33
|
+
* pedantry. 64 means the caller used the command wrongly; 1 means the command
|
|
34
|
+
* worked and the answer is no. `--mode direct-pr` against a document that
|
|
35
|
+
* declares only `full` is a well-formed question with a negative answer, and a
|
|
36
|
+
* caller that cannot tell those apart cannot script this.
|
|
37
|
+
*/
|
|
38
|
+
/** Exit code for usage errors, per BSD sysexits EX_USAGE. */
|
|
39
|
+
export declare const EX_USAGE = 64;
|
|
40
|
+
export declare function cmdMode(argv: string[]): number;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys mode show --mode <id> [--file <path>]` (kernel plan M3, M3-P3
|
|
3
|
+
* step 5; fix round 1 finding B-002).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS COMMAND EXISTS: a brief or a human has to be able to read what a
|
|
6
|
+
* declared mode requires without parsing YAML by hand. That is the whole
|
|
7
|
+
* scope. It shows; it does not resolve a project into a mode, does not
|
|
8
|
+
* enforce one, and does not run one.
|
|
9
|
+
*
|
|
10
|
+
* IT VALIDATES BEFORE IT SERVES, and that is the point of the fix round.
|
|
11
|
+
* As first shipped this command read the document and printed it, invoking
|
|
12
|
+
* neither the schema nor any registered check, so every guard the phase built
|
|
13
|
+
* was bypassed on the one path built for humans and briefs. The mechanism is
|
|
14
|
+
* A READER THAT DOES NOT VALIDATE WHAT IT READS, and the fix is not "detect
|
|
15
|
+
* one more bad state": it is that this command now runs exactly what
|
|
16
|
+
* `tiphys validate --type assurance-modes --context <dir>` runs, and REFUSES
|
|
17
|
+
* rather than printing when the document fails. A partial answer from an
|
|
18
|
+
* invalid document is worse than an error, because the reader cannot tell.
|
|
19
|
+
*
|
|
20
|
+
* THE CONTEXT IS THE DOCUMENT'S OWN DIRECTORY. The cross-document checks
|
|
21
|
+
* resolve `gate-registry.yaml` and `schemas/charter.schema.json` beside the
|
|
22
|
+
* document, which is where the npm package puts them and where this repository
|
|
23
|
+
* keeps them. That makes the two `requiresContext` checks run here rather than
|
|
24
|
+
* SKIP, which matters: a skip fails the run, and a command that always failed
|
|
25
|
+
* would be a command nobody uses.
|
|
26
|
+
*
|
|
27
|
+
* Exit codes:
|
|
28
|
+
* 0 the document is valid and the mode was found and printed
|
|
29
|
+
* 1 the document could not be read, is invalid, or no mode carries that id
|
|
30
|
+
* 64 usage error (BSD sysexits EX_USAGE)
|
|
31
|
+
*
|
|
32
|
+
* A MODE ID THAT IS NOT DECLARED IS 1, NOT 64, and the difference is not
|
|
33
|
+
* pedantry. 64 means the caller used the command wrongly; 1 means the command
|
|
34
|
+
* worked and the answer is no. `--mode direct-pr` against a document that
|
|
35
|
+
* declares only `full` is a well-formed question with a negative answer, and a
|
|
36
|
+
* caller that cannot tell those apart cannot script this.
|
|
37
|
+
*/
|
|
38
|
+
import { dirname } from "node:path";
|
|
39
|
+
import { runChecks } from "../checks.js";
|
|
40
|
+
import { formatDiagnostics, validateInstance } from "../validate.js";
|
|
41
|
+
import { loadTypeSchema } from "./validate.js";
|
|
42
|
+
import { readModes, renderMode } from "../modes.js";
|
|
43
|
+
/** The artifact type this command reads, and the type its checks are registered for. */
|
|
44
|
+
const MODES_TYPE = "assurance-modes";
|
|
45
|
+
/** Exit code for usage errors, per BSD sysexits EX_USAGE. */
|
|
46
|
+
export const EX_USAGE = 64;
|
|
47
|
+
function usage() {
|
|
48
|
+
return "usage: tiphys mode show --mode <id> [--file <path>]";
|
|
49
|
+
}
|
|
50
|
+
function parseArgs(argv) {
|
|
51
|
+
const options = {};
|
|
52
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
53
|
+
const argument = argv[index];
|
|
54
|
+
if (argument === "--mode" || argument === "--file") {
|
|
55
|
+
const value = argv[index + 1];
|
|
56
|
+
if (value === undefined || value.startsWith("--")) {
|
|
57
|
+
return { usageError: `${argument} requires a value` };
|
|
58
|
+
}
|
|
59
|
+
if (argument === "--mode") {
|
|
60
|
+
options.mode = value;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
options.file = value;
|
|
64
|
+
}
|
|
65
|
+
index += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return { usageError: `unknown argument ${argument}` };
|
|
69
|
+
}
|
|
70
|
+
if (options.mode === undefined) {
|
|
71
|
+
return { usageError: "--mode is required" };
|
|
72
|
+
}
|
|
73
|
+
return { options };
|
|
74
|
+
}
|
|
75
|
+
function fail(reason, code) {
|
|
76
|
+
process.stderr.write(`tiphys mode: ${reason}\n`);
|
|
77
|
+
if (code === EX_USAGE) {
|
|
78
|
+
process.stderr.write(`${usage()}\n`);
|
|
79
|
+
}
|
|
80
|
+
return code;
|
|
81
|
+
}
|
|
82
|
+
export function cmdMode(argv) {
|
|
83
|
+
const [subcommand, ...rest] = argv;
|
|
84
|
+
if (subcommand !== "show") {
|
|
85
|
+
return fail(subcommand === undefined
|
|
86
|
+
? "a subcommand is required"
|
|
87
|
+
: `unknown subcommand ${subcommand}`, EX_USAGE);
|
|
88
|
+
}
|
|
89
|
+
const parsed = parseArgs(rest);
|
|
90
|
+
if (parsed.options === undefined) {
|
|
91
|
+
return fail(parsed.usageError ?? "usage error", EX_USAGE);
|
|
92
|
+
}
|
|
93
|
+
let read;
|
|
94
|
+
try {
|
|
95
|
+
read = parsed.options.file === undefined ? readModes() : readModes(parsed.options.file);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
/* `packageRoot` throws when the shipped document is not above this module,
|
|
99
|
+
which is an incomplete installation rather than a caller error. One line,
|
|
100
|
+
never a stack trace, the same policy `tiphys validate` applies to its own
|
|
101
|
+
operator-supplied input. */
|
|
102
|
+
return fail(String(error instanceof Error ? error.message : error), 1);
|
|
103
|
+
}
|
|
104
|
+
if (!read.ok) {
|
|
105
|
+
return fail(read.reason, 1);
|
|
106
|
+
}
|
|
107
|
+
/* VALIDATE, THEN SERVE. Schema first and derived checks second, in the same
|
|
108
|
+
order and through the same functions `tiphys validate` uses, so the two
|
|
109
|
+
commands cannot drift into disagreeing about what a valid document is. */
|
|
110
|
+
const lines = formatDiagnostics(validateInstance(loadTypeSchema(MODES_TYPE), read.raw));
|
|
111
|
+
const checks = runChecks(MODES_TYPE, read.raw, dirname(read.path));
|
|
112
|
+
if (lines.length > 0 || checks.failed) {
|
|
113
|
+
process.stderr.write(`tiphys mode: ${read.path} is not a valid ${MODES_TYPE} document, so it is not served\n`);
|
|
114
|
+
for (const line of [...lines, ...checks.lines]) {
|
|
115
|
+
process.stderr.write(`${line}\n`);
|
|
116
|
+
}
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
const wanted = parsed.options.mode;
|
|
120
|
+
const mode = read.modes.find((candidate) => candidate.id === wanted);
|
|
121
|
+
if (mode === undefined) {
|
|
122
|
+
const declared = read.modes.map((candidate) => candidate.id).sort();
|
|
123
|
+
return fail(`${read.path} declares no mode ${wanted}; it declares ${declared.length === 0 ? "none" : declared.join(", ")}`, 1);
|
|
124
|
+
}
|
|
125
|
+
/* WHICH DOCUMENT THIS IS, derived from the invocation rather than from the
|
|
126
|
+
path: no `--file` means `readModes()` walked to the package root and read
|
|
127
|
+
the kernel's own document. CR-004 item 2 turns on that distinction, because
|
|
128
|
+
"no phase has been delivered under this mode" is a claim about the kernel's
|
|
129
|
+
own delivery and is not knowable for a document a consumer supplied. */
|
|
130
|
+
for (const line of renderMode(mode, { shippedDocument: parsed.options.file === undefined })) {
|
|
131
|
+
process.stdout.write(`${line}\n`);
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys plan project --phase-id <id> [--plan <file>] [--out <dir>]`
|
|
3
|
+
* (kernel plan M3, M3-P1 step 2; D-M3-18).
|
|
4
|
+
*
|
|
5
|
+
* Emits the M2-P4 scope auditor's phase declaration from the plan, so the
|
|
6
|
+
* auditor's input becomes a GENERATED VIEW of one source instead of a second
|
|
7
|
+
* hand-authored source that can drift.
|
|
8
|
+
*
|
|
9
|
+
* The output path is `<out>/<phase-id-lowercased>.json`, defaulting to
|
|
10
|
+
* `delivery/plan/phase-declarations`, which is the directory
|
|
11
|
+
* `gates.manifest.json` passes to the auditor as `--declarations`.
|
|
12
|
+
*
|
|
13
|
+
* With no `--out` the document is written; with `--stdout` it is printed and
|
|
14
|
+
* nothing is written, which is what a test or a reviewer wants when checking
|
|
15
|
+
* WHAT would be emitted without touching the tree.
|
|
16
|
+
*
|
|
17
|
+
* Every path this command touches is operator-supplied and is classified
|
|
18
|
+
* before it is opened or written (D-M3-27).
|
|
19
|
+
*/
|
|
20
|
+
export declare function cmdPlan(argv: string[]): number;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys plan project --phase-id <id> [--plan <file>] [--out <dir>]`
|
|
3
|
+
* (kernel plan M3, M3-P1 step 2; D-M3-18).
|
|
4
|
+
*
|
|
5
|
+
* Emits the M2-P4 scope auditor's phase declaration from the plan, so the
|
|
6
|
+
* auditor's input becomes a GENERATED VIEW of one source instead of a second
|
|
7
|
+
* hand-authored source that can drift.
|
|
8
|
+
*
|
|
9
|
+
* The output path is `<out>/<phase-id-lowercased>.json`, defaulting to
|
|
10
|
+
* `delivery/plan/phase-declarations`, which is the directory
|
|
11
|
+
* `gates.manifest.json` passes to the auditor as `--declarations`.
|
|
12
|
+
*
|
|
13
|
+
* With no `--out` the document is written; with `--stdout` it is printed and
|
|
14
|
+
* nothing is written, which is what a test or a reviewer wants when checking
|
|
15
|
+
* WHAT would be emitted without touching the tree.
|
|
16
|
+
*
|
|
17
|
+
* Every path this command touches is operator-supplied and is classified
|
|
18
|
+
* before it is opened or written (D-M3-27).
|
|
19
|
+
*/
|
|
20
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { EX_USAGE } from "../cli.js";
|
|
23
|
+
import { projectPhase, renderDeclaration } from "../plan.js";
|
|
24
|
+
import { refuseOpenForWrite } from "../task.js";
|
|
25
|
+
import { classifyContextDirectory, decodeDocument, readOperatorPath, } from "../validate.js";
|
|
26
|
+
const USAGE = "usage: tiphys plan project --phase-id <id> [--plan <file>] " +
|
|
27
|
+
"[--out <dir>] [--stdout]";
|
|
28
|
+
const DEFAULT_PLAN = "templates/plan.example.yaml";
|
|
29
|
+
const DEFAULT_OUT = join("delivery", "plan", "phase-declarations");
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const args = { plan: DEFAULT_PLAN, out: DEFAULT_OUT, toStdout: false };
|
|
32
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
33
|
+
const flag = argv[index];
|
|
34
|
+
if (flag === "--stdout") {
|
|
35
|
+
args.toStdout = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const value = argv[index + 1];
|
|
39
|
+
if (flag !== "--phase-id" && flag !== "--plan" && flag !== "--out") {
|
|
40
|
+
return { usageError: `unknown option ${flag}` };
|
|
41
|
+
}
|
|
42
|
+
if (value === undefined || value.startsWith("--")) {
|
|
43
|
+
return { usageError: `${flag} requires a value` };
|
|
44
|
+
}
|
|
45
|
+
index += 1;
|
|
46
|
+
if (flag === "--phase-id")
|
|
47
|
+
args.phaseId = value;
|
|
48
|
+
else if (flag === "--plan")
|
|
49
|
+
args.plan = value;
|
|
50
|
+
else
|
|
51
|
+
args.out = value;
|
|
52
|
+
}
|
|
53
|
+
if (args.phaseId === undefined) {
|
|
54
|
+
return { usageError: "--phase-id is required" };
|
|
55
|
+
}
|
|
56
|
+
return { args };
|
|
57
|
+
}
|
|
58
|
+
export function cmdPlan(argv) {
|
|
59
|
+
const [subcommand, ...rest] = argv;
|
|
60
|
+
if (subcommand !== "project") {
|
|
61
|
+
process.stderr.write(`${USAGE}\n`);
|
|
62
|
+
return EX_USAGE;
|
|
63
|
+
}
|
|
64
|
+
const parsed = parseArgs(rest);
|
|
65
|
+
if (parsed.args === undefined) {
|
|
66
|
+
process.stderr.write(`tiphys plan project: ${parsed.usageError ?? "usage error"}\n${USAGE}\n`);
|
|
67
|
+
return EX_USAGE;
|
|
68
|
+
}
|
|
69
|
+
const { phaseId, plan, out, toStdout } = parsed.args;
|
|
70
|
+
const read = readOperatorPath(plan);
|
|
71
|
+
if (!read.ok) {
|
|
72
|
+
process.stderr.write(`tiphys plan project: ${read.reason}\n`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
const decoded = decodeDocument(read.body, plan);
|
|
76
|
+
if (!decoded.ok) {
|
|
77
|
+
process.stderr.write(`tiphys plan project: ${decoded.reason}\n`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
const projection = projectPhase(decoded.value, phaseId);
|
|
81
|
+
if (!projection.ok) {
|
|
82
|
+
process.stderr.write(`tiphys plan project: ${projection.reason}\n`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
const body = renderDeclaration(projection.declaration);
|
|
86
|
+
if (toStdout) {
|
|
87
|
+
process.stdout.write(body);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
mkdirSync(out, { recursive: true });
|
|
91
|
+
const problem = classifyContextDirectory(out);
|
|
92
|
+
if (problem !== undefined) {
|
|
93
|
+
process.stderr.write(`tiphys plan project: ${problem}\n`);
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
const target = join(out, projection.filename);
|
|
97
|
+
const refusal = refuseOpenForWrite(target);
|
|
98
|
+
if (refusal !== undefined) {
|
|
99
|
+
process.stderr.write(`tiphys plan project: ${refusal}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
writeFileSync(target, body, "utf8");
|
|
103
|
+
process.stdout.write(`${target}\n`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdPool(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { EX_USAGE } from "../cli.js";
|
|
2
|
+
import { loadFleet } from "../fleet.js";
|
|
3
|
+
import { poolCreate, poolDestroy, poolList } from "../pool.js";
|
|
4
|
+
/**
|
|
5
|
+
* tiphys pool <create --task <id> --project <path> [--offline] | list |
|
|
6
|
+
* destroy --task <id> [--discard]> (kernel plan v1, M1-P3 steps 3-4).
|
|
7
|
+
* Runs in a fleet home (cwd). create emits the fetched base SHA as its
|
|
8
|
+
* only stdout line (EXT-F-03 step 3; M1-P4 spawn captures it).
|
|
9
|
+
*/
|
|
10
|
+
const USAGE = "usage: tiphys pool <create --task <id> --project <path> [--offline] | " +
|
|
11
|
+
"list | destroy --task <id> [--discard] [--delete-branch-force]>";
|
|
12
|
+
function usageError(message) {
|
|
13
|
+
if (message !== undefined) {
|
|
14
|
+
process.stderr.write(`tiphys pool: ${message}\n`);
|
|
15
|
+
}
|
|
16
|
+
process.stderr.write(`${USAGE}\n`);
|
|
17
|
+
return EX_USAGE;
|
|
18
|
+
}
|
|
19
|
+
function parseFlags(rest) {
|
|
20
|
+
const parsed = {
|
|
21
|
+
task: undefined,
|
|
22
|
+
project: undefined,
|
|
23
|
+
offline: false,
|
|
24
|
+
discard: false,
|
|
25
|
+
deleteBranchForce: false,
|
|
26
|
+
};
|
|
27
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
28
|
+
const flag = rest[i];
|
|
29
|
+
if (flag === "--task" && i + 1 < rest.length) {
|
|
30
|
+
parsed.task = rest[i + 1];
|
|
31
|
+
i += 1;
|
|
32
|
+
}
|
|
33
|
+
else if (flag === "--project" && i + 1 < rest.length) {
|
|
34
|
+
parsed.project = rest[i + 1];
|
|
35
|
+
i += 1;
|
|
36
|
+
}
|
|
37
|
+
else if (flag === "--offline") {
|
|
38
|
+
parsed.offline = true;
|
|
39
|
+
}
|
|
40
|
+
else if (flag === "--discard") {
|
|
41
|
+
parsed.discard = true;
|
|
42
|
+
}
|
|
43
|
+
else if (flag === "--delete-branch-force") {
|
|
44
|
+
parsed.deleteBranchForce = true;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
export async function cmdPool(args) {
|
|
53
|
+
const [subcommand, ...rest] = args;
|
|
54
|
+
if (subcommand === undefined) {
|
|
55
|
+
return usageError();
|
|
56
|
+
}
|
|
57
|
+
const flags = parseFlags(rest);
|
|
58
|
+
if (flags === undefined) {
|
|
59
|
+
return usageError();
|
|
60
|
+
}
|
|
61
|
+
let fleet;
|
|
62
|
+
try {
|
|
63
|
+
fleet = loadFleet(process.cwd());
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
process.stderr.write(`tiphys pool: ${error.message}\n`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
switch (subcommand) {
|
|
70
|
+
case "create": {
|
|
71
|
+
if (flags.task === undefined ||
|
|
72
|
+
flags.project === undefined ||
|
|
73
|
+
flags.discard ||
|
|
74
|
+
flags.deleteBranchForce) {
|
|
75
|
+
return usageError("create requires --task <id> and --project <path>");
|
|
76
|
+
}
|
|
77
|
+
const result = await poolCreate(fleet, {
|
|
78
|
+
taskId: flags.task,
|
|
79
|
+
project: flags.project,
|
|
80
|
+
offline: flags.offline,
|
|
81
|
+
});
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
process.stderr.write(`tiphys pool: ${result.reason}\n`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
process.stdout.write(`${result.value.baseSha}\n`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
case "list": {
|
|
90
|
+
if (flags.task !== undefined ||
|
|
91
|
+
flags.project !== undefined ||
|
|
92
|
+
flags.offline ||
|
|
93
|
+
flags.discard ||
|
|
94
|
+
flags.deleteBranchForce) {
|
|
95
|
+
return usageError("list takes no flags");
|
|
96
|
+
}
|
|
97
|
+
for (const entry of poolList(fleet)) {
|
|
98
|
+
process.stdout.write(`${entry.taskId} ${entry.headSha}\n`);
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
case "destroy": {
|
|
103
|
+
if (flags.task === undefined || flags.project !== undefined || flags.offline) {
|
|
104
|
+
return usageError("destroy requires --task <id>");
|
|
105
|
+
}
|
|
106
|
+
const result = await poolDestroy(fleet, {
|
|
107
|
+
taskId: flags.task,
|
|
108
|
+
discard: flags.discard,
|
|
109
|
+
deleteBranchForce: flags.deleteBranchForce,
|
|
110
|
+
});
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
process.stderr.write(`tiphys pool: ${result.reason}\n`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
// Name the deleted branch and its tip on success: that sha is the
|
|
116
|
+
// operator's only recovery handle if the destroy was a mistake
|
|
117
|
+
// (V-1), and it stays valid until the clone is garbage collected.
|
|
118
|
+
const { deletedBranch, deletedSha } = result.value;
|
|
119
|
+
const suffix = deletedBranch === undefined
|
|
120
|
+
? ""
|
|
121
|
+
: ` (deleted branch ${deletedBranch} was ${deletedSha ?? "unknown"})`;
|
|
122
|
+
process.stdout.write(`destroyed ${flags.task}${suffix}\n`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
return usageError(`unknown pool subcommand "${subcommand}"`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdSpawn(args: string[]): Promise<number>;
|