@feigi/fleet-ctl 3.82.5 → 3.82.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.82.5",
3
+ "version": "3.82.7",
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": {
@@ -21,8 +21,10 @@
21
21
  # directory git-env.mjs's resolveWorkspace() names for the repository: the
22
22
  # common git dir's parent when the common dir is named `.git` or that parent's
23
23
  # own `.git` resolves to it, otherwise the common git dir itself (a bare
24
- # repository, a submodule). That is the same place the
25
- # ledger and the heartbeat live — so a claimed worktree and a
24
+ # repository, a submodule). Only an absent `<parent>/.git` is "not this
25
+ # parent"; one that exists and that git refuses fails the probe, so a linked
26
+ # worktree never re-keys the cache to the common dir. The ledger and the
27
+ # heartbeat live in that same place — so a claimed worktree and a
26
28
  # review worktree read the one cache the main checkout holds rather than a
27
29
  # private copy each. A single JSON object:
28
30
  #
@@ -179,9 +181,18 @@ common=$(git -C "$repo" rev-parse --path-format=absolute --git-common-dir 2>&1)
179
181
  # shell rather than asked of node: the interpreter probe below must stay the
180
182
  # first node call, since its refusals are pinned in that order. A common dir
181
183
  # named `.git` belongs to its parent checkout. Otherwise the parent's `.git`,
182
- # a symlink or a gitfile, that resolves to the common dir makes the parent the
183
- # workspace. Otherwise the common dir is the workspace itself (a bare
184
- # repository, a submodule's git dir).
184
+ # a symlink, a gitfile or a directory, that git resolves to the common dir
185
+ # makes the parent the workspace. Only an absent `<parent>/.git` is "not this
186
+ # parent": one that exists and that git refuses (a dangling or invalid gitfile,
187
+ # an unreadable one) fails the probe, never re-keys the cache to the common dir.
188
+ # Otherwise the common dir is the workspace itself (a bare repository, a
189
+ # submodule's git dir). git prints the realpath for a gitfile but the path as
190
+ # spelled for a `.git` directory, so its answer is canonicalised before the
191
+ # compare; `$target` stays absolute, since a relative operand is answered
192
+ # relative to the cwd. Only git's stdout is the answer: anything it writes to
193
+ # stderr on a success would otherwise land inside the path and fail the
194
+ # compare, re-keying the cache in silence. A refusal asks once more for the
195
+ # reason, its stderr alone.
185
196
  real=$(cd -P "$common" 2>/dev/null && pwd -P) || real=$common
186
197
  parent=${common%/*}
187
198
  case ${real##*/} in
@@ -189,13 +200,16 @@ case ${real##*/} in
189
200
  *)
190
201
  ws=$common
