@feigi/fleet-ctl 3.23.8 → 3.23.9

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.23.8",
3
+ "version": "3.23.9",
4
4
  "description": "Agent fleet: run-team controller, merge bot, PR reviewer, and the ticket pipeline they share",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,80 @@
1
+ // reap.sh's branch sweep runs more checks than `git cherry` before its
2
+ // `git update-ref --no-deref -d`: the worktree registry and listing checks,
3
+ // the dirty check, the `worktree remove` refusal and the `wt_holding` re-read
4
+ // can each keep the branch. The cherry check is the only one of them that
5
+ // asks whether the tip is merged, so a comment saying the delete is
6
+ // "authorized by this check and by nothing else" is false as written. The
7
+ // component doc said reap deletes with `-D`; it deletes with `update-ref -d`.
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { bullet, paragraph, phrase, runAbove, stripHashGutter } from "./prose-pin.mjs";
13
+
14
+ const REPO = join(import.meta.dirname, "..", "..");
15
+ const SCRIPT = readFileSync(join(REPO, "plugin", "scripts", "reap.sh"), "utf8");
16
+ const DOC = readFileSync(join(REPO, "docs", "components", "reaping-and-liveness.md"), "utf8");
17
+
18
+ // The comment block above the branch sweep's cherry probe, and the one above
19
+ // its cherry scan: the two blocks that justify why the cherry check fails
20
+ // closed.
21
+ const probeComment = () =>
22
+ stripHashGutter(runAbove(SCRIPT, 'if ! cherry=$(git cherry "$base_rev" "$tip"', "reap.sh cherry probe", "#"));
23
+ const scanComment = () =>
24
+ stripHashGutter(
25
+ runAbove(SCRIPT, `if ! grep_probe "$cherry" '^+'; then keep "$b"`, "reap.sh cherry scan", "#"),
26
+ );
27
+
28
+ const NOTHING_ELSE = phrase("authorized by this check and by nothing else");
29
+
30
+ for (const [name, comment] of [
31
+ ["cherry-probe", probeComment],
32
+ ["cherry-scan", scanComment],
33
+ ]) {
34
+ test(`reap.sh: the ${name} comment does not call the cherry check the delete's sole authorizer`, () => {
35
+ assert.doesNotMatch(
36
+ comment(),
37
+ NOTHING_ELSE,
38
+ `reap.sh's ${name} comment says the delete is authorized by the cherry check and by nothing else — the worktree checks and \`wt_holding\` can stop the delete too`,
39
+ );
40
+ });
41
+ }
42
+
43
+ test("reap.sh: the cherry-probe comment does not quote the retired \"By nothing else\" sentence", () => {
44
+ assert.doesNotMatch(
45
+ probeComment(),
46
+ phrase('"By nothing else" bounds'),
47
+ "reap.sh's cherry-probe comment says \"By nothing else\" bounds what ELSE authorizes the delete — the worktree checks and `wt_holding` can stop the delete too, and the sentence restates the sole-authorizer claim",
48
+ );
49
+ });
50
+
51
+ test("reap.sh: the cherry comments still say why an unanswerable probe must keep", () => {
52
+ // Input the pins above must ACCEPT: the fail-closed argument the old
53
+ // sentence carried survives, restated as what makes the cherry check
54
+ // unique — it is the only check that asks whether the tip is merged.
55
+ assert.match(probeComment(), phrase("this is the only one that asks whether `$tip` is merged"));
56
+ assert.match(probeComment(), phrase("never a `$tip` that is unmerged — so an unanswerable probe must KEEP"));
57
+ assert.match(scanComment(), phrase("no check between here and the delete below asks whether the branch is merged"));
58
+ });
59
+
60
+ const reapStep = () =>
61
+ bullet(DOC, "1. **Reap after every merge pass**", "2. **Release a claim", "reaping-and-liveness.md reap step");
62
+ // The doc's last list item, so the blank-line bound is the end of the file.
63
+ const reapAndRelease = () =>
64
+ paragraph(
65
+ DOC,
66
+ "- **Reap and release are two different scripts on purpose.**",
67
+ "reaping-and-liveness.md reap-and-release choice",
68
+ );
69
+
70
+ test("reaping-and-liveness.md: the reap step names the delete as a compare-and-swap `update-ref -d`, not `-D`", () => {
71
+ const step = reapStep();
72
+ assert.match(step, phrase("compare-and-swap `git update-ref -d`"));
73
+ assert.doesNotMatch(step, /`-D`/, "reaping-and-liveness.md's reap step says the delete is `-D` — reap.sh deletes with `git update-ref -d`");
74
+ });
75
+
76
+ test("reaping-and-liveness.md: the reap-and-release choice names reap's delete as `update-ref -d`, not `-D`", () => {
77
+ const choice = reapAndRelease();
78
+ assert.match(choice, phrase("Reap authorizes a destructive `git update-ref -d`"));
79
+ assert.doesNotMatch(choice, /`-D`/, "reaping-and-liveness.md says reap authorizes a `-D` — reap.sh deletes with `git update-ref -d`");
80
+ });
package/scripts/reap.sh CHANGED
@@ -938,23 +938,26 @@ for b in $gone_branches; do
938
938
  # enough) prints nothing, grep sees empty input and exits 1 — the identical
