@feigi/fleet-ctl 3.82.18 → 3.82.19

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.18",
3
+ "version": "3.82.19",
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
@@ -375,7 +375,12 @@ the `render` command to run first, and refuses once the
375
375
  run is draining: drain stops supply. It also refuses a row whose `tier=`
376
376
  names no single definition — two different values, or one that is not a
377
377
  definition suffix — or names one with no `agents/<definition>.agent.md`,
378
- before the member is live; fix the row with `row` first.
378
+ before the member is live; fix the row with `row` first. Once every refusal
379
+ has passed, `dispatch` deletes the member's prior report,
380
+ `<scratch>/reports/<member>.json`, when its brief's sidecar names an absolute
381
+ `scratch`, so a prior run's report cannot settle this one. A sidecar naming
382
+ none clears nothing and says so on stderr; a report it cannot remove refuses
383
+ the dispatch.
379
384
 
380
385
  **The prompt carries only what varies.** Everything shared — the
381
386
  unattended-member block, the `edit`/`read` absolute-path rule, the