191
202
  target=${real%/*}/.git
192
- if [ -f "$target" ]; then
193
- IFS= read -r gl < "$target" || :
194
- gl=${gl#gitdir: }
195
- case $gl in /*) target=$gl ;; *) target=${real%/*}/$gl ;; esac
203
+ if [ -e "$target" ] || [ -L "$target" ]; then
204
+ named=$(git rev-parse --resolve-git-dir "$target" 2>/dev/null) || {
205
+ gitrc=$?
206
+ why=$(git rev-parse --resolve-git-dir "$target" 2>&1 >/dev/null) || :
207
+ case $gitrc in 128) die "cannot resolve the workspace of $repo: $target — $why" ;; esac
208
+ unrunnable "git did not resolve $target (exit $gitrc), so the Recipe cache was not read${why:+ — $why}"
209
+ }
210
+ named=$(cd -P "$named" 2>/dev/null && pwd -P) || named=
211
+ if [ "$named" = "$real" ]; then ws=$parent; fi
196
212
  fi
197
- named=$(cd -P "$target" 2>/dev/null && pwd -P) || named=
198
- if [ "$named" = "$real" ]; then ws=$parent; fi
199
213
  ;;
200
214
  esac
201
215
  cache="$ws/.fleet/recipe.json"
@@ -10,13 +10,16 @@
10
10
  // every worktree of one repository shares, and git-env.mjs's resolveWorkspace()
11
11
  // turns that answer into the workspace it names: the common git dir's parent
12
12
  // when the common dir is named `.git` or that parent's own `.git` resolves to
13
- // it, otherwise the common git dir itself. This module is the spawn around
14
- // that function — the scrubbed env, the bound, the join onto `.fleet/` — so a
15
- // new `.fleet/`-rooted file is one `fleetFile("name")` call rather than
16
- // another hand-spelled probe. The bound is per git child, not per call: when
17
- // the common git dir is not named `.git`, resolveWorkspace() runs a second
18
- // `rev-parse` after this module's own, with a bound of its own, so one call's
19
- // worst case approaches twice `timeoutMs`.
13
+ // it, otherwise the common git dir itself. Only an absent `<parent>/.git` is
14
+ // "not this parent"; one that exists and that git refuses fails the probe, so
15
+ // a linked worktree never re-keys to the common dir. This module is the spawn
16
+ // around that function — the scrubbed env, the bound, the join onto `.fleet/`
17
+ // — so a new `.fleet/`-rooted file is one `fleetFile("name")` call rather
18
+ // than another hand-spelled probe. The bound is per git child, not per call:
19
+ // when the common git dir is not named `.git` and the parent's `.git`
20
+ // exists, resolveWorkspace() runs a second `rev-parse` after this module's
21
+ // own, with a bound of its own, so one call's worst case approaches twice
22
+ // `timeoutMs`.
20
23
  //
21
24
  // One error policy: anything that leaves no workspace THROWS
22
25
  // FleetDirUnresolvable, never a null or an empty path. What a caller does
@@ -115,15 +118,16 @@ function workspaceOf(cwd, timeoutMs, canonicalise) {
115
118
  * that spelling without changing which file it names.
116
119
  *
117
120
  * `timeoutMs` (default git-env.mjs's DEFAULT_GIT_TIMEOUT_MS) bounds each git
118
- * child, not the call. When the common git dir is not named `.git` (the
119
- * `.bare` layout, a bare repository, a submodule, a `--separate-git-dir`
120
- * clone), resolveWorkspace() runs a second probe after `--git-common-dir`,
121
- * with its own full `timeoutMs`, so one call's worst case approaches twice
122
- * `timeoutMs`.
121
+ * child, not the call. When the common git dir is not named `.git` and the
122
+ * `.git` in its parent exists (the `.bare` layout, or any other git dir that
123
+ * shares a parent with a `.git`), resolveWorkspace()
124
+ * runs a second probe after `--git-common-dir`, with its own full `timeoutMs`,
125
+ * so one call's worst case approaches twice `timeoutMs`.
123
126
  *
124
127
  * Throws FleetDirUnresolvable for every probe that leaves no workspace — no
125
- * repository, an unreadable `.git`, git missing, a probe killed at
126
- * `timeoutMs` — with git's own reason in the message.
128
+ * repository, an unreadable `.git`, a parent's `.git` that git refuses, git
129
+ * missing, a probe killed at `timeoutMs` — with git's own reason in the
130
+ * message.
127
131
  */
128
132
  export function fleetFile(name, { cwd = process.cwd(), timeoutMs, canonicalise = false } = {}) {
129
133
  const { dir } = workspaceOf(cwd, timeoutMs, canonicalise);
@@ -146,9 +150,9 @@ export function fleetFile(name, { cwd = process.cwd(), timeoutMs, canonicalise =
146
150
  *
147
151
  * `home` exists for tests; it defaults to the user's home directory.
148
152
  * `timeoutMs` bounds each git child exactly as in fleetFile(): two may run in
149
- * sequence when the common git dir is not named `.git`, so one call's worst
150
- * case approaches twice `timeoutMs`. Throws FleetDirUnresolvable exactly as
151
- * fleetFile() does.
153
+ * sequence when the common git dir is not named `.git` and its parent's `.git`
154
+ * exists, so one call's worst case approaches twice `timeoutMs`. Throws
155
+ * FleetDirUnresolvable exactly as fleetFile() does.
152
156
  */
153
157
  export function scratchRoot({ cwd = process.cwd(), timeoutMs, home = homedir() } = {}) {
154
158
  const { dir, gitDir } = workspaceOf(cwd, timeoutMs, true);
@@ -63,8 +63,10 @@ export function gitEnv(overrides = {}, base = process.env) {
63
63
  // `.git` or when that parent's own `.git` resolves to it (the `.bare`
64
64
  // layout's gitfile); otherwise it is the common dir itself — a bare
65
65
  // repository's, a submodule's or a `--separate-git-dir` clone's git dir — so
66
- // two such dirs sharing a parent do not collapse onto one workspace. The
67
- // rule's full statement sits beside `resolveWorkspace()` below. The main
66
+ // two such dirs sharing a parent do not collapse onto one workspace. Only an
67
+ // absent `<parent>/.git` is "not this parent"; one that git refuses fails the
68
+ // probe, so a linked worktree never re-keys to the common dir. The rule's
69
+ // full statement sits beside `resolveWorkspace()` below. The main
68
70
  // checkout's work tree is a separate question with its own answer,
69
71
  // `mainCheckoutDirFromGitCommonDir()`: a `.git` symlinked to a git dir
70
72
  // elsewhere moves the workspace out of the checkout, never the work tree.
@@ -79,7 +81,7 @@ export function gitEnv(overrides = {}, base = process.env) {
79
81
  // composes the rest around it.
80
82
 
81
83
  import { spawnSync } from "node:child_process";
82
- import { realpathSync } from "node:fs";
84
+ import { lstatSync, realpathSync } from "node:fs";
83
85
  import { basename, dirname, join, resolve } from "node:path";
84
86
 
85
87
  // A `rev-parse` answers in milliseconds; this bound exists so a git that never
@@ -87,8 +89,10 @@ import { basename, dirname, join, resolve } from "node:path";
87
89
  export const DEFAULT_GIT_TIMEOUT_MS = 10_000;
88
90
 
89
91
  // Thrown when the one probe the rule needs beyond `--git-common-dir` could not
90
- // run to an answer: git missing, or killed at its bound. Not thrown for a git
91
- // that ran and refused — that is an answer, and the rule reads it as "no".
92
+ // run to an answer: git missing, killed at its bound, or a `<parent>/.git`
93
+ // that exists and that git refuses. Only an absent `<parent>/.git` is "no";
94
+ // one git refuses fails the probe, so a linked worktree never re-keys to the
95
+ // common dir.
92
96
  export class WorkspaceProbeFailed extends Error {
93
97
  constructor(message) {
94
98
  super(message);
@@ -107,25 +111,32 @@ function canonicalPath(path) {
107
111
  }
108
112
 
109
113
  // Why a probe that did not answer did not: the spawn error, the signal that
110
- // killed it, or its stderr. Its caller reaches it only for a spawn error or a
111
- // probe with no exit status, so no exit code is ever left to name.
114
+ // killed it, its stderr, or its exit status when stderr says nothing.
112
115
  function probeWhy(r) {
113
116
  if (r.error && r.error.message) return r.error.message;
114
117
  if (r.signal) return `killed by ${r.signal}`;
115
- return String(r.stderr ?? "").trim() || "no answer";
118
+ return String(r.stderr ?? "").trim() || `git exited ${r.status}`;
116
119
  }
117
120
 
118
121
  // Does `<parent>/.git` name `gitDir` (a realpath)? `--resolve-git-dir` follows
119
- // a gitfile, so the `.bare` layout's `.git` file answers with `.bare`.
122
+ // a gitfile, so the `.bare` layout's `.git` file answers with `.bare`. Only an
123
+ // absent `<parent>/.git` is no, without a spawn; one that exists and that git
124
+ // refuses (a dangling or invalid gitfile, an unreadable one) fails the probe.
125
+ // Git's stderr is only quoted, never read: its text depends on the locale.
120
126
  function parentGitDirNames(parent, gitDir, cwd, timeoutMs) {
121
127
  const candidate = join(parent, ".git");
128
+ try {
129
+ lstatSync(candidate);
130
+ } catch (e) {
131
+ if (e.code === "ENOENT") return false;
132
+ throw new WorkspaceProbeFailed(`could not resolve ${candidate}: ${e.message}`);
133
+ }
122
134
  const r = spawnSync("git", ["rev-parse", "--resolve-git-dir", candidate], {
123
135
  cwd, encoding: "utf8", timeout: timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS, env: gitEnv(),
124
136
  });
125
- if (r.error || r.status === null) {
137
+ if (r.error || r.status !== 0) {
126
138
  throw new WorkspaceProbeFailed(`could not resolve ${candidate}: ${probeWhy(r)}`);
127
139
  }
128
- if (r.status !== 0) return false;
129
140
  return realpathOr(resolve(parent, r.stdout.trim())) === gitDir;
130
141
  }
131
142
 
@@ -138,8 +149,9 @@ function parentGitDirNames(parent, gitDir, cwd, timeoutMs) {
138
149
  * The rule, on the common dir `C` (the realpath of the answer):
139
150
  * 1. `C` is named `.git`: the workspace is its parent. No spawn.
140
151
  * 2. Otherwise `<parent of C>/.git` is probed with `--resolve-git-dir`. If it
141
- * names `C`, the workspace is that parent (the `.bare` layout). A git that
142
- * refuses, or names something else, is "no".
152
+ * names `C`, the workspace is that parent (the `.bare` layout). A
153
+ * `.git` that is absent, or that git names as something else, is "no".
154
+ * A `.git` that exists and that git refuses fails the probe.
143
155
  * 3. Otherwise the workspace is `C` itself, with `gitDir: true`.
144
156
  *
145
157
  * `gitDir` is what the scratch root's key reads: a git dir's `.git` suffix
@@ -160,12 +172,13 @@ function parentGitDirNames(parent, gitDir, cwd, timeoutMs) {
160
172
  * uncanonicalised path.
161
173
  *
162
174
  * `timeoutMs` (default DEFAULT_GIT_TIMEOUT_MS) bounds this function's own
163
- * step-2 probe only, the one spawn it makes. The caller's `--git-common-dir`
175
+ * step-2 probe only, the one spawn it can make. The caller's `--git-common-dir`
164
176
  * probe that produced `gitCommonDir` is the caller's to bound, so a caller
165
177
  * passing the same `timeoutMs` to both can wait close to twice that in all.
166
178
  *
167
- * Throws WorkspaceProbeFailed only when the step-2 probe could not run to an
168
- * answer.
179
+ * Throws WorkspaceProbeFailed when the step-2 probe could not run to an
180
+ * answer: git missing or killed at its bound, or a `<parent of C>/.git` that
181
+ * exists and that git refuses.
169
182
  */
170
183
  export function resolveWorkspace(gitCommonDir, cwd = process.cwd(), { canonicalise = false, timeoutMs } = {}) {
171
184
  const common = String(gitCommonDir ?? "").trim();
@@ -126,20 +126,23 @@ function cause(...candidates) {
126
126
  // though "dominant" overstates that relationship: a normal invocation
127
127
  // without `--file` can pay this same budget TWICE in sequence, once here and
128
128
  // once more in repoCheck() below, before ever reaching `gh` at all — and
129
- // THREE times in layouts whose common git dir is not named `.git` (the
130
- // `.bare` layout, a bare repository, a submodule, a `--separate-git-dir`
131
- // clone), where fleetFile() runs a second probe with its own full budget:
129
+ // THREE times where the common git dir is not named `.git` and its parent
130
+ // holds a `.git` (the `.bare` layout, or any other git dir sharing a parent
131
+ // with a `.git`), where fleetFile() runs a second probe with its own full budget:
132
132
  // fleetFile()'s bound is per git child, not per call. At default settings
133
133
  // two stalled probes TIE `gh`'s own 20 s bound and three exceed it, and even
134
134
  // one stall can EXCEED it — up to GIT_TIMEOUT_MS + 20 s — when `gh` still
135
135
  // runs (measured). `check`'s worst case is the sum of whichever of its
136
136
  // children actually stall, not a single fixed term. That is three children
137
- // normally; in those layouts it is three git children and `gh` is a fourth.
137
+ // normally; in the `.bare` layout it is three git children and `gh` is a fourth.
138
+ // A bare repository, a submodule or a `--separate-git-dir` clone has no `.git`
139
+ // beside its git dir, so fleetFile() runs no second probe there.
138
140
  // Where the ledger's repository has no work tree (the `.bare` layout, a bare
139
141
  // repository, a `--separate-git-dir` clone; not a submodule) repoCheck()'s
140
142
  // `--show-toplevel` is refused and it asks the same directory for
141
- // `--git-common-dir` to bind `gh` to, so there it is four git children and
142
- // `gh` a fifth. `gh` runs in every one of those layouts. With `--file` the
143
+ // `--git-common-dir` to bind `gh` to: four git children and `gh` a fifth in
144
+ // the `.bare` layout, three and `gh` a fourth in the other two. `gh` runs in
145
+ // every one of those layouts. With `--file` the
143
146
  // fleetFile() children drop out: two children normally, three where the
144
147
  // repository has no work tree.
145
148
  //
@@ -1021,7 +1021,7 @@ export async function runReview(host, args) {
1021
1021
  SNAPTREE=$(git -C "$SNAP" rev-parse 'HEAD^{tree}' 2>/dev/null); SRCTREE=$(git -C ${worktree} rev-parse 'HEAD^{tree}')
1022
1022
  { [ -d "$SNAP/.git" ] && [ -n "$SNAPTREE" ] && [ "$SNAPTREE" = "$SRCTREE" ]; } && echo SNAPSHOT_TREE_MATCH || echo SNAPSHOT_TREE_MISMATCH="snapshot $SNAPTREE vs commit $SRCTREE"
1023
1023
  INSTALL=$(~/.fleet/bin/fleet-run derive-testcmd.sh ${worktree} install) && echo SNAPSHOT_INSTALL_STEP="$INSTALL" || { INSTALL=; echo SNAPSHOT_INSTALL_UNREAD; }
1024
- [ -z "$INSTALL" ] || { ( cd "$SNAP" && unset CI && sh -c "$INSTALL" ) >"$RUN/install.log" 2>&1; echo SNAPSHOT_INSTALL_EXIT=$?; }
1024
+ [ -z "$INSTALL" ] || { ( cd "$SNAP" && env -u CI sh -c "$INSTALL" ) >"$RUN/install.log" 2>&1; echo SNAPSHOT_INSTALL_EXIT=$?; }
1025
1025
  if [ -d "$SNAP/.git" ] && git -C "$SNAP" status --porcelain --untracked-files=all >"$RUN/status.txt"; then head -n 20 "$RUN/status.txt" | sed 's/^/SNAPSHOT_UNTRACKED=/'; echo SNAPSHOT_UNTRACKED_COUNT=$(wc -l <"$RUN/status.txt" | tr -d ' '); else echo SNAPSHOT_STATUS_FAILED; fi
1026
1026
 
1027
1027
  The Install step can take minutes: run the block as ONE blocking foreground