939
939
  # verdict a genuinely clean cherry produces, so "merged" is indistinguishable
940
940
  # from "the probe could not answer". An unmerged branch is never the ambiguous
941
- # one: its `+` line makes grep exit 0 and always keeps. The delete is
942
- # authorized by this check and by nothing else — its compare-and-swap refuses
943
- # a ref that moved off `$tip`, never a `$tip` that is unmerged — so an
944
- # unanswerable probe must KEEP, the same fail-closed shape the worktree
945
- # `status` check below already uses.
941
+ # one: its `+` line makes grep exit 0 and always keeps. Of the checks that
942
+ # must pass before the delete, this is the only one that asks whether `$tip`
943
+ # is merged — the worktree checks below and the `worktree remove` refusal
944
+ # can each keep the branch, but none of them looks at its commits, and the
945
+ # compare-and-swap refuses a ref that moved off `$tip`, never a `$tip` that
946
+ # is unmerged — so an unanswerable probe must KEEP, the same fail-closed
947
+ # shape the worktree `status` check below already uses.
946
948
  # `refs/heads/$b`, never a bare `$b`, where the tip is read above. Restoring
947
949
  # the bare name makes a branch that shares its name with a tag ambiguous AS A
948
950
  # REV again, and git resolves an ambiguous one by preferring refs/tags/ over
949
951
  # refs/heads/ (measured, git 2.50.1 Apple Git-155). This probe would then
950
952
  # answer about the TAG's commit while the delete below removes the BRANCH —
951
953
  # and a tag sitting on a merged commit reports clean for a branch whose
952
- # commits exist nowhere else. The delete is authorized by this check and by
953
- # nothing else, so that reads straight through to destroying them: measured
954
- # on a fixture, the enumeration fix alone turned a branch this script
955
- # currently KEEPS into `REAPED`, at exit 0, with an empty kept[]. Qualifying
956
- # changes nothing for an ordinary branch — both spellings name the same
957
- # commit — and it is the same key the worktree lookup below already builds.
954
+ # commits exist nowhere else. No other check before the delete asks whether
955
+ # those commits are merged, so that reads straight through to destroying
956
+ # them: measured on a fixture, the enumeration fix alone turned a branch
957
+ # this script currently KEEPS into `REAPED`, at exit 0, with an empty
958
+ # kept[]. Qualifying changes nothing for an ordinary branch — both spellings
959
+ # name the same commit — and it is the same key the worktree lookup below
960
+ # already builds.
958
961
  #
959
962
  # `$base_rev` is the other side of the same rule, and it was missing until
960
963
  # #924: `$base` reached this `git cherry` exactly as BASE_REF spelled it, so a
@@ -963,10 +966,10 @@ for b in $gone_branches; do
963
966
  # deleted the BRANCH — measured, an unmerged [gone] branch whose commit
964
967
  # existed nowhere else REAPED at exit 0 with an empty kept[]. The
965
968
  # qualification is built at the top of the file, where its own comment
966
- # records why the accept-list beside it is what makes prefixing sound. "By
967
- # nothing else" bounds what ELSE authorizes the delete, not whether this
968
- # check itself can be wrong — which is why BOTH revs it consumes are
969
- # qualified. #634
969
+ # records why the accept-list beside it is what makes prefixing sound. Being
970
+ # the only merge check bounds what ELSE could catch an unmerged branch, not
971
+ # whether this check itself can be wrong — which is why BOTH revs it
972
+ # consumes are qualified. #634
970
973
  if ! cherry=$(git cherry "$base_rev" "$tip" 2>&1); then
971
974
  keep "$b" "cherry probe failed — cannot tell if merged: $(printf '%s' "$cherry" | tr '\n' ' ')"
972
975
  continue
@@ -981,9 +984,9 @@ for b in $gone_branches; do
981
984
  # What "grep's is the only status left to take" missed, and what this shape
982
985
  # exists for: taking it is not the same as READING it. grep answers three
983
986
  # ways, and until #1419 the `if` had two arms — an rc 2 scan that never
984
- # examined `$cherry` fell into the merged arm, and the delete below is
985
- # authorized by this check and by nothing else. So the deletion went ahead on
986
- # a merge status no tool had established. `grep_probe` splits the two
987
+ # examined `$cherry` fell into the merged arm, and no check between here and
988
+ # the delete below asks whether the branch is merged. So the deletion went
989
+ # ahead on a merge status no tool had established. `grep_probe` splits the two
987
990
  # questions; the answers are ruled on here, in the order that makes the
988
991
  # unanswerable case fail CLOSED like every other could-not-check in this
989
992
  # file: scan first, verdict second.