@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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/runtime/skills/distribution/generated/recipes.json +43 -23
- package/runtime/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +1 -1
- package/runtime/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +23 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +65 -15
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +1 -1
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +23 -1
- package/skills/engineering/resolve-issues/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +55 -11
- package/skills/engineering/resolve-issues/scripts/run-state.mjs +4 -4
- package/skills/engineering/resolve-release/references/related-skills.md +1 -0
- package/skills/engineering/rush-issues/LICENSE +3 -0
- package/skills/engineering/rush-issues/SKILL.md +179 -0
- package/skills/engineering/rush-issues/agents/openai.yaml +9 -0
- package/skills/engineering/rush-issues/evals/evals.json +65 -0
- package/skills/engineering/rush-issues/references/canary.md +45 -0
- package/skills/engineering/rush-issues/references/cicd.md +37 -0
- package/skills/engineering/rush-issues/references/combine.md +51 -0
- package/skills/engineering/rush-issues/references/expire.md +55 -0
- package/skills/engineering/rush-issues/references/exploration.md +53 -0
- package/skills/engineering/rush-issues/references/implementation.md +66 -0
- package/skills/engineering/rush-issues/references/preflight.md +31 -0
- package/skills/engineering/rush-issues/references/profiling.md +78 -0
- package/skills/engineering/rush-issues/references/review.md +48 -0
- package/skills/engineering/rush-issues/references/shared-modules.md +66 -0
- package/skills/engineering/rush-issues/references/task-plan.md +110 -0
- package/skills/engineering/rush-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/rush-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/rush-issues/scripts/preflight-models.mjs +466 -0
- package/skills/engineering/rush-release/LICENSE +3 -0
- package/skills/engineering/rush-release/SKILL.md +99 -0
- package/skills/engineering/rush-release/agents/openai.yaml +8 -0
- package/skills/engineering/rush-release/evals/evals.json +44 -0
- package/skills/engineering/rush-release/references/candidate.md +30 -0
- package/skills/engineering/rush-release/references/cut.md +66 -0
- package/skills/engineering/rush-release/references/preflight.md +47 -0
- package/skills/engineering/rush-release/references/publish.md +100 -0
- package/skills/engineering/rush-release/scripts/apply.mjs +185 -0
- package/skills/engineering/rush-release/scripts/green-head.mjs +231 -0
- 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,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)).
|