@feigi/fleet-ctl 3.22.6 → 3.22.7
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/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// reaping.md's release section names what authorizes release-ticket.sh's
|
|
2
|
+
// `git update-ref -d`, ending "and by nothing else". The script halts the
|
|
3
|
+
// delete on a fourth thing too: a fresh worktree check (`wt_holding`) re-read
|
|
4
|
+
// right before it, which stopped a delete on its own when a worktree took the
|
|
5
|
+
// branch inside the window between the checks and the delete. A list that
|
|
6
|
+
// closes with "by nothing else" while omitting it is false as written.
|
|
7
|
+
//
|
|
8
|
+
// Kept apart from reaping-prose.test.mjs, which pins the dirty-check and
|
|
9
|
+
// `worktree remove` claims of the same section: this one pins the
|
|
10
|
+
// authorizer list only, so the two files can be edited independently.
|
|
11
|
+
import { test } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { between, phrase } from "./prose-pin.mjs";
|
|
16
|
+
|
|
17
|
+
const REPO = join(import.meta.dirname, "..");
|
|
18
|
+
const REAPING = readFileSync(
|
|
19
|
+
join(REPO, "skills", "run-team", "references", "reaping.md"),
|
|
20
|
+
"utf8",
|
|
21
|
+
);
|
|
22
|
+
const SCRIPT = readFileSync(join(REPO, "scripts", "release-ticket.sh"), "utf8");
|
|
23
|
+
|
|
24
|
+
const releaseSection = () =>
|
|
25
|
+
between(
|
|
26
|
+
REAPING,
|
|
27
|
+
"## Why reap declines a claim that was never dispatched",
|
|
28
|
+
"## Never reap a branch a live member is on",
|
|
29
|
+
"reaping.md",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// The sentence that lists the authorizers: from "authorized by" to the
|
|
33
|
+
// "and by nothing else" that closes it.
|
|
34
|
+
const authorizers = () =>
|
|
35
|
+
between(
|
|
36
|
+
releaseSection(),
|
|
37
|
+
"authorized by the `ahead` and `git cherry` checks above",
|
|
38
|
+
"and by nothing else",
|
|
39
|
+
"reaping.md's release section",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
test("reaping.md: the release delete's authorizer list names the fresh worktree-holds-branch check", () => {
|
|
43
|
+
// ONE contiguous span, not keywords: "fresh" and the negation each
|
|
44
|
+
// satisfied somewhere in the slice pass a clause with either one gutted
|
|
45
|
+
// ("a check that no worktree holds the branch" has no freshness; "a fresh
|
|
46
|
+
// check ... that a worktree holds the branch" has the opposite meaning).
|
|
47
|
+
// Only a span binds the two to each other.
|
|
48
|
+
assert.match(
|
|
49
|
+
authorizers(),
|
|
50
|
+
phrase("a fresh check, re-read immediately before the delete, that no worktree holds the branch"),
|
|
51
|
+
"reaping.md's release section closes its list of what authorizes the `update-ref` delete with \"and by nothing else\" but no longer names the fresh worktree check — release-ticket.sh halts the delete when any worktree holds the branch, and that check alone stopped a delete in the race window",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("reaping.md: the authorizer list still carries the three checks it always named", () => {
|
|
56
|
+
// Input the new assertion must ACCEPT: naming the worktree check must not
|
|
57
|
+
// cost the list the checks it already had.
|
|
58
|
+
const list = authorizers();
|
|
59
|
+
assert.match(list, phrase("`ahead` and `git cherry` checks above"));
|
|
60
|
+
assert.match(list, phrase("`ahead` recount re-run against `origin/main`"));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// release-ticket.sh's executable lines, comments and `echo` diagnostics
|
|
64
|
+
// dropped. The delete is logged by an `echo` that spells the command out in
|
|
65
|
+
// a form the real call does not take (the real call quotes its arguments),
|
|
66
|
+
// and the check's name recurs in comments: an indexOf over the raw text lands
|
|
67
|
+
// on either and reads as the code, so a real delete moved above the check, or
|
|
68
|
+
// the check commented out, stays green.
|
|
69
|
+
const liveLines = () => SCRIPT.split("\n").filter((l) => !/^\s*(?:#|echo\b)/.test(l));
|
|
70
|
+
|
|
71
|
+
// The one live line matching `re`. Exactly one: a first-hit search would
|
|
72
|
+
// bind the pin to whichever copy comes first.
|
|
73
|
+
const liveLineOf = (re, what) => {
|
|
74
|
+
const hits = liveLines().flatMap((l, i) => (re.test(l) ? [i] : []));
|
|
75
|
+
assert.equal(hits.length, 1, `release-ticket.sh: expected exactly one live line that ${what}, found ${hits.length}`);
|
|
76
|
+
return hits[0];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
test("release-ticket.sh: the worktree check the prose names sits before the delete", () => {
|
|
80
|
+
// The prose names a check the script runs; pin that the script still does,
|
|
81
|
+
// and runs it before the delete, so the prose cannot outlive the check.
|
|
82
|
+
const check = liveLineOf(/^\s*if\s+wt_holding\s+"refs\/heads\/\$branch"/, 'runs wt_holding on the branch');
|
|
83
|
+
const del = liveLineOf(/\bgit\s+update-ref\s+-d\s+"refs\/heads\/\$branch"\s+"\$tip"/, 'runs `git update-ref -d` on the branch at its tip');
|
|
84
|
+
assert.ok(check < del, "release-ticket.sh runs its worktree check after the delete, not before it");
|
|
85
|
+
});
|
|
@@ -58,7 +58,7 @@ So nothing in the run cleans it up. Next run, the in-flight probe's local-worktr
|
|
|
58
58
|
- **Remote check catches a pushed branch.** Live `git ls-remote`, so it cannot be stale. Its failure is an unknown answer, never a "not pushed" — swallow it and a release deletes the local copy of work already on the server.
|
|
59
59
|
- **Label read first, `gh` failure fatal.** Worktree and branch gone with `in-progress` left behind hides the ticket from `candidates.mjs` entirely — worse than not releasing. All three artefacts or none.
|
|
60
60
|
|
|
61
|
-
**The delete is `git update-ref -d`'s compare-and-swap (ADR 0018) here too, and for reap's reason** — authorized by the `ahead` and `git cherry` checks above, plus the `ahead` recount re-run against `origin/main` immediately before the delete itself (a commit can land across the `gh issue view` between the checks and the delete; `git cherry` is not re-run, because such a commit is ahead of `origin/main` by construction), and by nothing else. A claim used to carry `origin/main` as its upstream, so `-d` compared against *that* and forgave a behind local main; since #760 `claim-ticket.sh` passes `--no-track` (so `@{u}` cannot silently answer about main), a claim has no upstream until its first `push -u`, and `-d` falls back to comparing against local `HEAD`. Measured: with local `main` behind `origin/main`, `-d` refuses a pristine claim — half-releasing it, worktree deleted and branch stranded with `in-progress` still on the ticket. The two checks above are strictly stronger than what `-d` would have asked, and both measure against `origin/main` rather than a local ref.
|
|
61
|
+
**The delete is `git update-ref -d`'s compare-and-swap (ADR 0018) here too, and for reap's reason** — authorized by the `ahead` and `git cherry` checks above, plus the `ahead` recount re-run against `origin/main` immediately before the delete itself (a commit can land across the `gh issue view` between the checks and the delete; `git cherry` is not re-run, because such a commit is ahead of `origin/main` by construction), plus a fresh check, re-read immediately before the delete, that no worktree holds the branch (`update-ref` consults no worktree, where `-D` refused a held branch; a worktree it cannot resolve, or a listing that will not re-read, stops the delete too), and by nothing else. A claim used to carry `origin/main` as its upstream, so `-d` compared against *that* and forgave a behind local main; since #760 `claim-ticket.sh` passes `--no-track` (so `@{u}` cannot silently answer about main), a claim has no upstream until its first `push -u`, and `-d` falls back to comparing against local `HEAD`. Measured: with local `main` behind `origin/main`, `-d` refuses a pristine claim — half-releasing it, worktree deleted and branch stranded with `in-progress` still on the ticket. The two checks above are strictly stronger than what `-d` would have asked, and both measure against `origin/main` rather than a local ref.
|
|
62
62
|
|
|
63
63
|
**`worktree remove` without `--force`** refuses on modified and untracked files and deletes ignored files silently (verified, git 2.50.1). Ignored-silently is what is wanted: `./agent-test` materializes `.agent-test.sh` beside itself in every worktree it runs in (and before #55, `claim-ticket.sh` wrote an untracked `agent-test` into every worktree it claimed), so treating ignored files as dirt would strand every release. The same asymmetry is why the runner itself is tracked as a BOOTSTRAP rather than committed as a copy: a claim that overwrote a tracked `agent-test` would leave a modified tracked path, which this refusal does not forgive. Its refusal is the dirty check recomputed by git at the moment of the delete — for the live-directory case ONLY. `worktree remove` gates that clean check on the same `stat` the guard does, so it is not an independent second opinion: where the path cannot be stat'ed git reaches the same "gone" by the same means, accepts the entry at rc 0 and deregisters a worktree whose files are still on disk (measured, git 2.50.1: an untracked file in a worktree under a `chmod 000` parent). There the `gone` guard is sole arbiter — which is why it establishes absence instead of inferring it from a failed `-e`, and why the fail-open fixed in `1e54bac` survived being reasoned about: guard and backstop fail on the same inputs.
|
|
64
64
|
|