@feigi/fleet-ctl 3.82.18 → 3.82.20

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.
@@ -18,9 +18,9 @@ Your report is the one file you write outside it: `<scratch>/reports/merge-bot-<
18
18
  the path your brief names, holding every PR you merged in `merged`. Your final
19
19
  message is the one line your brief gives, `merge-bot-<n>: done <path>`.
20
20
 
21
- The shell traps below are copied from the controller's skill file,
22
- `run-team/SKILL.md`. In them **Own the CI waits** is that file's section on the
23
- controller's CI watcher, which you do not run, and "this file" is that skill file; each trap applies to every shell
21
+ The shell traps below are copied from the controller's skill, `run-team`. In
22
+ them `references/ci-watcher.md` is that skill's file on the controller's CI
23
+ watcher, which you do not run; each trap applies to every shell
24
24
  command and loop you run.
25
25
 
26
26
  **zsh word-splits an unquoted command substitution's result; it does NOT split
@@ -54,7 +54,7 @@ EOF
54
54
  ```
55
55
 
56
56
  The inline `$(printf '%s\n' "$prs")` form splits correctly too and is what the
57
- watcher block under **Own the CI waits** uses, but it splits on *every* whitespace character, so it is safe only
57
+ watcher block in `references/ci-watcher.md` uses, but it splits on *every* whitespace character, so it is safe only
58
58
  for values that cannot contain one — PR numbers cannot, branch names and paths
59
59
  can. Measured on a two-line list whose first value is `two words`: three
60
60
  iterations through `$(printf …)`, two through `while IFS= read -r`. On an empty
@@ -70,8 +70,8 @@ word-split bug, so a correct heredoc fix missing this half looks exactly like no
70
70
  fix at all, and reads as one.
71
71
 
72
72
  **A probe that could not look emits an event; it never `continue`s.** Stated in
73
- full at *A probe that cannot read is a transition event, not silence* under **Own the CI waits**,
74
- and it binds every watcher in this file rather than that one block: the third
73
+ full at *A probe that cannot read is a transition event, not silence* in `references/ci-watcher.md`,
74
+ and it binds every watcher rather than that one block: the third
75
75
  value beside pass and fail is what turns a broken loop into a loud failure
76
76
  instead of a quiet one. A guard that drops an unreadable probe — `[ -n "$json" ]
77
77
  || continue` — makes *could not look* indistinguishable from *nothing to report*.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.82.18",
3
+ "version": "3.82.20",
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": {
@@ -13,7 +13,9 @@
13
13
  // every liveness count from this file alone, rather than from row text a
14
14
  // controller typed by hand: `dispatch` writes a member's live token onto its
15
15
  // row and appends it to `## Dispatched` with `brief=<hash>`, the brief it
16
- // refuses to dispatch without, `settle` rewrites that token to
16
+ // refuses to dispatch without; when that brief's sidecar names an absolute
17
+ // scratch, it also clears the member's report file there, so a prior run's
18
+ // report is not read as this run's; `settle` rewrites that token to
17
19
  // `<member>=<outcome>` in both places (an implementer's settle to `PR#M` also
18
20
  // folds the PR's own `#M` row into its ticket's) — with `--from-report`, the
19
21
  // outcome derived from the member's report file and checked against git and
@@ -46,7 +48,7 @@
46
48
 
47
49
  import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync, linkSync } from "node:fs";
48
50
  import { createHash } from "node:crypto";
49
- import { dirname, resolve, join, basename } from "node:path";
51
+ import { dirname, resolve, join, basename, isAbsolute } from "node:path";
50
52
  import { fileURLToPath } from "node:url";
51
53
  import { spawnSync, execFileSync } from "node:child_process";
52
54
  import { makeDie, isFlagLike, hasEqualsForm, isDigits, writeAll } from "./arg.mjs";
@@ -56,7 +58,7 @@ import { deriveRun, LedgerError, labelOffMember, HELD, CONFLICT_HOLD } from "./f
56
58
  import { stateFileIn, readState, writeState, assessBeat, stallsAt } from "./fleet-state.mjs";
