@1aboveio/skills 0.19.0 → 0.19.2

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": "@1aboveio/skills",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "Install the 1AboveIO first-party skill groups through the native Skills CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "id": "first-party",
9
9
  "type": "first-party",
10
10
  "package": "@1aboveio/skills",
11
- "version": "0.19.0",
11
+ "version": "0.19.2",
12
12
  "updatePolicy": "pinned-npm-version",
13
13
  "members": [
14
14
  {
@@ -208,7 +208,7 @@
208
208
  "sourceId": "first-party",
209
209
  "sourceType": "first-party",
210
210
  "package": "@1aboveio/skills",
211
- "version": "0.19.0",
211
+ "version": "0.19.2",
212
212
  "installPath": null,
213
213
  "members": [
214
214
  "harness-runtime",
@@ -244,7 +244,7 @@
244
244
  "sourceId": "first-party",
245
245
  "sourceType": "first-party",
246
246
  "package": "@1aboveio/skills",
247
- "version": "0.19.0",
247
+ "version": "0.19.2",
248
248
  "installPath": null,
249
249
  "members": [
250
250
  "harness-runtime",
@@ -290,7 +290,7 @@
290
290
  "sourceId": "first-party",
291
291
  "sourceType": "first-party",
292
292
  "package": "@1aboveio/skills",
293
- "version": "0.19.0",
293
+ "version": "0.19.2",
294
294
  "installPath": null,
295
295
  "members": [
296
296
  "harness-runtime",
@@ -323,7 +323,7 @@
323
323
  "sourceId": "first-party",
324
324
  "sourceType": "first-party",
325
325
  "package": "@1aboveio/skills",
326
- "version": "0.19.0",
326
+ "version": "0.19.2",
327
327
  "installPath": null,
328
328
  "members": [
329
329
  "harness-runtime",
@@ -366,7 +366,7 @@
366
366
  "sourceId": "first-party",
367
367
  "sourceType": "first-party",
368
368
  "package": "@1aboveio/skills",
369
- "version": "0.19.0",
369
+ "version": "0.19.2",
370
370
  "installPath": null,
371
371
  "members": [
372
372
  "harness-runtime",
@@ -394,7 +394,7 @@
394
394
  "sourceId": "first-party",
395
395
  "sourceType": "first-party",
396
396
  "package": "@1aboveio/skills",
397
- "version": "0.19.0",
397
+ "version": "0.19.2",
398
398
  "installPath": null,
399
399
  "members": [
400
400
  "harness-runtime",
@@ -432,7 +432,7 @@
432
432
  "sourceId": "first-party",
433
433
  "sourceType": "first-party",
434
434
  "package": "@1aboveio/skills",
435
- "version": "0.19.0",
435
+ "version": "0.19.2",
436
436
  "installPath": null,
437
437
  "members": [
438
438
  "harness-runtime",
@@ -460,7 +460,7 @@
460
460
  "sourceId": "first-party",
461
461
  "sourceType": "first-party",
462
462
  "package": "@1aboveio/skills",
463
- "version": "0.19.0",
463
+ "version": "0.19.2",
464
464
  "installPath": null,
465
465
  "members": [
466
466
  "harness-runtime",
@@ -498,7 +498,7 @@
498
498
  "sourceId": "first-party",
499
499
  "sourceType": "first-party",
500
500
  "package": "@1aboveio/skills",
501
- "version": "0.19.0",
501
+ "version": "0.19.2",
502
502
  "installPath": null,
503
503
  "members": [
504
504
  "harness-runtime",
@@ -526,7 +526,7 @@
526
526
  "sourceId": "first-party",
527
527
  "sourceType": "first-party",
528
528
  "package": "@1aboveio/skills",
529
- "version": "0.19.0",
529
+ "version": "0.19.2",
530
530
  "installPath": null,
531
531
  "members": [
532
532
  "harness-runtime",
@@ -564,7 +564,7 @@
564
564
  "sourceId": "first-party",
565
565
  "sourceType": "first-party",
566
566
  "package": "@1aboveio/skills",
567
- "version": "0.19.0",
567
+ "version": "0.19.2",
568
568
  "installPath": null,
569
569
  "members": [
570
570
  "harness-runtime",
@@ -596,7 +596,7 @@
596
596
  "sourceId": "first-party",
597
597
  "sourceType": "first-party",
598
598
  "package": "@1aboveio/skills",
599
- "version": "0.19.0",
599
+ "version": "0.19.2",
600
600
  "installPath": null,
601
601
  "members": [
602
602
  "harness-runtime",
@@ -636,7 +636,7 @@
636
636
  "sourceId": "first-party",
637
637
  "sourceType": "first-party",
638
638
  "package": "@1aboveio/skills",
639
- "version": "0.19.0",
639
+ "version": "0.19.2",
640
640
  "installPath": null,
641
641
  "members": [
642
642
  "harness-runtime"
@@ -662,7 +662,7 @@
662
662
  "sourceId": "first-party",
663
663
  "sourceType": "first-party",
664
664
  "package": "@1aboveio/skills",
665
- "version": "0.19.0",
665
+ "version": "0.19.2",
666
666
  "installPath": null,
667
667
  "members": [
668
668
  "harness-runtime"
@@ -694,7 +694,7 @@
694
694
  "sourceId": "first-party",
695
695
  "sourceType": "first-party",
696
696
  "package": "@1aboveio/skills",
697
- "version": "0.19.0",
697
+ "version": "0.19.2",
698
698
  "installPath": null,
699
699
  "members": [
700
700
  "harness-runtime",
@@ -749,7 +749,7 @@
749
749
  "sourceId": "first-party",
750
750
  "sourceType": "first-party",
751
751
  "package": "@1aboveio/skills",
752
- "version": "0.19.0",
752
+ "version": "0.19.2",
753
753
  "installPath": null,
754
754
  "members": [
755
755
  "harness-runtime",
@@ -119,7 +119,7 @@ export const WORKFLOW_TRUSTED_SOURCES = deepFreeze({
119
119
  id: 'first-party',
120
120
  type: 'first-party',
121
121
  package: '@1aboveio/skills',
122
- version: '0.19.0',
122
+ version: '0.19.2',
123
123
  },
124
124
  matt: {
125
125
  id: 'matt-pocock',
@@ -20,12 +20,17 @@ The identical observation means opposite things on either side of queue entry:
20
20
  |---|---|---|
21
21
  | active queue membership absent | `NOT_YET_QUEUED` — healthy, keep waiting | **`DEQUEUED`** — terminal, read the reason |
22
22
 
23
- So the watcher's memory *is* the verdict. Four pieces of state must survive across polls:
23
+ So the watcher's memory *is* the verdict. Five pieces of state must survive across polls:
24
24
 
25
25
  1. **`seenQueued`** — has queue entry ever been observed? Every downstream transition depends on it. Set from the `queued` label **or** `mergify queue show`.
26
26
  2. **A re-read counter** (`missingActiveReads` / `absentStreak`) — one absence is suspicion, two consecutive *readable* absences settle `DEQUEUED`.
27
27
  3. **Readable vs. absent** — a failed `gh`/`mergify` call is *no information*: it must not advance that counter or overwrite that source's last readable contribution to the stall-state snapshot. During a partial outage, the still-readable sibling remains informative and genuine progress from it still resets the change clock.
28
28
  4. **A change clock** (`lastChangeMs`) — the stall bound is *no observable change*, not elapsed time.
29
+ 5. **The last synthetic batch PR number** — retain
30
+ `mergeability_check.queue_pull_request_number` from every readable active queue
31
+ observation (and accept the older top-level spelling). Mergify removes that relationship
32
+ from `queue show` after dequeue, but the terminal reason belongs to the synthetic batch,
33
+ not the original PR.
29
34
 
30
35
  ### Why a per-poll predicate cannot satisfy this
31
36
 
@@ -149,14 +154,24 @@ Suggested routing classes:
149
154
 
150
155
  | raw reason shape | route |
151
156
  |---|---|
152
- | `checks-failed` | fix round, fresh verdict, new enqueue |
157
+ | batch check `FAILURE`, `ERROR`, or `TIMED_OUT` | `checks-failed`; fix round, fresh verdict, new enqueue |
158
+ | batch checks cancelled before validation executes | `checks-cancelled`; report the cancelled checks and hand back so an unchanged-SHA requeue can be considered |
153
159
  | `pull-request-updated`, `draft-pull-request-changed` | HEAD moved; verdict stale; re-enter delivery after fresh validation |
154
160
  | `conflict`, `conflict-with-base-branch` | rebase/evidence refresh, then enqueue |
155
161
  | `conditions-unmet`, `frozen`, `manual`, `pr-manually-dequeued`, `checks-timeout` | hand back with the condition/reason quoted |
156
- | unknown | hand back; fail closed |
162
+ | unknown | `unknown`; hand back, fail closed, and preserve `rawReason` |
157
163
 
158
164
  When the reason is not machine-readable, quote the Mergify status comment, queue show condition leaf, or event-log URL in `rawReason` rather than inventing a reason.
159
165
 
166
+ The selected `state:"dequeued"` Mergify status comment is parsed as structured evidence:
167
+ `dequeuedAtMs`, `rawReason`, its draft PR fallback, and provider-reported failing checks. On
168
+ dequeue, the watcher queries the retained batch PR once and attaches its failed or cancelled
169
+ `statusCheckRollup` entries to the terminal verdict. A missing or deleted batch PR remains a
170
+ truthful `DEQUEUED`: `batchPr.readable` is false, the retained number and `rawReason` survive,
171
+ and any checks named by the provider comment remain available. An unchanged-SHA requeue can
172
+ produce a new synthetic PR; every readable active observation replaces the retained number,
173
+ so the newest batch wins over stale comment fallback.
174
+
160
175
  ## Watcher Requirements
161
176
 
162
177
  - Run as one background task or one script invocation; do not spend foreground turns polling.
@@ -195,13 +210,16 @@ Output streams:
195
210
  | stdout | exactly one pretty-printed terminal JSON verdict, at exit — carrying `phases`, `seenQueued`, and the outcome |
196
211
  | stderr | one compact JSON record per poll (`progress: true`), with the live outcome, PR labels, queue membership/position, `phases`, `changed`, and `sinceChangeMs` — the stall clock made visible |
197
212
 
198
- Each subprocess read (`gh pr view`, `mergify queue show`) also carries its own bounded
213
+ Each regular-poll subprocess read (`gh pr view`, `mergify queue show`) also carries its own bounded
199
214
  timeout — a small multiple of `--interval-seconds`, clamped to never exceed the **effective
200
215
  phase stall bound** (pre-admission: `--admission-stall-minutes`; post-entry: `--stall-minutes`)
201
216
  capped by `--max-minutes`, then divided by the number of reads made per poll (2 here:
202
217
  `gh pr view` + `mergify queue show`). The two reads together cannot outlive the bound that will
203
218
  actually fire `STALLED` on this poll — so a hung pre-admission poll cannot outlive the short
204
219
  admission clock, and a post-entry poll is not permanently clamped to the admission value.
220
+ After a terminal dequeue classification, one additional bounded `gh pr view` reads the
221
+ retained synthetic batch PR. It enriches the already-settled verdict and never participates
222
+ in queue membership, the absence counter, or the stall clock.
205
223
  Those stall/ceiling bounds are only evaluated after a poll returns, and a single hung read
206
224
  cannot alone consume more than half of a small stall/ceiling bound. (The sibling watcher makes
207
225
  one read per poll, so its clamp divides by 1 and the single-read guarantee is the whole bound
@@ -162,6 +162,7 @@ export function observationKey({ pr = {}, queue = {} } = {}) {
162
162
  queue.queued ?? null,
163
163
  queue.queueState ?? queue.state ?? null,
164
164
  queue.position ?? null,
165
+ queue.batchPrNumber ?? null,
165
166
  ])
166
167
  }
167
168
 
@@ -181,6 +182,7 @@ function stallQueueSnapshot(queue = {}) {
181
182
  queued: queue.queued ?? null,
182
183
  queueState: queue.queueState ?? queue.state ?? null,
183
184
  position: queue.position ?? null,
185
+ batchPrNumber: queue.batchPrNumber ?? null,
184
186
  }
185
187
  }
186
188
 
@@ -413,13 +415,16 @@ export function summarizeStatusChecks(rollup) {
413
415
  // (`startedMs`) — a locally-slow clock could in principle make a stale dequeue read as fresh;
414
416
  // low risk on an NTP-synced host, called out here rather than silently assumed away (NOTE-2).
415
417
  //
416
- // Returns epoch milliseconds, or null when no mergify comment's payload reports
417
- // `state: "dequeued"`, or that comment has no parseable "Left the queue" line callers must
418
- // treat null as "no reliable timestamp", never as "not dequeued".
418
+ // Parse the selected dequeue comment once and retain all provider evidence used by the
419
+ // terminal verdict. The rendered comment is also the only surviving source after Mergify's
420
+ // queue API drops active membership and its synthetic PR relationship.
419
421
  const MERGIFY_PAYLOAD_RE = /-\*- Mergify Payload -\*-\s*\n(\{[\s\S]*?\})\s*\n-\*- Mergify Payload End -\*-/
420
422
  const LEFT_QUEUE_RE = /Left the queue\*\*[^`\n]*`(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}) UTC`/g
423
+ const BATCH_PR_RE = /\bon draft #(\d+)\b/i
424
+ const REASON_RE = /^## Reason\s*\n+([^\n]+)/m
425
+ const PROVIDER_CHECK_RE = /^-\s*(❌|🟠|🛑)\s*\[([^\]]+)\]\(([^)]+)\)/gmu
421
426
 
422
- export function dequeuedAtFromComments(comments) {
427
+ export function dequeueInfoFromComments(comments) {
423
428
  const list = Array.isArray(comments) ? comments : []
424
429
  const mergifyComments = list.filter((c) => c && c.author && c.author.login === 'mergify')
425
430
  let target = null
@@ -431,15 +436,36 @@ export function dequeuedAtFromComments(comments) {
431
436
  try { payload = JSON.parse(payloadMatch[1]) } catch { continue }
432
437
  if (payload && payload.state === 'dequeued') target = comment // last match wins — comments are chronological
433
438
  }
434
- if (!target) return null
439
+ if (!target) return { dequeuedAtMs: null, rawReason: null, batchPrNumber: null, providerFailedChecks: [] }
435
440
  const body = typeof target.body === 'string' ? target.body : ''
436
441
  let match
437
442
  let last = null
438
443
  LEFT_QUEUE_RE.lastIndex = 0
439
444
  while ((match = LEFT_QUEUE_RE.exec(body))) last = match
440
- if (!last) return null
441
- const [, y, mo, d, h, mi] = last
442
- return Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi))
445
+ const dequeuedAtMs = last
446
+ ? Date.UTC(Number(last[1]), Number(last[2]) - 1, Number(last[3]), Number(last[4]), Number(last[5]))
447
+ : null
448
+ const reasonMatch = REASON_RE.exec(body)
449
+ const batchMatch = BATCH_PR_RE.exec(body)
450
+ const providerFailedChecks = []
451
+ PROVIDER_CHECK_RE.lastIndex = 0
452
+ while ((match = PROVIDER_CHECK_RE.exec(body))) {
453
+ providerFailedChecks.push({
454
+ name: match[2],
455
+ status: match[1] === '🟠' ? 'CANCELLED' : (match[1] === '🛑' ? 'TIMED_OUT' : 'FAILURE'),
456
+ detailsUrl: match[3] || null,
457
+ })
458
+ }
459
+ return {
460
+ dequeuedAtMs,
461
+ rawReason: reasonMatch?.[1]?.trim() || null,
462
+ batchPrNumber: batchMatch ? Number(batchMatch[1]) : null,
463
+ providerFailedChecks,
464
+ }
465
+ }
466
+
467
+ export function dequeuedAtFromComments(comments) {
468
+ return dequeueInfoFromComments(comments).dequeuedAtMs
443
469
  }
444
470
 
445
471
  // Exported so tests can pin that the live read timeout uses the same phase-aware bound the
@@ -470,6 +496,7 @@ function prObservation({ repo, pr, intervalSeconds, stallMinutes, admissionStall
470
496
  const data = parseJson(result.stdout)
471
497
  if (!data) return { readable: false, error: 'gh pr view returned invalid JSON' }
472
498
  const checks = summarizeStatusChecks(data.statusCheckRollup)
499
+ const dequeueInfo = dequeueInfoFromComments(data.comments)
473
500
  return {
474
501
  readable: true,
475
502
  state: data.state || null,
@@ -478,7 +505,8 @@ function prObservation({ repo, pr, intervalSeconds, stallMinutes, admissionStall
478
505
  headRefOid: data.headRefOid || null,
479
506
  labels: (data.labels || []).map((label) => label.name).filter(Boolean),
480
507
  url: data.url || null,
481
- dequeuedAtMs: dequeuedAtFromComments(data.comments),
508
+ dequeuedAtMs: dequeueInfo.dequeuedAtMs,
509
+ dequeueInfo,
482
510
  ...checks,
483
511
  }
484
512
  }
@@ -514,12 +542,46 @@ export function queueObservation({ pr, repo, intervalSeconds, stallMinutes, admi
514
542
  queued: parsed.queued === true || Boolean(parsed.queue_rule_name || parsed.queue_rule),
515
543
  queueState: parsed.status || parsed.state || parsed.mergeability_check?.state || null,
516
544
  position: typeof parsed.position === 'number' ? parsed.position : null,
545
+ batchPrNumber: positivePrNumber(
546
+ parsed.mergeability_check?.queue_pull_request_number
547
+ ?? parsed.queue_pull_request_number
548
+ ?? parsed.speculative_check_pr,
549
+ ),
517
550
  raw: parsed,
518
551
  }
519
552
  }
520
553
  const text = `${result.stdout}\n${result.stderr}`.trim()
521
- if (/not in the merge queue|not queued|not found/i.test(text)) return { readable: true, queued: false, queueState: 'not-queued', position: null, raw: text }
522
- return { readable: false, queued: false, queueState: null, position: null, raw: text || 'mergify queue show failed' }
554
+ if (/not in the merge queue|not queued|not found/i.test(text)) return { readable: true, queued: false, queueState: 'not-queued', position: null, batchPrNumber: null, raw: text }
555
+ return { readable: false, queued: false, queueState: null, position: null, batchPrNumber: null, raw: text || 'mergify queue show failed' }
556
+ }
557
+
558
+ function positivePrNumber(value) {
559
+ const number = Number(value)
560
+ return Number.isInteger(number) && number > 0 ? number : null
561
+ }
562
+
563
+ // The batch relationship is only queried after a terminal dequeue verdict, so normal polls
564
+ // remain the documented two reads. This one-shot enrichment cannot influence lifecycle state.
565
+ function batchPrObservation({ repo, batchPrNumber, intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive }) {
566
+ const result = run('gh', ['pr', 'view', String(batchPrNumber), '--repo', repo, '--json', 'state,url,statusCheckRollup'], { timeout: readTimeoutForArgs({ intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive }) })
567
+ if (!result.ok) return { readable: false, failedChecks: [], error: result.stderr || result.stdout }
568
+ const data = parseJson(result.stdout)
569
+ if (!data) return { readable: false, failedChecks: [], error: 'gh pr view returned invalid JSON for synthetic batch PR' }
570
+ return { readable: true, state: data.state || null, url: data.url || null, ...summarizeStatusChecks(data.statusCheckRollup) }
571
+ }
572
+
573
+ export function classifyDequeue({ rawReason = null, failedChecks = [] } = {}) {
574
+ const statuses = new Set((Array.isArray(failedChecks) ? failedChecks : []).map((check) => String(check?.status || '').toUpperCase()))
575
+ if (['FAILURE', 'ERROR', 'TIMED_OUT', 'ACTION_REQUIRED', 'STARTUP_FAILURE'].some((status) => statuses.has(status))) return 'checks-failed'
576
+ if (statuses.has('CANCELLED')) return 'checks-cancelled'
577
+ const raw = String(rawReason || '').trim().toLowerCase()
578
+ if (/failing checks|checks failed/.test(raw)) return 'checks-failed'
579
+ if (/pull[- ]request.*updated|draft[- ]pull[- ]request.*changed/.test(raw)) return 'pull-request-updated'
580
+ if (/conflict/.test(raw)) return 'conflict'
581
+ if (/conditions[- ]unmet/.test(raw)) return 'conditions-unmet'
582
+ if (/frozen/.test(raw)) return 'frozen'
583
+ if (/manual|manually dequeued|dequeued by .*\bdequeue\b.*command/.test(raw)) return 'manual'
584
+ return 'unknown'
523
585
  }
524
586
 
525
587
  // Every poll's observation, as one compact NDJSON record — the watcher's heartbeat.
@@ -657,6 +719,7 @@ export async function runCli(argv) {
657
719
  let lastChangeMs = startedMs
658
720
  let observations = 0
659
721
  let phases = []
722
+ let lastBatchPrNumber = null
660
723
 
661
724
  for (;;) {
662
725
  // Pass everActive into the reads so their per-child timeout tracks the phase bound that
@@ -666,6 +729,9 @@ export async function runCli(argv) {
666
729
  const pr = prObservation({ ...args, everActive })
667
730
  const queue = queueObservation({ ...args, everActive })
668
731
  observations += 1
732
+ if (queue.readable !== false && queue.queued === true && queue.batchPrNumber) {
733
+ lastBatchPrNumber = queue.batchPrNumber
734
+ }
669
735
  const active = (Array.isArray(pr.labels) && pr.labels.includes('queued')) || queue.queued === true
670
736
  const readable = pr.readable !== false || queue.readable !== false
671
737
  if (active) {
@@ -687,16 +753,37 @@ export async function runCli(argv) {
687
753
  if (verdict.seenQueued === true) seenQueued = true
688
754
  if (verdict.preAdmission === true) phases = nextPhases(phases, 'NOT_YET_QUEUED')
689
755
  phases = nextPhases(phases, verdict.outcome)
756
+ let dequeue = null
757
+ if (verdict.outcome === 'DEQUEUED') {
758
+ const info = pr.dequeueInfo || { rawReason: null, batchPrNumber: null, providerFailedChecks: [] }
759
+ const batchPrNumber = lastBatchPrNumber ?? info.batchPrNumber ?? null
760
+ const batch = batchPrNumber ? batchPrObservation({ ...args, everActive, batchPrNumber }) : null
761
+ const failedChecks = batch?.readable && batch.failedChecks.length > 0
762
+ ? batch.failedChecks
763
+ : (info.providerFailedChecks || [])
764
+ const classifiedReason = classifyDequeue({ rawReason: info.rawReason, failedChecks })
765
+ dequeue = {
766
+ reason: classifiedReason === 'unknown' && !info.rawReason && failedChecks.length === 0
767
+ ? verdict.reason
768
+ : classifiedReason,
769
+ rawReason: info.rawReason || null,
770
+ batchPrNumber,
771
+ failedChecks,
772
+ batch,
773
+ }
774
+ }
690
775
  const payload = {
691
776
  outcome: verdict.outcome,
692
777
  terminal: verdict.terminal,
693
- reason: verdict.reason || null,
778
+ reason: dequeue?.reason || verdict.reason || null,
779
+ rawReason: dequeue?.rawReason || null,
694
780
  detail: verdict.detail || null,
695
781
  mergedSha: verdict.mergedSha || null,
782
+ batchPrNumber: dequeue?.batchPrNumber || lastBatchPrNumber,
696
783
  seenQueued,
697
784
  everActive,
698
785
  phases,
699
- failedChecks: verdict.failedChecks || pr.failedChecks || [],
786
+ failedChecks: dequeue?.failedChecks || verdict.failedChecks || pr.failedChecks || [],
700
787
  missingActiveReads,
701
788
  observations,
702
789
  elapsedMs: Date.now() - startedMs,
@@ -711,7 +798,21 @@ export async function runCli(argv) {
711
798
  failedChecks: pr.failedChecks || [],
712
799
  pendingChecks: pr.pendingChecks || [],
713
800
  },
714
- queue: { queued: queue.queued === true, queueState: queue.queueState || null, position: queue.position ?? null, readable: queue.readable !== false },
801
+ queue: {
802
+ queued: queue.queued === true,
803
+ queueState: queue.queueState || null,
804
+ position: queue.position ?? null,
805
+ batchPrNumber: queue.batchPrNumber || null,
806
+ lastBatchPrNumber,
807
+ readable: queue.readable !== false,
808
+ },
809
+ batchPr: dequeue?.batch ? {
810
+ number: dequeue.batchPrNumber,
811
+ state: dequeue.batch.state || null,
812
+ url: dequeue.batch.url || null,
813
+ readable: dequeue.batch.readable !== false,
814
+ error: dequeue.batch.error || null,
815
+ } : null,
715
816
  }
716
817
 
717
818
  if (verdict.terminal) {
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: e2e-test
3
3
  description: "Playwright end-to-end standard: the 3-tier suite (render-health / functional / journey), generated route manifest, presentation sweep, and CI route gates. Use when browser routes or user flows ship or change, or a Playwright test flakes. NOT for non-browser tests, defining what to test (ensure-coverage), or the curated ALIVE/DEAD gate (smoke)."
4
+ disable-model-invocation: true
4
5
  ---
5
6
 
6
7
  # E2E Testing Standard
@@ -383,7 +383,7 @@
383
383
  "id": "first-party",
384
384
  "type": "first-party",
385
385
  "package": "@1aboveio/skills",
386
- "version": "0.19.0"
386
+ "version": "0.19.2"
387
387
  },
388
388
  "contentDigest": "eff6c7b5931bce5b2265a619bccddd371a89df6ce7bc2edc74f11d00060a71dd",
389
389
  "digestExcludes": []
@@ -396,9 +396,9 @@
396
396
  "id": "first-party",
397
397
  "type": "first-party",
398
398
  "package": "@1aboveio/skills",
399
- "version": "0.19.0"
399
+ "version": "0.19.2"
400
400
  },
401
- "contentDigest": "08230dc57a53d6526692b50138038abbd80abce1a067e01c52ad6acc182caf7e",
401
+ "contentDigest": "d97abeff7bdc5ae2e4be2f631d09dba46d4753ce1e67334793364329af59f066",
402
402
  "digestExcludes": []
403
403
  },
404
404
  {
@@ -409,9 +409,9 @@
409
409
  "id": "first-party",
410
410
  "type": "first-party",
411
411
  "package": "@1aboveio/skills",
412
- "version": "0.19.0"
412
+ "version": "0.19.2"
413
413
  },
414
- "contentDigest": "9eea7bfba348ddaea1b934c9a7fabdd9b101df8a146e1c93e947da4d335f1d98",
414
+ "contentDigest": "00cfdd3e5bfbc3e37b1369ad93aae91161e671829760a8867ea9be25047ba461",
415
415
  "digestExcludes": []
416
416
  },
417
417
  {
@@ -422,7 +422,7 @@
422
422
  "id": "first-party",
423
423
  "type": "first-party",
424
424
  "package": "@1aboveio/skills",
425
- "version": "0.19.0"
425
+ "version": "0.19.2"
426
426
  },
427
427
  "contentDigest": "58b0556228a9271cf9e33727a05fc08a4fdfd29512ec0936b169f0a55d60e6ef",
428
428
  "digestExcludes": []
@@ -435,9 +435,9 @@
435
435
  "id": "first-party",
436
436
  "type": "first-party",
437
437
  "package": "@1aboveio/skills",
438
- "version": "0.19.0"
438
+ "version": "0.19.2"
439
439
  },
440
- "contentDigest": "38347c30820ba7e8b10433aeb351b7df4fa457b143e4a109293ab53fda961459",
440
+ "contentDigest": "4664873230f22de2fcf46b7b529570cfedabdc850620031d8f96baf89fcdc5b4",
441
441
  "digestExcludes": []
442
442
  },
443
443
  {
@@ -448,9 +448,9 @@
448
448
  "id": "first-party",
449
449
  "type": "first-party",
450
450
  "package": "@1aboveio/skills",
451
- "version": "0.19.0"
451
+ "version": "0.19.2"
452
452
  },
453
- "contentDigest": "d32add939c8c673e3bb096dfc95a5aadfa31115c3a854b3a4e0eb3fed1b49794",
453
+ "contentDigest": "a2df894683012581ce57e92d6e2dacd7eb377e4a0547a98acaea04878ef6aa3a",
454
454
  "digestExcludes": []
455
455
  },
456
456
  {
@@ -461,9 +461,9 @@
461
461
  "id": "first-party",
462
462
  "type": "first-party",
463
463
  "package": "@1aboveio/skills",
464
- "version": "0.19.0"
464
+ "version": "0.19.2"
465
465
  },
466
- "contentDigest": "125318612fe3fd70354ba5393a7019b3e6db5acc14a0d3f653a31f6b06a2e267",
466
+ "contentDigest": "01c192d8b4fd416af4ea02e8000e5fce0cee723a6ac2f8c0c48372087e1a262e",
467
467
  "digestExcludes": []
468
468
  },
469
469
  {
@@ -474,7 +474,7 @@
474
474
  "id": "first-party",
475
475
  "type": "first-party",
476
476
  "package": "@1aboveio/skills",
477
- "version": "0.19.0"
477
+ "version": "0.19.2"
478
478
  },
479
479
  "contentDigest": "ffba0c83491676794c74721f4c6dbae430f5bc186e5a55f0764e29eaeb13829b",
480
480
  "digestExcludes": []
@@ -487,7 +487,7 @@
487
487
  "id": "first-party",
488
488
  "type": "first-party",
489
489
  "package": "@1aboveio/skills",
490
- "version": "0.19.0"
490
+ "version": "0.19.2"
491
491
  },
492
492
  "contentDigest": "f048fd00c69f2dc666fc7a3096cfeee6dfba73933bfb69602f3f0e26159798cc",
493
493
  "digestExcludes": []
@@ -500,7 +500,7 @@
500
500
  "id": "first-party",
501
501
  "type": "first-party",
502
502
  "package": "@1aboveio/skills",
503
- "version": "0.19.0"
503
+ "version": "0.19.2"
504
504
  },
505
505
  "contentDigest": "90a7c4e1ad6da1e632ea2c5259e4967ffebb84353adccbca8dfc5d6bc50b60c1",
506
506
  "digestExcludes": []
@@ -513,15 +513,15 @@
513
513
  "id": "first-party",
514
514
  "type": "first-party",
515
515
  "package": "@1aboveio/skills",
516
- "version": "0.19.0"
516
+ "version": "0.19.2"
517
517
  },
518
- "contentDigest": "16dcd5bc2b40c8ae7cf6bc41eadd943264b03e70162c3da85624f11a2112bf56",
518
+ "contentDigest": "ab98e937973720aedf6f79a63e99650fe3488f4f798d46ed3ae606f0b12a2a41",
519
519
  "digestExcludes": [
520
520
  "coherence/workflow.json"
521
521
  ]
522
522
  }
523
523
  ],
524
- "releaseIdentity": "6f89ee339ca1319fbd167658b1dbb66a1357b482463ba829bf22bf7b0c7c28a3",
524
+ "releaseIdentity": "0fb67095c481e3a37f57b149e3557c5e450fa93fa3077888ab1eb2bf1ac3d196",
525
525
  "lifecycleAuthority": "native-skills-cli",
526
526
  "repairRecipe": {
527
527
  "id": "engineering-workflow-dependency-first",
@@ -579,7 +579,7 @@
579
579
  "sourceId": "first-party",
580
580
  "sourceType": "first-party",
581
581
  "package": "@1aboveio/skills",
582
- "version": "0.19.0",
582
+ "version": "0.19.2",
583
583
  "installPath": null,
584
584
  "members": [
585
585
  "harness-runtime",
@@ -597,7 +597,7 @@
597
597
  "commands": [
598
598
  {
599
599
  "transport": "npm",
600
- "command": "npx @1aboveio/skills@0.19.0 install --group engineering-workflow --yes"
600
+ "command": "npx @1aboveio/skills@0.19.2 install --group engineering-workflow --yes"
601
601
  }
602
602
  ],
603
603
  "onFailure": {
@@ -119,7 +119,7 @@ export const WORKFLOW_TRUSTED_SOURCES = deepFreeze({
119
119
  id: 'first-party',
120
120
  type: 'first-party',
121
121
  package: '@1aboveio/skills',
122
- version: '0.19.0',
122
+ version: '0.19.2',
123
123
  },
124
124
  matt: {
125
125
  id: 'matt-pocock',
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: ensure-coverage
3
3
  description: "Define or audit behavior-first test criteria — ACs, required test levels, evidence expectations, mock/integration/E2E policy — across three axes: breadth (Coverage Ledger), depth (over-mocking), presentation (perceivability of browser surfaces). NOT the merge verdict on a PR (review-pr), authoring browser tests (e2e-test), or driving issues to PRs (resolve-issues)."
4
+ disable-model-invocation: true
4
5
  ---
5
6
 
6
7
  # Ensure Coverage
@@ -237,7 +237,7 @@
237
237
  "id": "first-party",
238
238
  "type": "first-party",
239
239
  "package": "@1aboveio/skills",
240
- "version": "0.19.0"
240
+ "version": "0.19.2"
241
241
  }
242
242
  },
243
243
  {
@@ -248,7 +248,7 @@
248
248
  "id": "first-party",
249
249
  "type": "first-party",
250
250
  "package": "@1aboveio/skills",
251
- "version": "0.19.0"
251
+ "version": "0.19.2"
252
252
  }
253
253
  },
254
254
  {
@@ -259,7 +259,7 @@
259
259
  "id": "first-party",
260
260
  "type": "first-party",
261
261
  "package": "@1aboveio/skills",
262
- "version": "0.19.0"
262
+ "version": "0.19.2"
263
263
  }
264
264
  },
265
265
  {
@@ -270,7 +270,7 @@
270
270
  "id": "first-party",
271
271
  "type": "first-party",
272
272
  "package": "@1aboveio/skills",
273
- "version": "0.19.0"
273
+ "version": "0.19.2"
274
274
  }
275
275
  },
276
276
  {
@@ -281,7 +281,7 @@
281
281
  "id": "first-party",
282
282
  "type": "first-party",
283
283
  "package": "@1aboveio/skills",
284
- "version": "0.19.0"
284
+ "version": "0.19.2"
285
285
  }
286
286
  },
287
287
  {
@@ -292,7 +292,7 @@
292
292
  "id": "first-party",
293
293
  "type": "first-party",
294
294
  "package": "@1aboveio/skills",
295
- "version": "0.19.0"
295
+ "version": "0.19.2"
296
296
  }
297
297
  },
298
298
  {
@@ -303,7 +303,7 @@
303
303
  "id": "first-party",
304
304
  "type": "first-party",
305
305
  "package": "@1aboveio/skills",
306
- "version": "0.19.0"
306
+ "version": "0.19.2"
307
307
  }
308
308
  },
309
309
  {
@@ -314,7 +314,7 @@
314
314
  "id": "first-party",
315
315
  "type": "first-party",
316
316
  "package": "@1aboveio/skills",
317
- "version": "0.19.0"
317
+ "version": "0.19.2"
318
318
  }
319
319
  },
320
320
  {
@@ -325,7 +325,7 @@
325
325
  "id": "first-party",
326
326
  "type": "first-party",
327
327
  "package": "@1aboveio/skills",
328
- "version": "0.19.0"
328
+ "version": "0.19.2"
329
329
  }
330
330
  },
331
331
  {
@@ -336,7 +336,7 @@
336
336
  "id": "first-party",
337
337
  "type": "first-party",
338
338
  "package": "@1aboveio/skills",
339
- "version": "0.19.0"
339
+ "version": "0.19.2"
340
340
  }
341
341
  },
342
342
  {
@@ -347,7 +347,7 @@
347
347
  "id": "first-party",
348
348
  "type": "first-party",
349
349
  "package": "@1aboveio/skills",
350
- "version": "0.19.0"
350
+ "version": "0.19.2"
351
351
  }
352
352
  }
353
353
  ],
@@ -468,7 +468,7 @@
468
468
  "sourceId": "first-party",
469
469
  "sourceType": "first-party",
470
470
  "package": "@1aboveio/skills",
471
- "version": "0.19.0",
471
+ "version": "0.19.2",
472
472
  "installPath": null,
473
473
  "members": [
474
474
  "harness-runtime",
@@ -486,7 +486,7 @@
486
486
  "commands": [
487
487
  {
488
488
  "transport": "npm",
489
- "command": "npx @1aboveio/skills@0.19.0 install --group engineering-workflow --yes"
489
+ "command": "npx @1aboveio/skills@0.19.2 install --group engineering-workflow --yes"
490
490
  }
491
491
  ],
492
492
  "onFailure": {
@@ -3412,7 +3412,7 @@ export const WORKFLOW_PREFLIGHT_COMMANDS = Object.freeze([
3412
3412
 
3413
3413
  const WORKFLOW_VERIFIER_URL = new URL('../../engineering-runtime/scripts/workflow-coherence.mjs', import.meta.url)
3414
3414
  const WORKFLOW_FALLBACK_POLICY_URL = new URL('../generated/workflow-repair-policy.json', import.meta.url)
3415
- export const WORKFLOW_TRUSTED_FALLBACK_POLICY_SHA256 = '27d1fb2d0bdf9ca37933dff2f0e1863d95f9f147baecbbce9dd3553a4c71a915'
3415
+ export const WORKFLOW_TRUSTED_FALLBACK_POLICY_SHA256 = '8fb804c3034b780877774f6228166716ee98d1b39986adac397ac33c0feebfb5'
3416
3416
  const WORKFLOW_REPAIR_RECIPE_REFERENCE = Object.freeze({
3417
3417
  id: 'engineering-workflow-dependency-first',
3418
3418
  generatedFrom: 'skills/distribution/generated/recipes.json',
@@ -72,6 +72,10 @@ before its stage; do not invent an unlinked substitute.
72
72
  canary and requires validation plus a fresh canary on the new HEAD.
73
73
  - Review facts independently. Producer summaries, claims, logs, and evidence
74
74
  may locate work but cannot establish correctness for the reviewer.
75
+ - Treat review P0/P1 findings as blocking. P2/P3 findings do not expand the
76
+ current PR, but each must be linked to a concrete follow-up before review
77
+ succeeds; a finding that proves a current AC or mandatory standard is unmet
78
+ must be reclassified as P1, not deferred under a softer label.
75
79
  - Do not manufacture review evidence collateral. Keep only the stage result,
76
80
  commands actually run, findings, task-plan transitions, and profiling events.
77
81
  - Persist each task-plan transition and profiling event under one monotonically
@@ -144,8 +148,8 @@ the original spec and repository standards, computes its own diff fixed point,
144
148
  inspects the implementation, and runs checks needed to verify findings. It does
145
149
  not accept producer evidence as proof. Fix blocking findings on the combined
146
150
  branch, validate, rerun the warehouse canary when required, then run a fresh
147
- review until successful; after 2 failed fix/review cycles, diagnose and continue
148
- with a revised approach.
151
+ review until every P0/P1 is resolved and every P2/P3 has a tracked follow-up;
152
+ after 2 failed fix/review cycles, diagnose and continue with a revised approach.
149
153
 
150
154
  ### 7. CICD
151
155
 
@@ -24,17 +24,41 @@ new evidence collateral as a substitute for verification.
24
24
  for the current exact HEAD. Then invoke the absolute `code-review` skill path
25
25
  on the combined HEAD.
26
26
  2. Review both axes: repository **Standards** and original **Spec**.
27
+ Treat `code-review` as a barrier: buffer individual Standards and Spec
28
+ completions, and do not classify, defer, or fix any finding until both axes
29
+ have returned and one aggregated review result exists.
27
30
  3. Independently run or reproduce the checks needed to support each material
28
31
  conclusion. A producer-provided green log is not a factual substitute.
29
- 4. Return findings with severity and file/line references, plus commands the
30
- reviewer actually ran. The review result is coordination state, not an
32
+ 4. Return findings with P0-P3 severity and file/line references, plus commands
33
+ the reviewer actually ran. The review result is coordination state, not an
31
34
  evidence-generation deliverable.
32
- 5. Succeed only with no Spec gap/wrongness/creep and no hard Standards
33
- violation. Record non-blocking baseline observations as deferrals.
35
+ 5. Apply the severity/disposition policy below. Succeed only when every finding
36
+ has an explicit resolved or deferred disposition.
37
+
38
+ ## Severity and disposition
39
+
40
+ - **P0/P1 are blocking.** Fix them on the combined branch, validate, rerun the
41
+ required canary, and obtain a fresh review before recording `reviewedHead`.
42
+ - **P2/P3 are non-blocking by default.** Do not expand the current PR to address
43
+ them. Create or link a concrete follow-up issue before review succeeds and
44
+ record its URL with the finding.
45
+ - **No finding disappears.** Each finding is either `resolved` on the exact
46
+ reviewed HEAD or `deferred` with a follow-up URL. A prose note without a
47
+ trackable issue is not a disposition.
48
+ - **Severity cannot waive the current contract.** If a purported P2/P3 proves
49
+ that a current acceptance criterion is unmet, required behavior is wrong, or
50
+ a mandatory repository standard or gate is violated, ask the reviewer to
51
+ reclassify it as P1. Do not silently override the reviewer and do not defer it
52
+ under the softer label.
53
+
54
+ P2/P3 follow-ups are appropriate for optional hardening, cleanup, cosmetic
55
+ improvements, and architecture work outside the current acceptance criteria.
56
+ Link each follow-up to the combined PR and original spec, and deduplicate it
57
+ against existing issues before creating a new one.
34
58
 
35
59
  ## Fix loop
36
60
 
37
- For blocking findings, fix the combined PR branch, run relevant lint/build/
61
+ For P0/P1 findings, fix the combined PR branch, run relevant lint/build/
38
62
  test/smoke, rerun [canary.md](canary.md) when required, then spawn a fresh
39
63
  review of the new HEAD. Record that exact SHA as `reviewedHead` only after
40
64
  success. Never treat a fix as accepted before validation, required canary, and
@@ -47,7 +47,11 @@ Run-level fields include `planRevision`, models/efforts, `targetBranch`, run
47
47
  workspace/branch, original spec locator, explicit merge authorization when one
48
48
  was supplied, exploration path, combined PR/HEAD, `reviewedHead`, and
49
49
  `warehouseCanaryRequired`, its classification reason, durable canary result
50
- path, and start/update timestamps.
50
+ path, `reviewFindings`, and start/update timestamps. Each `reviewFindings` entry
51
+ records a stable finding id, P0-P3 `severity`, summary and source locator,
52
+ `status` (`open`, `resolved`, or `deferred`), the exact `resolvedHead` when
53
+ resolved, and `followUpUrl` when deferred. P0/P1 cannot be deferred; P2/P3
54
+ cannot remain open when `reviewedHead` is recorded.
51
55
 
52
56
  Persist the machine-readable plan at:
53
57
 
@@ -51,8 +51,13 @@ git commit -m "chore(release): <nextVersion>"
51
51
  `apply.mjs` requires the confirmed plan's `changelogMarkdown` on stdin, writes
52
52
  that section, and bumps the discovered identity files. It refuses a missing or
53
53
  empty changelog section rather than silently replacing the plan with a bare
54
- version heading. If the human edited the notes, write the confirmed markdown
55
- to stdin instead of the raw plan field.
54
+ version heading. When the plan names `skills/distribution/manifest.json`, it
55
+ delegates to `distribution.mjs version-cut`; that operation updates the public
56
+ npm identity, trusted installed-workflow identity and fallback hash together,
57
+ then regenerates the repository's deterministic distribution artifacts. If any
58
+ step fails it restores every identity and generated file before returning.
59
+ If the human edited the notes, write the confirmed markdown to stdin instead
60
+ of the raw plan field.
56
61
 
57
62
  Then:
58
63
 
@@ -28,14 +28,22 @@ workflow publishes on `release: published` rather than on tag push, note that
28
28
 
29
29
  ## 3. Version files
30
30
 
31
- List identity files present at the repo root (and only those):
31
+ First check for `skills/distribution/manifest.json` with exactly one `public-npm`
32
+ render target and `skills/distribution/scripts/distribution.mjs`. That manifest is
33
+ the release identity when present; record it and use the distribution-aware cut in
34
+ `apply.mjs`. It takes precedence over a private root `package.json` with no version,
35
+ because adding a synthetic root version would not change what the publisher ships.
36
+
37
+ Otherwise, list identity files present at the repo root (and only those):
32
38
 
33
39
  - `package.json` (and `package-lock.json` when it exists)
34
40
  - `pyproject.toml`
35
41
  - `VERSION`
36
42
 
37
- Do not bump nested workspace packages unless the human names them. The cut
38
- must stay metadata-only.
43
+ Do not bump other nested workspace packages unless the human names them. The cut
44
+ must stay metadata-only. A distribution-aware cut may also update trusted runtime
45
+ identity constants, test fixtures that pin that identity, and deterministic generated
46
+ policy/inventory artifacts; those are version metadata, not product behavior.
39
47
 
40
48
  ## 4. Baseline
41
49
 
@@ -7,8 +7,9 @@
7
7
  // Exit: 0 ok · 1 IO/parse failed · 2 usage
8
8
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
9
9
  import { join } from 'node:path'
10
+ import { spawnSync } from 'node:child_process'
10
11
  import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
11
- import { discoverVersionFiles, parseSemver } from './plan.mjs'
12
+ import { DISTRIBUTION_MANIFEST, discoverVersionFiles, parseSemver } from './plan.mjs'
12
13
 
13
14
  export function insertChangelog(markdown, { version, date, section }) {
14
15
  const heading = `## [${version}] - ${date}`
@@ -90,11 +91,32 @@ export function bumpPyproject(text, version) {
90
91
  return out.join('\n')
91
92
  }
92
93
 
94
+ export function cutDistributionVersion({ version, cwd }) {
95
+ const script = join(cwd, 'skills/distribution/scripts/distribution.mjs')
96
+ const result = spawnSync(process.execPath, [script, 'version-cut', '--version', version, '--repo-root', cwd, '--json'], {
97
+ cwd,
98
+ encoding: 'utf8',
99
+ })
100
+ if (result.status !== 0) {
101
+ const error = new Error(result.stderr.trim() || 'distribution version cut failed')
102
+ error.exitCode = result.status || 1
103
+ throw error
104
+ }
105
+ const parsed = JSON.parse(result.stdout)
106
+ if (!Array.isArray(parsed.changed) || parsed.changed.length === 0) {
107
+ const error = new Error('distribution version cut returned no changed files')
108
+ error.exitCode = 1
109
+ throw error
110
+ }
111
+ return parsed.changed
112
+ }
113
+
93
114
  export function applyRelease({
94
115
  version,
95
116
  date,
96
117
  cwd = process.cwd(),
97
118
  changelogSection = null,
119
+ cutDistribution = cutDistributionVersion,
98
120
  now = new Date(),
99
121
  } = {}) {
100
122
  if (!parseSemver(version)) {
@@ -115,6 +137,11 @@ export function applyRelease({
115
137
  writeFileSync(changelogPath, insertChangelog(previous, { version, date: stamp, section: changelogSection }))
116
138
  changed.push('CHANGELOG.md')
117
139
 
140
+ if (files.includes(DISTRIBUTION_MANIFEST)) {
141
+ changed.push(...cutDistribution({ version, cwd }))
142
+ return { version, date: stamp, changed: [...new Set(changed)] }
143
+ }
144
+
118
145
  if (files.includes('package.json')) {
119
146
  writeFileSync(join(cwd, 'package.json'), bumpPackageJson(readFileSync(join(cwd, 'package.json'), 'utf8'), version))
120
147
  changed.push('package.json')
@@ -22,6 +22,7 @@ const SECTION_BY_TYPE = {
22
22
  }
23
23
  const INTERNAL_TYPES = new Set(['build', 'chore', 'ci', 'docs', 'style', 'test'])
24
24
  const SECTION_ORDER = ['Breaking', 'Added', 'Changed', 'Removed', 'Fixed', 'Security']
25
+ export const DISTRIBUTION_MANIFEST = 'skills/distribution/manifest.json'
25
26
 
26
27
  export function parseSemver(input) {
27
28
  const match = SEMVER_RE.exec(String(input || '').trim())
@@ -104,7 +105,24 @@ export function renderChangelog({ version, date, changes }) {
104
105
  return `${lines.join('\n')}\n`
105
106
  }
106
107
 
108
+ function distributionVersion(text) {
109
+ try {
110
+ const manifest = JSON.parse(text)
111
+ const targets = (manifest.sources ?? [])
112
+ .flatMap((source) => source.renderTargets ?? [])
113
+ .filter((target) => target.id === 'public-npm')
114
+ if (targets.length !== 1 || !parseSemver(targets[0].version)) return null
115
+ return formatVersion(parseSemver(targets[0].version))
116
+ } catch {
117
+ return null
118
+ }
119
+ }
120
+
107
121
  export function discoverVersionFiles(root) {
122
+ const distributionPath = join(root, DISTRIBUTION_MANIFEST)
123
+ if (existsSync(distributionPath) && distributionVersion(readFileSync(distributionPath, 'utf8'))) {
124
+ return [DISTRIBUTION_MANIFEST]
125
+ }
108
126
  const files = []
109
127
  for (const name of ['package.json', 'package-lock.json', 'pyproject.toml', 'VERSION']) {
110
128
  if (existsSync(join(root, name))) files.push(name)
@@ -113,6 +131,9 @@ export function discoverVersionFiles(root) {
113
131
  }
114
132
 
115
133
  export function currentVersionFromFiles(root, files = discoverVersionFiles(root)) {
134
+ if (files.includes(DISTRIBUTION_MANIFEST)) {
135
+ return distributionVersion(readFileSync(join(root, DISTRIBUTION_MANIFEST), 'utf8'))
136
+ }
116
137
  if (files.includes('package.json')) {
117
138
  const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
118
139
  const parsed = parseSemver(pkg.version)
@@ -146,12 +167,19 @@ function versionFilesAt(sha, cwd) {
146
167
  .split('\n')
147
168
  .map((line) => line.trim())
148
169
  .filter(Boolean))
170
+ if (tree.has(DISTRIBUTION_MANIFEST)) {
171
+ const version = distributionVersion(git(['show', `${sha}:${DISTRIBUTION_MANIFEST}`], cwd))
172
+ if (version) return [DISTRIBUTION_MANIFEST]
173
+ }
149
174
  return ['package.json', 'package-lock.json', 'pyproject.toml', 'VERSION']
150
175
  .filter((name) => tree.has(name))
151
176
  }
152
177
 
153
178
  function versionFromGitFiles(sha, files, cwd) {
154
179
  const contents = (path) => git(['show', `${sha}:${path}`], cwd)
180
+ if (files.includes(DISTRIBUTION_MANIFEST)) {
181
+ return distributionVersion(contents(DISTRIBUTION_MANIFEST))
182
+ }
155
183
  if (files.includes('package.json')) {
156
184
  const parsed = parseSemver(JSON.parse(contents('package.json')).version)
157
185
  if (parsed) return formatVersion(parsed)