@tiphys/kernel 0.0.0 → 0.1.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 +611 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +278 -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 +14 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +59 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +828 -0
- package/dist/src/checks.js +3314 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +57 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +320 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/doctor.d.ts +35 -0
- package/dist/src/commands/doctor.js +605 -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 +128 -0
- package/dist/src/commands/lock.d.ts +1 -0
- package/dist/src/commands/lock.js +229 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -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 +128 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +146 -0
- package/dist/src/commands/status.d.ts +19 -0
- package/dist/src/commands/status.js +120 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +79 -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 +360 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/exec/env.d.ts +128 -0
- package/dist/src/exec/env.js +190 -0
- package/dist/src/fleet.d.ts +51 -0
- package/dist/src/fleet.js +80 -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 +1163 -0
- package/dist/src/gates/coverage.d.ts +284 -0
- package/dist/src/gates/coverage.js +701 -0
- package/dist/src/gates/credentials.d.ts +74 -0
- package/dist/src/gates/credentials.js +533 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -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 +390 -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 +566 -0
- package/dist/src/gates/run.js +1536 -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 +42 -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 +927 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +32 -0
- package/dist/src/hooks.js +62 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +178 -0
- package/dist/src/lock.js +500 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +2 -0
- package/dist/src/path-identity.js +10 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +130 -0
- package/dist/src/pool.js +721 -0
- package/dist/src/roles.d.ts +430 -0
- package/dist/src/roles.js +734 -0
- package/dist/src/spawn.d.ts +177 -0
- package/dist/src/spawn.js +332 -0
- package/dist/src/status.d.ts +91 -0
- package/dist/src/status.js +119 -0
- package/dist/src/task.d.ts +264 -0
- package/dist/src/task.js +305 -0
- package/dist/src/teardown.d.ts +32 -0
- package/dist/src/teardown.js +314 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +230 -0
- package/dist/src/validate.js +732 -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 +859 -0
- package/dist/src/witness/run.d.ts +274 -0
- package/dist/src/witness/run.js +1327 -0
- package/dist/src/witness/spec.d.ts +102 -0
- package/dist/src/witness/spec.js +253 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +390 -0
- package/gates.manifest.json +195 -0
- package/package.json +57 -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 +460 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +81 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +166 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/decision-record.schema.json +88 -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/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 +289 -0
- package/schemas/work-history.schema.json +183 -0
- package/templates/charter.example.yaml +54 -0
- package/templates/decision-record.example.yaml +27 -0
- package/templates/final-report.example.yaml +80 -0
- package/templates/plan.example.yaml +87 -0
- package/templates/report.example.yaml +236 -0
- package/templates/warnings.md +74 -0
- package/templates/work-history.example.yaml +185 -0
- package/tuition/README.md +76 -0
- package/tuition/T-001.yaml +48 -0
- package/tuition/T-002.yaml +51 -0
- package/tuition/T-003.yaml +100 -0
- package/tuition/T-004.yaml +52 -0
- package/tuition/T-005.yaml +72 -0
- package/tuition/T-006.yaml +81 -0
- package/tuition/T-007.yaml +56 -0
- package/tuition/T-008.yaml +111 -0
- package/tuition/T-009.yaml +50 -0
- package/tuition/T-015.yaml +36 -0
- package/tuition/T-016.yaml +36 -0
- package/tuition/T-017.yaml +46 -0
- package/tuition/T-018.yaml +84 -0
- package/tuition/T-021.yaml +40 -0
- package/tuition/T-022.yaml +36 -0
- package/tuition/mechanism-index.yaml +256 -0
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
export declare function teardownTask(fleet: Fleet, options: TeardownOptions): Promise<TeardownResult>;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { MACHINE_IDENTITY_EMAIL, MACHINE_IDENTITY_NAME } from "./commands/init.js";
|
|
4
|
+
import { poolDestroy, readPoolRecord, worktreePath } from "./pool.js";
|
|
5
|
+
import { checkHoldership, metaPath, readTaskMeta, reportPath, runStep, setTaskStatus, singleLine, } from "./task.js";
|
|
6
|
+
/**
|
|
7
|
+
* Local git runner. It is deliberately not the pool's: src/pool.ts is
|
|
8
|
+
* explicitly out of this phase's edit scope, and teardown needs no
|
|
9
|
+
* contention retry, because nothing in M1 runs teardown concurrently
|
|
10
|
+
* with anything else (parallelism is off until M5). The locale is pinned
|
|
11
|
+
* for the same reason the pool pins it: reproducible output.
|
|
12
|
+
*/
|
|
13
|
+
function runGit(cwd, args, extraEnv) {
|
|
14
|
+
const result = spawnSync("git", ["-C", cwd, ...args], {
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C", ...extraEnv },
|
|
17
|
+
});
|
|
18
|
+
if (result.error !== undefined) {
|
|
19
|
+
return { status: null, stdout: "", stderr: String(result.error) };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
status: result.status,
|
|
23
|
+
stdout: result.stdout ?? "",
|
|
24
|
+
stderr: result.stderr ?? "",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The commit-message prefix every salvaged commit carries (R-081a). */
|
|
28
|
+
export const SALVAGE_PREFIX = "WIP-UNREVIEWED (do not treat as reviewed):";
|
|
29
|
+
/**
|
|
30
|
+
* Is branchRef landed on defaultRef? Both are resolved in contextDir,
|
|
31
|
+
* and defaultRef is always the just-fetched remote-tracking ref.
|
|
32
|
+
*/
|
|
33
|
+
export function landedness(contextDir, branchRef, defaultRef) {
|
|
34
|
+
const ancestor = runGit(contextDir, [
|
|
35
|
+
"merge-base",
|
|
36
|
+
"--is-ancestor",
|
|
37
|
+
branchRef,
|
|
38
|
+
defaultRef,
|
|
39
|
+
]);
|
|
40
|
+
if (ancestor.status === 0) {
|
|
41
|
+
return { kind: "landed", how: "ancestor" };
|
|
42
|
+
}
|
|
43
|
+
if (ancestor.status !== 1) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "inconclusive",
|
|
46
|
+
detail: `git merge-base --is-ancestor exited ${String(ancestor.status)}: ${singleLine(ancestor.stderr)}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const defaultTree = runGit(contextDir, ["rev-parse", "--verify", `${defaultRef}^{tree}`]);
|
|
50
|
+
if (defaultTree.status !== 0) {
|
|
51
|
+
return {
|
|
52
|
+
kind: "inconclusive",
|
|
53
|
+
detail: `cannot resolve the tree of ${defaultRef}: ${singleLine(defaultTree.stderr)}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const merged = runGit(contextDir, ["merge-tree", "--write-tree", defaultRef, branchRef]);
|
|
57
|
+
if (merged.status === 1) {
|
|
58
|
+
// Conflicts: merging is not a no-op, so the branch is not landed.
|
|
59
|
+
return { kind: "unlanded" };
|
|
60
|
+
}
|
|
61
|
+
if (merged.status !== 0) {
|
|
62
|
+
return {
|
|
63
|
+
kind: "inconclusive",
|
|
64
|
+
detail: `git merge-tree --write-tree exited ${String(merged.status)}: ${singleLine(merged.stderr)}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const mergedTree = merged.stdout.split("\n")[0]?.trim() ?? "";
|
|
68
|
+
if (mergedTree === "") {
|
|
69
|
+
return {
|
|
70
|
+
kind: "inconclusive",
|
|
71
|
+
detail: "git merge-tree --write-tree produced no tree id",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return mergedTree === defaultTree.stdout.trim()
|
|
75
|
+
? { kind: "landed", how: "squash" }
|
|
76
|
+
: { kind: "unlanded" };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve everything teardown needs, and perform the mandatory fetch.
|
|
80
|
+
* Read-only with respect to the task: the fetch updates only the
|
|
81
|
+
* project's remote-tracking ref.
|
|
82
|
+
*/
|
|
83
|
+
function resolveContext(fleet, taskId) {
|
|
84
|
+
const meta = readTaskMeta(fleet, taskId);
|
|
85
|
+
if (meta === undefined) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: `no readable task meta for task id ${taskId}; teardown needs tasks/${taskId}/meta.json`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const record = readPoolRecord(fleet, taskId);
|
|
92
|
+
if (record === undefined) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: `no readable pool record for task id ${taskId}; teardown needs it for the ` +
|
|
96
|
+
`project remote and default branch, and refuses rather than guessing them`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const worktree = worktreePath(fleet, taskId);
|
|
100
|
+
const defaultRef = `refs/remotes/${record.remote}/${record.branch}`;
|
|
101
|
+
// PR-001: fetch first, always, and judge landed-ness only against this.
|
|
102
|
+
const fetched = runGit(record.project, [
|
|
103
|
+
"fetch",
|
|
104
|
+
record.remote,
|
|
105
|
+
`+refs/heads/${record.branch}:${defaultRef}`,
|
|
106
|
+
]);
|
|
107
|
+
if (fetched.status !== 0) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
reason: `fetch of ${record.remote}/${record.branch} failed, so landed-ness cannot ` +
|
|
111
|
+
`be judged against fresh remote state: ${singleLine(fetched.stderr)}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, value: { meta, record, worktree, defaultRef } };
|
|
115
|
+
}
|
|
116
|
+
/** Uncommitted changes or untracked files in the task worktree. */
|
|
117
|
+
function worktreeDirty(worktree) {
|
|
118
|
+
const status = runGit(worktree, ["status", "--porcelain"]);
|
|
119
|
+
if (status.status !== 0) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason: `cannot verify worktree cleanliness at ${worktree}: ${singleLine(status.stderr)}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return { ok: true, dirty: status.stdout.trim() !== "" };
|
|
126
|
+
}
|
|
127
|
+
/** Commit the leavings under the WIP label and push the branch (R-081a). */
|
|
128
|
+
function salvageLeavings(context) {
|
|
129
|
+
const { worktree, record } = context;
|
|
130
|
+
const added = runGit(worktree, ["add", "-A"]);
|
|
131
|
+
if (added.status !== 0) {
|
|
132
|
+
return { ok: false, reason: `salvage failed at git add: ${singleLine(added.stderr)}` };
|
|
133
|
+
}
|
|
134
|
+
const message = `${SALVAGE_PREFIX} leavings salvaged by tiphys teardown for task ${context.meta.id}`;
|
|
135
|
+
// CI runners have no git identity, and the fleet never reads or writes
|
|
136
|
+
// user or global git configuration (EXT-F-02 option B): the machine
|
|
137
|
+
// identity is set command-scoped, exactly as init's bootstrap commit
|
|
138
|
+
// does it.
|
|
139
|
+
const committed = runGit(worktree, ["commit", "-m", message], {
|
|
140
|
+
GIT_AUTHOR_NAME: MACHINE_IDENTITY_NAME,
|
|
141
|
+
GIT_AUTHOR_EMAIL: MACHINE_IDENTITY_EMAIL,
|
|
142
|
+
GIT_COMMITTER_NAME: MACHINE_IDENTITY_NAME,
|
|
143
|
+
GIT_COMMITTER_EMAIL: MACHINE_IDENTITY_EMAIL,
|
|
144
|
+
});
|
|
145
|
+
if (committed.status !== 0) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: `salvage failed at git commit: ${singleLine(committed.stderr) || singleLine(committed.stdout)}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const pushed = runGit(record.project, [
|
|
152
|
+
"push",
|
|
153
|
+
record.remote,
|
|
154
|
+
`refs/heads/${record.branchName}:refs/heads/${record.branchName}`,
|
|
155
|
+
]);
|
|
156
|
+
if (pushed.status !== 0) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: `salvage committed the leavings as "${SALVAGE_PREFIX} ..." but the push of ` +
|
|
160
|
+
`${record.branchName} failed: ${singleLine(pushed.stderr)}; the commit is local only`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { ok: true };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Remove the worktree and close the task. Never called before every
|
|
167
|
+
* refusal rule has passed.
|
|
168
|
+
*/
|
|
169
|
+
async function finish(fleet, context, options) {
|
|
170
|
+
const destroyed = await poolDestroy(fleet, {
|
|
171
|
+
taskId: context.meta.id,
|
|
172
|
+
discard: options.discard,
|
|
173
|
+
deleteBranchForce: options.deleteBranchForce,
|
|
174
|
+
});
|
|
175
|
+
if (!destroyed.ok) {
|
|
176
|
+
// The destroy's own reason distinguishes a stage-2 refusal (a true
|
|
177
|
+
// no-op) from a stage-3 partial failure (worktree already removed,
|
|
178
|
+
// survivors enumerated). It is passed through verbatim, and the only
|
|
179
|
+
// thing added is what this layer knows for certain.
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
reason: `pool destroy did not complete: ${destroyed.reason}; task ${context.meta.id} stays open`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// F-1. The worktree is GONE by this point, so a raised write here is
|
|
186
|
+
// not a refusal and must never crash the command: an uncaught throw
|
|
187
|
+
// left meta.json reading "open" beside a worktree that no longer
|
|
188
|
+
// exists, which is the single state authority (C-1) telling a later
|
|
189
|
+
// reader, and the M1-P5 watcher, something false. It is the same
|
|
190
|
+
// partial-failure shape M1-P3 defined for destroy, reported in the
|
|
191
|
+
// same vocabulary, with the manual remedy named.
|
|
192
|
+
const closed = runStep(`marking task ${context.meta.id} closed`, () => {
|
|
193
|
+
setTaskStatus(fleet, context.meta, "closed");
|
|
194
|
+
});
|
|
195
|
+
if (!closed.ok) {
|
|
196
|
+
const removed = destroyed.value.deletedBranch === undefined
|
|
197
|
+
? `worktree ${context.worktree} HAS BEEN REMOVED`
|
|
198
|
+
: `worktree ${context.worktree} HAS BEEN REMOVED and branch ` +
|
|
199
|
+
`${destroyed.value.deletedBranch} was deleted (it was ` +
|
|
200
|
+
`${destroyed.value.deletedSha ?? "unknown"})`;
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
reason: `partial teardown of task id ${context.meta.id}: ${removed}, but ` +
|
|
204
|
+
`${metaPath(fleet, context.meta.id)} could not be marked closed ` +
|
|
205
|
+
`(${closed.reason}); the task record still reads status open although its ` +
|
|
206
|
+
`worktree is gone, so repair that file and set "status": "closed" by hand`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return { ok: true, value: { taskId: context.meta.id, salvaged: options.salvaged } };
|
|
210
|
+
}
|
|
211
|
+
export async function teardownTask(fleet, options) {
|
|
212
|
+
const holdership = checkHoldership(fleet);
|
|
213
|
+
if (!holdership.ok) {
|
|
214
|
+
return { ok: false, reason: holdership.reason };
|
|
215
|
+
}
|
|
216
|
+
const resolved = resolveContext(fleet, options.taskId);
|
|
217
|
+
if (!resolved.ok) {
|
|
218
|
+
return resolved;
|
|
219
|
+
}
|
|
220
|
+
const context = resolved.value;
|
|
221
|
+
const { meta, record, worktree } = context;
|
|
222
|
+
if (meta.shape === "scout") {
|
|
223
|
+
// (a) A scout is judged by its report, never by its scratch tree.
|
|
224
|
+
if (!existsSync(reportPath(fleet, options.taskId))) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
reason: `scout task ${options.taskId} has no report: ${reportPath(fleet, options.taskId)} ` +
|
|
228
|
+
`is absent, and a scout is torn down only once it has reported`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// Scout worktrees are scratch and scouts never push (PR-010), so the
|
|
232
|
+
// dirty tree is discarded. --delete-branch-force is deliberately NOT
|
|
233
|
+
// passed: --discard's plan-defined meaning is the dirty-tree
|
|
234
|
+
// override only, and a scout that committed to its scratch branch is
|
|
235
|
+
// refused rather than having those commits deleted silently, which is
|
|
236
|
+
// exactly the M1-P3 V-1 defect.
|
|
237
|
+
//
|
|
238
|
+
// CR-304: teardown answers that question ITSELF, before calling
|
|
239
|
+
// destroy, because the pool's own refusal advises passing
|
|
240
|
+
// --delete-branch-force, a flag teardown does not accept, so the
|
|
241
|
+
// operator was told to do something impossible through the command
|
|
242
|
+
// that printed it and the task could never reach closed. This
|
|
243
|
+
// refusal is a true no-op and names a route that works; the pool's
|
|
244
|
+
// gate stays behind it as the enforcer.
|
|
245
|
+
const tip = runGit(record.project, [
|
|
246
|
+
"rev-parse",
|
|
247
|
+
"--verify",
|
|
248
|
+
"--quiet",
|
|
249
|
+
`refs/heads/${record.branchName}^{commit}`,
|
|
250
|
+
]);
|
|
251
|
+
if (tip.status === 0 && tip.stdout.trim() !== record.baseSha) {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
reason: `scout task ${options.taskId} has commits on its scratch branch ` +
|
|
255
|
+
`${record.branchName} (tip ${tip.stdout.trim()}, base ${record.baseSha}) and ` +
|
|
256
|
+
`teardown never deletes committed work: copy or push them somewhere ` +
|
|
257
|
+
`durable, then release the branch with "git -C ${record.project} update-ref ` +
|
|
258
|
+
`refs/heads/${record.branchName} ${record.baseSha}" and re-run teardown`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return finish(fleet, context, {
|
|
262
|
+
discard: true,
|
|
263
|
+
deleteBranchForce: false,
|
|
264
|
+
salvaged: false,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// (b) ship.
|
|
268
|
+
const dirty = worktreeDirty(worktree);
|
|
269
|
+
if (!dirty.ok) {
|
|
270
|
+
return { ok: false, reason: dirty.reason };
|
|
271
|
+
}
|
|
272
|
+
if (dirty.dirty && !options.salvage) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
reason: `worktree ${worktree} has uncommitted changes or untracked files; commit ` +
|
|
276
|
+
`or land them first, or pass --salvage to commit them as ` +
|
|
277
|
+
`"${SALVAGE_PREFIX} ..." and push`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const branchRef = `refs/heads/${record.branchName}`;
|
|
281
|
+
const landed = landedness(record.project, branchRef, context.defaultRef);
|
|
282
|
+
if (landed.kind === "inconclusive") {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
reason: `cannot determine whether ${record.branchName} is landed on ` +
|
|
286
|
+
`${record.remote}/${record.branch} (${landed.detail}); refusing`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (landed.kind === "unlanded") {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
reason: `branch ${record.branchName} is not landed on ${record.remote}/${record.branch}; ` +
|
|
293
|
+
`land it before tearing the task down` +
|
|
294
|
+
(options.salvage ? " (--salvage rescues leavings, it never lands work)" : ""),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
let salvaged = false;
|
|
298
|
+
if (dirty.dirty) {
|
|
299
|
+
const rescue = salvageLeavings(context);
|
|
300
|
+
if (!rescue.ok) {
|
|
301
|
+
return rescue;
|
|
302
|
+
}
|
|
303
|
+
salvaged = true;
|
|
304
|
+
}
|
|
305
|
+
// The landed judgement is what authorizes deleting the task branch
|
|
306
|
+
// (V-1's explicit flag, which src/pool.ts documents as coming from
|
|
307
|
+
// exactly here). After a salvage the branch carries the WIP commit,
|
|
308
|
+
// which is on the remote, so the local branch is safe to remove.
|
|
309
|
+
return finish(fleet, context, {
|
|
310
|
+
discard: false,
|
|
311
|
+
deleteBranchForce: true,
|
|
312
|
+
salvaged,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE TUITION FEED AND THE MECHANISM INDEX (kernel plan M3, M3-P8; R-091).
|
|
3
|
+
*
|
|
4
|
+
* A tuition entry records one failure mode a delivery paid for. The MECHANISM
|
|
5
|
+
* INDEX is a PROJECTION of the `mechanisms[]` field of every entry in the
|
|
6
|
+
* feed, keyed by mechanism, and it is generated rather than authored. T-005 is
|
|
7
|
+
* why both halves exist and why they are one artifact rather than two: a rule
|
|
8
|
+
* M1-P3 paid for did not reach M1-P5, which reimplemented the same claim-file
|
|
9
|
+
* mechanism silently and produced the most severe defect found in M1, and a
|
|
10
|
+
* second hand-maintained copy of a rule is the state that produced it.
|
|
11
|
+
*
|
|
12
|
+
* THE TWO LAYERS ARE STRUCTURALLY DISTINCT, which is the plan's compaction
|
|
13
|
+
* model (step 2c). The index is the READ layer: dense, consulted at every
|
|
14
|
+
* dispatch under the `mechanism-lookup` obligation. The entries are the
|
|
15
|
+
* ARCHIVE layer: longer, read when a rule is disputed. `tuition index --check`
|
|
16
|
+
* is what keeps the first honest about the second.
|
|
17
|
+
*
|
|
18
|
+
* PATHS THIS MODULE READS ARE NOT ITS OWN (D-M3-27, the mechanism index's own
|
|
19
|
+
* row "Reading a path whose type is not established"). Every read goes through
|
|
20
|
+
* `readOperatorPath`, so a named pipe at an entry path is refused with the
|
|
21
|
+
* observed entry type instead of blocking the command forever.
|
|
22
|
+
*/
|
|
23
|
+
/** The generated index, relative to a tuition directory. */
|
|
24
|
+
export declare const MECHANISM_INDEX_FILE = "mechanism-index.yaml";
|
|
25
|
+
/** One mechanism as a tuition entry declares it. */
|
|
26
|
+
export interface MechanismDeclaration {
|
|
27
|
+
mechanism: string;
|
|
28
|
+
rule: string;
|
|
29
|
+
siblings?: string[];
|
|
30
|
+
"machine-readable-form"?: {
|
|
31
|
+
path: string;
|
|
32
|
+
key: string;
|
|
33
|
+
};
|
|
34
|
+
evidence: string[];
|
|
35
|
+
}
|
|
36
|
+
/** One tuition entry, after schema validation. */
|
|
37
|
+
export interface TuitionEntry {
|
|
38
|
+
kind: "tuition";
|
|
39
|
+
version: number;
|
|
40
|
+
id: string;
|
|
41
|
+
project: string;
|
|
42
|
+
date: string;
|
|
43
|
+
stage: string;
|
|
44
|
+
"kernel-relevant": boolean;
|
|
45
|
+
"what-happened": string;
|
|
46
|
+
lesson: string[];
|
|
47
|
+
mechanisms?: MechanismDeclaration[];
|
|
48
|
+
"structural-consequence"?: {
|
|
49
|
+
target: string;
|
|
50
|
+
status: "proposed" | "applied" | "ticketed";
|
|
51
|
+
change: string;
|
|
52
|
+
record?: string;
|
|
53
|
+
}[];
|
|
54
|
+
evidence: string[];
|
|
55
|
+
}
|
|
56
|
+
/** One row of the generated index, plus the entry it was projected from. */
|
|
57
|
+
export interface IndexRow {
|
|
58
|
+
key: string;
|
|
59
|
+
name: string;
|
|
60
|
+
rule: string;
|
|
61
|
+
siblings?: string[];
|
|
62
|
+
"machine-readable-form"?: {
|
|
63
|
+
path: string;
|
|
64
|
+
key: string;
|
|
65
|
+
};
|
|
66
|
+
evidence: string[];
|
|
67
|
+
/** The tuition id this row came from. Not part of the rendered document. */
|
|
68
|
+
source: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* THE KEY IS DERIVED FROM THE NAME, NEVER INVENTED: lowercase, every run of
|
|
72
|
+
* characters outside [a-z0-9] collapsed to one hyphen, ends trimmed. The same
|
|
73
|
+
* derivation `schemas/mechanism-index.schema.json` documents and M3-P6's
|
|
74
|
+
* registered test applies, stated once in code so the generator and the check
|
|
75
|
+
* cannot disagree about it.
|
|
76
|
+
*/
|
|
77
|
+
export declare function mechanismKey(name: string): string;
|
|
78
|
+
export type EntryFileListing = {
|
|
79
|
+
ok: true;
|
|
80
|
+
paths: string[];
|
|
81
|
+
} | {
|
|
82
|
+
ok: false;
|
|
83
|
+
reason: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Every tuition ENTRY file in a directory, sorted by name.
|
|
87
|
+
*
|
|
88
|
+
* The generated index lives in the same directory and is EXCLUDED BY NAME: it
|
|
89
|
+
* is the projection's output, and a projection that reads its own output is a
|
|
90
|
+
* loop rather than a check. Anything that is not a `.yaml` file is ignored, so
|
|
91
|
+
* a README beside the feed is not an entry.
|
|
92
|
+
*/
|
|
93
|
+
export declare function listEntryFiles(directory: string): EntryFileListing;
|
|
94
|
+
export type EntryLoad = {
|
|
95
|
+
ok: true;
|
|
96
|
+
entry: TuitionEntry;
|
|
97
|
+
body: string;
|
|
98
|
+
} | {
|
|
99
|
+
ok: false;
|
|
100
|
+
reason: string;
|
|
101
|
+
diagnostics: string[];
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Read, decode and schema-validate one tuition entry.
|
|
105
|
+
*
|
|
106
|
+
* THE RAW BYTES COME BACK WITH THE ENTRY, and that is not a convenience. An
|
|
107
|
+
* earlier version had `tuition add` call this and then read the same path a
|
|
108
|
+
* second time for the bytes to write. Two reads meant two independent
|
|
109
|
+
* refusals of a non-regular path, and a refusal that another refusal shadows
|
|
110
|
+
* cannot be witnessed: mutating either left the other rejecting the same
|
|
111
|
+
* input, which is exactly the shape T-018 records. One read, one
|
|
112
|
+
* classification, one guard.
|
|
113
|
+
*/
|
|
114
|
+
export declare function loadEntry(path: string): EntryLoad;
|
|
115
|
+
export type Projection = {
|
|
116
|
+
ok: true;
|
|
117
|
+
rows: IndexRow[];
|
|
118
|
+
} | {
|
|
119
|
+
ok: false;
|
|
120
|
+
reason: string;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Project the mechanism index out of a set of entries, sorted by key.
|
|
124
|
+
*
|
|
125
|
+
* A KEY CLAIMED BY TWO ENTRIES IS AN ERROR NAMING BOTH, never a silent
|
|
126
|
+
* first-wins. Two entries stating the same mechanism differently is exactly
|
|
127
|
+
* the divergence this document exists to prevent, and the projection cannot
|
|
128
|
+
* decide which of the two rules the project actually learned. The resolution
|
|
129
|
+
* is an editorial one: merge the two entries' rules by hand into whichever one
|
|
130
|
+
* owns the mechanism.
|
|
131
|
+
*/
|
|
132
|
+
export declare function projectIndex(entries: readonly TuitionEntry[]): Projection;
|
|
133
|
+
/**
|
|
134
|
+
* Emit a YAML scalar, quoting only when a plain one would not round trip.
|
|
135
|
+
*
|
|
136
|
+
* THE CONDITION IS THE POINT: this generator's output is re-read by
|
|
137
|
+
* `--check`, so a scalar that YAML would decode as something other than the
|
|
138
|
+
* string handed in makes the projection disagree with itself. The reserved
|
|
139
|
+
* leading indicators, an embedded `: ` or ` #`, and a trailing colon are the
|
|
140
|
+
* cases; everything else is emitted plain, which is what keeps the document
|
|
141
|
+
* readable.
|
|
142
|
+
*/
|
|
143
|
+
export declare function yamlScalar(value: string): string;
|
|
144
|
+
/** Render the index document. The bytes are what `--check` compares. */
|
|
145
|
+
export declare function renderIndex(rows: readonly IndexRow[]): string;
|
|
146
|
+
/**
|
|
147
|
+
* Compare a committed index document against a fresh projection.
|
|
148
|
+
*
|
|
149
|
+
* NAMES THE MECHANISM AND THE ENTRY IT CAME FROM, which is the criterion's
|
|
150
|
+
* letter (4): a reader of a red `--check` must be able to go straight to the
|
|
151
|
+
* file that changed. Both directions are reported, because a row DELETED from
|
|
152
|
+
* the feed and a row ADDED to it are different faults with the same symptom.
|
|
153
|
+
*
|
|
154
|
+
* The field comparison is over the DECODED values rather than the bytes, so a
|
|
155
|
+
* rewrap of a folded scalar is not reported as a rule change; a byte
|
|
156
|
+
* comparison is done by the caller afterwards and reported as formatting,
|
|
157
|
+
* which keeps "the rule changed" and "the file was hand-edited" apart.
|
|
158
|
+
*/
|
|
159
|
+
export declare function driftLines(committed: unknown, rows: readonly IndexRow[]): string[];
|