@1aboveio/skills 0.18.0 → 0.19.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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/runtime/skills/distribution/generated/recipes.json +43 -23
  4. package/runtime/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +1 -1
  5. package/runtime/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +23 -1
  6. package/skills/engineering/engineering-runtime/coherence/workflow.json +65 -15
  7. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +1 -1
  8. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +23 -1
  9. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  10. package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +55 -11
  11. package/skills/engineering/resolve-issues/scripts/run-state.mjs +4 -4
  12. package/skills/engineering/resolve-release/references/related-skills.md +1 -0
  13. package/skills/engineering/rush-issues/LICENSE +3 -0
  14. package/skills/engineering/rush-issues/SKILL.md +179 -0
  15. package/skills/engineering/rush-issues/agents/openai.yaml +9 -0
  16. package/skills/engineering/rush-issues/evals/evals.json +65 -0
  17. package/skills/engineering/rush-issues/references/canary.md +45 -0
  18. package/skills/engineering/rush-issues/references/cicd.md +37 -0
  19. package/skills/engineering/rush-issues/references/combine.md +51 -0
  20. package/skills/engineering/rush-issues/references/expire.md +55 -0
  21. package/skills/engineering/rush-issues/references/exploration.md +53 -0
  22. package/skills/engineering/rush-issues/references/implementation.md +66 -0
  23. package/skills/engineering/rush-issues/references/preflight.md +31 -0
  24. package/skills/engineering/rush-issues/references/profiling.md +78 -0
  25. package/skills/engineering/rush-issues/references/review.md +48 -0
  26. package/skills/engineering/rush-issues/references/shared-modules.md +66 -0
  27. package/skills/engineering/rush-issues/references/task-plan.md +110 -0
  28. package/skills/engineering/rush-issues/scripts/discover-models.mjs +9 -0
  29. package/skills/engineering/rush-issues/scripts/model-catalog.mjs +9 -0
  30. package/skills/engineering/rush-issues/scripts/preflight-models.mjs +466 -0
  31. package/skills/engineering/rush-release/LICENSE +3 -0
  32. package/skills/engineering/rush-release/SKILL.md +99 -0
  33. package/skills/engineering/rush-release/agents/openai.yaml +8 -0
  34. package/skills/engineering/rush-release/evals/evals.json +44 -0
  35. package/skills/engineering/rush-release/references/candidate.md +30 -0
  36. package/skills/engineering/rush-release/references/cut.md +66 -0
  37. package/skills/engineering/rush-release/references/preflight.md +47 -0
  38. package/skills/engineering/rush-release/references/publish.md +100 -0
  39. package/skills/engineering/rush-release/scripts/apply.mjs +185 -0
  40. package/skills/engineering/rush-release/scripts/green-head.mjs +231 -0
  41. package/skills/engineering/rush-release/scripts/plan.mjs +264 -0
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env node
2
+ // Rush fast path for model selection.
3
+ //
4
+ // One command: discover → tier (shared harness-runtime catalog) → recommend
5
+ // implementer + fill explorer + fill reviewer ≠ implementer. Prints a ready
6
+ // ask payload so the orchestrator can feed it into the shared preflight planner
7
+ // without reading longer workflow doctrine before model selection.
8
+ //
9
+ // Maintenance (sync / bank) is deferred until AFTER the human answers — never
10
+ // block the picker on catalog writes.
11
+ //
12
+ // Usage:
13
+ // node preflight-models.mjs --harness <pi|codex|claude-code> [--risk routine|high]
14
+ // [--available id,id,...] [--human-named id,id,...] [--ask-only]
15
+ //
16
+ // Exit: 0 ok · 2 usage · 3 discovery yielded nothing proposable
17
+ import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
18
+ import {
19
+ detectHarness,
20
+ discover,
21
+ } from '../../harness-runtime/discover-models.mjs'
22
+ import {
23
+ tierProposals,
24
+ pointsFromTiers,
25
+ isSlotEligible,
26
+ DEFAULT_EFFORT,
27
+ readCatalog,
28
+ pathForHarness,
29
+ readSeed,
30
+ defaultSeedPath,
31
+ findSeedEntry,
32
+ } from '../../harness-runtime/model-catalog.mjs'
33
+
34
+ export const RISK = Object.freeze(['routine', 'high'])
35
+
36
+ // Preferred fills when discovery actually yielded the id (rush skill defaults).
37
+ export const TEAM_REVIEWER_DEFAULT = Object.freeze({
38
+ pi: { id: 'openai/gpt-5.5', family: 'openai', effort: 'high', tier: 'deep-reasoner' },
39
+ codex: { id: 'gpt-5.5', family: 'openai', effort: 'high', tier: 'deep-reasoner' },
40
+ 'claude-code': { id: 'gpt-5.5', family: 'openai', effort: 'high', tier: 'deep-reasoner' },
41
+ })
42
+
43
+ export const TEAM_EXPLORER_DEFAULT = Object.freeze({
44
+ pi: { id: 'openai/gpt-5.6-terra', family: 'openai', effort: 'high', tier: 'balanced-coder' },
45
+ codex: { id: 'gpt-5.6-terra', family: 'openai', effort: 'high', tier: 'balanced-coder' },
46
+ 'claude-code': { id: 'gpt-5.6-terra', family: 'openai', effort: 'high', tier: 'balanced-coder' },
47
+ })
48
+
49
+ const TIER_RANK = Object.freeze({
50
+ frontier: 0,
51
+ 'deep-reasoner': 1,
52
+ 'balanced-coder': 2,
53
+ })
54
+
55
+ function usage() {
56
+ return `preflight-models — discover + recommend implementer, fill explorer + reviewer (rush fast path)
57
+
58
+ Usage:
59
+ node preflight-models.mjs --harness <pi|codex|claude-code> [--risk routine|high]
60
+ [--available <id,id,...>] [--human-named <id,id,...>] [--ask-only]
61
+
62
+ Output JSON:
63
+ { harness, risk, discovery, models, implementer, explorer, reviewer, ask, deferred }
64
+
65
+ --ask-only prints only the ask block (for piping into a question UI).
66
+ Deferred: run catalog sync/bank AFTER the human answers — never before the ask.`
67
+ }
68
+
69
+ function parseList(value) {
70
+ return String(value || '')
71
+ .split(',')
72
+ .map((s) => s.trim())
73
+ .filter(Boolean)
74
+ }
75
+
76
+ export function parseArgs(argv) {
77
+ const out = {
78
+ harness: null,
79
+ risk: 'routine',
80
+ available: [],
81
+ humanNamed: [],
82
+ askOnly: false,
83
+ help: false,
84
+ }
85
+ for (let i = 0; i < argv.length; i++) {
86
+ const a = argv[i]
87
+ if (a === '--help' || a === '-h' || a === 'help') out.help = true
88
+ else if (a === '--ask-only') out.askOnly = true
89
+ else if (a === '--harness') out.harness = argv[++i]
90
+ else if (a === '--risk') out.risk = argv[++i]
91
+ else if (a === '--available') out.available = parseList(argv[++i])
92
+ else if (a === '--human-named') out.humanNamed = parseList(argv[++i])
93
+ else if (a.startsWith('--harness=')) out.harness = a.slice('--harness='.length)
94
+ else if (a.startsWith('--risk=')) out.risk = a.slice('--risk='.length)
95
+ else if (a.startsWith('--available=')) out.available = parseList(a.slice('--available='.length))
96
+ else if (a.startsWith('--human-named=')) out.humanNamed = parseList(a.slice('--human-named='.length))
97
+ else throw new Error(`unknown argument: ${a}`)
98
+ }
99
+ return out
100
+ }
101
+
102
+ function familyOf(id, hinted) {
103
+ if (hinted) return hinted
104
+ if (id.includes('/')) return id.slice(0, id.indexOf('/'))
105
+ return 'unknown'
106
+ }
107
+
108
+ /** Prefer banked catalog family, then seed, then id prefix. */
109
+ export function resolveFamily(harness, id, { catalog = null, seed = null } = {}) {
110
+ const banked = (catalog?.models || []).find((m) => m && m.id === id)
111
+ if (banked?.family) return banked.family
112
+ const seeded = seed ? findSeedEntry(seed, harness, id) : null
113
+ if (seeded?.family) return seeded.family
114
+ return familyOf(id)
115
+ }
116
+
117
+ function pointKey(id, effort) {
118
+ return `${id}@${effort || DEFAULT_EFFORT}`
119
+ }
120
+
121
+ /** Expand tierProposals().models into slot-eligible operating points. */
122
+ export function slotPoints(tieredModels, { harness, catalog = null, seed = null } = {}) {
123
+ const points = []
124
+ for (const model of tieredModels) {
125
+ const entries = Object.keys(model.tiers || {}).length
126
+ ? pointsFromTiers(model.tiers)
127
+ : []
128
+ const family = resolveFamily(harness, model.id, { catalog, seed })
129
+ for (const p of entries) {
130
+ if (!isSlotEligible(p.tier)) continue
131
+ points.push({
132
+ id: model.id,
133
+ effort: p.effort || DEFAULT_EFFORT,
134
+ tier: p.tier,
135
+ family,
136
+ source: model.source,
137
+ citation: model.citation || null,
138
+ key: pointKey(model.id, p.effort || DEFAULT_EFFORT),
139
+ })
140
+ }
141
+ }
142
+ return points
143
+ }
144
+
145
+ function preferRisk(tier, risk) {
146
+ if (risk === 'high') {
147
+ if (tier === 'deep-reasoner') return 0
148
+ if (tier === 'balanced-coder') return 1
149
+ return 2 // frontier never recommended for implementer
150
+ }
151
+ // routine
152
+ if (tier === 'balanced-coder') return 0
153
+ if (tier === 'deep-reasoner') return 1
154
+ return 2
155
+ }
156
+
157
+ /**
158
+ * Recommend implementer: never frontier as the recommendation (still listed).
159
+ * routine → balanced-coder; high → deep-reasoner.
160
+ */
161
+ export function recommendImplementer(points, risk = 'routine') {
162
+ if (!points.length) return null
163
+ const sorted = [...points].sort((a, b) => {
164
+ const ra = preferRisk(a.tier, risk)
165
+ const rb = preferRisk(b.tier, risk)
166
+ if (ra !== rb) return ra - rb
167
+ // Prefer the floor effort when tied; then stable id.
168
+ if (a.effort !== b.effort) {
169
+ if (a.effort === DEFAULT_EFFORT) return -1
170
+ if (b.effort === DEFAULT_EFFORT) return 1
171
+ return a.effort < b.effort ? -1 : 1
172
+ }
173
+ return a.id.localeCompare(b.id)
174
+ })
175
+ // First non-frontier if any; else first point (single-model / only-frontier harness)
176
+ return sorted.find((p) => p.tier !== 'frontier') || sorted[0]
177
+ }
178
+
179
+ function findPreferredPoint(points, preferred) {
180
+ if (!preferred) return null
181
+ return points.find(
182
+ (p) => p.id === preferred.id && (p.effort || DEFAULT_EFFORT) === (preferred.effort || DEFAULT_EFFORT),
183
+ ) || null
184
+ }
185
+
186
+ /**
187
+ * Explorer fill: balanced-coder; prefers gpt-5.6-terra@high when discovered.
188
+ */
189
+ export function fillExplorer(points, harness) {
190
+ const preferred = TEAM_EXPLORER_DEFAULT[harness] || null
191
+ const preferredPoint = findPreferredPoint(points, preferred)
192
+ if (preferredPoint && preferredPoint.tier === 'balanced-coder') {
193
+ return { fill: preferredPoint, reason: 'team-default' }
194
+ }
195
+ const balanced = points
196
+ .filter((p) => p.tier === 'balanced-coder')
197
+ .sort((a, b) => {
198
+ if (a.effort === DEFAULT_EFFORT && b.effort !== DEFAULT_EFFORT) return -1
199
+ if (b.effort === DEFAULT_EFFORT && a.effort !== DEFAULT_EFFORT) return 1
200
+ return a.id.localeCompare(b.id)
201
+ })
202
+ const fill = balanced[0] || null
203
+ return {
204
+ fill,
205
+ reason: fill ? (preferredPoint ? 'ladder' : 'balanced-coder') : 'none',
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Reviewer fill: deep-reasoner preferred (gpt-5.5@high when discovered),
211
+ * must differ from implementer. Ladder is diversity-first among remaining points.
212
+ */
213
+ export function rankReviewers(points, implementer, harness) {
214
+ const team = TEAM_REVIEWER_DEFAULT[harness] || null
215
+ const deepPool = points.filter((p) => p.tier === 'deep-reasoner' || p.tier === 'frontier')
216
+ const pool = deepPool.length ? deepPool : points
217
+ const scored = pool.map((p) => {
218
+ const sameId = p.id === implementer.id
219
+ const sameFamily = p.family && implementer.family && p.family === implementer.family
220
+ let diversity
221
+ if (sameId) diversity = 5
222
+ else if (!sameFamily && p.tier === 'deep-reasoner') diversity = 0
223
+ else if (!sameFamily && p.tier === 'frontier') diversity = 1
224
+ else if (sameFamily && p.tier === 'deep-reasoner') diversity = 2
225
+ else if (!sameFamily) diversity = 3
226
+ else diversity = 4
227
+ return { ...p, diversity, tierRank: TIER_RANK[p.tier] ?? 9 }
228
+ })
229
+ scored.sort((a, b) => {
230
+ if (a.diversity !== b.diversity) return a.diversity - b.diversity
231
+ if (a.tierRank !== b.tierRank) return a.tierRank - b.tierRank
232
+ return a.id.localeCompare(b.id)
233
+ })
234
+
235
+ const teamPoint = findPreferredPoint(points, team)
236
+
237
+ let fill
238
+ let reason
239
+ if (teamPoint && teamPoint.id !== implementer.id) {
240
+ fill = teamPoint
241
+ reason = 'team-default'
242
+ } else {
243
+ fill = scored.find((p) => p.id !== implementer.id) || scored[0]
244
+ reason = fill?.id === implementer.id ? 'no-distinct-candidate' : 'ladder'
245
+ }
246
+
247
+ return {
248
+ fill,
249
+ reason,
250
+ ranking: scored,
251
+ teamDefaultApplied: reason === 'team-default',
252
+ reducedDiversity: !!(fill && implementer && fill.family && fill.family === implementer.family && fill.id !== implementer.id),
253
+ notIndependent: !!(fill && implementer && fill.id === implementer.id),
254
+ }
255
+ }
256
+
257
+ function optionFromPoint(p, { recommended = false, role }) {
258
+ const why =
259
+ role === 'implementer'
260
+ ? p.tier === 'balanced-coder'
261
+ ? 'routine implementer band'
262
+ : p.tier === 'deep-reasoner'
263
+ ? 'deep-reasoner for risk-heavy work'
264
+ : p.tier === 'frontier'
265
+ ? 'frontier (alternate only — not the rush recommendation)'
266
+ : p.tier
267
+ : p.tier
268
+ return {
269
+ id: p.key, // operating point id for the picker; parse back to model+effort
270
+ modelId: p.id,
271
+ label: `${p.id} @ ${p.effort}`,
272
+ description: `${p.tier}${recommended ? ' · recommended' : ''} · ${why}`,
273
+ family: p.family,
274
+ effort: p.effort,
275
+ tier: p.tier,
276
+ }
277
+ }
278
+
279
+ /** Native question UIs typically allow 2–4 options. Keep the rest as `alternates`. */
280
+ export const ASK_OPTION_LIMIT = 4
281
+
282
+ function slotFillPayload(slot) {
283
+ if (!slot?.fill) return null
284
+ return {
285
+ id: slot.fill.key,
286
+ modelId: slot.fill.id,
287
+ effort: slot.fill.effort,
288
+ family: slot.fill.family,
289
+ tier: slot.fill.tier,
290
+ reason: slot.reason,
291
+ reducedDiversity: slot.reducedDiversity || false,
292
+ notIndependent: slot.notIndependent || false,
293
+ }
294
+ }
295
+
296
+ export function buildAsk(implementer, points, reviewer, { risk = 'routine', explorer = null } = {}) {
297
+ const seen = new Set()
298
+ const ordered = []
299
+ const push = (p) => {
300
+ if (!p || seen.has(p.key)) return
301
+ seen.add(p.key)
302
+ ordered.push(p)
303
+ }
304
+ push(implementer)
305
+ // Prefer risk-matched bands next; always try to keep one frontier visible.
306
+ const rest = [...points].sort((a, b) => preferRisk(a.tier, risk) - preferRisk(b.tier, risk) || a.id.localeCompare(b.id))
307
+ for (const p of rest) push(p)
308
+ const frontier = ordered.find((p) => p.tier === 'frontier' && p.key !== implementer?.key)
309
+ let primary = ordered.slice(0, ASK_OPTION_LIMIT)
310
+ if (frontier && !primary.some((p) => p.key === frontier.key) && primary.length === ASK_OPTION_LIMIT) {
311
+ primary = [...primary.slice(0, ASK_OPTION_LIMIT - 1), frontier]
312
+ }
313
+ const primaryKeys = new Set(primary.map((p) => p.key))
314
+ const options = primary.map((p) => optionFromPoint(p, { recommended: p.key === implementer?.key, role: 'implementer' }))
315
+ const alternates = ordered
316
+ .filter((p) => !primaryKeys.has(p.key))
317
+ .map((p) => optionFromPoint(p, { recommended: false, role: 'implementer' }))
318
+
319
+ return {
320
+ header: 'Implementer',
321
+ question: 'Which model should implement every issue in this rush run? (Explorer + reviewer are filled automatically; reviewer ≠ implementer.)',
322
+ recommended: implementer?.key || null,
323
+ options,
324
+ alternates,
325
+ explorerFill: slotFillPayload(explorer),
326
+ reviewerFill: slotFillPayload(reviewer),
327
+ }
328
+ }
329
+
330
+ export function buildPreflightModels({
331
+ harness,
332
+ risk = 'routine',
333
+ available = [],
334
+ humanNamed = [],
335
+ env = process.env,
336
+ discoverFn = discover,
337
+ tierFn = tierProposals,
338
+ } = {}) {
339
+ if (!harness) throw new Error('harness is required')
340
+ if (!RISK.includes(risk)) throw new Error(`risk must be one of ${RISK.join('|')}`)
341
+
342
+ const discovery = discoverFn(harness, env, {
343
+ spawnModelIds: available,
344
+ humanNamedModelIds: humanNamed,
345
+ })
346
+ const seenIds = discovery.discovered.map((d) => d.id)
347
+ const tiered = tierFn(harness, seenIds)
348
+ const catalog = readCatalog(pathForHarness(harness, env))
349
+ const seed = readSeed(defaultSeedPath())
350
+ const points = slotPoints(tiered.models, { harness, catalog, seed })
351
+ const implementer = recommendImplementer(points, risk)
352
+ if (!implementer) {
353
+ return {
354
+ ok: false,
355
+ code: 'no-slot-eligible',
356
+ harness,
357
+ risk,
358
+ discovery,
359
+ models: tiered,
360
+ points: [],
361
+ implementer: null,
362
+ explorer: null,
363
+ reviewer: null,
364
+ ask: null,
365
+ deferred: {
366
+ afterConfirm: [
367
+ 'model-catalog.mjs sync --harness <h> --seen <ids>',
368
+ 'model-catalog.mjs bank / bank-from-seed for any human confirmation',
369
+ ],
370
+ },
371
+ }
372
+ }
373
+ const explorer = fillExplorer(points, harness)
374
+ const reviewer = rankReviewers(points, implementer, harness)
375
+ const ask = buildAsk(implementer, points, reviewer, { risk, explorer })
376
+ return {
377
+ ok: true,
378
+ harness,
379
+ risk,
380
+ discovery: {
381
+ completeness: discovery.completeness,
382
+ discovered: discovery.discovered,
383
+ notes: discovery.notes,
384
+ },
385
+ models: tiered,
386
+ points,
387
+ implementer,
388
+ explorer: {
389
+ fill: explorer.fill,
390
+ reason: explorer.reason,
391
+ },
392
+ reviewer: {
393
+ fill: reviewer.fill,
394
+ reason: reviewer.reason,
395
+ ranking: reviewer.ranking.map((p) => p.key),
396
+ teamDefaultApplied: reviewer.teamDefaultApplied,
397
+ reducedDiversity: reviewer.reducedDiversity,
398
+ notIndependent: reviewer.notIndependent,
399
+ },
400
+ ask,
401
+ deferred: {
402
+ afterConfirm: [
403
+ `model-catalog.mjs sync --harness ${harness} --seen ${seenIds.join(',')}`,
404
+ 'bank confirmations only after the human answers — never before the ask',
405
+ ],
406
+ doNotReadBeforeAsk: [
407
+ 'resolve-issues/references/pre-flight.md',
408
+ 'resolve-issues/references/pre-flight-model-slots.md',
409
+ 'harness-runtime/references/model-catalog.md',
410
+ ],
411
+ },
412
+ }
413
+ }
414
+
415
+ export function runCli(argv, env = process.env) {
416
+ let args
417
+ try {
418
+ args = parseArgs(argv)
419
+ } catch (e) {
420
+ console.error(`preflight-models: ${e.message}`)
421
+ console.error(usage())
422
+ return 2
423
+ }
424
+ if (args.help) {
425
+ console.log(usage())
426
+ return 0
427
+ }
428
+ const harness = args.harness || detectHarness(env)
429
+ if (!harness) {
430
+ console.error('preflight-models: pass --harness <pi|codex|claude-code>')
431
+ return 2
432
+ }
433
+ if (!RISK.includes(args.risk)) {
434
+ console.error(`preflight-models: --risk must be ${RISK.join('|')}`)
435
+ return 2
436
+ }
437
+
438
+ let result
439
+ try {
440
+ result = buildPreflightModels({
441
+ harness,
442
+ risk: args.risk,
443
+ available: args.available,
444
+ humanNamed: args.humanNamed,
445
+ env,
446
+ })
447
+ } catch (e) {
448
+ console.error(`preflight-models: ${e.message}`)
449
+ return 2
450
+ }
451
+
452
+ if (!result.ok) {
453
+ console.log(JSON.stringify(result, null, 2))
454
+ return 3
455
+ }
456
+ if (args.askOnly) {
457
+ console.log(JSON.stringify(result.ask, null, 2))
458
+ return 0
459
+ }
460
+ console.log(JSON.stringify(result, null, 2))
461
+ return 0
462
+ }
463
+
464
+ if (isMainModule(import.meta.url)) {
465
+ process.exit(runCli(process.argv.slice(2)))
466
+ }
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1AboveIO
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: rush-release
3
+ description: "Slash/explicit-only (/rush-release). Cut a GitHub Flow release from main: freeze the latest CI-green trunk SHA, write the changelog, choose SemVer, merge the metadata cut through a verified PR, tag the exact merged main SHA, and watch Cloud Build or npm publish. Do not auto-select. Use only on explicit user/orchestrator invoke. NOT for GitFlow/release-branch/0%-traffic promotion (resolve-release) and NOT for implementing a spec (rush-issues)."
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Rush release
8
+
9
+ Cut a **GitHub Flow** release from **main**. Freeze one already-green trunk
10
+ SHA, write a metadata-only version cut on that parent, merge it through a
11
+ verified PR, tag the exact merged SHA on `main`, and watch the publisher.
12
+
13
+ The orchestrator sequences local programs and one confirmation. It does not
14
+ assemble a release branch, shift traffic, or verify production like
15
+ `resolve-release`.
16
+
17
+ ```text
18
+ preflight (main + publisher)
19
+ -> latest CI-green HEAD on main
20
+ -> changelog + SemVer plan (show user)
21
+ -> confirm
22
+ -> bump version files + commit the cut
23
+ -> merge metadata PR + verify merged main SHA
24
+ -> annotated tag on the merged SHA
25
+ -> push tag -> watch Cloud Build or npm
26
+ ```
27
+
28
+ This file is the workflow and index. Read the linked procedure immediately
29
+ before its stage; do not invent an unlinked substitute.
30
+
31
+ | Stage / concern | Procedure |
32
+ |---|---|
33
+ | Trunk, publisher, version files | [references/preflight.md](references/preflight.md) |
34
+ | Latest CI-green HEAD | [references/candidate.md](references/candidate.md) |
35
+ | Changelog, SemVer, file bump, commit | [references/cut.md](references/cut.md) |
36
+ | Tag, push, watch deploy | [references/publish.md](references/publish.md) |
37
+
38
+ Run every script as
39
+ `node <skillsRoot>/rush-release/scripts/<name>.mjs …` with the absolute skill
40
+ root this file was loaded from. A CWD-relative `scripts/…` path resolves in the
41
+ target repo and will not find these files.
42
+
43
+ ## Run invariants
44
+
45
+ - Trunk is `main`. Another default branch is a different workflow.
46
+ - The candidate is the newest SHA on `main` whose required CI is green — not
47
+ "the tip looks fine" and not a commit that is still pending.
48
+ - The cut's parent is that frozen SHA. Do not rebase, merge, or cherry-pick
49
+ later `main` onto the cut branch; the provider merge is a separate identity.
50
+ - Never publish an off-main cut. Merge the metadata PR through the repository's
51
+ queue, verify its exact merged SHA is reachable from `origin/main`, then tag
52
+ that merged SHA. Its Git tree must exactly equal the approved cut tree; any
53
+ post-freeze trunk content requires a new plan. The queue verdict is the
54
+ release tree's CI evidence.
55
+ - Tags are immutable annotated `vMAJOR.MINOR.PATCH` names. Do not move, delete,
56
+ or force-push a tag that reached a remote.
57
+ - Show the plan and wait for confirmation before any git mutation or push.
58
+ - A failed watch is a hand-back. This lane does not roll back production.
59
+ - The cut is metadata only (changelog + version identity files). Refuse a
60
+ cut that changes product code.
61
+
62
+ ## Workflow
63
+
64
+ ### 1. Preflight
65
+
66
+ Confirm `main` is the trunk, detect whether the tag should fire Cloud Build,
67
+ npm, or both, and list the files that carry the version. Details:
68
+ [preflight.md](references/preflight.md).
69
+
70
+ ### 2. Candidate
71
+
72
+ Fetch `origin/main` and pick the newest green SHA. Freeze it. Details:
73
+ [candidate.md](references/candidate.md).
74
+
75
+ ### 3. Plan and confirm
76
+
77
+ Derive the changelog and SemVer bump from `baselineTag..frozenSHA`. Show SHA,
78
+ bump, version, tag, files, publisher, and notes. Do not mutate until the human
79
+ confirms. Details: [cut.md](references/cut.md).
80
+
81
+ ### 4. Cut
82
+
83
+ Branch from the frozen SHA, apply the changelog and version files, commit.
84
+ Keep the diff metadata-only.
85
+
86
+ ### 5. Merge, tag, and watch
87
+
88
+ Push the cut ref, merge its PR through the repository queue, verify the exact
89
+ merged SHA on `main`, create the annotated tag on that SHA, push
90
+ `refs/tags/<tag>` with hook-safe `--no-verify`, then watch Cloud Build and/or
91
+ npm. Details: [publish.md](references/publish.md).
92
+
93
+ ## Done
94
+
95
+ Success is the confirmed version tagged at the verified merged `main` SHA, the tag visible on the
96
+ remote, and the chosen publisher reporting that version (Cloud Build SUCCESS
97
+ and/or `npm view <pkg>@<version>`). Hand back the cut SHA, merged SHA, tag,
98
+ changelog excerpt, publisher evidence, and the merged metadata PR. A publisher failure hands back the same identities without claiming
99
+ the version shipped to users.
@@ -0,0 +1,8 @@
1
+ interface:
2
+ display_name: "Rush Release"
3
+ short_description: "Slash/explicit-only GitHub Flow release from green main"
4
+
5
+ policy:
6
+ # Codex counterpart to SKILL.md disable-model-invocation: true.
7
+ # Keeps $rush-release / explicit invoke while blocking description-based selection.
8
+ allow_implicit_invocation: false
@@ -0,0 +1,44 @@
1
+ {
2
+ "skill_name": "rush-release",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "prompt": "Dry-run only: do not modify Git, push a tag, or contact a publisher. Plan a GitHub Flow release from main. origin/main tip is pending CI; the previous first-parent commit abcdef1 is green. Commits since v1.4.0 are feat: export csv and fix: timezone. package.json is 1.4.0. Publisher is npm. Show the frozen SHA, changelog, SemVer, tag target, and watch command you would use after confirmation.",
7
+ "expected_output": "A dry-run plan that freezes abcdef1 rather than the pending tip, proposes a minor bump to 1.5.0 from the feat, writes changelog entries for both commits, merges the metadata cut through the queue, tags the verified merged main SHA only if its tree exactly equals the cut tree, and watches npm view rather than a Cloud Build traffic shift.",
8
+ "files": [],
9
+ "expectations": [
10
+ "Freezes the green SHA abcdef1 instead of the pending origin/main tip.",
11
+ "Proposes a minor SemVer bump to 1.5.0 because of feat: export csv.",
12
+ "Includes changelog entries derived from both the feat and the fix.",
13
+ "Tags only the queue-verified merged main SHA whose tree exactly equals the version-cut tree.",
14
+ "Watches npm publish (npm view) and does not describe a 0% traffic candidate or GitFlow release branch."
15
+ ]
16
+ },
17
+ {
18
+ "id": 2,
19
+ "prompt": "Dry-run only. Main is protected. Latest green HEAD is 2222222. Plan says v2.0.0 from a breaking change. The metadata PR merges through the queue as 3333333 and its tree exactly equals the approved cut. After you would push tag v2.0.0, Cloud Build returns FAILURE. What do you tag, how do you push it, and what happens after the failed watch? Assume cloudbuild.yaml is present.",
20
+ "expected_output": "A dry-run that verifies 3333333 is on main and tree-identical to the cut, tags merged SHA 3333333 with annotated v2.0.0, pushes refs/tags/v2.0.0 with --no-verify, watches gcloud builds list, and on FAILURE leaves the tag in place, treats the version as burned, and hands back without deleting the tag or rolling back production.",
21
+ "files": [],
22
+ "expectations": [
23
+ "Creates an annotated v2.0.0 tag on the verified merged main SHA 3333333.",
24
+ "Pushes the fully-qualified tag ref with --no-verify.",
25
+ "Watches Cloud Build filtered by that tag.",
26
+ "On FAILURE, does not move or delete the tag and does not roll back production.",
27
+ "States the next attempt takes a new patch from a new green HEAD."
28
+ ]
29
+ },
30
+ {
31
+ "id": 3,
32
+ "prompt": "Dry-run only. origin/main moved after the candidate was frozen: green SHA is 3333333, then an ungreen commit 4444444 landed on main. How do you cut 1.2.3 so the ungreen commit is not in the tag? Publisher is both Cloud Build and npm.",
33
+ "expected_output": "A dry-run that branches from 3333333 and commits metadata only, but refuses publication because a merge containing 4444444 cannot be tree-identical to the approved cut. It creates no tag, waits for a new eligible main state, then restarts candidate selection and the changelog/SemVer plan before watching either publisher.",
34
+ "files": [],
35
+ "expectations": [
36
+ "Branches the cut from frozen SHA 3333333.",
37
+ "Refuses to rebase or merge the later ungreen commit into the tagged object.",
38
+ "Creates no tag while the only merged result would contain unplanned commit 4444444.",
39
+ "Restarts the changelog and SemVer plan from a new eligible main state before watching Cloud Build and npm.",
40
+ "Does not squash-merge the metadata PR onto moved main."
41
+ ]
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,30 @@
1
+ # Candidate
2
+
3
+ The release candidate is the newest commit that is on `main` and already green.
4
+ The current tip is eligible only when that exact SHA is green.
5
+
6
+ ## Procedure
7
+
8
+ 1. Fetch `origin/main`.
9
+ 2. Walk first-parent history of `origin/main`, newest first.
10
+ 3. Run the picker:
11
+
12
+ ```bash
13
+ node <skillsRoot>/rush-release/scripts/green-head.mjs --trunk main --json
14
+ ```
15
+
16
+ 4. Freeze `candidateSha` from the JSON. Do not keep walking after a green SHA
17
+ in the hope of a "nicer" commit.
18
+ 5. If the script exits 1 (`no-green-head`), stop. Repair CI or wait; do not
19
+ release a pending or failed SHA, and do not treat "no checks configured" as
20
+ green.
21
+
22
+ ## Why freeze
23
+
24
+ `main` keeps moving. The changelog, the bump, and the tag all describe one
25
+ commit. If a later commit lands while you write notes, including it would ship
26
+ code that never passed this candidate check.
27
+
28
+ The frozen SHA is the **parent of the cut**, not yet the tag. Version files
29
+ still need a metadata commit on top; that child is what gets tagged
30
+ ([cut.md](cut.md)).