@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,307 @@
1
+ import { existsSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /* IMPORTED UNDER THE OLD NAMES rather than re-exported straight through,
4
+ because this module CALLS them as well as publishing them. A bare
5
+ `export ... from` publishes without binding, so the internal call sites below
6
+ would not resolve and the only signal would be a build error. */
7
+ import { classifyPathEntry as classifyEntry, readRegularPathIfPresent as readRegularFileIfPresent, refuseOpenPathForWrite as refuseOpenForWrite, } from "./fleet.js";
8
+ import { leaseStatus } from "./lock.js";
9
+ export { classifyEntry, readRegularFileIfPresent, refuseOpenForWrite };
10
+ export const TASK_SHAPES = ["ship", "scout"];
11
+ export function taskDir(fleet, taskId) {
12
+ return join(fleet.tasksDir, taskId);
13
+ }
14
+ export function metaPath(fleet, taskId) {
15
+ return join(taskDir(fleet, taskId), "meta.json");
16
+ }
17
+ export function briefPath(fleet, taskId) {
18
+ return join(taskDir(fleet, taskId), "brief.md");
19
+ }
20
+ export function turnEndPath(fleet, taskId) {
21
+ return join(taskDir(fleet, taskId), "turn-end");
22
+ }
23
+ export function executorRecordPath(fleet, taskId) {
24
+ return join(taskDir(fleet, taskId), "executor.json");
25
+ }
26
+ export function reportPath(fleet, taskId) {
27
+ return join(taskDir(fleet, taskId), "report.md");
28
+ }
29
+ /** Serialize meta the way every kernel JSON state file is written. */
30
+ export function renderTaskMeta(meta) {
31
+ return `${JSON.stringify(meta, null, 2)}\n`;
32
+ }
33
+ export function writeTaskMeta(fleet, meta) {
34
+ writeFileSync(metaPath(fleet, meta.id), renderTaskMeta(meta));
35
+ }
36
+ /** Which required field first failed, or undefined when all of them hold. */
37
+ function firstBadMetaField(candidate) {
38
+ if (typeof candidate.id !== "string")
39
+ return "id (a string)";
40
+ if (typeof candidate.project !== "string")
41
+ return "project (a string)";
42
+ if (candidate.shape !== "ship" && candidate.shape !== "scout") {
43
+ return 'shape (either "ship" or "scout")';
44
+ }
45
+ if (typeof candidate.branch !== "string")
46
+ return "branch (a string)";
47
+ if (typeof candidate.worktree !== "string")
48
+ return "worktree (a string)";
49
+ if (typeof candidate.baseSha !== "string")
50
+ return "baseSha (a string)";
51
+ if (typeof candidate.baseOffline !== "boolean")
52
+ return "baseOffline (a boolean)";
53
+ if (candidate.status !== "open" && candidate.status !== "closed") {
54
+ return 'status (either "open" or "closed")';
55
+ }
56
+ if (typeof candidate.createdAt !== "string")
57
+ return "createdAt (a string)";
58
+ return undefined;
59
+ }
60
+ /**
61
+ * THE ONE READ of a task record, and the one that says WHICH of the four
62
+ * outcomes happened.
63
+ *
64
+ * The type probe is INSIDE this function and not in front of one of its
65
+ * callers (CR-520, CR-521). There is exactly one implementation of "read a
66
+ * task record", every caller of it is protected, and adding a caller
67
+ * cannot reopen the hole: src/teardown.ts reaches this directly, without
68
+ * going through the liveness classifier, and a named pipe here used to
69
+ * hang it forever.
70
+ *
71
+ * WHY THIS IS A SEPARATE FUNCTION FROM `readTaskMeta` RATHER THAN ITS NEW
72
+ * SIGNATURE, declared rather than left to be inferred. Widening the return
73
+ * type of `readTaskMeta` is the shape this repair wants, and it is a
74
+ * compile-time break in four modules that this fix round's file list does not
75
+ * own (src/pool.ts, src/liveness.ts, src/teardown.ts, src/commands/next.ts).
76
+ * The distinction is therefore made AVAILABLE upstream of all of them here,
77
+ * `readTaskMeta` is documented as the deliberate NARROWING of it, and the
78
+ * sites that still collapse are named in the fix round's work history rather
79
+ * than quietly left.
80
+ */
81
+ export function classifyTaskMeta(fleet, taskId) {
82
+ const path = metaPath(fleet, taskId);
83
+ const read = readRegularFileIfPresent(path);
84
+ if (read.kind === "absent") {
85
+ return { kind: "absent" };
86
+ }
87
+ if (read.kind === "refused") {
88
+ return { kind: "unreadable", reason: read.reason };
89
+ }
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(read.body);
93
+ }
94
+ catch (error) {
95
+ const detail = error instanceof Error ? error.message : String(error);
96
+ return {
97
+ kind: "unparsable",
98
+ reason: `${path} is present (${read.body.length} byte(s)) and does not parse as ` +
99
+ `JSON (${detail}), which is what a task record killed mid-write looks ` +
100
+ `like; it is not evidence that there is no task here`,
101
+ };
102
+ }
103
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
104
+ return {
105
+ kind: "malformed",
106
+ reason: `${path} parses as JSON but not as an object, so it is not a task record`,
107
+ };
108
+ }
109
+ const candidate = parsed;
110
+ const bad = firstBadMetaField(candidate);
111
+ if (bad !== undefined) {
112
+ return {
113
+ kind: "malformed",
114
+ reason: `${path} parses as JSON but is not a task record: it needs ${bad}; it is ` +
115
+ `not evidence that there is no task here`,
116
+ };
117
+ }
118
+ return { kind: "read", meta: candidate };
119
+ }
120
+ /**
121
+ * Read meta.json, or undefined when it is absent, is not a regular file,
122
+ * does not parse, or parses and is not a task record.
123
+ *
124
+ * THIS IS A DELIBERATE NARROWING OF `classifyTaskMeta` AND THE COLLAPSE IS THE
125
+ * WHOLE OF CR-F02. `undefined` answers "is there a readable record" and it
126
+ * cannot answer "is there a task here", because it is returned both when the
127
+ * kernel looked and found nothing and when it found something it could not
128
+ * read. A caller that reports an absence, skips an entry, or decides nothing
129
+ * is in flight must call `classifyTaskMeta` instead; a caller that REFUSES on
130
+ * every one of the four (teardown does) loses nothing by using this.
131
+ */
132
+ export function readTaskMeta(fleet, taskId) {
133
+ const read = classifyTaskMeta(fleet, taskId);
134
+ return read.kind === "read" ? read.meta : undefined;
135
+ }
136
+ /** Set meta.json status (teardown's last step; C-1's state authority). */
137
+ export function setTaskStatus(fleet, meta, status) {
138
+ writeTaskMeta(fleet, { ...meta, status });
139
+ }
140
+ /**
141
+ * Holdership guard for the task-mutating commands (PR-203), shared by
142
+ * spawn and teardown so there is exactly one implementation of the rule.
143
+ * It lives here rather than in a guard module of its own because this
144
+ * phase's files-to-touch list has no shared-guard file and the rule is a
145
+ * precondition of every task mutation.
146
+ *
147
+ * The transport is M1-P3's, unchanged and not re-invented: lock acquire
148
+ * PRINTS the opaque holderId, the operator carries it, and here it
149
+ * arrives as the TIPHYS_HOLDER_ID environment variable. No second
150
+ * identity mechanism exists, and nothing about the holder is derived
151
+ * from the running program (plan constraint C-2): this reads the lease
152
+ * FILE and nothing else.
153
+ *
154
+ * With no lease file present the command proceeds (the plan's M1 test
155
+ * contexts). A lease that exists but is unreadable, expired, or held by
156
+ * a different holder refuses: fail closed, because every one of those
157
+ * states means this caller cannot prove it is the one orchestrator.
158
+ */
159
+ export function checkHoldership(fleet) {
160
+ const status = leaseStatus(fleet.lockPath);
161
+ if (status.state === "free") {
162
+ return { ok: true };
163
+ }
164
+ if (status.state === "corrupt") {
165
+ return {
166
+ ok: false,
167
+ reason: `lease file ${fleet.lockPath} exists but does not parse; refusing without provable holdership`,
168
+ };
169
+ }
170
+ const holder = process.env.TIPHYS_HOLDER_ID;
171
+ if (status.state === "expired") {
172
+ return {
173
+ ok: false,
174
+ reason: `lease ${fleet.lockPath} expired ${status.lease.expiresAt} (holder ` +
175
+ `${status.lease.holderId}); re-acquire or take over before mutating tasks`,
176
+ };
177
+ }
178
+ if (holder === undefined || holder === "") {
179
+ return {
180
+ ok: false,
181
+ reason: `lease ${fleet.lockPath} is held by ${status.lease.holderId} and ` +
182
+ `TIPHYS_HOLDER_ID is not set; set it to the holder id lock acquire printed`,
183
+ };
184
+ }
185
+ if (holder !== status.lease.holderId) {
186
+ return {
187
+ ok: false,
188
+ reason: `lease ${fleet.lockPath} is held by ${status.lease.holderId}, not by ` +
189
+ `TIPHYS_HOLDER_ID ${holder}`,
190
+ };
191
+ }
192
+ return { ok: true };
193
+ }
194
+ /** True when the task directory already exists (spawn's rollback scope). */
195
+ export function taskDirExists(fleet, taskId) {
196
+ return existsSync(taskDir(fleet, taskId));
197
+ }
198
+ /**
199
+ * True when tasks/<id>/ already holds a previous incarnation of the task
200
+ * id, which spawn refuses (CR-301).
201
+ *
202
+ * The task directory is the DURABLE record: teardown removes the worktree
203
+ * and the pool record but deliberately leaves tasks/<id>/ behind, so the
204
+ * id is free from the pool's point of view and occupied from the task
205
+ * state's. Spawning into it would overwrite the closed task's records,
206
+ * hand the launch-failure rollback files it did not create, and leave the
207
+ * previous incarnation's turn-end file readable while the new
208
+ * incarnation's meta says open, which is a completion that did not happen
209
+ * sitting under the C-1 state authority.
210
+ *
211
+ * A path that exists but is not a directory counts as occupied too: it is
212
+ * not a state this kernel may write into, and refusing costs the operator
213
+ * one rename while guessing could cost the record.
214
+ */
215
+ export function taskDirOccupied(fleet, taskId) {
216
+ const dir = taskDir(fleet, taskId);
217
+ if (!existsSync(dir)) {
218
+ return false;
219
+ }
220
+ try {
221
+ if (!statSync(dir).isDirectory()) {
222
+ return true;
223
+ }
224
+ return readdirSync(dir).length > 0;
225
+ }
226
+ catch {
227
+ // Unreadable is not empty: fail closed.
228
+ return true;
229
+ }
230
+ }
231
+ /**
232
+ * Collapse captured git or error output to ONE line (CR-303). Plan step 5 ends
233
+ * "every refusal is exit nonzero plus a single reason line", and git's
234
+ * own stderr is routinely five lines, so any interpolation of it must be
235
+ * flattened rather than trusted to be short. The M1-P6 harness reads
236
+ * these reason lines as evidence.
237
+ */
238
+ export function singleLine(text) {
239
+ return text
240
+ .split("\n")
241
+ .map((line) => line.trim())
242
+ .filter((line) => line !== "")
243
+ .join("; ");
244
+ }
245
+ /**
246
+ * Run one step that may signal failure by THROWING, and fold a raised
247
+ * error into the same ok/reason shape every other step in spawn and
248
+ * teardown returns (F-1, F-2).
249
+ *
250
+ * This exists because the modules were written as a result type end to
251
+ * end while the Node fs calls underneath them are not: writeFileSync and
252
+ * mkdirSync raise. Every returned failure was handled correctly and every
253
+ * THROWN one walked straight past the handler, out of the command, and
254
+ * onto stderr as a stack trace, taking spawn's rollback and teardown's
255
+ * state update with it. Wrapping is therefore not defensive decoration
256
+ * for a state M1 never reaches: it is the difference between a rollback
257
+ * that runs and an orphaned worktree, and between a task marked closed
258
+ * and a meta.json that lies about a worktree that is already gone.
259
+ *
260
+ * It never swallows: the caller still gets a reason naming the step, and
261
+ * still decides whether to roll back, report a partial failure, or refuse.
262
+ */
263
+ export function runStep(what, step) {
264
+ try {
265
+ return { ok: true, value: step() };
266
+ }
267
+ catch (error) {
268
+ const detail = error instanceof Error ? error.message : String(error);
269
+ return { ok: false, reason: `${what} failed: ${detail}` };
270
+ }
271
+ }
272
+ /**
273
+ * runStep's shape for a step that returns a PROMISE (M4-P2 step 2).
274
+ *
275
+ * This is a PROMOTION, not a new function. It lived module-private in
276
+ * src/watcher.ts, where its own comment said "src/task.ts covers the sync
277
+ * one"; spawn's launch call site needs the same shape now that
278
+ * `ExecutorAdapter.launch` returns a promise, and T-005's one-mechanism
279
+ * rule makes copying it a third time the wrong answer. The promotion was
280
+ * measured before it was made: the two functions were run side by side over
281
+ * six `what`/thrown-error pairs covering an ordinary Error, an Error with an
282
+ * empty message, a thrown string, a thrown object with a custom toString, an
283
+ * Error subclass and a multi-line pair, and their reason strings were
284
+ * compared as buffers. All six were byte-identical, so no watcher message
285
+ * changes on any of those six. The capture is in the M4-P2 work history
286
+ * under `delivery/work-history/m4-p2.md`.
287
+ *
288
+ * WHY A SEPARATE FUNCTION RATHER THAN WIDENING runStep. `runStep` returns
289
+ * `{ok: true, value}` the instant its callback returns, and a callback that
290
+ * returns a promise returns one IMMEDIATELY. So `runStep` over an async
291
+ * step is not merely imprecise: its `ok: true` is a claim that the step
292
+ * SUCCEEDED when nothing has run yet, and a later rejection escapes the
293
+ * result type entirely as an unhandled rejection. That is the exact defect
294
+ * this phase's criterion 4 witness reddens against.
295
+ *
296
+ * It never swallows, on the same terms as the sync one: the caller gets a
297
+ * reason naming the step and still decides what to do about it.
298
+ */
299
+ export async function runStepAsync(what, step) {
300
+ try {
301
+ return { ok: true, value: await step() };
302
+ }
303
+ catch (error) {
304
+ const detail = error instanceof Error ? error.message : String(error);
305
+ return { ok: false, reason: `${what} failed: ${detail}` };
306
+ }
307
+ }
@@ -0,0 +1,39 @@
1
+ import type { Fleet } from "./fleet.ts";
2
+ /** The commit-message prefix every salvaged commit carries (R-081a). */
3
+ export declare const SALVAGE_PREFIX = "WIP-UNREVIEWED (do not treat as reviewed):";
4
+ export type TeardownResult = {
5
+ ok: true;
6
+ value: {
7
+ taskId: string;
8
+ salvaged: boolean;
9
+ };
10
+ } | {
11
+ ok: false;
12
+ reason: string;
13
+ };
14
+ export type Landedness = {
15
+ kind: "landed";
16
+ how: "ancestor" | "squash";
17
+ } | {
18
+ kind: "unlanded";
19
+ } | {
20
+ kind: "inconclusive";
21
+ detail: string;
22
+ };
23
+ /**
24
+ * Is branchRef landed on defaultRef? Both are resolved in contextDir,
25
+ * and defaultRef is always the just-fetched remote-tracking ref.
26
+ */
27
+ export declare function landedness(contextDir: string, branchRef: string, defaultRef: string): Landedness;
28
+ export interface TeardownOptions {
29
+ taskId: string;
30
+ salvage: boolean;
31
+ /**
32
+ * Proceed on a pool record RECONSTRUCTED from tasks/<id>/meta.json and
33
+ * git, when worktrees/<id>.pool.json did not survive a reclaim
34
+ * (M4-P19, M4-D-12). See the FROM-RECONSTRUCTED header below for what
35
+ * this flag does and, more importantly, what it does NOT do.
36
+ */
37
+ fromReconstructed: boolean;
38
+ }
39
+ export declare function teardownTask(fleet: Fleet, options: TeardownOptions): Promise<TeardownResult>;