@feigi/fleet-ctl 3.81.6 → 3.82.0

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.81.6",
3
+ "version": "3.82.0",
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": {
package/scripts/pull.mjs CHANGED
@@ -54,9 +54,16 @@
54
54
  // 1 inflight `inflight.sh <N>`: anything but exit 0 is taken.
55
55
  // 2 issue `gh issue view <N> --json title,body,comments,labels,createdAt`
56
56
  // written to `<scratch>/impl-<N>/issue.json`. Without the
57
- // `ready-for-agent` label the ticket is gone. The distilled
58
- // brief and the paths the router's extractor finds in it are
59
- // printed.
57
+ // `ready-for-agent` label the ticket is gone. A comment with a
58
+ // `## Ruling` or `## Triage` heading line and no `## Agent
59
+ // Brief` one, newer than the last comment that has one or on a
60
+ // ticket with none, is a stranded ruling: the newest is named,
61
+ // with its URL, in a comment, and the label replaces
62
+ // `ready-for-agent` with `ready-for-human`, as a Q2 cause does;
63
+ // a newest one `gh` printed with no URL is exit 2, nothing
64
+ // posted.
65
+ // Otherwise the distilled brief and the paths the router's
66
+ // extractor finds in it are printed.
60
67
  // 3 Q1 staleness `gone` / `present` (each with `--path <p>` and the string
61
68
  // as the text) run `staleness.mjs --path <p> --gone|--present --
62
69
  // <string>`; `none <why>` and `done` end the question. Exit 1
@@ -127,7 +134,7 @@ import { gitEnv } from "./git-env.mjs";
127
134
  import { isCLI } from "./is-cli.mjs";
128
135
  import { memberTokens, premisesOf, tierValues } from "./ledger-grammar.mjs";
129
136
  import { sessionFromReceipt } from "./phase0-receipt.mjs";
130
- import { briefPaths, briefText, outOfScope } from "./ticket-router.mjs";
137
+ import { briefPaths, briefText, outOfScope, strandedRuling } from "./ticket-router.mjs";
131
138
 
132
139
  const NAME = "pull";
133
140
  const SCRIPTS = dirname(fileURLToPath(import.meta.url));
@@ -169,8 +176,18 @@ function handsParts(text) {
169
176
  return { cause: null, line: "" };
170
177
  }
171
178
 
