@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 +108 -0
- package/dist/index.d.ts +244 -0
- package/dist/index.js +89 -0
- package/package.json +35 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|