@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,47 @@
1
+ /** The remote a fleet home pushes to unless told otherwise. */
2
+ export declare const DEFAULT_REMOTE = "origin";
3
+ /**
4
+ * The commit subject, and it carries NO PATHS ON PURPOSE (criterion 6,
5
+ * CLAUDE.md binding convention 7: commit messages carry no AI model or tool
6
+ * names). A message composed from the paths it commits inherits whatever
7
+ * those paths are named, and a fleet's decision records and task directories
8
+ * are named after the things they decide, which routinely includes the
9
+ * harness or the model a decision is about. A count cannot carry a name.
10
+ */
11
+ export declare function syncCommitMessage(count: number): string;
12
+ /** One path git reports as changed, with its two status columns. */
13
+ export interface ChangedPath {
14
+ path: string;
15
+ /** The index column (X). A space means unstaged; `?` means untracked. */
16
+ index: string;
17
+ /** The worktree column (Y). */
18
+ worktree: string;
19
+ }
20
+ /**
21
+ * Parse `git status --porcelain=v1 -z --untracked-files=all`.
22
+ *
23
+ * The NUL form is the only safe one: a path holding a space, a quote or a
24
+ * newline is printed raw here and is C-quoted in the newline form, so the
25
+ * newline form would need an unquoting pass that is its own defect surface.
26
+ * A rename or copy record carries TWO paths, the new one first and the
27
+ * original second, and both are returned: a rename out of the durable half
28
+ * into the ephemeral half must be visible as a change to both names.
29
+ */
30
+ export declare function parsePorcelainStatus(payload: string): ChangedPath[];
31
+ /** The `.gitignore` rule that makes one path ephemeral. */
32
+ export interface IgnoreRule {
33
+ source: string;
34
+ line: string;
35
+ pattern: string;
36
+ }
37
+ /**
38
+ * Parse `git check-ignore --no-index -v -z --stdin`, whose output is a flat
39
+ * NUL-separated stream of four fields per MATCHING path: source, line
40
+ * number, pattern, pathname. Paths that match no rule are absent from the
41
+ * output entirely, which is what makes the result a set of the ephemeral
42
+ * ones rather than a verdict per input.
43
+ */
44
+ export declare function parseCheckIgnore(payload: string): Map<string, IgnoreRule>;
45
+ /** How an ignore rule is named in every line this command prints. */
46
+ export declare function renderRule(rule: IgnoreRule): string;
47
+ export declare function cmdSync(argv: string[]): number;
@@ -0,0 +1,341 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { EX_USAGE } from "../cli.js";
3
+ import { FLEET_SCRATCH_SUFFIXES, isFleetScratchPath, loadFleet } from "../fleet.js";
4
+ import { MACHINE_IDENTITY_EMAIL, MACHINE_IDENTITY_NAME } from "./init.js";
5
+ /**
6
+ * `tiphys sync`: commit the durable half of a fleet home and push it
7
+ * (kernel plan M4, M4-P18).
8
+ *
9
+ * WHAT THIS IS THE EXECUTABLE HALF OF. AGENTS.md's
10
+ * `fleet-state-commit-discipline` clause says durable state is committed and
11
+ * pushed at the moment it changes, and that the PUSH is the half that gets
12
+ * dropped. Until this phase the whole clause was discharged by an agent
13
+ * remembering to run git, which is the shape this repository has recorded
14
+ * three times: a rule that depends on remembering does not survive a busy
15
+ * session, and the answer is a mechanism.
16
+ *
17
+ * THE DURABLE SET IS DERIVED, NEVER LISTED HERE (criterion 2). A second list
18
+ * of ephemeral prefixes inside this file would be a second thing to keep in
19
+ * step with the fleet's own `.gitignore`, and the first divergence would be
20
+ * silent: a prefix added to the ignore set would keep being committed by a
21
+ * command that had never heard of it. So the question "is this path
22
+ * ephemeral" is answered by GIT, against the fleet `.gitignore` that
23
+ * `tiphys init` wrote, and this module holds no prefix list at all.
24
+ *
25
+ * `--no-index` IS THE LOAD-BEARING FLAG AND THE REASON IS THE HAZARD ITSELF.
26
+ * `git check-ignore` without it answers "would git ignore this path", and a
27
+ * TRACKED file is never ignored whatever `.gitignore` says, so it answers 1
28
+ * for exactly the file this command must exclude. The dangerous state is a
29
+ * lease or a scratch file that got tracked once (by a `git add -f`, or by
30
+ * being committed before the prefix was ignored): from then on every `git
31
+ * add -A` re-commits it, and a fleet's lease travels through the remote to
32
+ * environments that must rebuild it rather than restore it. With
33
+ * `--no-index` the answer is about the RULES, which is the question being
34
+ * asked. Measured contract in witness/captures/m4-p18-git-contracts.txt.
35
+ *
36
+ * WHY A STAGED EPHEMERAL PATH IS A REFUSAL AND NOT A SKIP (criterion 4).
37
+ * This command stages the durable paths it enumerated and then commits, and
38
+ * `git commit` commits the INDEX, not the pathspec it was handed. An
39
+ * operator who ran `git add -A` first has already put the lease in the
40
+ * index, so committing at all would commit it. Unstaging on the operator's
41
+ * behalf is a destructive act on work this command did not create, so the
42
+ * refusal names the path and the rule that makes it ephemeral, and commits
43
+ * nothing.
44
+ *
45
+ * SUBSTRATE-NEUTRAL (DR-0007) and C-2 clean: pure git and filesystem, no
46
+ * process probing, no pid, no signal.
47
+ */
48
+ const USAGE = "usage: tiphys sync [--remote <name>]";
49
+ /** The remote a fleet home pushes to unless told otherwise. */
50
+ export const DEFAULT_REMOTE = "origin";
51
+ /**
52
+ * The commit subject, and it carries NO PATHS ON PURPOSE (criterion 6,
53
+ * CLAUDE.md binding convention 7: commit messages carry no AI model or tool
54
+ * names). A message composed from the paths it commits inherits whatever
55
+ * those paths are named, and a fleet's decision records and task directories
56
+ * are named after the things they decide, which routinely includes the
57
+ * harness or the model a decision is about. A count cannot carry a name.
58
+ */
59
+ export function syncCommitMessage(count) {
60
+ return `tiphys sync: ${String(count)} durable path(s)`;
61
+ }
62
+ function runGit(root, args, options = {}) {
63
+ const run = spawnSync("git", ["-C", root, ...args], {
64
+ encoding: "utf8",
65
+ input: options.input,
66
+ env: options.extraEnv === undefined
67
+ ? process.env
68
+ : { ...process.env, ...options.extraEnv },
69
+ });
70
+ return {
71
+ status: run.status,
72
+ stdout: run.stdout ?? "",
73
+ stderr: run.stderr ?? "",
74
+ };
75
+ }
76
+ /**
77
+ * Parse `git status --porcelain=v1 -z --untracked-files=all`.
78
+ *
79
+ * The NUL form is the only safe one: a path holding a space, a quote or a
80
+ * newline is printed raw here and is C-quoted in the newline form, so the
81
+ * newline form would need an unquoting pass that is its own defect surface.
82
+ * A rename or copy record carries TWO paths, the new one first and the
83
+ * original second, and both are returned: a rename out of the durable half
84
+ * into the ephemeral half must be visible as a change to both names.
85
+ */
86
+ export function parsePorcelainStatus(payload) {
87
+ const fields = payload.split("\0");
88
+ const found = [];
89
+ for (let index = 0; index < fields.length; index += 1) {
90
+ const record = fields[index];
91
+ if (record === "") {
92
+ continue;
93
+ }
94
+ const x = record.slice(0, 1);
95
+ const y = record.slice(1, 2);
96
+ const path = record.slice(3);
97
+ found.push({ path, index: x, worktree: y });
98
+ if (x === "R" || x === "C" || y === "R" || y === "C") {
99
+ const original = fields[index + 1];
100
+ index += 1;
101
+ if (original !== undefined && original !== "") {
102
+ found.push({ path: original, index: x, worktree: y });
103
+ }
104
+ }
105
+ }
106
+ return found;
107
+ }
108
+ /**
109
+ * Parse `git check-ignore --no-index -v -z --stdin`, whose output is a flat
110
+ * NUL-separated stream of four fields per MATCHING path: source, line
111
+ * number, pattern, pathname. Paths that match no rule are absent from the
112
+ * output entirely, which is what makes the result a set of the ephemeral
113
+ * ones rather than a verdict per input.
114
+ */
115
+ export function parseCheckIgnore(payload) {
116
+ const fields = payload.split("\0");
117
+ const found = new Map();
118
+ for (let index = 0; index + 3 < fields.length; index += 4) {
119
+ const source = fields[index];
120
+ const line = fields[index + 1];
121
+ const pattern = fields[index + 2];
122
+ const path = fields[index + 3];
123
+ if (path === "") {
124
+ continue;
125
+ }
126
+ found.set(path, { source, line, pattern });
127
+ }
128
+ return found;
129
+ }
130
+ /** How an ignore rule is named in every line this command prints. */
131
+ export function renderRule(rule) {
132
+ return `${rule.source}:${rule.line} ${rule.pattern}`;
133
+ }
134
+ /**
135
+ * Ask git which of these paths the fleet `.gitignore` covers.
136
+ *
137
+ * THE THREE EXIT CODES ARE NOT TWO. 0 means at least one path matched, 1
138
+ * means none did, and 128 means git could not answer at all. Folding 128
139
+ * into "nothing is ephemeral" is how a command reports a clean sync while
140
+ * committing a lease, so it is a refusal here and the exit code is named.
141
+ */
142
+ function classify(root, paths) {
143
+ if (paths.length === 0) {
144
+ return { ok: true, ephemeral: new Map() };
145
+ }
146
+ const run = runGit(root, ["check-ignore", "--no-index", "-v", "-z", "--stdin"], {
147
+ input: `${paths.join("\0")}\0`,
148
+ });
149
+ if (run.status === 0) {
150
+ return { ok: true, ephemeral: parseCheckIgnore(run.stdout) };
151
+ }
152
+ if (run.status === 1) {
153
+ return { ok: true, ephemeral: new Map() };
154
+ }
155
+ return {
156
+ ok: false,
157
+ reason: `git check-ignore exited ${String(run.status)} and could not say which paths are ephemeral, ` +
158
+ `so nothing was committed: ${run.stderr.split("\n")[0] ?? ""}`,
159
+ };
160
+ }
161
+ function parseArgs(argv) {
162
+ let remote = DEFAULT_REMOTE;
163
+ for (let index = 0; index < argv.length; index += 1) {
164
+ const flag = argv[index];
165
+ if (flag !== "--remote") {
166
+ return { usageError: `unknown option ${flag}` };
167
+ }
168
+ const value = argv[index + 1];
169
+ if (value === undefined) {
170
+ return { usageError: "--remote requires a value" };
171
+ }
172
+ remote = value;
173
+ index += 1;
174
+ }
175
+ return { remote };
176
+ }
177
+ export function cmdSync(argv) {
178
+ const parsed = parseArgs(argv);
179
+ if (parsed.remote === undefined) {
180
+ process.stderr.write(`tiphys sync: ${parsed.usageError ?? "usage error"}\n${USAGE}\n`);
181
+ return EX_USAGE;
182
+ }
183
+ const remote = parsed.remote;
184
+ const fleet = loadFleet(process.cwd());
185
+ /* THE REMOTE IS ESTABLISHED BEFORE ANYTHING IS COMMITTED. The discipline
186
+ is commit AND push; a sync that commits and then discovers there is
187
+ nowhere to push has done the half that gets dropped and reported the
188
+ half that does not. */
189
+ const remotes = runGit(fleet.root, ["remote"]);
190
+ if (remotes.status !== 0) {
191
+ process.stderr.write(`tiphys sync: git remote failed in ${fleet.root}: ${remotes.stderr.split("\n")[0] ?? ""}\n`);
192
+ return 1;
193
+ }
194
+ const known = remotes.stdout.split("\n").map((line) => line.trim());
195
+ if (!known.includes(remote)) {
196
+ process.stderr.write(`tiphys sync: ${fleet.root} has no remote named ${remote}, so durable state cannot be pushed; ` +
197
+ `add one with git remote add ${remote} <url>, nothing was committed\n`);
198
+ return 1;
199
+ }
200
+ const status = runGit(fleet.root, [
201
+ "status",
202
+ "--porcelain=v1",
203
+ "-z",
204
+ "--untracked-files=all",
205
+ ]);
206
+ if (status.status !== 0) {
207
+ process.stderr.write(`tiphys sync: git status failed in ${fleet.root}: ${status.stderr.split("\n")[0] ?? ""}\n`);
208
+ return 1;
209
+ }
210
+ const changed = parsePorcelainStatus(status.stdout);
211
+ const classified = classify(fleet.root, [...new Set(changed.map((entry) => entry.path))]);
212
+ if (!classified.ok) {
213
+ process.stderr.write(`tiphys sync: ${classified.reason}\n`);
214
+ return 1;
215
+ }
216
+ const ephemeral = classified.ephemeral;
217
+ /* THE SCRATCH REFUSAL RUNS BEFORE THE STAGED-EPHEMERAL ONE, and the order
218
+ is stated because it is a choice. A fleet carrying both a stray scratch
219
+ file and a staged ephemeral path reports the scratch file and stops; both
220
+ arms refuse and commit nothing, so no path is committed either way, and
221
+ the operator sees one refusal at a time rather than two. It also keeps the
222
+ staged-ephemeral block the last `return 1` before `const durable`, which
223
+ is where witness/sync-staged-lease-refused.json aims its second member. */
224
+ /* THE DENYLIST IS THREE DIRECTORY PREFIXES AND THE QUESTION IT ANSWERS IS
225
+ NARROWER THAN THE ONE THIS COMMAND ASKS.
226
+
227
+ `git check-ignore --no-index` answers "is this path covered by a rule
228
+ someone wrote down". This command reads that as "is this path meant to
229
+ be committed". Those are the same question only for paths someone
230
+ thought to declare, and the fleet ignore set (src/fleet.ts:29) declares
231
+ three directory prefixes and nothing else. Anything the kernel writes
232
+ outside them is DURABLE BY DEFAULT.
233
+
234
+ Two paths the kernel itself creates land there, and neither is fleet
235
+ content: the atomic-rename scratch file src/status.ts:142 writes beside
236
+ its TRACKED target, and the one src/cutover.ts:278 writes at the fleet
237
+ ROOT under a random name. Both are write-then-rename scratch, both are
238
+ left behind by the failure this project's own tuition calls ordinary
239
+ rather than exceptional (a process killed mid-write), and once either is
240
+ committed it is tracked forever by ordinary git semantics.
241
+
242
+ A REFUSAL AND NOT A SILENT EXCLUSION, for the reason criterion 4 already
243
+ gives about a staged ephemeral path: this command must not quietly drop
244
+ a path an operator can see in `git status`. The cost is stated rather
245
+ than discovered: a `tiphys sync` that races a live `tiphys status emit`
246
+ now fails with a named path instead of committing a temp file, and the
247
+ operator re-runs.
248
+
249
+ THE FLEET IGNORE SET IS NOT WIDENED TO DO THIS, and this module still
250
+ holds no copy of it. That constant also drives `EPHEMERAL_DIRS`,
251
+ `DURABLE_DIRS` and the `.gitignore` `tiphys init` writes, so a glob
252
+ added there would become a directory name `tiphys resume` tried to
253
+ rebuild. Nothing stops being synced; this is a second, separately named
254
+ rule about SUFFIXES, declared once in src/fleet.ts and read from there,
255
+ exactly as the ignore question is asked of git rather than answered
256
+ here. */
257
+ const scratch = [...new Set(changed.map((entry) => entry.path))]
258
+ .filter((path) => !ephemeral.has(path))
259
+ .map((path) => ({ path, suffix: isFleetScratchPath(path) }))
260
+ .filter((candidate) => candidate.suffix !== undefined)
261
+ .sort((a, b) => a.path.localeCompare(b.path));
262
+ if (scratch.length > 0) {
263
+ for (const candidate of scratch) {
264
+ process.stderr.write(`tiphys sync: ${candidate.path} ends in ${candidate.suffix}, which is a ` +
265
+ `write-then-rename scratch suffix this kernel uses (${FLEET_SCRATCH_SUFFIXES.join(", ")}), ` +
266
+ `and no fleet .gitignore rule covers it, so committing it would track it forever; ` +
267
+ `remove it and re-run, nothing was committed\n`);
268
+ }
269
+ return 1;
270
+ }
271
+ /* CRITERION 4, and the order matters: every staged ephemeral path is
272
+ reported before anything is staged or committed, so the refusal is a
273
+ statement about the tree as the operator left it. */
274
+ const stagedEphemeral = changed.filter((entry) => ephemeral.has(entry.path) && entry.index !== " " && entry.index !== "?");
275
+ if (stagedEphemeral.length > 0) {
276
+ for (const entry of stagedEphemeral) {
277
+ const rule = ephemeral.get(entry.path);
278
+ process.stderr.write(`tiphys sync: ${entry.path} is staged and is ephemeral by ${renderRule(rule)}; ` +
279
+ `unstage it with git restore --staged -- ${entry.path} and re-run, nothing was committed\n`);
280
+ }
281
+ return 1;
282
+ }
283
+ const durable = [
284
+ ...new Set(changed
285
+ .map((entry) => entry.path)
286
+ .filter((path) => !ephemeral.has(path))),
287
+ ].sort();
288
+ const excluded = [
289
+ ...new Set(changed.map((entry) => entry.path).filter((path) => ephemeral.has(path))),
290
+ ].sort();
291
+ /* The excluded paths are PRINTED WITH THE RULE THAT EXCLUDED THEM. A
292
+ command that silently drops paths is indistinguishable from one that
293
+ never saw them, and the rule reference is what makes the derivation
294
+ observable rather than asserted. */
295
+ for (const path of excluded) {
296
+ process.stdout.write(`EXCLUDED ${path} ${renderRule(ephemeral.get(path))}\n`);
297
+ }
298
+ if (durable.length === 0) {
299
+ process.stdout.write("NOTHING TO COMMIT\n");
300
+ }
301
+ else {
302
+ const added = runGit(fleet.root, ["add", "--", ...durable]);
303
+ if (added.status !== 0) {
304
+ process.stderr.write(`tiphys sync: git add failed in ${fleet.root}: ${added.stderr.split("\n")[0] ?? ""}\n`);
305
+ return 1;
306
+ }
307
+ const committed = runGit(fleet.root, ["commit", "-m", syncCommitMessage(durable.length)], {
308
+ /* The documented deterministic machine identity, command-scoped,
309
+ exactly as the bootstrap commit does it: CI runners have no git
310
+ identity and this must never touch user or global config
311
+ (CLAUDE.md standing warning 5). */
312
+ extraEnv: {
313
+ GIT_AUTHOR_NAME: MACHINE_IDENTITY_NAME,
314
+ GIT_AUTHOR_EMAIL: MACHINE_IDENTITY_EMAIL,
315
+ GIT_COMMITTER_NAME: MACHINE_IDENTITY_NAME,
316
+ GIT_COMMITTER_EMAIL: MACHINE_IDENTITY_EMAIL,
317
+ },
318
+ });
319
+ if (committed.status !== 0) {
320
+ process.stderr.write(`tiphys sync: git commit failed in ${fleet.root}: ${committed.stderr.split("\n")[0] ?? ""}\n`);
321
+ return 1;
322
+ }
323
+ for (const path of durable) {
324
+ process.stdout.write(`COMMITTED ${path}\n`);
325
+ }
326
+ }
327
+ /* THE PUSH ARM IS NOT OPTIONAL AND ITS FAILURE IS NOT A WARNING. The
328
+ failure carries GIT'S OWN first stderr line rather than a message
329
+ composed here: a retry signature or a diagnosis derived from a
330
+ hand-written example is the failure T-003 and CLAUDE.md standing warning
331
+ 10 both record. */
332
+ const pushed = runGit(fleet.root, ["push", remote, "HEAD"]);
333
+ if (pushed.status !== 0) {
334
+ const first = pushed.stderr.split("\n")[0] ?? "";
335
+ process.stderr.write(`tiphys sync: git push to ${remote} exited ${String(pushed.status)}, ` +
336
+ `so the durable state is committed locally and NOT pushed: ${first}\n`);
337
+ return 1;
338
+ }
339
+ process.stdout.write(`PUSHED ${remote}\n`);
340
+ return 0;
341
+ }
@@ -0,0 +1 @@
1
+ export declare function cmdTeardown(args: string[]): Promise<number>;
@@ -0,0 +1,87 @@
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] [--from-reconstructed]";
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 = {
27
+ task: undefined,
28
+ salvage: false,
29
+ fromReconstructed: false,
30
+ };
31
+ for (let i = 0; i < args.length; i += 1) {
32
+ const flag = args[i];
33
+ const value = args[i + 1];
34
+ if (flag === "--task" && value !== undefined) {
35
+ parsed.task = value;
36
+ i += 1;
37
+ }
38
+ else if (flag === "--salvage") {
39
+ parsed.salvage = true;
40
+ }
41
+ else if (flag === "--from-reconstructed") {
42
+ parsed.fromReconstructed = true;
43
+ }
44
+ else {
45
+ return undefined;
46
+ }
47
+ }
48
+ return parsed;
49
+ }
50
+ export async function cmdTeardown(args) {
51
+ const flags = parseFlags(args);
52
+ if (flags === undefined) {
53
+ return usageError();
54
+ }
55
+ if (flags.task === undefined) {
56
+ return usageError("teardown requires --task <id>");
57
+ }
58
+ let fleet;
59
+ try {
60
+ fleet = loadFleet(process.cwd());
61
+ }
62
+ catch (error) {
63
+ process.stderr.write(`tiphys teardown: ${singleLine(error.message)}\n`);
64
+ return 1;
65
+ }
66
+ // Liveness guard (M1-P5 step 2): one stderr warning line when work is
67
+ // in flight and supervision has gone quiet, and then the teardown
68
+ // proceeds exactly as it would have. It never blocks and never changes
69
+ // an exit code (criteria 10 and 11).
70
+ warnIfWatcherStale(fleet);
71
+ const result = await teardownTask(fleet, {
72
+ taskId: flags.task,
73
+ salvage: flags.salvage,
74
+ fromReconstructed: flags.fromReconstructed,
75
+ });
76
+ if (!result.ok) {
77
+ // Plan step 5: "every refusal is exit nonzero plus a single reason
78
+ // line". Enforced here structurally (CR-303) rather than trusted of
79
+ // every reason string, because reasons interpolate git output and a
80
+ // raised error message, neither of which is guaranteed to be short.
81
+ process.stderr.write(`tiphys teardown: ${singleLine(result.reason)}\n`);
82
+ return 1;
83
+ }
84
+ const suffix = result.value.salvaged ? " (leavings salvaged and pushed)" : "";
85
+ process.stdout.write(`torn down ${result.value.taskId}${suffix}\n`);
86
+ return 0;
87
+ }
@@ -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;