@sprigr/apps-fleet-conventions 0.1.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/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @sprigr/apps-fleet-conventions
2
+
3
+ The Sprigr dev-fleet's GitHub comment conventions as pure functions: how a
4
+ tech-lead-reviewer verdict is written and parsed, and which half of the issue
5
+ backlog each fixer agent owns.
6
+
7
+ ```bash
8
+ npm install @sprigr/apps-fleet-conventions
9
+ ```
10
+
11
+ No runtime dependencies. Everything is a regex or a small function over a
12
+ comment body, an issue number, or an agent slug.
13
+
14
+ ## Why this package exists
15
+
16
+ Two codebases gate real behaviour on these conventions: the sprigr-team
17
+ platform (the merge gate in `packages/agent-core` and the fleet event router in
18
+ `workers/provisioning`) and the `github` marketplace app. Each hand-synced copy
19
+ of a regex has already failed in production once (sprigr-team#6334: two copies
20
+ of the verdict pattern went blind together on a markdown-bolded APPROVE). This
21
+ package is the single implementation. sprigr-team's `@sprigr/team-shared`
22
+ depends on it and re-exports it, so the platform and the app run the same code.
23
+
24
+ The sources are byte-for-byte ports of sprigr-team's
25
+ `packages/shared/src/utils/fleet-verdict.ts` and `fleet-shard.ts`, plus
26
+ `verdictNamedSha` from `packages/agent-core/src/tools/github-merge-gate.ts`.
27
+ Their doc comments carry the incident history behind every rule and reference
28
+ sprigr-team paths and issue numbers. Do not "tidy" a regex here without reading
29
+ that history: each odd clause closes a measured hole.
30
+
31
+ ## The verdict convention
32
+
33
+ The tech-lead-reviewer posts one machine-readable verdict per review round:
34
+
35
+ ```
36
+ <!-- verdict sha=69acb41 -->
37
+ [tech-lead-reviewer] VERDICT: APPROVE | one-line reason
38
+ ```
39
+
40
+ - Line 1 is the **machine marker** `<!-- verdict sha=<7-char head SHA> -->`.
41
+ It names the PR head the reviewer looked at, which is what lets the merge
42
+ gate refuse an approval that predates the current head. Prose that merely
43
+ quotes a verdict never carries the marker.
44
+ - Line 2 is the **identity prefix** `[tech-lead-reviewer]` followed by
45
+ `VERDICT:` and one ruling: `APPROVE`, `REQUEST_CHANGES` or `ESCALATE`.
46
+ Markdown emphasis (`**`, `_`), newlines and a leading heading are tolerated;
47
+ free prose between the prefix and `VERDICT:` is not. `REQUEST CHANGES` with a
48
+ space is folded to `REQUEST_CHANGES`.
49
+ - `APPROVE` and `REQUEST_CHANGES` are actionable by the merge gate. `ESCALATE`
50
+ is a legitimate verdict for humans that the gate must fail closed on.
51
+
52
+ ### Exports (verdict)
53
+
54
+ | export | what it is |
55
+ | --- | --- |
56
+ | `TECH_LEAD_VERDICT_RE` | Unanchored pattern for the two merge-actionable rulings. Use for "is there an APPROVE or REQUEST_CHANGES in this body". |
57
+ | `TECH_LEAD_VERDICT_FORM_RE` | Anchored to the start of the body, admits `ESCALATE`. The post-time form check; never used to authorize a merge. |
58
+ | `VERDICT_MARKER_RE` | Detects the machine marker anywhere in a body. |
59
+ | `VERDICT_MARKER_STRIP_RE` | Strips a LEADING marker so a fresh one can be prepended. Anchored on purpose. |
60
+ | `parseTechLeadVerdict(body)` | `'APPROVE' \| 'REQUEST_CHANGES' \| null` via the unanchored pattern. |
61
+ | `parseAnchoredVerdictRuling(body)` | `TechLeadRuling \| null` via the anchored form pattern, so a quoted verdict later in the body can never be read as the comment's own ruling. |
62
+ | `normalizeVerdictRuling(raw)` | Folds a captured ruling to canonical spelling (`REQUEST CHANGES` becomes `REQUEST_CHANGES`). |
63
+ | `isUnparsedVerdictShaped(body)` | COUNT-ONLY: looks like a verdict but did not parse. Never gate on it. |
64
+ | `isUnactionableRuling(body)` | Machine-form ruling the merge gate cannot act on (ESCALATE). The only shape a gate may fail closed on. |
65
+ | `isMalformedVerdictPost(body)` | POST-TIME ONLY: opens as a reviewer comment, states `VERDICT:` near the top, and fails the form pattern. Refuse the post so the author finds out now. |
66
+ | `verdictNamedSha(body)` | The head SHA a verdict names (marker first, then the `on head \`<sha>\`` prose form), lowercased, or `undefined`. |
67
+ | `TechLeadVerdict`, `TechLeadRuling` | The ruling union types. |
68
+
69
+ ## The shard convention
70
+
71
+ Since 2026-08-09 the fleet splits work by **issue-number parity** across every
72
+ sprigr repo: the primary Dev Fixer (`dev-fixer`) owns ODD issue numbers, Dev
73
+ Fixer B (`dev-fixer-b`) owns EVEN, and the two Issue Diagnosticians split the
74
+ same way. Parity balances load about 50/50 and depends on nothing mutable:
75
+ labels change, the number never does. Both halves still prioritise severity
76
+ within their shard.
77
+
78
+ ### Exports (shard)
79
+
80
+ | export | what it is |
81
+ | --- | --- |
82
+ | `issueShardParity(n)` | `'odd' \| 'even'`, or `null` for a non-integer. Callers must fail OPEN on `null` (show the item). |
83
+ | `FLEET_SHARD_OWNERS` | `{ fixer: { odd, even }, diagnostician: { odd, even } }` agent slugs per parity. |
84
+ | `shardParityForAgentSlug(slug)` | The parity an agent may work, or `null` for every agent outside the sharded fleet. Deliberately the two fixers only: the diagnosticians' step-0 recovery sweep is parity-exempt. |
85
+ | `isInShard(n, parity)` | Whether an issue number is inside a shard. Unclassifiable numbers answer `true`. |
86
+ | `ShardParity` | The parity type. |
87
+
88
+ ## Usage
89
+
90
+ ```ts
91
+ import {
92
+ parseAnchoredVerdictRuling,
93
+ verdictNamedSha,
94
+ isMalformedVerdictPost,
95
+ shardParityForAgentSlug,
96
+ isInShard,
97
+ } from '@sprigr/apps-fleet-conventions';
98
+
99
+ const ruling = parseAnchoredVerdictRuling(comment.body); // 'APPROVE' | 'REQUEST_CHANGES' | 'ESCALATE' | null
100
+ const reviewedHead = verdictNamedSha(comment.body); // '69acb41' | undefined
101
+
102
+ const parity = shardParityForAgentSlug(agentSlug); // null for a normal company agent
103
+ const visible = parity ? issues.filter((i) => isInShard(i.number, parity)) : issues;
104
+ ```
105
+
106
+ Pin an exact version, as with every `@sprigr/apps-*` package: the marketplace
107
+ build-runner runs a plain `npm install` per install build, so a caret range
108
+ would roll a new version into production with no app change and no review.
@@ -0,0 +1,244 @@
1
+ /**
2
+ * @sprigr/apps-fleet-conventions: tech-lead-reviewer verdict conventions.
3
+ *
4
+ * Ported byte-for-byte (apart from this header) from sprigr-team
5
+ * `packages/shared/src/utils/fleet-verdict.ts`, followed by `verdictNamedSha`
6
+ * and its pattern table from `packages/agent-core/src/tools/github-merge-gate.ts`,
7
+ * so the platform (`@sprigr/team-shared`) and the github marketplace app share
8
+ * one implementation instead of two hand-synced copies. The file paths named
9
+ * in the doc comments below point into sprigr-team, where the consumers live.
10
+ */
11
+ /**
12
+ * The single source of truth for parsing tech-lead-reviewer verdict comments.
13
+ *
14
+ * Two consumers gate real behaviour on this pattern: the merge gate in
15
+ * packages/agent-core/src/tools/github-merge-gate.ts (`assertApprovalIsCurrent`) and the
16
+ * fleet event router in workers/provisioning/src/github-event-router.ts
17
+ * (verdict-posted wakes). They previously carried two hand-synced copies of
18
+ * the regex; both were blind to markdown emphasis, and both failed together
19
+ * on the same comment (#6334).
20
+ *
21
+ * History (#6334): the reviewer posted a genuine APPROVE written as
22
+ * `[tech-lead-reviewer]\n\n**VERDICT: APPROVE**`. The old
23
+ * `\[tech-lead-reviewer\]\s*VERDICT:` pattern matched the newlines but not
24
+ * the `**`, so the merge gate reported the older REQUEST_CHANGES as "most
25
+ * recent" and the router never fired a wake — a message that read exactly
26
+ * like a stale/forged-verdict attack and manufactured a critical-severity
27
+ * security escalation. No forgery occurred; the parser could not survive
28
+ * bold. The fix is parser tolerance, not dropping the machine format:
29
+ * `[\s*_]*` admits whitespace/newlines plus `*`/`_` emphasis markers between
30
+ * the identity prefix, `VERDICT:`, and the ruling word.
31
+ */
32
+ declare const TECH_LEAD_VERDICT_RE: RegExp;
33
+ /**
34
+ * The canonical machine marker the reviewer persona puts on the first line of
35
+ * every verdict (`<!-- verdict sha=<7-char head SHA> -->`). Comments carrying
36
+ * it are machine artifacts by contract, which is what makes post-time
37
+ * validation of them safe: prose that merely QUOTES a verdict never carries
38
+ * the marker.
39
+ */
40
+ declare const VERDICT_MARKER_RE: RegExp;
41
+ /**
42
+ * Strips a LEADING machine marker so a fresh one can be prepended in its
43
+ * place (`add_pull_request_comment`'s unconditional re-derive, #6482).
44
+ * Anchored to the start of the body on purpose: a marker that shows up
45
+ * later in the body — e.g. inside quoted prose from an earlier comment — is
46
+ * not "the" marker and must never be touched. Exported so every caller that
47
+ * needs to replace a marker uses this one definition instead of a hand-rolled
48
+ * copy that could drift out of anchor.
49
+ */
50
+ declare const VERDICT_MARKER_STRIP_RE: RegExp;
51
+ /**
52
+ * Post-time form check: like TECH_LEAD_VERDICT_RE but also admitting the
53
+ * ESCALATE ruling, which is a legitimate verdict for humans that the merge
54
+ * gate deliberately cannot act on. Used to validate a verdict body BEFORE
55
+ * posting; never used to authorize a merge.
56
+ *
57
+ * Anchored to the START of the body (#6484): an unanchored version matched
58
+ * `[tech-lead-reviewer] VERDICT: <ruling>` anywhere in the text, including
59
+ * prose that only QUOTES a verdict rather than stating one. Real proof case:
60
+ * comment 5454053747 on #6470 is a `[dev-fixer specialist]` STATUS NOTE whose
61
+ * body opens with that prefix, not `[tech-lead-reviewer]`, but quotes
62
+ * `` `[tech-lead-reviewer] VERDICT: APPROVE` `` mid-sentence while recapping
63
+ * the PR's comment trail — the unanchored pattern matched that quote and
64
+ * would have routed the note through the CI-gate/marker-attach path in
65
+ * `add_pull_request_comment` as if it were a real verdict. The allowed prefix
66
+ * is `(optional machine marker)(optional markdown heading)(optional leading
67
+ * emphasis)[tech-lead-reviewer]` — the marker and heading are real, observed
68
+ * reviewer habits that still precede the identity prefix at the true start of
69
+ * a body (see `VERDICT_MARKER_STRIP_RE` and the `HEADERED_VERDICT` fixture
70
+ * below); prose between `[tech-lead-reviewer]` and `VERDICT:` itself still
71
+ * fails to parse, unchanged from before.
72
+ *
73
+ * Round 2 (#6484): the first anchored cut above admitted the marker and the
74
+ * heading but not leading `**`/`_` emphasis directly on the identity prefix,
75
+ * which is a live reviewer habit and not a hypothetical — comments
76
+ * `5451246349` and `5451314888` on #6456 both open
77
+ * `<!-- verdict sha=69acb41 -->\n**[tech-lead-reviewer] VERDICT: …**` and fell
78
+ * out of the anchored form entirely. That silently re-opened the #6334 hole
79
+ * this pattern exists to close: `applyVerdictCiGate` passed the bolded body
80
+ * through untouched (no CI gate, no marker re-derive), and separately
81
+ * `isUnactionableRuling` stopped recognizing a bolded ESCALATE as a ruling the
82
+ * merge gate must fail closed on. `[*_]*` ahead of the prefix admits the
83
+ * emphasis the same way `[\s*_]*` already does between the prefix and
84
+ * `VERDICT:`, without loosening the anchor itself.
85
+ */
86
+ declare const TECH_LEAD_VERDICT_FORM_RE: RegExp;
87
+ type TechLeadVerdict = 'APPROVE' | 'REQUEST_CHANGES';
88
+ /**
89
+ * Fold a captured ruling to its canonical spelling.
90
+ *
91
+ * Both patterns admit `REQUEST CHANGES` with a SPACE as well as the canonical
92
+ * `REQUEST_CHANGES`, so every capture must come through here before it is used
93
+ * or compared — `github-merge-gate.ts` casts the raw capture straight to the
94
+ * union type, which would otherwise put the literal string "REQUEST CHANGES"
95
+ * into a field typed `'REQUEST_CHANGES'` and make every later equality check
96
+ * silently false.
97
+ *
98
+ * Why admit the space at all rather than refusing it (#7653): measured over
99
+ * 60 h, 19 of 39 non-parsing reviewer comments differed from the contract by
100
+ * exactly this one character. It is unambiguous — no other ruling it could
101
+ * mean — and every consumer treats the two identically, so folding it is
102
+ * strictly safer than leaving those verdicts INVISIBLE to the merge gate,
103
+ * which is what the strict spelling was actually achieving. The canonical form
104
+ * stays `REQUEST_CHANGES` (scripts/seed-fleet-agents.ts) and is what every
105
+ * consumer sees; this only stops a separator from voiding a real ruling.
106
+ */
107
+ declare function normalizeVerdictRuling(raw: string): TechLeadRuling;
108
+ type TechLeadRuling = 'APPROVE' | 'REQUEST_CHANGES' | 'ESCALATE';
109
+ /**
110
+ * Parse a comment body's ruling using the ANCHORED TECH_LEAD_VERDICT_FORM_RE
111
+ * pattern (admits ESCALATE), never the unanchored TECH_LEAD_VERDICT_RE.
112
+ *
113
+ * #6584 round 2: the duplicate-verdict backstop in
114
+ * packages/agent-core/src/tools/github.ts originally read both sides of the
115
+ * comparison with parseTechLeadVerdict (unanchored, APPROVE|REQUEST_CHANGES
116
+ * only). An ESCALATE comment that QUOTES the verdict it supersedes — e.g.
117
+ * "...supersedes my earlier `[tech-lead-reviewer] VERDICT: APPROVE` note" —
118
+ * has no APPROVE or REQUEST_CHANGES at the true start of its body, but the
119
+ * unanchored reader matched the quoted text anywhere in the string and
120
+ * reported the whole comment as APPROVE. That misclassified a non-terminal
121
+ * ESCALATE as the terminal ruling, which refused every later verdict on that
122
+ * head — reproducing the exact #6584 deadlock inside the escalation lane.
123
+ * This reader is anchored to the start of the body (same anchor as
124
+ * TECH_LEAD_VERDICT_FORM_RE itself), so a quote appearing later can never be
125
+ * mistaken for the comment's own ruling.
126
+ */
127
+ declare function parseAnchoredVerdictRuling(body: string): TechLeadRuling | null;
128
+ /** Parse a comment body into a verdict, or null if none parses. */
129
+ declare function parseTechLeadVerdict(body: string): TechLeadVerdict | null;
130
+ /**
131
+ * True when a body looks like a reviewer verdict but the strict pattern
132
+ * cannot parse a ruling out of it. COUNT-ONLY: built for reporting ("N
133
+ * comments look like verdicts but did not parse"), never for blocking or
134
+ * authorizing — the loose shape matches any reviewer note that merely says
135
+ * the word "verdict", and using it to block parked an already-approved PR in
136
+ * the #6367 round-2 measurement: 80 real comments replayed across 8 PRs,
137
+ * 2 blocked, 1 falsely (comment 5429229662 on #6344, a consult note posted
138
+ * after its own APPROVE). Source: the round-2 verdict on PR #6367, which
139
+ * records the PR list and per-comment results.
140
+ * A gate that finds any of these MUST say so in its output instead of
141
+ * reporting the newest PARSEABLE verdict as "the most recent verdict" —
142
+ * stating more than it measured is what escalated #6334 from a formatting
143
+ * bug to a security incident.
144
+ */
145
+ declare function isUnparsedVerdictShaped(body: string): boolean;
146
+ /**
147
+ * True when a body states a machine-form `VERDICT: <ruling>` whose ruling the
148
+ * merge gate cannot act on (today: ESCALATE TO A HUMAN). This is the ONLY
149
+ * shape a gate may fail closed on: it is a deliberate ruling in the machine
150
+ * format, not prose that happens to mention verdicts. Malformed rulings that
151
+ * fail even the FORM pattern are prevented at post time instead
152
+ * (add_pull_request_comment's marker validation).
153
+ */
154
+ declare function isUnactionableRuling(body: string): boolean;
155
+ /**
156
+ * POST-TIME ONLY: this body is trying to be a verdict and is malformed.
157
+ *
158
+ * True when the body opens as a reviewer comment AND states a `VERDICT:` line
159
+ * near the top AND does not match `TECH_LEAD_VERDICT_FORM_RE`. Used by
160
+ * `applyVerdictCiGate` to refuse the post outright, so the author learns at
161
+ * post time instead of the verdict being silently invisible to the merge gate
162
+ * for hours.
163
+ *
164
+ * NOT `isUnparsedVerdictShaped`, which its own docstring reserves for COUNTING
165
+ * and which must never gate: that one is unanchored (so it fires on any note
166
+ * quoting a verdict — measured, it flags four `[dev-fixer]` status notes here)
167
+ * and it reads with `parseTechLeadVerdict`, which does not admit ESCALATE, so
168
+ * gating on it would refuse legitimate ESCALATE verdicts. Both were measured
169
+ * against 95 live comments before this predicate was written (#7653).
170
+ *
171
+ * Fail-open by construction: a reviewer note that states no `VERDICT:` at all
172
+ * is a consult and passes untouched (11 of 39 non-parsing bodies in that
173
+ * sample), as does anything not opening with the reviewer's own prefix.
174
+ */
175
+ declare function isMalformedVerdictPost(body: string): boolean;
176
+ /** The head SHA a verdict names, from whichever form it used. */
177
+ declare function verdictNamedSha(body: string): string | undefined;
178
+
179
+ /**
180
+ * @sprigr/apps-fleet-conventions: the fleet's odd/even issue shard rule.
181
+ *
182
+ * Ported byte-for-byte (apart from this header) from sprigr-team
183
+ * `packages/shared/src/utils/fleet-shard.ts`, so the platform
184
+ * (`@sprigr/team-shared`) and the github marketplace app share one
185
+ * implementation. The file paths named in the doc comments below point into
186
+ * sprigr-team, where the consumers live.
187
+ */
188
+ /**
189
+ * The single source of truth for the fleet's issue-number SHARD rule.
190
+ *
191
+ * Since 2026-08-09 the fleet splits work by issue-number PARITY across every
192
+ * sprigr repo: the primary Dev Fixer owns ODD, Dev Fixer B owns EVEN, and the
193
+ * two Issue Diagnosticians split the same way. Parity balances the load ~50/50,
194
+ * depends on nothing mutable (labels change, the number never does), and both
195
+ * halves still prioritise severity WITHIN their shard.
196
+ *
197
+ * Three surfaces need the rule and used to state it separately: the personas
198
+ * (prose), the fleet event router (`workers/provisioning/src/github-event-router.ts`),
199
+ * and now the `github` tool's `list_issues` candidate scan. This module is the
200
+ * one definition they share, in the same spirit as `fleet-verdict.ts`.
201
+ *
202
+ * WHY THE TOOL FILTERS AT ALL (sprigr-team#6581). The shard was enforced only
203
+ * by a model choosing to obey an instruction on each pick, and it was measured
204
+ * being violated in both directions: seven `fix/issue-*` branches (the ODD
205
+ * owner) opened on EVEN issues, and one `fixb/issue-*` branch on an ODD issue.
206
+ * #6598 was implemented twice as a result (PRs #6603 and #6604, the second
207
+ * discarded as a duplicate). Rewording the personas did not close it: the
208
+ * expired `(no shard test while you are the only fixer)` carve-out was deleted
209
+ * in PR #6648 and verified live, and a violation still landed 4h10m later.
210
+ * A wrong-parity issue the agent can see is a wrong-parity issue the agent can
211
+ * pick, so the fix is to keep it out of the candidate list.
212
+ */
213
+ /** Which half of the fleet an issue number belongs to. */
214
+ type ShardParity = 'odd' | 'even';
215
+ /**
216
+ * The parity of an issue number. Non-integers are not issue numbers and have
217
+ * no shard; callers must fail OPEN on `null` (show the item) rather than hide
218
+ * something they could not classify.
219
+ */
220
+ declare function issueShardParity(issueNumber: number): ShardParity | null;
221
+ /** The agent slug that owns each parity, per family. */
222
+ declare const FLEET_SHARD_OWNERS: {
223
+ readonly fixer: {
224
+ readonly odd: "dev-fixer";
225
+ readonly even: "dev-fixer-b";
226
+ };
227
+ readonly diagnostician: {
228
+ readonly odd: "issue-diagnostician-v2";
229
+ readonly even: "issue-diagnostician-b";
230
+ };
231
+ };
232
+ /**
233
+ * The parity an agent may work, or `null` for every agent outside the sharded
234
+ * fleet (which is almost all of them: a normal company agent must never have
235
+ * its issue list silently halved).
236
+ */
237
+ declare function shardParityForAgentSlug(slug: string | null | undefined): ShardParity | null;
238
+ /**
239
+ * Is this issue number inside `parity`'s shard? Unclassifiable numbers answer
240
+ * `true` (fail open) for the reason given on `issueShardParity`.
241
+ */
242
+ declare function isInShard(issueNumber: number, parity: ShardParity): boolean;
243
+
244
+ export { FLEET_SHARD_OWNERS, type ShardParity, TECH_LEAD_VERDICT_FORM_RE, TECH_LEAD_VERDICT_RE, type TechLeadRuling, type TechLeadVerdict, VERDICT_MARKER_RE, VERDICT_MARKER_STRIP_RE, isInShard, isMalformedVerdictPost, isUnactionableRuling, isUnparsedVerdictShaped, issueShardParity, normalizeVerdictRuling, parseAnchoredVerdictRuling, parseTechLeadVerdict, shardParityForAgentSlug, verdictNamedSha };
package/dist/index.js ADDED
@@ -0,0 +1,89 @@
1
+ // src/verdict.ts
2
+ var TECH_LEAD_VERDICT_RE = /\[tech-lead-reviewer\][\s*_]*VERDICT:[\s*_]*(APPROVE|REQUEST[_ ]CHANGES)/i;
3
+ var VERDICT_MARKER_RE = /<!--\s*verdict\s+sha=/i;
4
+ var VERDICT_MARKER_STRIP_RE = /^\s*<!--\s*verdict\s+sha=[^>]*-->\s*\n?/i;
5
+ var VERDICT_SHAPED_RE = /\[tech-lead-reviewer\][\s\S]{0,80}?VERDICT/i;
6
+ var TECH_LEAD_VERDICT_FORM_RE = /^(?:<!--\s*verdict\s+sha=[^>]*-->\s*\n?)?\s*(?:#{1,6}\s*)?[*_]*\[tech-lead-reviewer\][\s*_]*VERDICT:[\s*_]*(APPROVE|REQUEST[_ ]CHANGES|ESCALATE)/i;
7
+ function normalizeVerdictRuling(raw) {
8
+ return raw.toUpperCase().replace(/\s+/g, "_");
9
+ }
10
+ function parseAnchoredVerdictRuling(body) {
11
+ const m = body.match(TECH_LEAD_VERDICT_FORM_RE);
12
+ return m ? normalizeVerdictRuling(m[1]) : null;
13
+ }
14
+ function parseTechLeadVerdict(body) {
15
+ const m = body.match(TECH_LEAD_VERDICT_RE);
16
+ return m ? normalizeVerdictRuling(m[1]) : null;
17
+ }
18
+ function isUnparsedVerdictShaped(body) {
19
+ return VERDICT_SHAPED_RE.test(body) && parseTechLeadVerdict(body) === null;
20
+ }
21
+ function isUnactionableRuling(body) {
22
+ return TECH_LEAD_VERDICT_FORM_RE.test(body) && parseTechLeadVerdict(body) === null;
23
+ }
24
+ var REVIEWER_ANCHOR_RE = /^(?:<!--\s*verdict\s+sha=[^>]*-->\s*\n?)?\s*(?:#{1,6}\s*)?[*_]*\[tech-lead-reviewer\]/i;
25
+ var VERDICT_STATEMENT_WINDOW = 800;
26
+ function isMalformedVerdictPost(body) {
27
+ if (!REVIEWER_ANCHOR_RE.test(body)) return false;
28
+ if (!/VERDICT:/i.test(body.slice(0, VERDICT_STATEMENT_WINDOW))) return false;
29
+ return !TECH_LEAD_VERDICT_FORM_RE.test(body);
30
+ }
31
+ var VERDICT_HEAD_SHA_PATTERNS = [
32
+ // Canonical machine marker. seed-fleet-agents.ts mandates it as the FIRST
33
+ // line of every verdict ("<!-- verdict sha=<7-char head SHA> -->") and calls
34
+ // it "what makes your verdict idempotent - every owed-a-verdict predicate
35
+ // keys on it". It was NOT read here, so the strong server-side comparison
36
+ // below was skipped for template-compliant verdicts and the weak
37
+ // committer-date fallback ran instead (#6054). Verified against four real
38
+ // verdict bodies on PRs #6063/#6092/#6087: none matched the prose form.
39
+ /<!--\s*verdict\s+sha=\s*`?([0-9a-f]{7,40})`?\s*-->/i,
40
+ // Prose form, kept for verdicts that spell it out ("First verdict on head
41
+ // `<sha>`" / "Round N of 2 on head `<sha>`").
42
+ /\bon head\s+`?([0-9a-f]{7,40})`?/i
43
+ ];
44
+ function verdictNamedSha(body) {
45
+ for (const re of VERDICT_HEAD_SHA_PATTERNS) {
46
+ const m = body.match(re);
47
+ if (m?.[1]) return m[1].toLowerCase();
48
+ }
49
+ return void 0;
50
+ }
51
+
52
+ // src/shard.ts
53
+ function issueShardParity(issueNumber) {
54
+ if (!Number.isInteger(issueNumber)) return null;
55
+ return Math.abs(issueNumber) % 2 === 1 ? "odd" : "even";
56
+ }
57
+ var FLEET_SHARD_OWNERS = {
58
+ fixer: { odd: "dev-fixer", even: "dev-fixer-b" },
59
+ diagnostician: { odd: "issue-diagnostician-v2", even: "issue-diagnostician-b" }
60
+ };
61
+ var SHARD_FILTERED_AGENT_SLUGS = {
62
+ [FLEET_SHARD_OWNERS.fixer.odd]: "odd",
63
+ [FLEET_SHARD_OWNERS.fixer.even]: "even"
64
+ };
65
+ function shardParityForAgentSlug(slug) {
66
+ if (!slug) return null;
67
+ return SHARD_FILTERED_AGENT_SLUGS[slug] ?? null;
68
+ }
69
+ function isInShard(issueNumber, parity) {
70
+ const own = issueShardParity(issueNumber);
71
+ return own === null || own === parity;
72
+ }
73
+ export {
74
+ FLEET_SHARD_OWNERS,
75
+ TECH_LEAD_VERDICT_FORM_RE,
76
+ TECH_LEAD_VERDICT_RE,
77
+ VERDICT_MARKER_RE,
78
+ VERDICT_MARKER_STRIP_RE,
79
+ isInShard,
80
+ isMalformedVerdictPost,
81
+ isUnactionableRuling,
82
+ isUnparsedVerdictShaped,
83
+ issueShardParity,
84
+ normalizeVerdictRuling,
85
+ parseAnchoredVerdictRuling,
86
+ parseTechLeadVerdict,
87
+ shardParityForAgentSlug,
88
+ verdictNamedSha
89
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@sprigr/apps-fleet-conventions",
3
+ "version": "0.1.0",
4
+ "description": "Sprigr dev-fleet GitHub comment conventions: tech-lead verdict parsing and the odd/even issue shard rule",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "build": "tsup src/index.ts --format esm --dts --out-dir dist --clean --tsconfig tsconfig.build.json",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.6.0",
23
+ "vitest": "^2.0.0"
24
+ },
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/sprigr/sprigr-app-kit.git",
29
+ "directory": "packages/fleet-conventions"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md"
34
+ ]
35
+ }