@tiphys/kernel 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +663 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +299 -0
- package/checklists/clean-room.yaml +325 -0
- package/checklists/env-failure-diagnosis.yaml +68 -0
- package/checklists/flake-playbook.yaml +68 -0
- package/checklists/hazard-review.yaml +144 -0
- package/checklists/plan-review.yaml +103 -0
- package/dist/bin/tiphys.d.ts +2 -0
- package/dist/bin/tiphys.js +92 -0
- package/dist/src/adapters/load.d.ts +202 -0
- package/dist/src/adapters/load.js +440 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +66 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +1539 -0
- package/dist/src/checks.js +5025 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +68 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +343 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/cutover.d.ts +35 -0
- package/dist/src/commands/cutover.js +448 -0
- package/dist/src/commands/doctor.d.ts +264 -0
- package/dist/src/commands/doctor.js +1546 -0
- package/dist/src/commands/gates.d.ts +9 -0
- package/dist/src/commands/gates.js +360 -0
- package/dist/src/commands/init.d.ts +30 -0
- package/dist/src/commands/init.js +177 -0
- package/dist/src/commands/lock.d.ts +34 -0
- package/dist/src/commands/lock.js +340 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -0
- package/dist/src/commands/next.d.ts +130 -0
- package/dist/src/commands/next.js +597 -0
- package/dist/src/commands/plan.d.ts +20 -0
- package/dist/src/commands/plan.js +105 -0
- package/dist/src/commands/pool.d.ts +1 -0
- package/dist/src/commands/pool.js +139 -0
- package/dist/src/commands/resume.d.ts +1 -0
- package/dist/src/commands/resume.js +88 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +195 -0
- package/dist/src/commands/status.d.ts +21 -0
- package/dist/src/commands/status.js +122 -0
- package/dist/src/commands/sync.d.ts +47 -0
- package/dist/src/commands/sync.js +341 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +87 -0
- package/dist/src/commands/tuition.d.ts +21 -0
- package/dist/src/commands/tuition.js +218 -0
- package/dist/src/commands/validate.d.ts +78 -0
- package/dist/src/commands/validate.js +430 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/cutover.d.ts +584 -0
- package/dist/src/cutover.js +1444 -0
- package/dist/src/exclusion.d.ts +389 -0
- package/dist/src/exclusion.js +843 -0
- package/dist/src/exec/env.d.ts +278 -0
- package/dist/src/exec/env.js +334 -0
- package/dist/src/fleet.d.ts +223 -0
- package/dist/src/fleet.js +298 -0
- package/dist/src/gates/adapters/http-json.d.ts +5 -0
- package/dist/src/gates/adapters/http-json.js +283 -0
- package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
- package/dist/src/gates/adapters/migrations-command.js +373 -0
- package/dist/src/gates/citations.d.ts +408 -0
- package/dist/src/gates/citations.js +1169 -0
- package/dist/src/gates/coverage.d.ts +375 -0
- package/dist/src/gates/coverage.js +836 -0
- package/dist/src/gates/credentials.d.ts +233 -0
- package/dist/src/gates/credentials.js +752 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/gate-classes.d.ts +56 -0
- package/dist/src/gates/gate-classes.js +633 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -0
- package/dist/src/gates/merge-preconditions.d.ts +319 -0
- package/dist/src/gates/merge-preconditions.js +932 -0
- package/dist/src/gates/migrations.d.ts +1 -0
- package/dist/src/gates/migrations.js +36 -0
- package/dist/src/gates/pin.d.ts +114 -0
- package/dist/src/gates/pin.js +154 -0
- package/dist/src/gates/red-witness.d.ts +22 -0
- package/dist/src/gates/red-witness.js +482 -0
- package/dist/src/gates/release.d.ts +283 -0
- package/dist/src/gates/release.js +820 -0
- package/dist/src/gates/result.d.ts +116 -0
- package/dist/src/gates/result.js +91 -0
- package/dist/src/gates/run.d.ts +614 -0
- package/dist/src/gates/run.js +1614 -0
- package/dist/src/gates/schemas/citation-config.schema.json +59 -0
- package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
- package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
- package/dist/src/gates/schemas/gate-result.schema.json +160 -0
- package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
- package/dist/src/gates/schemas/release-record.schema.json +119 -0
- package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
- package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
- package/dist/src/gates/scope.d.ts +131 -0
- package/dist/src/gates/scope.js +1018 -0
- package/dist/src/gates/suite.d.ts +217 -0
- package/dist/src/gates/suite.js +968 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +84 -0
- package/dist/src/hooks.js +125 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.js +30 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +256 -0
- package/dist/src/lock.js +792 -0
- package/dist/src/model-resolution.d.ts +159 -0
- package/dist/src/model-resolution.js +307 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +34 -0
- package/dist/src/path-identity.js +48 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +326 -0
- package/dist/src/pool.js +988 -0
- package/dist/src/roles.d.ts +461 -0
- package/dist/src/roles.js +776 -0
- package/dist/src/spawn.d.ts +482 -0
- package/dist/src/spawn.js +1003 -0
- package/dist/src/status.d.ts +116 -0
- package/dist/src/status.js +148 -0
- package/dist/src/task.d.ts +504 -0
- package/dist/src/task.js +307 -0
- package/dist/src/teardown.d.ts +39 -0
- package/dist/src/teardown.js +422 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +263 -0
- package/dist/src/validate.js +742 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.js +38 -0
- package/dist/src/watcher.d.ts +275 -0
- package/dist/src/watcher.js +849 -0
- package/dist/src/witness/run.d.ts +299 -0
- package/dist/src/witness/run.js +1373 -0
- package/dist/src/witness/spec.d.ts +270 -0
- package/dist/src/witness/spec.js +475 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +526 -0
- package/gates.manifest.json +257 -0
- package/package.json +72 -3
- package/role-model-config.yaml +88 -0
- package/roles/README.md +128 -0
- package/roles/_shared-dispatch-contract.md +87 -0
- package/roles/adversarial-plan-reviewer.md +80 -0
- package/roles/clean-room-reviewer.md +140 -0
- package/roles/implementer.md +463 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +82 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +185 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/cutover-state.schema.json +64 -0
- package/schemas/decision-record.schema.json +88 -0
- package/schemas/executor-record.schema.json +36 -0
- package/schemas/final-report.schema.json +90 -0
- package/schemas/finding.schema.json +106 -0
- package/schemas/gate-registry.schema.json +260 -0
- package/schemas/mechanism-index.schema.json +94 -0
- package/schemas/model-resolution.schema.json +362 -0
- package/schemas/plan.schema.json +300 -0
- package/schemas/report.schema.json +579 -0
- package/schemas/role-brief.schema.json +105 -0
- package/schemas/role-model-config.schema.json +90 -0
- package/schemas/status-line.schema.json +40 -0
- package/schemas/tuition.schema.json +191 -0
- package/schemas/verdict.schema.json +295 -0
- package/schemas/work-history.schema.json +183 -0
- package/schemas/write-bypass.schema.json +69 -0
- package/templates/charter.example.yaml +54 -0
- package/templates/decision-record.example.yaml +27 -0
- package/templates/final-report.example.yaml +80 -0
- package/templates/plan.example.yaml +87 -0
- package/templates/report.example.yaml +236 -0
- package/templates/warnings.md +74 -0
- package/templates/work-history.example.yaml +185 -0
- package/tuition/README.md +76 -0
- package/tuition/T-001.yaml +48 -0
- package/tuition/T-002.yaml +51 -0
- package/tuition/T-003.yaml +100 -0
- package/tuition/T-004.yaml +52 -0
- package/tuition/T-005.yaml +72 -0
- package/tuition/T-006.yaml +81 -0
- package/tuition/T-007.yaml +56 -0
- package/tuition/T-008.yaml +111 -0
- package/tuition/T-009.yaml +50 -0
- package/tuition/T-015.yaml +36 -0
- package/tuition/T-016.yaml +36 -0
- package/tuition/T-017.yaml +46 -0
- package/tuition/T-018.yaml +84 -0
- package/tuition/T-021.yaml +40 -0
- package/tuition/T-022.yaml +36 -0
- package/tuition/mechanism-index.yaml +256 -0
package/dist/src/task.js
ADDED
|
@@ -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>;
|