172
- /** The comment, and the label that replaces `ready-for-agent`, for a Q2 answer other than `decided`. */
179
+ /**
180
+ * The comment, and the label that replaces `ready-for-agent`, for a Q2 answer
181
+ * other than `decided` — or, for `stranded-ruling`, for a ruling or triage
182
+ * comment newer than the brief, `text` naming its heading line and its URL.
183
+ */
173
184
  export function relabel(token, text) {
185
+ if (token === "stranded-ruling") {
186
+ return {
187
+ label: HUMAN,
188
+ comment: `${RELABELLED} a ruling or triage comment is newer than the brief, and the Pull does not read it: ${text}. To make it the brief, rename its heading to \`## Agent Brief — ruling\` and drop anything that is not decided scope, or delete it; then restore \`${READY}\`.`,
189
+ };
190
+ }
174
191
  if (token === "needs-triage") {
175
192
  return {
176
193
  label: TRIAGE,
@@ -802,6 +819,11 @@ class Pull {
802
819
  return this.stop(`cannot write ${this.p.issue}: ${e.message}`);
803
820
  }
804
821
  if (!issue.labels.some((l) => l?.name === READY)) return this.leave("relabelled elsewhere");
822
+ const stranded = strandedRuling(issue);
823
+ if (stranded !== null) {
824
+ if (typeof stranded.url !== "string") return this.stop(`gh issue view printed a ruling comment with no url: "${stranded.heading}"`);
825
+ return this.relabelled("stranded-ruling", `"${stranded.heading}" ${stranded.url}`);
826
+ }
805
827
  this.log.push(...this.brief());
806
828
  st.stage = "staleness";
807
829
  return null;
@@ -1078,9 +1100,15 @@ class Pull {
1078
1100
  st.stage = "collision";
1079
1101
  return null;
1080
1102
  }
1103
+ return this.relabelled(token, text);
1104
+ }
1105
+
1106
+ // Posts the cause's comment and swaps `ready-for-agent` for its label. One
1107
+ // comment per cause: an edit that failed after its comment landed is run
1108
+ // again without a second comment.
1109
+ relabelled(token, text) {
1110
+ const { st } = this;
1081
1111
  const { label, comment } = relabel(token, text);
1082
- // One comment per cause: an edit that failed after its comment landed is
1083
- // answered again without a second comment.
1084
1112
  if (st.commented !== comment) {
1085
1113
  const c = this.gh("relabel", ["issue", "comment", String(st.ticket), "--body", comment]);
1086
1114
  if (c.status !== 0) return this.stop(withText(`gh issue comment failed (${c.how})`, c));
@@ -80,16 +80,39 @@ const DAY_MS = 86_400_000;
80
80
  // metrics — every one free, deterministic and known before dispatch
81
81
  // ---------------------------------------------------------------------------
82
82
 
83
+ /** An `## Agent Brief` heading line: what marks the comment the brief is taken from. */
84
+ const AGENT_BRIEF = /^## Agent Brief/m;
85
+
83
86
  /** The brief: the last comment carrying an `## Agent Brief` heading line, else the body. */
84
87
  export function briefText(issue) {
85
88
  const comments = Array.isArray(issue.comments) ? issue.comments : [];
86
89
  for (let i = comments.length - 1; i >= 0; i--) {
87
90
  const body = String(comments[i]?.body ?? "");
88
- if (/^## Agent Brief/m.test(body)) return body;
91
+ if (AGENT_BRIEF.test(body)) return body;
89
92
  }
90
93
  return String(issue.body ?? "");
91
94
  }
92
95
 
96
+ /** A ruling or triage heading line: what a decision posted outside an `## Agent Brief` comment is headed with. */
97
+ const RULING_HEADING = /^## (?:Ruling|Triage).*$/m;
98
+
99
+ /**
100
+ * The newest comment with a ruling or triage heading line and no `## Agent
101
+ * Brief` heading line that comes after the last comment `briefText` takes the
102
+ * brief from, or after none when there is no such comment, as its first ruling
103
+ * or triage heading line and its `url`; null when none is.
104
+ */
105
+ export function strandedRuling(issue) {
106
+ const comments = Array.isArray(issue.comments) ? issue.comments : [];
107
+ for (let i = comments.length - 1; i >= 0; i--) {
108
+ const body = String(comments[i]?.body ?? "");
109
+ if (AGENT_BRIEF.test(body)) return null;
110
+ const heading = RULING_HEADING.exec(body);
111
+ if (heading !== null) return { heading: heading[0], url: comments[i].url };
112
+ }
113
+ return null;
114
+ }
115
+
93
116
  /** The line span `[at, end)` of `lines`' `Out of scope` section, heading to the next heading of its level or above; null when there is none. */
94
117
  function outOfScopeSpan(lines) {
95
118
  const at = lines.findIndex((l) => /^#{1,6}\s*out of scope\b/i.test(l));
@@ -52,6 +52,20 @@ terminating in exactly one dispatch or an empty shortlist:
52
52
  scope` sequencing wherever that section is found. Then judge **decided?**
53
53
  — never size.
54
54
 
55
+ **A ruling the brief does not carry stops the Pull first.** The brief is
56
+ the last `## Agent Brief` comment, else the issue body, so a decision posted
57
+ under another heading never reaches any question the Pull asks. A comment
58
+ with a `## Ruling` or `## Triage` heading line and no `## Agent Brief`
59
+ heading line that is newer than the last `## Agent Brief` comment, or on a
60
+ ticket with none, is a stranded ruling. `pull.mjs` checks for one right
61
+ after the `ready-for-agent` label check, before printing the brief or
62
+ asking anything. On a hit it posts one comment naming the newest stranded
63
+ comment's heading line and URL, which says to rename that heading to
64
+ `## Agent Brief — ruling` and drop anything that is not decided scope, or
65
+ to delete the comment, then restore `ready-for-agent`. It then swaps
66
+ `ready-for-agent` for `ready-for-human` and leaves with `LEFT #N:
67
+ relabelled ready-for-human: stranded-ruling`.
68
+
55
69
  **Still live?** A backlog ticket's claims rot. Before `decided?`, settle the
56
70
  ticket's central claim against `origin/main` — never the working tree, never
57
71
  the ticket's own line numbers, which drift. One `git show origin/main:<file>`