57
59
  import { processSource, ancestry, controllerOf, judgeController, ProcUnreadable } from "./proc.mjs";
58
60
  import { attemptLock, releaseLockFile, reapPathOf } from "./pid-lock.mjs";
59
- import { readCheckRecordIn, readReportIn, reportsDir, reportFamily, defaultScratch } from "./report-files.mjs";
61
+ import { readCheckRecordIn, readReportIn, reportsDir, reportFamily, defaultScratch, reportPath } from "./report-files.mjs";
60
62
 
61
63
  const NAME = "ledger";
62
64
  // The plugin's own agent definitions — the directory tier-check.mjs reads
@@ -2032,7 +2034,12 @@ function runDispatch() {
2032
2034
 
2033
2035
  // Last, so every refusal above still names its own cause first, and before
2034
2036
  // anything is written.
2035
- const brief = renderedBrief(member.name, memberRole(member.family));
2037
+ const { brief, scratch } = renderedBrief(member.name, memberRole(member.family));
2038
+
2039
+ // The report a prior run left for this member is that run's last claim.
2040
+ // Cleared here, after every refusal above, so settle --from-report and the
2041
+ // tick's SETTLE row cannot read it as this run's.
2042
+ clearReport(scratch, member.name);
2036
2043
 
2037
2044
  let line = null;
2038
2045
  const created = member.bound !== null && i === -1;
@@ -2059,9 +2066,11 @@ function runDispatch() {
2059
2066
  // --member <name>` left it in the briefs directory beside the ledger: the
2060
2067
  // sidecar `<name>.json` must exist, name `role` — the template of the
2061
2068
  // member's family — and carry the brief's sha256, and the brief file beside
2062
- // it, `<name>.md`, must exist and hash to it. `{path, sha256}` comes back,
2063
- // `path` that file, so the controller hands the `task` call its content from
2064
- // the path printed. Refused, nothing is written.
2069
+ // it, `<name>.md`, must exist and hash to it. `{brief: {path, sha256},
2070
+ // scratch}` comes back, `path` that file, so the controller hands the `task`
2071
+ // call its content from the path printed, and `scratch` the sidecar's
2072
+ // `args.scratch` when it is an absolute path, else null. Refused, nothing is
2073
+ // written.
2065
2074
  function renderedBrief(name, role) {
2066
2075
  const dir = resolve(dirname(file), BRIEFS);
2067
2076
  const sidecar = join(dir, `${name}.json`);
@@ -2098,7 +2107,28 @@ function renderedBrief(name, role) {
2098
2107
  if (actual !== s.sha256) {
2099
2108
  die(`${path} hashes to ${actual}, but ${sidecar} carries ${s.sha256} — ${render}`);
2100
2109
  }
2101
- return { path, sha256: s.sha256 };
2110
+ const scratch = typeof s.args?.scratch === "string" && isAbsolute(s.args.scratch) ? s.args.scratch : null;
2111
+ return { brief: { path, sha256: s.sha256 }, scratch };
2112
+ }
2113
+
2114
+ // Removes `name`'s report under `scratch`, the last run's claim about the
2115
+ // member, if one is there. A null `scratch` — the brief's sidecar named none —
2116
+ // clears nothing and says so, since the report's location is then unknown.
2117
+ function clearReport(scratch, name) {
2118
+ if (scratch === null) {
2119
+ console.error(`${NAME}: WARNING ${name}'s brief sidecar names no scratch, so its prior report was not cleared`);
2120
+ return;
2121
+ }
2122
+ const path = reportPath(scratch, name);
2123
+ try {
2124
+ unlinkSync(path);
2125
+ } catch (e) {
2126
+ // ENOTDIR: something other than a directory sits where `reports` would
2127
+ // be, so no report is there either — readReportIn and the tick's
2128
+ // readReports read it as absent too.
2129
+ if (e.code === "ENOENT" || e.code === "ENOTDIR") return;
2130
+ die(`cannot clear ${path}: ${e.message} — not dispatching ${name}`);
2131
+ }
2102
2132
  }
2103
2133
 
2104
2134
  // Whether `outcome` is one whose claim a member's report carries and `settle