@tiphys/kernel 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +663 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +299 -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 +92 -0
- package/dist/src/adapters/load.d.ts +202 -0
- package/dist/src/adapters/load.js +440 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +66 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +1539 -0
- package/dist/src/checks.js +5025 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +68 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +343 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/cutover.d.ts +35 -0
- package/dist/src/commands/cutover.js +448 -0
- package/dist/src/commands/doctor.d.ts +264 -0
- package/dist/src/commands/doctor.js +1546 -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 +177 -0
- package/dist/src/commands/lock.d.ts +34 -0
- package/dist/src/commands/lock.js +340 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -0
- package/dist/src/commands/next.d.ts +130 -0
- package/dist/src/commands/next.js +597 -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 +139 -0
- package/dist/src/commands/resume.d.ts +1 -0
- package/dist/src/commands/resume.js +88 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +195 -0
- package/dist/src/commands/status.d.ts +21 -0
- package/dist/src/commands/status.js +122 -0
- package/dist/src/commands/sync.d.ts +47 -0
- package/dist/src/commands/sync.js +341 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +87 -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 +430 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/cutover.d.ts +584 -0
- package/dist/src/cutover.js +1444 -0
- package/dist/src/exclusion.d.ts +389 -0
- package/dist/src/exclusion.js +843 -0
- package/dist/src/exec/env.d.ts +278 -0
- package/dist/src/exec/env.js +334 -0
- package/dist/src/fleet.d.ts +223 -0
- package/dist/src/fleet.js +298 -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 +1169 -0
- package/dist/src/gates/coverage.d.ts +375 -0
- package/dist/src/gates/coverage.js +836 -0
- package/dist/src/gates/credentials.d.ts +233 -0
- package/dist/src/gates/credentials.js +752 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/gate-classes.d.ts +56 -0
- package/dist/src/gates/gate-classes.js +633 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -0
- package/dist/src/gates/merge-preconditions.d.ts +319 -0
- package/dist/src/gates/merge-preconditions.js +932 -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 +482 -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 +614 -0
- package/dist/src/gates/run.js +1614 -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 +87 -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 +968 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +84 -0
- package/dist/src/hooks.js +125 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.js +30 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +256 -0
- package/dist/src/lock.js +792 -0
- package/dist/src/model-resolution.d.ts +159 -0
- package/dist/src/model-resolution.js +307 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +34 -0
- package/dist/src/path-identity.js +48 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +326 -0
- package/dist/src/pool.js +988 -0
- package/dist/src/roles.d.ts +461 -0
- package/dist/src/roles.js +776 -0
- package/dist/src/spawn.d.ts +482 -0
- package/dist/src/spawn.js +1003 -0
- package/dist/src/status.d.ts +116 -0
- package/dist/src/status.js +148 -0
- package/dist/src/task.d.ts +504 -0
- package/dist/src/task.js +307 -0
- package/dist/src/teardown.d.ts +39 -0
- package/dist/src/teardown.js +422 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +263 -0
- package/dist/src/validate.js +742 -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 +849 -0
- package/dist/src/witness/run.d.ts +299 -0
- package/dist/src/witness/run.js +1373 -0
- package/dist/src/witness/spec.d.ts +270 -0
- package/dist/src/witness/spec.js +475 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +526 -0
- package/gates.manifest.json +257 -0
- package/package.json +72 -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 +463 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +82 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +185 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/cutover-state.schema.json +64 -0
- package/schemas/decision-record.schema.json +88 -0
- package/schemas/executor-record.schema.json +36 -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/model-resolution.schema.json +362 -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 +295 -0
- package/schemas/work-history.schema.json +183 -0
- package/schemas/write-bypass.schema.json +69 -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,125 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { taskDir, turnEndPath } from "./task.js";
|
|
4
|
+
/**
|
|
5
|
+
* Turn-end hook (kernel plan v1, M1-P4 step 3; R-082b, PR-209).
|
|
6
|
+
*
|
|
7
|
+
* The hook is a generated script in the TASK directory (never inside the
|
|
8
|
+
* worktree, FM-059) that the executor invokes when the payload command
|
|
9
|
+
* exits. It writes tasks/<id>/turn-end as JSON:
|
|
10
|
+
*
|
|
11
|
+
* {"endedAt": "<ISO-8601>", "exitCode": <number>}
|
|
12
|
+
*
|
|
13
|
+
* That file is the M1-P5 watcher's wake signal. It is a notification,
|
|
14
|
+
* not the task's state: meta.json status plus this exit code together
|
|
15
|
+
* are the single current-state authority (plan constraint C-1), and a
|
|
16
|
+
* MISSING turn-end file never means success (tuition T-002: an agent
|
|
17
|
+
* that dies quietly leaves no turn-end and a dirty worktree).
|
|
18
|
+
*
|
|
19
|
+
* The hook takes the payload's exit code as its single argument, so any
|
|
20
|
+
* adapter (the M1 subprocess adapter, and the window or cloud-session
|
|
21
|
+
* adapters of the M4 era) can invoke it without knowing anything else
|
|
22
|
+
* about the task. It is written with the .mjs extension so it parses as
|
|
23
|
+
* ESM regardless of the nearest package.json, since it runs inside the
|
|
24
|
+
* fleet home rather than inside this package.
|
|
25
|
+
*/
|
|
26
|
+
/** The generated hook script for a task. */
|
|
27
|
+
export function turnEndHookPath(fleet, taskId) {
|
|
28
|
+
return join(taskDir(fleet, taskId), "turn-end-hook.mjs");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate the hook script. The turn-end path is baked in as a literal,
|
|
32
|
+
* so the hook needs no fleet resolution.
|
|
33
|
+
*
|
|
34
|
+
* THE CHILD-OBSERVED POINTER RECORD (CR-B-001, the half that closes the hole
|
|
35
|
+
* rather than the half that stops mis-asserting it).
|
|
36
|
+
*
|
|
37
|
+
* `observeNames`, when given, is baked in as a literal array and the hook
|
|
38
|
+
* writes an `env` object holding what each of those names ACTUALLY IS in the
|
|
39
|
+
* environment the hook was launched with, or `null` where the name is unset.
|
|
40
|
+
* The kernel compares those against the harness-owned paths it handed over.
|
|
41
|
+
*
|
|
42
|
+
* WHY THIS IS STRONGER THAN AN ADAPTER'S REPORT AND WHY IT IS NOT PROOF.
|
|
43
|
+
* This script is written BY THE KERNEL and, for every adapter that honours
|
|
44
|
+
* the documented contract, runs in the SAME environment as the payload (the
|
|
45
|
+
* built-in adapter spreads the same `request.env` into both spawnSync calls,
|
|
46
|
+
* and M2R-004 edit 4 is the record of why a second unscrubbed launch is
|
|
47
|
+
* itself the leak). Against an adapter that does not invoke this script at
|
|
48
|
+
* all it proves nothing, and the record no longer says otherwise.
|
|
49
|
+
*
|
|
50
|
+
* THE COST SENTENCE THAT STOOD HERE IS WITHDRAWN, BECAUSE IT WAS REFUTED BY
|
|
51
|
+
* MEASUREMENT (CR-F-CRED-001, MEDIUM).
|
|
52
|
+
*
|
|
53
|
+
* It read: an adapter that quietly reverted `HOME` for the payload "has to
|
|
54
|
+
* revert it for the payload and NOT for the hook, which means launching two
|
|
55
|
+
* children with two different environments and is a substantially different
|
|
56
|
+
* act from passing a mutated copy once". Two children is one way to do it and
|
|
57
|
+
* it is not the cheap way. The cheap way is ONE child with the mutated
|
|
58
|
+
* environment plus a single `writeFileSync` of the turn-end path, which is
|
|
59
|
+
* STRICTLY LESS work than the honest path, because the honest path also
|
|
60
|
+
* spawns the hook. The turn-end path is handed to the adapter beside
|
|
61
|
+
* `hookPath`, and this generated script names it as a literal, so an adapter
|
|
62
|
+
* that never runs it can still produce a byte-identical record.
|
|
63
|
+
*
|
|
64
|
+
* WHY A NONCE DOES NOT CLOSE THIS, stated because it is the obvious repair and
|
|
65
|
+
* it was considered and refused rather than overlooked. Baking a per-task
|
|
66
|
+
* secret into this script and requiring it in the record moves the forgery
|
|
67
|
+
* from "know the path" to "read the file", and the adapter is HANDED the path
|
|
68
|
+
* of this file: it runs at the same uid, on the same filesystem, in a
|
|
69
|
+
* directory it must be able to read to invoke the hook at all. A guard whose
|
|
70
|
+
* condition the adversary can satisfy by reading one file is green and
|
|
71
|
+
* worthless, which is this repository's own recorded shape (T-008's
|
|
72
|
+
* postscript, the red-witness rule one level up). No artifact this script can
|
|
73
|
+
* write is unforgeable by a party that can read this script.
|
|
74
|
+
*
|
|
75
|
+
* So the repair is on the RECORD rather than on the check: the value is
|
|
76
|
+
* `turn-end-record`, it names the artifact the values were read from, and
|
|
77
|
+
* `CredentialHandoverRecord` (src/task.ts) says the artifact is
|
|
78
|
+
* adapter-reachable. A dishonest adapter is still not caught here, and that
|
|
79
|
+
* residue is declared at delivery/work-history/m4-p8.md item 3; what changed
|
|
80
|
+
* is that `meta.json` no longer positively asserts a child-side observation
|
|
81
|
+
* that no child made.
|
|
82
|
+
*
|
|
83
|
+
* The hook still reads no environment it was not told to read, and a hook
|
|
84
|
+
* generated with no `observeNames` behaves exactly as it did before.
|
|
85
|
+
*/
|
|
86
|
+
export function renderTurnEndHook(turnEndFile, observeNames) {
|
|
87
|
+
const observing = observeNames !== undefined && observeNames.length > 0;
|
|
88
|
+
const envLines = observing
|
|
89
|
+
? `const observed = {};
|
|
90
|
+
for (const name of ${JSON.stringify([...observeNames])}) {
|
|
91
|
+
const value = process.env[name];
|
|
92
|
+
observed[name] = typeof value === "string" ? value : null;
|
|
93
|
+
}
|
|
94
|
+
record.env = observed;
|
|
95
|
+
`
|
|
96
|
+
: "";
|
|
97
|
+
return `#!/usr/bin/env node
|
|
98
|
+
// Generated by tiphys spawn (kernel plan v1, M1-P4 step 3). Invoked by the
|
|
99
|
+
// executor adapter when the payload command exits, with the payload's exit
|
|
100
|
+
// code as its single argument. Writes the turn-end record the M1-P5 watcher
|
|
101
|
+
// wakes on. Do not edit: spawn regenerates it.
|
|
102
|
+
import { writeFileSync } from "node:fs";
|
|
103
|
+
|
|
104
|
+
const exitCode = Number(process.argv[2]);
|
|
105
|
+
if (!Number.isInteger(exitCode)) {
|
|
106
|
+
process.stderr.write(
|
|
107
|
+
"tiphys turn-end hook: expected one integer exit-code argument\\n",
|
|
108
|
+
);
|
|
109
|
+
process.exit(64);
|
|
110
|
+
}
|
|
111
|
+
const record = { endedAt: new Date().toISOString(), exitCode };
|
|
112
|
+
${envLines}writeFileSync(
|
|
113
|
+
${JSON.stringify(turnEndFile)},
|
|
114
|
+
\`\${JSON.stringify(record, null, 2)}\\n\`,
|
|
115
|
+
);
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
/** Write the hook for a task and return its path. */
|
|
119
|
+
export function writeTurnEndHook(fleet, taskId, observeNames) {
|
|
120
|
+
const path = turnEndHookPath(fleet, taskId);
|
|
121
|
+
writeFileSync(path, renderTurnEndHook(turnEndPath(fleet, taskId), observeNames), {
|
|
122
|
+
mode: 0o755,
|
|
123
|
+
});
|
|
124
|
+
return path;
|
|
125
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE KERNEL'S PUBLIC ENTRY POINT (kernel plan M4, M4-P4 criteria 7 and 8).
|
|
3
|
+
*
|
|
4
|
+
* Until this phase `@tiphys/kernel` declared `bin` and nothing else
|
|
5
|
+
* (package.json:14): no `main`, no `exports`, no `types`, and no
|
|
6
|
+
* `src/index.ts`. A separately published plugin could therefore not import
|
|
7
|
+
* `ExecutorAdapter` at all, which made the executor seam unreachable from
|
|
8
|
+
* outside this repository however well the interface was written.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IS PUBLISHED HERE IS A COMMITMENT AND IS DELIBERATELY SMALL. An
|
|
11
|
+
* `exports` map is a semver promise and a patch release cannot take one back,
|
|
12
|
+
* so this file names the adapter CONTRACT and nothing else. In particular it
|
|
13
|
+
* does not export `spawnTask`, the fleet accessors, the gate runner or
|
|
14
|
+
* anything under `src/exec/`: those are the kernel's internals, they change
|
|
15
|
+
* without notice, and `package.json`'s `exports` map has no wildcard subpath
|
|
16
|
+
* precisely so that they stay unreachable through the package name
|
|
17
|
+
* (criterion 8, whose dangerous state is an `exports` map written
|
|
18
|
+
* `"./*": "./dist/src/*"`, which satisfies criterion 7 and publishes the
|
|
19
|
+
* entire kernel as API).
|
|
20
|
+
*
|
|
21
|
+
* THE TWO VALUES ARE PART OF THE CONTRACT, not a convenience. An adapter
|
|
22
|
+
* author has to answer two questions that types cannot answer at runtime:
|
|
23
|
+
* which names may appear in `requires` (the closed set the kernel checks
|
|
24
|
+
* against, src/spawn.ts's `requirableRequestFields`), and which name is
|
|
25
|
+
* reserved for the built-in adapter (`BUILT_IN_ADAPTER_NAME`, refused by the
|
|
26
|
+
* loader). Publishing the answers is cheaper than publishing a document that
|
|
27
|
+
* drifts from them.
|
|
28
|
+
*/
|
|
29
|
+
export type { ExecutorAdapter, ExecutorRecord, ExecutorRequest, LaunchOutcome, } from "./spawn.ts";
|
|
30
|
+
export { requirableRequestFields } from "./spawn.ts";
|
|
31
|
+
export { BUILT_IN_ADAPTER_NAME } from "./adapters/load.ts";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE KERNEL'S PUBLIC ENTRY POINT (kernel plan M4, M4-P4 criteria 7 and 8).
|
|
3
|
+
*
|
|
4
|
+
* Until this phase `@tiphys/kernel` declared `bin` and nothing else
|
|
5
|
+
* (package.json:14): no `main`, no `exports`, no `types`, and no
|
|
6
|
+
* `src/index.ts`. A separately published plugin could therefore not import
|
|
7
|
+
* `ExecutorAdapter` at all, which made the executor seam unreachable from
|
|
8
|
+
* outside this repository however well the interface was written.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IS PUBLISHED HERE IS A COMMITMENT AND IS DELIBERATELY SMALL. An
|
|
11
|
+
* `exports` map is a semver promise and a patch release cannot take one back,
|
|
12
|
+
* so this file names the adapter CONTRACT and nothing else. In particular it
|
|
13
|
+
* does not export `spawnTask`, the fleet accessors, the gate runner or
|
|
14
|
+
* anything under `src/exec/`: those are the kernel's internals, they change
|
|
15
|
+
* without notice, and `package.json`'s `exports` map has no wildcard subpath
|
|
16
|
+
* precisely so that they stay unreachable through the package name
|
|
17
|
+
* (criterion 8, whose dangerous state is an `exports` map written
|
|
18
|
+
* `"./*": "./dist/src/*"`, which satisfies criterion 7 and publishes the
|
|
19
|
+
* entire kernel as API).
|
|
20
|
+
*
|
|
21
|
+
* THE TWO VALUES ARE PART OF THE CONTRACT, not a convenience. An adapter
|
|
22
|
+
* author has to answer two questions that types cannot answer at runtime:
|
|
23
|
+
* which names may appear in `requires` (the closed set the kernel checks
|
|
24
|
+
* against, src/spawn.ts's `requirableRequestFields`), and which name is
|
|
25
|
+
* reserved for the built-in adapter (`BUILT_IN_ADAPTER_NAME`, refused by the
|
|
26
|
+
* loader). Publishing the answers is cheaper than publishing a document that
|
|
27
|
+
* drifts from them.
|
|
28
|
+
*/
|
|
29
|
+
export { requirableRequestFields } from "./spawn.js";
|
|
30
|
+
export { BUILT_IN_ADAPTER_NAME } from "./adapters/load.js";
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type { Fleet } from "./fleet.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Liveness guard and the one cadence configuration both halves of this
|
|
4
|
+
* phase share (kernel plan v1, M1-P5 step 2; R-079, R-095; PR-009,
|
|
5
|
+
* PR-206; plan constraint C-2).
|
|
6
|
+
*
|
|
7
|
+
* WHAT LIVENESS MEANS HERE. Freshness of the beacon FILE, and nothing
|
|
8
|
+
* else. This module never asks the operating system about any running
|
|
9
|
+
* program, never inspects a process table, and holds no identity of any
|
|
10
|
+
* kind (plan constraint C-2, FM-053). "Supervision is alive" means "a
|
|
11
|
+
* watcher evaluation wrote state/watcher.beacon recently enough", which
|
|
12
|
+
* is true in exactly the same way for a resident watcher and for an
|
|
13
|
+
* external scheduler driving watch --once (DR-0007, PR-206).
|
|
14
|
+
*
|
|
15
|
+
* WHAT THE GUARD IS FOR (R-079). Supervision must never silently
|
|
16
|
+
* disappear while work is in flight. So the predicate has TWO halves and
|
|
17
|
+
* both are load-bearing: work in flight (read from the C-1 state
|
|
18
|
+
* authority, tasks/<id>/meta.json status, never from a log tail) AND a
|
|
19
|
+
* beacon that is not evidence of recent supervision. A fleet with no
|
|
20
|
+
* open task cannot be stale, because there is nothing to supervise; a
|
|
21
|
+
* fleet with open tasks and a silent watcher is exactly the T-002
|
|
22
|
+
* incident shape, and the guard says so.
|
|
23
|
+
*
|
|
24
|
+
* NO HEALTH FROM AN ABSENCE OF EVIDENCE (fix round). Both halves are
|
|
25
|
+
* evaluated so that the QUIET outcome is the one that needs evidence,
|
|
26
|
+
* never the loud one:
|
|
27
|
+
*
|
|
28
|
+
* - in flight counts open tasks AND tasks whose meta.json exists and
|
|
29
|
+
* cannot be read, because an unreadable record is not evidence that
|
|
30
|
+
* a task finished (second reviewer, finding 2);
|
|
31
|
+
* - a beacon that is absent, unparseable, or dated in the FUTURE are
|
|
32
|
+
* one class, no evidence that supervision ran (CR-501);
|
|
33
|
+
* - the freshness threshold is at least the cadence the watcher itself
|
|
34
|
+
* declared in the beacon it wrote, so a differently configured
|
|
35
|
+
* reader cannot call a healthy watcher stale (CR-503).
|
|
36
|
+
*
|
|
37
|
+
* The first draft of this module got each of these the other way round,
|
|
38
|
+
* and every one of them turned into a fleet that reported health it
|
|
39
|
+
* could not support.
|
|
40
|
+
*
|
|
41
|
+
* THE PR-009 INVARIANT, enforced at load. A healthy resident watcher
|
|
42
|
+
* writes the beacon on its heartbeat schedule, which backs off by
|
|
43
|
+
* doubling up to a cap, and it notices that a heartbeat is due only at
|
|
44
|
+
* its next poll tick. The oldest a healthy beacon can therefore be is
|
|
45
|
+
* one backoff cap plus one poll interval. The stale threshold must be
|
|
46
|
+
* strictly greater than that sum or a perfectly healthy idle watcher
|
|
47
|
+
* reads as stale, so loadCadence refuses any configuration that
|
|
48
|
+
* violates it, naming both values. In --once-only operation the same
|
|
49
|
+
* threshold additionally bounds the acceptable external trigger period:
|
|
50
|
+
* a scheduler must fire at least as often as the threshold to keep the
|
|
51
|
+
* fleet fresh (PR-206), which is an operator setting, not something the
|
|
52
|
+
* kernel can enforce.
|
|
53
|
+
*
|
|
54
|
+
* WARN, NEVER BLOCK (blueprint liveness-guard contract). The guard's
|
|
55
|
+
* consumers (spawn, teardown, doctor) print one stderr line containing
|
|
56
|
+
* "watcher stale" and then do exactly what they would have done anyway.
|
|
57
|
+
* A raise inside an advisory must not be able to take down the command it
|
|
58
|
+
* is advising, and this project has already paid twice for raises walking
|
|
59
|
+
* past handlers that only understood returned failures (M1-P4 F-1 and
|
|
60
|
+
* F-2). So guard() classifies rather than propagates: every filesystem
|
|
61
|
+
* read it performs is wrapped, and a raised error becomes "this file is
|
|
62
|
+
* not readable".
|
|
63
|
+
*
|
|
64
|
+
* WHAT THAT SENTENCE DOES NOT SAY, corrected in place (CR-520). An earlier
|
|
65
|
+
* version of this paragraph read "guard() is TOTAL: every filesystem read
|
|
66
|
+
* it performs is wrapped". The wrapping was real and the word TOTAL did
|
|
67
|
+
* not follow from it. Totality against RAISES is not totality against
|
|
68
|
+
* BLOCKS, and open(2) on a named pipe with no writer blocks in the kernel
|
|
69
|
+
* without raising anything. While that sentence stood, a named pipe at
|
|
70
|
+
* state/watcher.beacon hung guard() forever, and with it doctor, spawn,
|
|
71
|
+
* teardown and both watcher entry modes, each producing no output at all.
|
|
72
|
+
* The claim was written without being executed; tuition T-006 is about
|
|
73
|
+
* exactly that habit.
|
|
74
|
+
*
|
|
75
|
+
* What is true now, and only this: guard() reads two kinds of path, task
|
|
76
|
+
* records and the beacon, and BOTH go through readRegularFileIfPresent in
|
|
77
|
+
* src/task.ts, which establishes the type before it opens anything. A
|
|
78
|
+
* non-regular entry at either path is classified as unreadable without
|
|
79
|
+
* being opened, which the guard already treats as "no evidence", so the
|
|
80
|
+
* advisory stays loud instead of stopping. Two syscalls remain between the
|
|
81
|
+
* probe and the open, and that residual is stated at the helper.
|
|
82
|
+
*/
|
|
83
|
+
/** Cadence and freshness, in milliseconds. One authority for both. */
|
|
84
|
+
export interface WatchCadence {
|
|
85
|
+
/** First heartbeat interval; it doubles from here (FM-044). */
|
|
86
|
+
baseIntervalMs: number;
|
|
87
|
+
/** How often a resident watcher re-checks its wake sources. */
|
|
88
|
+
pollIntervalMs: number;
|
|
89
|
+
/** Ceiling the doubling heartbeat interval stops at. */
|
|
90
|
+
backoffCapMs: number;
|
|
91
|
+
/** Beacon age at which supervision counts as stale. */
|
|
92
|
+
staleThresholdMs: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Defaults, in seconds. FM-044 is a calibration starting point, not a
|
|
96
|
+
* measured optimum, and these are chosen so the PR-009 invariant holds
|
|
97
|
+
* with room to spare (900 + 15 = 915 < 1200).
|
|
98
|
+
*/
|
|
99
|
+
export declare const DEFAULT_BASE_INTERVAL_SECONDS = 60;
|
|
100
|
+
export declare const DEFAULT_POLL_INTERVAL_SECONDS = 15;
|
|
101
|
+
export declare const DEFAULT_BACKOFF_CAP_SECONDS = 900;
|
|
102
|
+
export declare const DEFAULT_STALE_THRESHOLD_SECONDS = 1200;
|
|
103
|
+
/**
|
|
104
|
+
* Environment overrides. The watch command also carries --interval,
|
|
105
|
+
* --poll and --backoff-cap flags for a single invocation (the M1-P6
|
|
106
|
+
* harness needs the cadence short); these variables exist because the
|
|
107
|
+
* guard runs inside spawn, teardown and doctor, which have no watch
|
|
108
|
+
* flags, and because a fleet's cadence and its freshness threshold have
|
|
109
|
+
* to agree across all four commands.
|
|
110
|
+
*/
|
|
111
|
+
export declare const ENV_BASE_INTERVAL = "TIPHYS_WATCH_INTERVAL_SECONDS";
|
|
112
|
+
export declare const ENV_POLL_INTERVAL = "TIPHYS_WATCH_POLL_SECONDS";
|
|
113
|
+
export declare const ENV_BACKOFF_CAP = "TIPHYS_WATCH_BACKOFF_CAP_SECONDS";
|
|
114
|
+
export declare const ENV_STALE_THRESHOLD = "TIPHYS_WATCH_STALE_SECONDS";
|
|
115
|
+
/**
|
|
116
|
+
* The PR-009 invariant, enforced wherever a cadence is built (at module
|
|
117
|
+
* load for the process-wide one, and again for every set of watch
|
|
118
|
+
* flags). The message names both sides because the operator has to know
|
|
119
|
+
* which of the two to change.
|
|
120
|
+
*/
|
|
121
|
+
export declare function assertCadenceInvariant(cadence: WatchCadence): void;
|
|
122
|
+
/** Build a cadence from environment overrides, validated. */
|
|
123
|
+
export declare function loadCadence(env: NodeJS.ProcessEnv): WatchCadence;
|
|
124
|
+
/**
|
|
125
|
+
* Apply per-invocation overrides (the watch flags) and re-validate, so
|
|
126
|
+
* a short --backoff-cap or a long one can never quietly break the
|
|
127
|
+
* invariant the guard depends on.
|
|
128
|
+
*/
|
|
129
|
+
export declare function withCadenceOverrides(base: WatchCadence, overrides: Partial<WatchCadence>): WatchCadence;
|
|
130
|
+
/**
|
|
131
|
+
* The process-wide cadence. Building it at module load is what makes
|
|
132
|
+
* criterion 12's second clause real: importing this module under a
|
|
133
|
+
* configuration that violates the invariant FAILS, with both values in
|
|
134
|
+
* the message, instead of running on with a guard that cries wolf.
|
|
135
|
+
*/
|
|
136
|
+
export declare const CADENCE: WatchCadence;
|
|
137
|
+
/** The beacon record (JSON per DR-0006, plan decision D-3). */
|
|
138
|
+
export interface BeaconRecord {
|
|
139
|
+
/** ISO-8601 instant of the evaluation that wrote it. */
|
|
140
|
+
writtenAt: string;
|
|
141
|
+
/** Backoff streak at that moment, so a reader can see the cadence. */
|
|
142
|
+
backoffStreak: number;
|
|
143
|
+
/** The heartbeat interval in force at that moment, milliseconds. */
|
|
144
|
+
intervalMs: number;
|
|
145
|
+
}
|
|
146
|
+
export declare function renderBeacon(record: BeaconRecord): string;
|
|
147
|
+
/**
|
|
148
|
+
* Read the beacon, or undefined when it is absent, is not a regular file,
|
|
149
|
+
* cannot be read, or does not parse. All of those are the same thing to
|
|
150
|
+
* the guard: no evidence that supervision ran. That is the
|
|
151
|
+
* fail-toward-warning direction, and for an advisory that never blocks it
|
|
152
|
+
* is the right one.
|
|
153
|
+
*
|
|
154
|
+
* The type probe is INSIDE this reader (CR-520). It is not enough for
|
|
155
|
+
* judgeBeacon to have lstat'ed the path first: that call establishes
|
|
156
|
+
* PRESENCE, not TYPE, and this reader has a second caller in
|
|
157
|
+
* src/watcher.ts (writeBeacon) which does not lstat at all. A probe in
|
|
158
|
+
* front of one caller protects one caller.
|
|
159
|
+
*/
|
|
160
|
+
export declare function readBeacon(beaconPath: string): BeaconRecord | undefined;
|
|
161
|
+
/** What the guard reports (the plan's shape, M1-P5 step 2). */
|
|
162
|
+
export interface GuardReport {
|
|
163
|
+
/**
|
|
164
|
+
* Work this fleet cannot be shown to be free of: tasks whose meta.json
|
|
165
|
+
* says open, PLUS tasks whose meta.json exists and cannot be read (see
|
|
166
|
+
* surveyTasks).
|
|
167
|
+
*/
|
|
168
|
+
inFlight: number;
|
|
169
|
+
/** How many of inFlight are unreadable records rather than open tasks. */
|
|
170
|
+
unreadable: number;
|
|
171
|
+
/** Beacon age in milliseconds, or undefined when there is no evidence. */
|
|
172
|
+
beaconAgeMs: number | undefined;
|
|
173
|
+
/** Open work exists and supervision has not been witnessed recently. */
|
|
174
|
+
stale: boolean;
|
|
175
|
+
/** One line of detail; the warning text when stale. */
|
|
176
|
+
detail: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* How far a beacon may sit in the FUTURE before it stops counting as
|
|
180
|
+
* evidence. Nothing legitimate puts it there: writeBeacon stamps the
|
|
181
|
+
* current time and only ever bumps by a millisecond to keep the advance
|
|
182
|
+
* strict. A beacon well ahead of the local clock means the clock moved
|
|
183
|
+
* backwards under a running watcher (an NTP step, a suspend and resume,
|
|
184
|
+
* a session resumed on another host), and until it is rewritten from the
|
|
185
|
+
* present it proves nothing about whether supervision is still running.
|
|
186
|
+
* The allowance absorbs ordinary jitter and the monotonic bump.
|
|
187
|
+
*/
|
|
188
|
+
export declare const BEACON_FUTURE_TOLERANCE_MS = 5000;
|
|
189
|
+
export interface TaskSurvey {
|
|
190
|
+
/** Task ids whose meta.json parses and says open, sorted. */
|
|
191
|
+
open: string[];
|
|
192
|
+
/**
|
|
193
|
+
* Task ids where SOMETHING EXISTS at tasks/<id>/meta.json, by any file
|
|
194
|
+
* type, and cannot be read as a task record. Sorted.
|
|
195
|
+
*/
|
|
196
|
+
unreadable: string[];
|
|
197
|
+
/**
|
|
198
|
+
* Things this survey could not establish at all (the tasks directory
|
|
199
|
+
* could not be listed, an entry could not be stat'ed), one reason line
|
|
200
|
+
* each. NEITHER CALLER MAY READ AN EMPTY RESULT AS "no work": the
|
|
201
|
+
* watcher reports these loudly and the guard counts them as in flight,
|
|
202
|
+
* because a survey that did not complete is not evidence of an idle
|
|
203
|
+
* fleet.
|
|
204
|
+
*/
|
|
205
|
+
problems: string[];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* THE ONE CLASSIFIER OF TASK RECORDS. src/watcher.ts and this module both
|
|
209
|
+
* call it, and neither has a second opinion about what a task record is.
|
|
210
|
+
*
|
|
211
|
+
* This exists because the fix round wrote the same classification twice
|
|
212
|
+
* with different conditions, and the copies disagreed: one asked whether
|
|
213
|
+
* the meta path was a regular FILE, the other only that a stat succeeded,
|
|
214
|
+
* so a meta.json that existed as a directory was surfaced by the watcher
|
|
215
|
+
* and reported by the guard as nothing in flight, which is a
|
|
216
|
+
* counterexample to the very property that round declared (delta review
|
|
217
|
+
* NEW-1, and tuition T-005 on rules that fail to propagate). One property
|
|
218
|
+
* gets one implementation; two that agree today drift the moment someone
|
|
219
|
+
* edits one of them.
|
|
220
|
+
*
|
|
221
|
+
* The rules, in one place:
|
|
222
|
+
*
|
|
223
|
+
* - A TASK IS A DIRECTORY under tasks/. The entry is resolved with stat,
|
|
224
|
+
* so a symlink to a real task directory is a task; init's own .gitkeep
|
|
225
|
+
* and any other stray file is not, which is checked by TYPE and never
|
|
226
|
+
* by name.
|
|
227
|
+
* - A record that PARSES is authoritative: its status decides open or
|
|
228
|
+
* closed (plan constraint C-1).
|
|
229
|
+
* - THE RECORD IS PROBED BEFORE IT IS READ, AND THE PROBE IS NOT HERE.
|
|
230
|
+
* Only a path that resolves to a REGULAR FILE is ever opened; anything
|
|
231
|
+
* else that exists there (a directory, a FIFO, a socket, a device node,
|
|
232
|
+
* a symlink resolving to any of those, a dangling symlink) is classified
|
|
233
|
+
* as a record that cannot be read WITHOUT opening it. That ordering is
|
|
234
|
+
* load-bearing: opening a FIFO with no writer blocks in the kernel and
|
|
235
|
+
* is not an exception, so a classifier that read first would hang
|
|
236
|
+
* instead of classifying, and would take the guard's three callers down
|
|
237
|
+
* with it.
|
|
238
|
+
*
|
|
239
|
+
* The ordering is enforced INSIDE readTaskMeta (src/task.ts), not in
|
|
240
|
+
* front of the call below. A fix round put it here instead, which
|
|
241
|
+
* protected this one call site and left every other caller of
|
|
242
|
+
* readTaskMeta exposed; src/teardown.ts is one, and a named pipe at a
|
|
243
|
+
* task record hung teardown forever while this classifier reported the
|
|
244
|
+
* same file cleanly (CR-520, CR-521). The recorded reason for declining
|
|
245
|
+
* to protect the other callers was that it would mean a second reader
|
|
246
|
+
* beside readTaskMeta. That reason was wrong: moving the probe INTO
|
|
247
|
+
* readTaskMeta is still exactly one implementation of "read a task
|
|
248
|
+
* record", and it is the shape now shipped. What this classifier keeps
|
|
249
|
+
* is the finer split it genuinely needs, unreadable versus a survey that
|
|
250
|
+
* did not complete, computed from the same classifyEntry the reader uses.
|
|
251
|
+
* - A regular file that does not parse is likewise unreadable: it exists
|
|
252
|
+
* and it is not evidence that the task finished.
|
|
253
|
+
* - NOTHING at the meta.json path is not a record at all: that is the
|
|
254
|
+
* normal transient shape of a spawn in progress or a rollback residue.
|
|
255
|
+
* - Residual, stated rather than papered over: the probe and the read are
|
|
256
|
+
* two syscalls, so a path that changes type between them could still be
|
|
257
|
+
* opened as something other than a regular file. Nothing in this kernel
|
|
258
|
+
* writes that state. The residual is stated once, at classifyEntry.
|
|
259
|
+
*
|
|
260
|
+
* Total by construction: it never raises, because one of its two callers
|
|
261
|
+
* is an advisory that must not be able to take down the command it is
|
|
262
|
+
* advising. It does not swallow either: what it could not establish comes
|
|
263
|
+
* back in problems, and both callers are required to act on that.
|
|
264
|
+
*/
|
|
265
|
+
export declare function surveyTaskRecords(fleet: Fleet): TaskSurvey;
|
|
266
|
+
/**
|
|
267
|
+
* THE ONE FRESHNESS THRESHOLD. The configured threshold, raised to the
|
|
268
|
+
* cadence the WATCHER ITSELF declared in the beacon it wrote plus one
|
|
269
|
+
* poll interval (CR-503). Every consumer of beacon freshness calls this,
|
|
270
|
+
* so a single run cannot produce two verdicts about one file.
|
|
271
|
+
*/
|
|
272
|
+
export declare function effectiveThresholdMs(beacon: BeaconRecord | undefined, cadence: WatchCadence): number;
|
|
273
|
+
/**
|
|
274
|
+
* What a beacon file is evidence of. THE ONE BEACON VERDICT: the guard
|
|
275
|
+
* and doctor's own beacon check both call this, so they cannot disagree
|
|
276
|
+
* about the same file in the same run (delta review CR-508, where they
|
|
277
|
+
* did). Each caller decides how to PRESENT the verdict; neither decides
|
|
278
|
+
* what it is.
|
|
279
|
+
*/
|
|
280
|
+
export type BeaconVerdict = {
|
|
281
|
+
kind: "absent";
|
|
282
|
+
} | {
|
|
283
|
+
kind: "unreadable";
|
|
284
|
+
} | {
|
|
285
|
+
kind: "ahead";
|
|
286
|
+
aheadMs: number;
|
|
287
|
+
thresholdMs: number;
|
|
288
|
+
} | {
|
|
289
|
+
kind: "fresh";
|
|
290
|
+
ageMs: number;
|
|
291
|
+
thresholdMs: number;
|
|
292
|
+
} | {
|
|
293
|
+
kind: "stale";
|
|
294
|
+
ageMs: number;
|
|
295
|
+
thresholdMs: number;
|
|
296
|
+
};
|
|
297
|
+
export declare function judgeBeacon(beaconPath: string, nowMs?: number, cadence?: WatchCadence): BeaconVerdict;
|
|
298
|
+
/** Counts, for the guard. The classification itself is not repeated. */
|
|
299
|
+
export declare function surveyTasks(fleet: Fleet): {
|
|
300
|
+
open: number;
|
|
301
|
+
unreadable: number;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* The guard predicate (plan step 2). Total: it never raises, whatever
|
|
305
|
+
* the filesystem does, because its callers use it as an advisory.
|
|
306
|
+
*
|
|
307
|
+
* NO HEALTH FROM AN ABSENCE OF EVIDENCE. "Not stale" is only said when
|
|
308
|
+
* this function can point at the evidence for it: either nothing is in
|
|
309
|
+
* flight, or a beacon judged fresh. What counts as work in flight comes
|
|
310
|
+
* from surveyTaskRecords and what a beacon is evidence of comes from
|
|
311
|
+
* judgeBeacon; this function decides neither of those questions itself,
|
|
312
|
+
* so doctor and the guard cannot answer them differently.
|
|
313
|
+
*/
|
|
314
|
+
export declare function guard(fleet: Fleet, nowMs?: number, cadence?: WatchCadence): GuardReport;
|
|
315
|
+
/**
|
|
316
|
+
* The one call site shape spawn, teardown and doctor share: evaluate the
|
|
317
|
+
* guard and, when stale, write exactly one stderr line containing
|
|
318
|
+
* "watcher stale". It returns the report so a caller can use it, and it
|
|
319
|
+
* never changes what the caller does next (warn, never block).
|
|
320
|
+
*/
|
|
321
|
+
export declare function warnIfWatcherStale(fleet: Fleet, write?: (text: string) => void): GuardReport;
|