@tiphys/kernel 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/AGENTS.md +663 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +299 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +92 -0
  11. package/dist/src/adapters/load.d.ts +202 -0
  12. package/dist/src/adapters/load.js +440 -0
  13. package/dist/src/brief.d.ts +32 -0
  14. package/dist/src/brief.js +66 -0
  15. package/dist/src/checklists.d.ts +169 -0
  16. package/dist/src/checklists.js +310 -0
  17. package/dist/src/checks.d.ts +1539 -0
  18. package/dist/src/checks.js +5025 -0
  19. package/dist/src/cli.d.ts +9 -0
  20. package/dist/src/cli.js +68 -0
  21. package/dist/src/commands/brief.d.ts +92 -0
  22. package/dist/src/commands/brief.js +343 -0
  23. package/dist/src/commands/checklist.d.ts +42 -0
  24. package/dist/src/commands/checklist.js +168 -0
  25. package/dist/src/commands/cutover.d.ts +35 -0
  26. package/dist/src/commands/cutover.js +448 -0
  27. package/dist/src/commands/doctor.d.ts +264 -0
  28. package/dist/src/commands/doctor.js +1546 -0
  29. package/dist/src/commands/gates.d.ts +9 -0
  30. package/dist/src/commands/gates.js +360 -0
  31. package/dist/src/commands/init.d.ts +30 -0
  32. package/dist/src/commands/init.js +177 -0
  33. package/dist/src/commands/lock.d.ts +34 -0
  34. package/dist/src/commands/lock.js +340 -0
  35. package/dist/src/commands/mode.d.ts +40 -0
  36. package/dist/src/commands/mode.js +134 -0
  37. package/dist/src/commands/next.d.ts +130 -0
  38. package/dist/src/commands/next.js +597 -0
  39. package/dist/src/commands/plan.d.ts +20 -0
  40. package/dist/src/commands/plan.js +105 -0
  41. package/dist/src/commands/pool.d.ts +1 -0
  42. package/dist/src/commands/pool.js +139 -0
  43. package/dist/src/commands/resume.d.ts +1 -0
  44. package/dist/src/commands/resume.js +88 -0
  45. package/dist/src/commands/spawn.d.ts +1 -0
  46. package/dist/src/commands/spawn.js +195 -0
  47. package/dist/src/commands/status.d.ts +21 -0
  48. package/dist/src/commands/status.js +122 -0
  49. package/dist/src/commands/sync.d.ts +47 -0
  50. package/dist/src/commands/sync.js +341 -0
  51. package/dist/src/commands/teardown.d.ts +1 -0
  52. package/dist/src/commands/teardown.js +87 -0
  53. package/dist/src/commands/tuition.d.ts +21 -0
  54. package/dist/src/commands/tuition.js +218 -0
  55. package/dist/src/commands/validate.d.ts +78 -0
  56. package/dist/src/commands/validate.js +430 -0
  57. package/dist/src/commands/watch.d.ts +1 -0
  58. package/dist/src/commands/watch.js +172 -0
  59. package/dist/src/cutover.d.ts +584 -0
  60. package/dist/src/cutover.js +1444 -0
  61. package/dist/src/exclusion.d.ts +389 -0
  62. package/dist/src/exclusion.js +843 -0
  63. package/dist/src/exec/env.d.ts +278 -0
  64. package/dist/src/exec/env.js +334 -0
  65. package/dist/src/fleet.d.ts +223 -0
  66. package/dist/src/fleet.js +298 -0
  67. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  68. package/dist/src/gates/adapters/http-json.js +283 -0
  69. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  70. package/dist/src/gates/adapters/migrations-command.js +373 -0
  71. package/dist/src/gates/citations.d.ts +408 -0
  72. package/dist/src/gates/citations.js +1169 -0
  73. package/dist/src/gates/coverage.d.ts +375 -0
  74. package/dist/src/gates/coverage.js +836 -0
  75. package/dist/src/gates/credentials.d.ts +233 -0
  76. package/dist/src/gates/credentials.js +752 -0
  77. package/dist/src/gates/deploy.d.ts +1 -0
  78. package/dist/src/gates/deploy.js +33 -0
  79. package/dist/src/gates/gate-classes.d.ts +56 -0
  80. package/dist/src/gates/gate-classes.js +633 -0
  81. package/dist/src/gates/manifest.d.ts +99 -0
  82. package/dist/src/gates/manifest.js +208 -0
  83. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  84. package/dist/src/gates/merge-preconditions.js +932 -0
  85. package/dist/src/gates/migrations.d.ts +1 -0
  86. package/dist/src/gates/migrations.js +36 -0
  87. package/dist/src/gates/pin.d.ts +114 -0
  88. package/dist/src/gates/pin.js +154 -0
  89. package/dist/src/gates/red-witness.d.ts +22 -0
  90. package/dist/src/gates/red-witness.js +482 -0
  91. package/dist/src/gates/release.d.ts +283 -0
  92. package/dist/src/gates/release.js +820 -0
  93. package/dist/src/gates/result.d.ts +116 -0
  94. package/dist/src/gates/result.js +91 -0
  95. package/dist/src/gates/run.d.ts +614 -0
  96. package/dist/src/gates/run.js +1614 -0
  97. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  98. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  99. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  100. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  101. package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
  102. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  103. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  104. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  105. package/dist/src/gates/scope.d.ts +131 -0
  106. package/dist/src/gates/scope.js +1018 -0
  107. package/dist/src/gates/suite.d.ts +217 -0
  108. package/dist/src/gates/suite.js +968 -0
  109. package/dist/src/gates/validate.d.ts +121 -0
  110. package/dist/src/gates/validate.js +414 -0
  111. package/dist/src/hooks.d.ts +84 -0
  112. package/dist/src/hooks.js +125 -0
  113. package/dist/src/index.d.ts +31 -0
  114. package/dist/src/index.js +30 -0
  115. package/dist/src/liveness.d.ts +321 -0
  116. package/dist/src/liveness.js +396 -0
  117. package/dist/src/lock.d.ts +256 -0
  118. package/dist/src/lock.js +792 -0
  119. package/dist/src/model-resolution.d.ts +159 -0
  120. package/dist/src/model-resolution.js +307 -0
  121. package/dist/src/modes.d.ts +149 -0
  122. package/dist/src/modes.js +258 -0
  123. package/dist/src/path-identity.d.ts +34 -0
  124. package/dist/src/path-identity.js +48 -0
  125. package/dist/src/plan.d.ts +73 -0
  126. package/dist/src/plan.js +153 -0
  127. package/dist/src/pool.d.ts +326 -0
  128. package/dist/src/pool.js +988 -0
  129. package/dist/src/roles.d.ts +461 -0
  130. package/dist/src/roles.js +776 -0
  131. package/dist/src/spawn.d.ts +482 -0
  132. package/dist/src/spawn.js +1003 -0
  133. package/dist/src/status.d.ts +116 -0
  134. package/dist/src/status.js +148 -0
  135. package/dist/src/task.d.ts +504 -0
  136. package/dist/src/task.js +307 -0
  137. package/dist/src/teardown.d.ts +39 -0
  138. package/dist/src/teardown.js +422 -0
  139. package/dist/src/tuition.d.ts +159 -0
  140. package/dist/src/tuition.js +311 -0
  141. package/dist/src/validate.d.ts +263 -0
  142. package/dist/src/validate.js +742 -0
  143. package/dist/src/version.d.ts +3 -0
  144. package/dist/src/version.js +38 -0
  145. package/dist/src/watcher.d.ts +275 -0
  146. package/dist/src/watcher.js +849 -0
  147. package/dist/src/witness/run.d.ts +299 -0
  148. package/dist/src/witness/run.js +1373 -0
  149. package/dist/src/witness/spec.d.ts +270 -0
  150. package/dist/src/witness/spec.js +475 -0
  151. package/dist/tsconfig.src.tsbuildinfo +1 -0
  152. package/gate-registry.yaml +526 -0
  153. package/gates.manifest.json +257 -0
  154. package/package.json +72 -3
  155. package/role-model-config.yaml +88 -0
  156. package/roles/README.md +128 -0
  157. package/roles/_shared-dispatch-contract.md +87 -0
  158. package/roles/adversarial-plan-reviewer.md +80 -0
  159. package/roles/clean-room-reviewer.md +140 -0
  160. package/roles/implementer.md +463 -0
  161. package/roles/investigator.md +138 -0
  162. package/roles/plan-writer.md +95 -0
  163. package/schemas/README.md +82 -0
  164. package/schemas/assurance-modes.schema.json +264 -0
  165. package/schemas/charter.schema.json +185 -0
  166. package/schemas/checklist.schema.json +114 -0
  167. package/schemas/cutover-state.schema.json +64 -0
  168. package/schemas/decision-record.schema.json +88 -0
  169. package/schemas/executor-record.schema.json +36 -0
  170. package/schemas/final-report.schema.json +90 -0
  171. package/schemas/finding.schema.json +106 -0
  172. package/schemas/gate-registry.schema.json +260 -0
  173. package/schemas/mechanism-index.schema.json +94 -0
  174. package/schemas/model-resolution.schema.json +362 -0
  175. package/schemas/plan.schema.json +300 -0
  176. package/schemas/report.schema.json +579 -0
  177. package/schemas/role-brief.schema.json +105 -0
  178. package/schemas/role-model-config.schema.json +90 -0
  179. package/schemas/status-line.schema.json +40 -0
  180. package/schemas/tuition.schema.json +191 -0
  181. package/schemas/verdict.schema.json +295 -0
  182. package/schemas/work-history.schema.json +183 -0
  183. package/schemas/write-bypass.schema.json +69 -0
  184. package/templates/charter.example.yaml +54 -0
  185. package/templates/decision-record.example.yaml +27 -0
  186. package/templates/final-report.example.yaml +80 -0
  187. package/templates/plan.example.yaml +87 -0
  188. package/templates/report.example.yaml +236 -0
  189. package/templates/warnings.md +74 -0
  190. package/templates/work-history.example.yaml +185 -0
  191. package/tuition/README.md +76 -0
  192. package/tuition/T-001.yaml +48 -0
  193. package/tuition/T-002.yaml +51 -0
  194. package/tuition/T-003.yaml +100 -0
  195. package/tuition/T-004.yaml +52 -0
  196. package/tuition/T-005.yaml +72 -0
  197. package/tuition/T-006.yaml +81 -0
  198. package/tuition/T-007.yaml +56 -0
  199. package/tuition/T-008.yaml +111 -0
  200. package/tuition/T-009.yaml +50 -0
  201. package/tuition/T-015.yaml +36 -0
  202. package/tuition/T-016.yaml +36 -0
  203. package/tuition/T-017.yaml +46 -0
  204. package/tuition/T-018.yaml +84 -0
  205. package/tuition/T-021.yaml +40 -0
  206. package/tuition/T-022.yaml +36 -0
  207. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,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;