@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,146 @@
|
|
|
1
|
+
import { EX_USAGE } from "../cli.js";
|
|
2
|
+
import { loadFleet } from "../fleet.js";
|
|
3
|
+
import { warnIfWatcherStale } from "../liveness.js";
|
|
4
|
+
import { spawnTask } from "../spawn.js";
|
|
5
|
+
import { singleLine } from "../task.js";
|
|
6
|
+
/**
|
|
7
|
+
* tiphys spawn --task <id> --project <path> --brief <file> --shape
|
|
8
|
+
* ship|scout --exec <cmd> [--deadline <seconds>] [--offline]
|
|
9
|
+
* (kernel plan v1, M1-P4 step 4). Runs in a fleet home (cwd).
|
|
10
|
+
*
|
|
11
|
+
* --exec is REQUIRED in M1: spawn without it exits 64 with usage,
|
|
12
|
+
* because the multiplexer-window adapter that would make an exec-less
|
|
13
|
+
* spawn meaningful is M4-era work (PR-013). The payload runs to
|
|
14
|
+
* completion before this command returns (plan constraint C-3: nothing
|
|
15
|
+
* is auto-backgrounded).
|
|
16
|
+
*
|
|
17
|
+
* --offline is passed through to pool create, and is a deviation from
|
|
18
|
+
* the plan's enumerated flag list, recorded in the M1-P4 work history:
|
|
19
|
+
* meta.json's baseOffline field (criterion 13 of M1-P3, this phase's
|
|
20
|
+
* named obligation) can only ever be true for a worktree created under
|
|
21
|
+
* pool create --offline, and spawn is the only command that creates the
|
|
22
|
+
* worktree a task's meta describes.
|
|
23
|
+
*/
|
|
24
|
+
const USAGE = "usage: tiphys spawn --task <id> --project <path> --brief <file> " +
|
|
25
|
+
"--shape ship|scout --exec <cmd> [--deadline <seconds>] [--offline]";
|
|
26
|
+
function usageError(message) {
|
|
27
|
+
if (message !== undefined) {
|
|
28
|
+
process.stderr.write(`tiphys spawn: ${message}\n`);
|
|
29
|
+
}
|
|
30
|
+
process.stderr.write(`${USAGE}\n`);
|
|
31
|
+
return EX_USAGE;
|
|
32
|
+
}
|
|
33
|
+
function parseFlags(args) {
|
|
34
|
+
const parsed = {
|
|
35
|
+
task: undefined,
|
|
36
|
+
project: undefined,
|
|
37
|
+
brief: undefined,
|
|
38
|
+
shape: undefined,
|
|
39
|
+
exec: undefined,
|
|
40
|
+
deadlineSeconds: undefined,
|
|
41
|
+
offline: false,
|
|
42
|
+
};
|
|
43
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
44
|
+
const flag = args[i];
|
|
45
|
+
const value = args[i + 1];
|
|
46
|
+
if (flag === "--task" && value !== undefined) {
|
|
47
|
+
parsed.task = value;
|
|
48
|
+
i += 1;
|
|
49
|
+
}
|
|
50
|
+
else if (flag === "--project" && value !== undefined) {
|
|
51
|
+
parsed.project = value;
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
else if (flag === "--brief" && value !== undefined) {
|
|
55
|
+
parsed.brief = value;
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
else if (flag === "--shape" && value !== undefined) {
|
|
59
|
+
if (value !== "ship" && value !== "scout") {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
parsed.shape = value;
|
|
63
|
+
i += 1;
|
|
64
|
+
}
|
|
65
|
+
else if (flag === "--exec" && value !== undefined) {
|
|
66
|
+
parsed.exec = value;
|
|
67
|
+
i += 1;
|
|
68
|
+
}
|
|
69
|
+
else if (flag === "--deadline" && value !== undefined) {
|
|
70
|
+
const seconds = Number(value);
|
|
71
|
+
// N-403: finite and positive is not enough. The adapter turns the
|
|
72
|
+
// deadline into an instant, and any value at or above roughly
|
|
73
|
+
// 8.64e12 seconds is outside the Date range, so it used to raise
|
|
74
|
+
// INSIDE the adapter, after pool create had made a worktree, a
|
|
75
|
+
// branch and a pool record. A deadline this kernel cannot
|
|
76
|
+
// represent is a usage error, and a usage error creates nothing.
|
|
77
|
+
if (!Number.isFinite(seconds) ||
|
|
78
|
+
seconds <= 0 ||
|
|
79
|
+
!Number.isFinite(new Date(Date.now() + seconds * 1000).getTime())) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
parsed.deadlineSeconds = seconds;
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
else if (flag === "--offline") {
|
|
86
|
+
parsed.offline = true;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
export async function cmdSpawn(args) {
|
|
95
|
+
const flags = parseFlags(args);
|
|
96
|
+
if (flags === undefined) {
|
|
97
|
+
return usageError();
|
|
98
|
+
}
|
|
99
|
+
if (flags.task === undefined ||
|
|
100
|
+
flags.project === undefined ||
|
|
101
|
+
flags.brief === undefined ||
|
|
102
|
+
flags.shape === undefined) {
|
|
103
|
+
return usageError("spawn requires --task <id> --project <path> --brief <file> --shape ship|scout");
|
|
104
|
+
}
|
|
105
|
+
if (flags.exec === undefined) {
|
|
106
|
+
// PR-013: an exec-less spawn has no meaning in M1.
|
|
107
|
+
return usageError("spawn requires --exec <cmd> in M1");
|
|
108
|
+
}
|
|
109
|
+
let fleet;
|
|
110
|
+
try {
|
|
111
|
+
fleet = loadFleet(process.cwd());
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
process.stderr.write(`tiphys spawn: ${singleLine(error.message)}\n`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
// Liveness guard (M1-P5 step 2), completing the seam this phase left.
|
|
118
|
+
// It WARNS and never blocks: one stderr line when work is in flight
|
|
119
|
+
// and supervision has gone quiet, and the command then does exactly
|
|
120
|
+
// what it would have done anyway (blueprint liveness-guard contract,
|
|
121
|
+
// criteria 10 and 11).
|
|
122
|
+
warnIfWatcherStale(fleet);
|
|
123
|
+
const result = await spawnTask(fleet, {
|
|
124
|
+
taskId: flags.task,
|
|
125
|
+
project: flags.project,
|
|
126
|
+
briefFile: flags.brief,
|
|
127
|
+
shape: flags.shape,
|
|
128
|
+
exec: flags.exec,
|
|
129
|
+
deadlineSeconds: flags.deadlineSeconds,
|
|
130
|
+
offline: flags.offline,
|
|
131
|
+
});
|
|
132
|
+
if (!result.ok) {
|
|
133
|
+
// One reason line, structurally (CR-303): a reason may carry a
|
|
134
|
+
// raised error message or git output, and an operator or the M1-P6
|
|
135
|
+
// harness reading "the reason line" must get all of it.
|
|
136
|
+
process.stderr.write(`tiphys spawn: ${singleLine(result.reason)}\n`);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
// The payload's exit code is reported, not adopted: the task's state
|
|
140
|
+
// authority is meta.json plus the turn-end record (plan constraint
|
|
141
|
+
// C-1), and a nonzero payload is a completed task with a failing
|
|
142
|
+
// payload, not a failed spawn.
|
|
143
|
+
const { meta, exitCode } = result.value;
|
|
144
|
+
process.stdout.write(`spawned ${meta.id} worktree ${meta.worktree} exec exited ${String(exitCode)}\n`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys status <emit | show>` (kernel plan M3, M3-P1 step 6; R-084).
|
|
3
|
+
*
|
|
4
|
+
* tiphys status emit --run <id> --state <state> [--project <name>]
|
|
5
|
+
* [--detail <text>] [--ref <r>]...
|
|
6
|
+
* tiphys status show
|
|
7
|
+
*
|
|
8
|
+
* Runs in a fleet home (cwd). `emit` composes a record, VALIDATES it against
|
|
9
|
+
* the shipped `status-line` schema before writing anything, appends one line
|
|
10
|
+
* to `state/status/stream.jsonl`, then rewrites `state/status/current.json`
|
|
11
|
+
* atomically. `show` reads `current.json` ONLY and never opens the stream
|
|
12
|
+
* (constraint C-1: never read current state from the tail of an append-only
|
|
13
|
+
* log).
|
|
14
|
+
*
|
|
15
|
+
* The state vocabulary is closed and is enforced, not requested: `--state
|
|
16
|
+
* progress` is refused naming the permitted values, because R-084's
|
|
17
|
+
* sparseness is worth nothing if the emitter accepts whatever it is handed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function cmdStatus(argv: string[]): number;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys status <emit | show>` (kernel plan M3, M3-P1 step 6; R-084).
|
|
3
|
+
*
|
|
4
|
+
* tiphys status emit --run <id> --state <state> [--project <name>]
|
|
5
|
+
* [--detail <text>] [--ref <r>]...
|
|
6
|
+
* tiphys status show
|
|
7
|
+
*
|
|
8
|
+
* Runs in a fleet home (cwd). `emit` composes a record, VALIDATES it against
|
|
9
|
+
* the shipped `status-line` schema before writing anything, appends one line
|
|
10
|
+
* to `state/status/stream.jsonl`, then rewrites `state/status/current.json`
|
|
11
|
+
* atomically. `show` reads `current.json` ONLY and never opens the stream
|
|
12
|
+
* (constraint C-1: never read current state from the tail of an append-only
|
|
13
|
+
* log).
|
|
14
|
+
*
|
|
15
|
+
* The state vocabulary is closed and is enforced, not requested: `--state
|
|
16
|
+
* progress` is refused naming the permitted values, because R-084's
|
|
17
|
+
* sparseness is worth nothing if the emitter accepts whatever it is handed.
|
|
18
|
+
*/
|
|
19
|
+
import { basename } from "node:path";
|
|
20
|
+
import { EX_USAGE } from "../cli.js";
|
|
21
|
+
import { loadFleet } from "../fleet.js";
|
|
22
|
+
import { loadTypeSchema } from "./validate.js";
|
|
23
|
+
import { formatDiagnostics, validateInstance } from "../validate.js";
|
|
24
|
+
import { STATUS_STATES, emitStatus, makeStatusRecord, readCurrent, renderStatus, } from "../status.js";
|
|
25
|
+
const USAGE = "usage: tiphys status <emit --run <id> --state <" +
|
|
26
|
+
STATUS_STATES.join("|") +
|
|
27
|
+
"> [--project <name>] [--detail <text>] [--ref <r>] | show>";
|
|
28
|
+
function parseEmit(argv) {
|
|
29
|
+
const args = { refs: [] };
|
|
30
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
31
|
+
const flag = argv[index];
|
|
32
|
+
const value = argv[index + 1];
|
|
33
|
+
if (flag !== "--run" &&
|
|
34
|
+
flag !== "--state" &&
|
|
35
|
+
flag !== "--project" &&
|
|
36
|
+
flag !== "--detail" &&
|
|
37
|
+
flag !== "--ref" &&
|
|
38
|
+
flag !== "--at") {
|
|
39
|
+
return { usageError: `unknown option ${flag}` };
|
|
40
|
+
}
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
return { usageError: `${flag} requires a value` };
|
|
43
|
+
}
|
|
44
|
+
index += 1;
|
|
45
|
+
if (flag === "--run")
|
|
46
|
+
args.run = value;
|
|
47
|
+
else if (flag === "--state")
|
|
48
|
+
args.state = value;
|
|
49
|
+
else if (flag === "--project")
|
|
50
|
+
args.project = value;
|
|
51
|
+
else if (flag === "--detail")
|
|
52
|
+
args.detail = value;
|
|
53
|
+
else if (flag === "--at")
|
|
54
|
+
args.at = value;
|
|
55
|
+
else
|
|
56
|
+
args.refs.push(value);
|
|
57
|
+
}
|
|
58
|
+
if (args.run === undefined) {
|
|
59
|
+
return { usageError: "--run is required" };
|
|
60
|
+
}
|
|
61
|
+
if (args.state === undefined) {
|
|
62
|
+
return { usageError: "--state is required" };
|
|
63
|
+
}
|
|
64
|
+
return { args };
|
|
65
|
+
}
|
|
66
|
+
export function cmdStatus(argv) {
|
|
67
|
+
const [subcommand, ...rest] = argv;
|
|
68
|
+
if (subcommand === "show") {
|
|
69
|
+
if (rest.length > 0) {
|
|
70
|
+
process.stderr.write(`tiphys status show: takes no arguments\n${USAGE}\n`);
|
|
71
|
+
return EX_USAGE;
|
|
72
|
+
}
|
|
73
|
+
const fleet = loadFleet(process.cwd());
|
|
74
|
+
const current = readCurrent(fleet.root);
|
|
75
|
+
if (!current.ok) {
|
|
76
|
+
process.stderr.write(`tiphys status show: ${current.reason}\n`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(`${renderStatus(current.record)}\n`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
if (subcommand !== "emit") {
|
|
83
|
+
process.stderr.write(`${USAGE}\n`);
|
|
84
|
+
return EX_USAGE;
|
|
85
|
+
}
|
|
86
|
+
const parsed = parseEmit(rest);
|
|
87
|
+
if (parsed.args === undefined) {
|
|
88
|
+
process.stderr.write(`tiphys status emit: ${parsed.usageError ?? "usage error"}\n${USAGE}\n`);
|
|
89
|
+
return EX_USAGE;
|
|
90
|
+
}
|
|
91
|
+
const fleet = loadFleet(process.cwd());
|
|
92
|
+
const record = makeStatusRecord({
|
|
93
|
+
run: parsed.args.run,
|
|
94
|
+
project: parsed.args.project ?? basename(fleet.root),
|
|
95
|
+
state: parsed.args.state,
|
|
96
|
+
detail: parsed.args.detail,
|
|
97
|
+
refs: parsed.args.refs,
|
|
98
|
+
at: parsed.args.at,
|
|
99
|
+
});
|
|
100
|
+
/* The record is validated against the SHIPPED schema before anything is
|
|
101
|
+
written. Validating a composed record against the same document the
|
|
102
|
+
validator command uses is what makes the closed state vocabulary one
|
|
103
|
+
rule rather than two: `--state progress` fails here for exactly the
|
|
104
|
+
reason it would fail `tiphys validate --type status-line`. */
|
|
105
|
+
const diagnostics = validateInstance(loadTypeSchema("status-line"), record);
|
|
106
|
+
if (diagnostics.length > 0) {
|
|
107
|
+
for (const line of formatDiagnostics(diagnostics)) {
|
|
108
|
+
process.stderr.write(`${line}\n`);
|
|
109
|
+
}
|
|
110
|
+
process.stderr.write(`tiphys status emit: the permitted states are ${STATUS_STATES.join(", ")}\n`);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
const emitted = emitStatus(fleet.root, record);
|
|
114
|
+
if (!emitted.ok) {
|
|
115
|
+
process.stderr.write(`tiphys status emit: ${emitted.reason}\n`);
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write(`${renderStatus(emitted.record)}\n`);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdTeardown(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { EX_USAGE } from "../cli.js";
|
|
2
|
+
import { loadFleet } from "../fleet.js";
|
|
3
|
+
import { warnIfWatcherStale } from "../liveness.js";
|
|
4
|
+
import { singleLine } from "../task.js";
|
|
5
|
+
import { teardownTask } from "../teardown.js";
|
|
6
|
+
/**
|
|
7
|
+
* tiphys teardown --task <id> [--salvage] (kernel plan v1, M1-P4
|
|
8
|
+
* step 5). Runs in a fleet home (cwd).
|
|
9
|
+
*
|
|
10
|
+
* Every refusal is exit nonzero plus a SINGLE reason line naming the
|
|
11
|
+
* blocking condition. A failure of the destroy itself is passed through
|
|
12
|
+
* with the pool's own wording, which distinguishes a refusal (a true
|
|
13
|
+
* no-op) from a partial failure (the worktree is already gone and the
|
|
14
|
+
* survivors are enumerated); this command adds no framing of its own to
|
|
15
|
+
* either, because the two must never be described alike.
|
|
16
|
+
*/
|
|
17
|
+
const USAGE = "usage: tiphys teardown --task <id> [--salvage]";
|
|
18
|
+
function usageError(message) {
|
|
19
|
+
if (message !== undefined) {
|
|
20
|
+
process.stderr.write(`tiphys teardown: ${message}\n`);
|
|
21
|
+
}
|
|
22
|
+
process.stderr.write(`${USAGE}\n`);
|
|
23
|
+
return EX_USAGE;
|
|
24
|
+
}
|
|
25
|
+
function parseFlags(args) {
|
|
26
|
+
const parsed = { task: undefined, salvage: false };
|
|
27
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
28
|
+
const flag = args[i];
|
|
29
|
+
const value = args[i + 1];
|
|
30
|
+
if (flag === "--task" && value !== undefined) {
|
|
31
|
+
parsed.task = value;
|
|
32
|
+
i += 1;
|
|
33
|
+
}
|
|
34
|
+
else if (flag === "--salvage") {
|
|
35
|
+
parsed.salvage = true;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
export async function cmdTeardown(args) {
|
|
44
|
+
const flags = parseFlags(args);
|
|
45
|
+
if (flags === undefined) {
|
|
46
|
+
return usageError();
|
|
47
|
+
}
|
|
48
|
+
if (flags.task === undefined) {
|
|
49
|
+
return usageError("teardown requires --task <id>");
|
|
50
|
+
}
|
|
51
|
+
let fleet;
|
|
52
|
+
try {
|
|
53
|
+
fleet = loadFleet(process.cwd());
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
process.stderr.write(`tiphys teardown: ${singleLine(error.message)}\n`);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
// Liveness guard (M1-P5 step 2): one stderr warning line when work is
|
|
60
|
+
// in flight and supervision has gone quiet, and then the teardown
|
|
61
|
+
// proceeds exactly as it would have. It never blocks and never changes
|
|
62
|
+
// an exit code (criteria 10 and 11).
|
|
63
|
+
warnIfWatcherStale(fleet);
|
|
64
|
+
const result = await teardownTask(fleet, {
|
|
65
|
+
taskId: flags.task,
|
|
66
|
+
salvage: flags.salvage,
|
|
67
|
+
});
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
// Plan step 5: "every refusal is exit nonzero plus a single reason
|
|
70
|
+
// line". Enforced here structurally (CR-303) rather than trusted of
|
|
71
|
+
// every reason string, because reasons interpolate git output and a
|
|
72
|
+
// raised error message, neither of which is guaranteed to be short.
|
|
73
|
+
process.stderr.write(`tiphys teardown: ${singleLine(result.reason)}\n`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const suffix = result.value.salvaged ? " (leavings salvaged and pushed)" : "";
|
|
77
|
+
process.stdout.write(`torn down ${result.value.taskId}${suffix}\n`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys tuition add|list|index` (kernel plan M3, M3-P8 steps 2 and 6;
|
|
3
|
+
* R-091).
|
|
4
|
+
*
|
|
5
|
+
* add --file <entry> [--into <dir>] validate an entry and file it
|
|
6
|
+
* list [--kernel-relevant] [--dir <d>] one line per entry
|
|
7
|
+
* index [--out <f>] [--check] [--dir <d>] project the mechanism index
|
|
8
|
+
*
|
|
9
|
+
* PROMOTION IS NOT A SUBCOMMAND, and that is a decision rather than an
|
|
10
|
+
* omission. R-091 says kernel-relevant tuition ships upstream as a kernel pull
|
|
11
|
+
* request; the kernel never opens pull requests, so promotion is a documented
|
|
12
|
+
* orchestrator procedure (M3-P9's `AGENTS.md`) and building a promoter that M3
|
|
13
|
+
* would use once is the M1-P3 mistake.
|
|
14
|
+
*
|
|
15
|
+
* `add` WRITES WITH AN EXCLUSIVE CREATE, so a second entry claiming an id that
|
|
16
|
+
* already exists fails loudly and names the file rather than overwriting a
|
|
17
|
+
* record. A `T-nnn` id is never renumbered and never reused after retirement,
|
|
18
|
+
* which makes an overwrite a silent loss of exactly the artifact this feed
|
|
19
|
+
* exists to keep.
|
|
20
|
+
*/
|
|
21
|
+
export declare function cmdTuition(argv: string[]): number;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys tuition add|list|index` (kernel plan M3, M3-P8 steps 2 and 6;
|
|
3
|
+
* R-091).
|
|
4
|
+
*
|
|
5
|
+
* add --file <entry> [--into <dir>] validate an entry and file it
|
|
6
|
+
* list [--kernel-relevant] [--dir <d>] one line per entry
|
|
7
|
+
* index [--out <f>] [--check] [--dir <d>] project the mechanism index
|
|
8
|
+
*
|
|
9
|
+
* PROMOTION IS NOT A SUBCOMMAND, and that is a decision rather than an
|
|
10
|
+
* omission. R-091 says kernel-relevant tuition ships upstream as a kernel pull
|
|
11
|
+
* request; the kernel never opens pull requests, so promotion is a documented
|
|
12
|
+
* orchestrator procedure (M3-P9's `AGENTS.md`) and building a promoter that M3
|
|
13
|
+
* would use once is the M1-P3 mistake.
|
|
14
|
+
*
|
|
15
|
+
* `add` WRITES WITH AN EXCLUSIVE CREATE, so a second entry claiming an id that
|
|
16
|
+
* already exists fails loudly and names the file rather than overwriting a
|
|
17
|
+
* record. A `T-nnn` id is never renumbered and never reused after retirement,
|
|
18
|
+
* which makes an overwrite a silent loss of exactly the artifact this feed
|
|
19
|
+
* exists to keep.
|
|
20
|
+
*/
|
|
21
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { basename, join } from "node:path";
|
|
23
|
+
import { EX_USAGE } from "../cli.js";
|
|
24
|
+
import { refuseOpenForWrite } from "../task.js";
|
|
25
|
+
import { decodeDocument, readOperatorPath } from "../validate.js";
|
|
26
|
+
import { MECHANISM_INDEX_FILE, driftLines, listEntryFiles, loadEntry, projectIndex, renderIndex, } from "../tuition.js";
|
|
27
|
+
const USAGE = "usage: tiphys tuition add --file <entry> [--into <dir>] | " +
|
|
28
|
+
"tiphys tuition list [--kernel-relevant] [--dir <dir>] | " +
|
|
29
|
+
"tiphys tuition index [--out <file>] [--check] [--dir <dir>]";
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const options = { check: false, kernelRelevant: false };
|
|
32
|
+
const valued = new Map([
|
|
33
|
+
["--file", "file"],
|
|
34
|
+
["--into", "into"],
|
|
35
|
+
["--dir", "dir"],
|
|
36
|
+
["--out", "out"],
|
|
37
|
+
]);
|
|
38
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
39
|
+
const argument = argv[index];
|
|
40
|
+
if (argument === "--check") {
|
|
41
|
+
options.check = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (argument === "--kernel-relevant") {
|
|
45
|
+
options.kernelRelevant = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const field = valued.get(argument);
|
|
49
|
+
if (field === undefined) {
|
|
50
|
+
return { usageError: `unknown option ${argument}` };
|
|
51
|
+
}
|
|
52
|
+
const value = argv[index + 1];
|
|
53
|
+
if (value === undefined || value.startsWith("--")) {
|
|
54
|
+
return { usageError: `${argument} requires a value` };
|
|
55
|
+
}
|
|
56
|
+
options[field] = value;
|
|
57
|
+
index += 1;
|
|
58
|
+
}
|
|
59
|
+
return { options };
|
|
60
|
+
}
|
|
61
|
+
/** The feed directory a subcommand works in: `--dir`, else `<cwd>/tuition`. */
|
|
62
|
+
function feedDirectory(options) {
|
|
63
|
+
return options.dir ?? join(process.cwd(), "tuition");
|
|
64
|
+
}
|
|
65
|
+
/** Load every entry in the feed, reporting the first that does not validate. */
|
|
66
|
+
function loadFeed(directory) {
|
|
67
|
+
const listing = listEntryFiles(directory);
|
|
68
|
+
if (!listing.ok) {
|
|
69
|
+
process.stderr.write(`tiphys tuition: ${listing.reason}\n`);
|
|
70
|
+
return { ok: false };
|
|
71
|
+
}
|
|
72
|
+
const entries = [];
|
|
73
|
+
let failed = false;
|
|
74
|
+
for (const path of listing.paths) {
|
|
75
|
+
const loaded = loadEntry(path);
|
|
76
|
+
if (!loaded.ok) {
|
|
77
|
+
process.stderr.write(`tiphys tuition: ${loaded.reason}\n`);
|
|
78
|
+
for (const line of loaded.diagnostics) {
|
|
79
|
+
process.stdout.write(`${line}\n`);
|
|
80
|
+
}
|
|
81
|
+
failed = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
entries.push(loaded.entry);
|
|
85
|
+
}
|
|
86
|
+
return failed ? { ok: false } : { ok: true, entries };
|
|
87
|
+
}
|
|
88
|
+
function cmdAdd(options) {
|
|
89
|
+
if (options.file === undefined) {
|
|
90
|
+
process.stderr.write(`tiphys tuition: --file is required\n${USAGE}\n`);
|
|
91
|
+
return EX_USAGE;
|
|
92
|
+
}
|
|
93
|
+
/* VALIDATE FIRST, WRITE SECOND. An invalid entry must leave the feed
|
|
94
|
+
directory byte-identical, which is criterion 7's own test. */
|
|
95
|
+
const loaded = loadEntry(options.file);
|
|
96
|
+
if (!loaded.ok) {
|
|
97
|
+
process.stderr.write(`tiphys tuition: ${loaded.reason}\n`);
|
|
98
|
+
for (const line of loaded.diagnostics) {
|
|
99
|
+
process.stdout.write(`${line}\n`);
|
|
100
|
+
}
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const directory = options.into ?? join(process.cwd(), "tuition");
|
|
104
|
+
const target = join(directory, `${loaded.entry.id}.yaml`);
|
|
105
|
+
const refusal = refuseOpenForWrite(target);
|
|
106
|
+
if (refusal !== undefined) {
|
|
107
|
+
process.stderr.write(`tiphys tuition: ${refusal}\n`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
mkdirSync(directory, { recursive: true });
|
|
112
|
+
/* EXCLUSIVE CREATE: an id already in the feed is a loud refusal, never an
|
|
113
|
+
overwrite. Ids are never reused, so the collision is the fault. */
|
|
114
|
+
writeFileSync(target, loaded.body, { flag: "wx" });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const code = error.code;
|
|
118
|
+
process.stderr.write(code === "EEXIST"
|
|
119
|
+
? `tiphys tuition: ${target} already exists, and a tuition id is never reused\n`
|
|
120
|
+
: `tiphys tuition: ${target} could not be written: ${String(error)}\n`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
process.stdout.write(`added ${loaded.entry.id} to ${target}\n`);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
function cmdList(options) {
|
|
127
|
+
const feed = loadFeed(feedDirectory(options));
|
|
128
|
+
if (!feed.ok) {
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
for (const entry of feed.entries) {
|
|
132
|
+
if (options.kernelRelevant && !entry["kernel-relevant"]) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const targets = entry["structural-consequence"]?.length ?? 0;
|
|
136
|
+
process.stdout.write(`${entry.id} ${entry.date} targets=${String(targets)}\n`);
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
function cmdIndex(options) {
|
|
141
|
+
const directory = feedDirectory(options);
|
|
142
|
+
const feed = loadFeed(directory);
|
|
143
|
+
if (!feed.ok) {
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
const projection = projectIndex(feed.entries);
|
|
147
|
+
if (!projection.ok) {
|
|
148
|
+
process.stderr.write(`tiphys tuition: ${projection.reason}\n`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
const rendered = renderIndex(projection.rows);
|
|
152
|
+
const out = options.out ?? join(directory, MECHANISM_INDEX_FILE);
|
|
153
|
+
if (options.check) {
|
|
154
|
+
const read = readOperatorPath(out);
|
|
155
|
+
if (!read.ok) {
|
|
156
|
+
process.stderr.write(`tiphys tuition: ${read.reason}\n`);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
const decoded = decodeDocument(read.body, out);
|
|
160
|
+
if (!decoded.ok) {
|
|
161
|
+
process.stderr.write(`tiphys tuition: ${decoded.reason}\n`);
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
const problems = driftLines(decoded.value, projection.rows);
|
|
165
|
+
for (const problem of problems) {
|
|
166
|
+
process.stdout.write(`DRIFT ${problem}\n`);
|
|
167
|
+
}
|
|
168
|
+
if (problems.length > 0) {
|
|
169
|
+
process.stderr.write(`tiphys tuition: ${basename(out)} is not the projection of the feed in ${directory}\n`);
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
/* THE BYTE COMPARISON IS SEPARATE AND SECOND. The structural diff above
|
|
173
|
+
names WHICH mechanism drifted, which a byte comparison cannot; this
|
|
174
|
+
catches a hand edit that changed nothing a reader of the decoded
|
|
175
|
+
document would see (a reordering, a rewrap, an added comment) and says
|
|
176
|
+
so in those words rather than claiming a rule changed. */
|
|
177
|
+
if (read.body !== rendered) {
|
|
178
|
+
process.stdout.write(`DRIFT ${out} decodes to the projection and its bytes differ from it, so it has been hand-edited\n`);
|
|
179
|
+
process.stderr.write(`tiphys tuition: ${basename(out)} is generated; regenerate it with tiphys tuition index\n`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
process.stdout.write(`${String(projection.rows.length)} mechanism(s) projected from ${String(feed.entries.length)} entr(ies); the committed index matches\n`);
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
const refusal = refuseOpenForWrite(out);
|
|
186
|
+
if (refusal !== undefined) {
|
|
187
|
+
process.stderr.write(`tiphys tuition: ${refusal}\n`);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync(out, rendered);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
process.stderr.write(`tiphys tuition: ${out} could not be written: ${String(error)}\n`);
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
process.stdout.write(`wrote ${String(projection.rows.length)} mechanism(s) from ${String(feed.entries.length)} entr(ies) to ${out}\n`);
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
export function cmdTuition(argv) {
|
|
201
|
+
const [subcommand, ...rest] = argv;
|
|
202
|
+
const parsed = parseArgs(rest);
|
|
203
|
+
if (parsed.options === undefined) {
|
|
204
|
+
process.stderr.write(`tiphys tuition: ${parsed.usageError ?? "usage error"}\n${USAGE}\n`);
|
|
205
|
+
return EX_USAGE;
|
|
206
|
+
}
|
|
207
|
+
if (subcommand === "add") {
|
|
208
|
+
return cmdAdd(parsed.options);
|
|
209
|
+
}
|
|
210
|
+
if (subcommand === "list") {
|
|
211
|
+
return cmdList(parsed.options);
|
|
212
|
+
}
|
|
213
|
+
if (subcommand === "index") {
|
|
214
|
+
return cmdIndex(parsed.options);
|
|
215
|
+
}
|
|
216
|
+
process.stderr.write(`tiphys tuition: ${subcommand === undefined ? "a subcommand is required" : `unknown subcommand ${subcommand}`}\n${USAGE}\n`);
|
|
217
|
+
return EX_USAGE;
|
|
218
|
+
}
|