@ikon85/agent-workflow-kit 0.44.1 → 0.45.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/.agents/skills/audit-skills/SKILL.md +7 -4
- package/.agents/skills/code-review/SKILL.md +7 -4
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
- package/.agents/skills/improve-codebase-architecture/SKILL.md +7 -4
- package/.agents/skills/orchestrate-wave/SKILL.md +1 -1
- package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
- package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
- package/.agents/skills/research/SKILL.md +7 -4
- package/.agents/skills/to-issues/SKILL.md +25 -4
- package/.claude/skills/audit-skills/SKILL.md +7 -4
- package/.claude/skills/code-review/SKILL.md +7 -4
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +7 -4
- package/.claude/skills/codex-build/SKILL.md +13 -0
- package/.claude/skills/codex-review/SKILL.md +13 -0
- package/.claude/skills/grill-me-codex/SKILL.md +14 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +14 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +7 -4
- package/.claude/skills/improve-codebase-architecture/SKILL.md +7 -4
- package/.claude/skills/orchestrate-wave/SKILL.md +1 -1
- package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +9 -0
- package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +9 -0
- package/.claude/skills/research/SKILL.md +7 -4
- package/.claude/skills/skill-manifest.json +34 -23
- package/.claude/skills/to-issues/SKILL.md +25 -4
- package/README.md +71 -0
- package/agent-workflow-kit.package.json +149 -45
- package/package.json +1 -1
- package/scripts/doctrine-migration/index.mjs +296 -0
- package/scripts/kit-release.mjs +41 -9
- package/src/cli.mjs +521 -80
- package/src/commands/routing-status.mjs +288 -0
- package/src/commands/update.mjs +9 -1
- package/src/consumer-migrations.json +23 -1
- package/src/lib/bundle.mjs +158 -2
- package/src/lib/consumerMigrations.mjs +55 -0
- package/src/lib/dispatchJournal.mjs +300 -0
- package/src/lib/dispatchPlan.mjs +286 -0
- package/src/lib/dispatchReceipt.mjs +226 -89
- package/src/lib/frontendWorkloads.mjs +35 -33
- package/src/lib/routeDispatcher.mjs +367 -70
- package/src/lib/routingAccessGraph.mjs +265 -20
- package/src/lib/routingAccessGraphStore.mjs +300 -0
- package/src/lib/routingAdapters/claude.mjs +104 -4
- package/src/lib/routingAdapters/codex.mjs +132 -7
- package/src/lib/routingAdapters/hostBridge.mjs +291 -0
- package/src/lib/routingCatalog.mjs +201 -24
- package/src/lib/routingDispatchLease.mjs +253 -0
- package/src/lib/routingEvidenceCache.mjs +78 -0
- package/src/lib/routingIntent.mjs +176 -10
- package/src/lib/routingIntentClassifier.mjs +137 -0
- package/src/lib/routingInventory/snapshots/claude.json +49 -0
- package/src/lib/routingInventory/snapshots/codex.json +109 -0
- package/src/lib/routingInventory.mjs +182 -0
- package/src/lib/routingPolicy.mjs +193 -6
- package/src/lib/routingProfile.mjs +1251 -123
- package/src/lib/routingProfilePolicy.mjs +156 -0
- package/src/lib/routingProfileStorage.mjs +299 -0
- package/src/lib/routingResolver.mjs +369 -86
- package/src/lib/routingSources/artificialAnalysis.mjs +19 -7
- package/src/lib/routingSources/benchlm.mjs +5 -0
- package/src/lib/routingSources/codeArena.mjs +13 -3
- package/src/lib/routingSources/deepswe.mjs +19 -5
- package/src/lib/routingSources/openhands.mjs +17 -4
- package/src/lib/routingSources/openhandsFrontend.mjs +13 -3
- package/src/lib/safeText.mjs +26 -0
- package/src/lib/updateCandidate.mjs +36 -3
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Doctrine migration — retire the hand-maintained model-and-effort routing
|
|
4
|
+
* table from the user-global instruction file.
|
|
5
|
+
*
|
|
6
|
+
* The table competes with the configuration that now holds the same knowledge:
|
|
7
|
+
* the Routing profile carries the Model roster and the three Standard routes,
|
|
8
|
+
* the Routing policy is derived from it, and the Route decision names the model
|
|
9
|
+
* and effort one dispatch applies. What the table never was is judgment — when
|
|
10
|
+
* delegation pays for itself, the escalation rule, and one worktree per
|
|
11
|
+
* parallel writing agent are decisions no roster can hold, so they stay.
|
|
12
|
+
*
|
|
13
|
+
* The file is the user's own, outside this repository and outside the consumer
|
|
14
|
+
* manifest, and may carry unrelated instructions. Every step is therefore
|
|
15
|
+
* fail-closed: an unplaceable bullet blocks the whole migration instead of
|
|
16
|
+
* being guessed away, and so does a judgment surviving only inside a
|
|
17
|
+
* data-bearing bullet; the removal is previewed as an exact diff, the original
|
|
18
|
+
* copied to a named backup before a byte is written, the destination re-read
|
|
19
|
+
* against the previewed fingerprint, and nothing written without acceptance.
|
|
20
|
+
*
|
|
21
|
+
* Runtime precedence does not wait for any of that. Where a Routing profile
|
|
22
|
+
* decides model and effort, it decides them whether or not the old table is
|
|
23
|
+
* still in the file — `resolveDoctrinePrecedence` reads the profile, never the
|
|
24
|
+
* doctrine text — so deferring the migration forever still yields the profile's
|
|
25
|
+
* answer. The table is only ever the fallback for a profile that cannot decide,
|
|
26
|
+
* which is why applying the removal is refused while it still is one.
|
|
27
|
+
*
|
|
28
|
+
* Run: node scripts/doctrine-migration/index.mjs [--file <path>] [--apply --accept]
|
|
29
|
+
*/
|
|
30
|
+
import { readFile } from 'node:fs/promises';
|
|
31
|
+
import { createHash } from 'node:crypto';
|
|
32
|
+
import { homedir } from 'node:os';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
|
|
35
|
+
import { backupFile, lineDiff, writeAtomic } from '../../src/lib/atomicWrite.mjs';
|
|
36
|
+
import { readComposedRoutingProfile } from '../../src/lib/routingProfile.mjs';
|
|
37
|
+
|
|
38
|
+
/** The section that owns the doctrine, and the heading level that ends it. */
|
|
39
|
+
export const DOCTRINE_SECTION_PATTERN = /^##\s+Task-Routing\b/;
|
|
40
|
+
const SECTION_END_PATTERN = /^##\s/;
|
|
41
|
+
|
|
42
|
+
/** The one sentence that replaces the table, and the substring that proves it is there. */
|
|
43
|
+
export const PRECEDENCE_MARKER = 'decides model and effort';
|
|
44
|
+
export const PRECEDENCE_BULLET = '- Where a Routing profile exists, it decides model and effort;\n'
|
|
45
|
+
+ ' what stays here is the judgment that is not data.';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The judgment that is not data. Each id must survive in a retained bullet or
|
|
49
|
+
* the migration blocks: losing one of these is the failure this tool exists to
|
|
50
|
+
* prevent, and "it was probably still somewhere" is not a check.
|
|
51
|
+
*/
|
|
52
|
+
export const RETAINED_JUDGMENTS = Object.freeze([
|
|
53
|
+
{ id: 'delegation-pays-for-itself', pattern: /delegat/i },
|
|
54
|
+
{ id: 'parallel-writes-need-a-worktree', pattern: /worktree/i },
|
|
55
|
+
{ id: 'escalation-after-repeated-failure', pattern: /eskalat|escalat/i },
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What makes a bullet table content. A data marker outranks a judgment marker:
|
|
60
|
+
* a bullet that names a model, an effort level or the table's own companion
|
|
61
|
+
* file is routing data even when it also mentions delegation, and the judgment
|
|
62
|
+
* coverage check is what proves nothing was lost by removing it.
|
|
63
|
+
*/
|
|
64
|
+
export const ROUTING_DATA_MARKERS = Object.freeze([
|
|
65
|
+
{ id: 'model-name', pattern: /\b(sonnet|opus|haiku|fable|gpt-[\w.-]+|claude-[\w[\].-]+)\b/i },
|
|
66
|
+
{ id: 'effort-level', pattern: /\beffort\b|\b(low|medium|high|xhigh)\b/i },
|
|
67
|
+
{ id: 'routing-table-pointer', pattern: /task-routing\.md/i },
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const sha256 = (text) => createHash('sha256').update(text).digest('hex');
|
|
71
|
+
|
|
72
|
+
const stampOf = (now) => now.toISOString().replace(/[:.]/g, '-');
|
|
73
|
+
|
|
74
|
+
const blocked = (status, reasons) => Object.freeze({
|
|
75
|
+
status, reasons: Object.freeze(reasons), retained: [], removed: [], migrated: null,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** Where the doctrine section starts and ends, or `null` when the file has none. */
|
|
79
|
+
function sectionRange(lines) {
|
|
80
|
+
const start = lines.findIndex((line) => DOCTRINE_SECTION_PATTERN.test(line));
|
|
81
|
+
if (start < 0) return null;
|
|
82
|
+
const rest = lines.slice(start + 1).findIndex((line) => SECTION_END_PATTERN.test(line));
|
|
83
|
+
return { start, end: rest < 0 ? lines.length : start + 1 + rest };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The section body as blocks: one per bullet, wrapped continuation lines kept
|
|
88
|
+
* with the bullet they belong to, and any non-bullet prose as its own block so
|
|
89
|
+
* it is classified rather than swept along.
|
|
90
|
+
*/
|
|
91
|
+
export function doctrineBlocks(bodyLines) {
|
|
92
|
+
const blocks = [];
|
|
93
|
+
let open = null;
|
|
94
|
+
for (const line of bodyLines) {
|
|
95
|
+
if (line.trim() === '') { open = null; continue; }
|
|
96
|
+
const bullet = /^-\s/.test(line);
|
|
97
|
+
if (bullet || open === null) {
|
|
98
|
+
open = { kind: bullet ? 'bullet' : 'prose', lines: [] };
|
|
99
|
+
blocks.push(open);
|
|
100
|
+
}
|
|
101
|
+
open.lines.push(line);
|
|
102
|
+
}
|
|
103
|
+
return blocks;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const matched = (text, markers) => markers.filter(({ pattern }) => pattern.test(text))
|
|
107
|
+
.map(({ id }) => id);
|
|
108
|
+
|
|
109
|
+
/** Split the section's blocks into table data, retained judgment, and the unplaceable rest. */
|
|
110
|
+
export function classifyDoctrine(blocks) {
|
|
111
|
+
const retained = [];
|
|
112
|
+
const removed = [];
|
|
113
|
+
const unclassified = [];
|
|
114
|
+
for (const block of blocks) {
|
|
115
|
+
const text = block.lines.join(' ');
|
|
116
|
+
const markers = matched(text, ROUTING_DATA_MARKERS);
|
|
117
|
+
const judgments = matched(text, RETAINED_JUDGMENTS);
|
|
118
|
+
if (markers.length) removed.push({ ...block, markers });
|
|
119
|
+
else if (judgments.length) retained.push({ ...block, judgments });
|
|
120
|
+
else unclassified.push(block);
|
|
121
|
+
}
|
|
122
|
+
return { retained, removed, unclassified };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const rebuild = (lines, range, retained) => [
|
|
126
|
+
...lines.slice(0, range.start), lines[range.start], '', ...PRECEDENCE_BULLET.split('\n'),
|
|
127
|
+
...retained.flatMap((block) => block.lines), '', ...lines.slice(range.end),
|
|
128
|
+
].join('\n');
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The exact removal, decided from the file text alone. Returns the migrated
|
|
132
|
+
* text only when every block was placed and every promised judgment survived.
|
|
133
|
+
*/
|
|
134
|
+
export function planDoctrineMigration(text) {
|
|
135
|
+
const lines = text.split('\n');
|
|
136
|
+
const range = sectionRange(lines);
|
|
137
|
+
if (!range) return blocked('section-missing', ['no `## Task-Routing` section']);
|
|
138
|
+
const body = lines.slice(range.start + 1, range.end);
|
|
139
|
+
if (body.some((line) => line.includes(PRECEDENCE_MARKER))) {
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
status: 'already-migrated', reasons: [], retained: [], removed: [], migrated: text,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const { retained, removed, unclassified } = classifyDoctrine(doctrineBlocks(body));
|
|
145
|
+
if (unclassified.length) {
|
|
146
|
+
return blocked('blocked', unclassified.map((block) => `unclassified: ${block.lines[0]}`));
|
|
147
|
+
}
|
|
148
|
+
const kept = new Set(retained.flatMap((block) => block.judgments));
|
|
149
|
+
const lost = RETAINED_JUDGMENTS.filter(({ id }) => !kept.has(id));
|
|
150
|
+
if (lost.length) {
|
|
151
|
+
return blocked('blocked', lost.map(({ id }) => `judgment-not-retained: ${id}`));
|
|
152
|
+
}
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
status: 'ready',
|
|
155
|
+
reasons: [],
|
|
156
|
+
retained: Object.freeze(retained),
|
|
157
|
+
removed: Object.freeze(removed),
|
|
158
|
+
migrated: rebuild(lines, range, retained),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Who decides model and effort right now. The doctrine text is deliberately not
|
|
164
|
+
* read: precedence is a property of the configuration, so the answer cannot
|
|
165
|
+
* depend on whether the old table has been removed yet. `tablePresent` only
|
|
166
|
+
* records whether the removal still has anything to remove.
|
|
167
|
+
*/
|
|
168
|
+
export function resolveDoctrinePrecedence({ composed, tablePresent = false, reasons = [] }) {
|
|
169
|
+
const routes = composed?.standardRoutes ?? {};
|
|
170
|
+
const decides = Object.entries(routes)
|
|
171
|
+
.filter(([, route]) => route?.state === 'configured')
|
|
172
|
+
.map(([workload, route]) => Object.freeze({
|
|
173
|
+
workload, model: route.model, effort: route.effort ?? null,
|
|
174
|
+
}));
|
|
175
|
+
return Object.freeze({
|
|
176
|
+
source: decides.length ? 'routing-profile' : 'doctrine',
|
|
177
|
+
decides: Object.freeze(decides),
|
|
178
|
+
tablePresent: Boolean(tablePresent),
|
|
179
|
+
supersededTable: Boolean(decides.length && tablePresent),
|
|
180
|
+
reasons: Object.freeze([...reasons]),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The same answer read from the real two-level Routing profile store. */
|
|
185
|
+
export async function readDoctrinePrecedence({ tablePresent = false, ...options } = {}) {
|
|
186
|
+
try {
|
|
187
|
+
const snapshot = await readComposedRoutingProfile({
|
|
188
|
+
projectRoot: process.cwd(), ...options,
|
|
189
|
+
});
|
|
190
|
+
return resolveDoctrinePrecedence({
|
|
191
|
+
composed: snapshot.composed, tablePresent, reasons: snapshot.reasons,
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return resolveDoctrinePrecedence({
|
|
195
|
+
composed: null, tablePresent, reasons: [`profile-unreadable: ${error.message}`],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read the file, plan the removal, report it — the fingerprint the apply step
|
|
202
|
+
* re-checks, the backup path it would write, the exact diff. Writes nothing.
|
|
203
|
+
*/
|
|
204
|
+
export async function previewDoctrineMigration({
|
|
205
|
+
path, now = new Date(), resolvePrecedence = readDoctrinePrecedence,
|
|
206
|
+
}) {
|
|
207
|
+
const text = await readFile(path, 'utf8');
|
|
208
|
+
const plan = planDoctrineMigration(text);
|
|
209
|
+
const stamp = stampOf(now);
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
path,
|
|
212
|
+
status: plan.status,
|
|
213
|
+
reasons: plan.reasons,
|
|
214
|
+
retained: plan.retained,
|
|
215
|
+
removed: plan.removed,
|
|
216
|
+
migrated: plan.migrated,
|
|
217
|
+
fingerprint: sha256(text),
|
|
218
|
+
stamp,
|
|
219
|
+
backupPath: `${path}.${stamp}.bak`,
|
|
220
|
+
diff: plan.migrated === null || plan.migrated === text ? '' : lineDiff(text, plan.migrated),
|
|
221
|
+
precedence: await resolvePrecedence({ tablePresent: plan.removed.length > 0 }),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const refused = (reason, preview) => Object.freeze({
|
|
226
|
+
status: 'refused', reason, path: preview.path, backupPath: null,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Write the previewed removal — and only that. Every gate below is a separate
|
|
231
|
+
* reason a caller can report, because "it did not run" is not a diagnosis.
|
|
232
|
+
*/
|
|
233
|
+
export async function applyDoctrineMigration({ preview, accept = false }) {
|
|
234
|
+
if (preview.status === 'already-migrated') {
|
|
235
|
+
return Object.freeze({ status: 'already-migrated', path: preview.path, backupPath: null });
|
|
236
|
+
}
|
|
237
|
+
if (accept !== true) return refused('acceptance-required', preview);
|
|
238
|
+
if (preview.status !== 'ready') return refused(`not-ready: ${preview.status}`, preview);
|
|
239
|
+
if (preview.precedence?.source !== 'routing-profile') {
|
|
240
|
+
return refused('doctrine-table-still-authoritative', preview);
|
|
241
|
+
}
|
|
242
|
+
const current = await readFile(preview.path, 'utf8');
|
|
243
|
+
if (sha256(current) !== preview.fingerprint) return refused('destination-changed', preview);
|
|
244
|
+
const backupPath = await backupFile(preview.path, preview.stamp);
|
|
245
|
+
await writeAtomic(preview.path, preview.migrated);
|
|
246
|
+
return Object.freeze({ status: 'applied', path: preview.path, backupPath });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function parseArgs(argv) {
|
|
250
|
+
const args = { apply: false, accept: false, file: null };
|
|
251
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
252
|
+
if (argv[i] === '--apply') args.apply = true;
|
|
253
|
+
else if (argv[i] === '--accept') args.accept = true;
|
|
254
|
+
else if (argv[i] === '--file') args.file = argv[i += 1];
|
|
255
|
+
else throw new Error(`unknown argument: ${argv[i]}`);
|
|
256
|
+
}
|
|
257
|
+
return args;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function render(preview, out) {
|
|
261
|
+
const { precedence } = preview;
|
|
262
|
+
out.log(`file: ${preview.path}`);
|
|
263
|
+
out.log(`status: ${preview.status}`);
|
|
264
|
+
for (const reason of preview.reasons) out.log(` reason: ${reason}`);
|
|
265
|
+
out.log(`precedence: ${precedence.source}`
|
|
266
|
+
+ (precedence.supersededTable ? ' (the table below is already inert)' : ''));
|
|
267
|
+
for (const { workload, model, effort } of precedence.decides) {
|
|
268
|
+
out.log(` ${workload}: ${model}${effort ? ` ${effort}` : ''}`);
|
|
269
|
+
}
|
|
270
|
+
for (const reason of precedence.reasons) out.log(` profile: ${reason}`);
|
|
271
|
+
out.log(`backup: ${preview.backupPath}`);
|
|
272
|
+
out.log(preview.diff ? `\n${preview.diff}` : '\n(no change)');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function main(argv = process.argv.slice(2), out = console) {
|
|
276
|
+
const args = parseArgs(argv);
|
|
277
|
+
const path = args.file ?? join(homedir(), '.claude', 'CLAUDE.md');
|
|
278
|
+
const preview = await previewDoctrineMigration({ path }).catch((error) => error);
|
|
279
|
+
if (preview instanceof Error) {
|
|
280
|
+
out.log(`file: ${path}\nstatus: unreadable: ${preview.code ?? preview.message}`);
|
|
281
|
+
return 1;
|
|
282
|
+
}
|
|
283
|
+
render(preview, out);
|
|
284
|
+
if (!args.apply) {
|
|
285
|
+
out.log('\npreview only — re-run with --apply --accept to write it.');
|
|
286
|
+
return preview.status === 'ready' || preview.status === 'already-migrated' ? 0 : 1;
|
|
287
|
+
}
|
|
288
|
+
const result = await applyDoctrineMigration({ preview, accept: args.accept });
|
|
289
|
+
out.log(`\n${result.status}${result.reason ? `: ${result.reason}` : ''}`);
|
|
290
|
+
if (result.backupPath) out.log(`backup written: ${result.backupPath}`);
|
|
291
|
+
return result.status === 'refused' ? 1 : 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
295
|
+
process.exitCode = await main();
|
|
296
|
+
}
|
package/scripts/kit-release.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { verifyBundle } from '../src/lib/bundle.mjs';
|
|
8
9
|
import { applyProjectRelease } from '../src/lib/release-apply.mjs';
|
|
9
10
|
import { previewProjectRelease } from '../src/lib/release-preview.mjs';
|
|
10
11
|
import { nextVersion } from '../src/lib/semver.mjs';
|
|
@@ -50,12 +51,31 @@ async function updateMetadata(repoRoot, targetVersion, delta) {
|
|
|
50
51
|
return resumed;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Build the bundle and keep it on disk until it has been verified — the
|
|
56
|
+
* manifest is only believable together with the bytes it describes. The build
|
|
57
|
+
* reads the committed, provenance-hashed inventory snapshots from the
|
|
58
|
+
* repository; nothing here reaches the network.
|
|
59
|
+
*/
|
|
60
|
+
async function freshBundle(repoRoot) {
|
|
54
61
|
const distDir = await mkdtemp(join(tmpdir(), 'awkit-release-'));
|
|
55
62
|
try {
|
|
56
63
|
await buildKit({ repoRoot, distDir });
|
|
57
|
-
return
|
|
58
|
-
|
|
64
|
+
return {
|
|
65
|
+
manifest: JSON.parse(await readFile(join(distDir, 'agent-workflow-kit.package.json'), 'utf8')),
|
|
66
|
+
bundleRoot: distDir,
|
|
67
|
+
cleanup: () => rm(distDir, { recursive: true, force: true }),
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
await rm(distDir, { recursive: true, force: true });
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertVerified(report) {
|
|
76
|
+
if (report.ok) return;
|
|
77
|
+
const detail = report.findings.map(({ check, detail: line }) => ` ${check}: ${line}`).join('\n');
|
|
78
|
+
throw new Error(`bundle verification failed:\n${detail}`);
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
async function defaultRun(command, args, repoRoot) {
|
|
@@ -66,16 +86,28 @@ export async function prepareRelease(options) {
|
|
|
66
86
|
const { repoRoot, targetVersion, delta } = options;
|
|
67
87
|
if (!/^\d+\.\d+\.\d+$/.test(targetVersion)) throw new Error(`invalid target version: ${targetVersion}`);
|
|
68
88
|
const resumed = await updateMetadata(repoRoot, targetVersion, delta);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
const built = await (options.buildBundle ?? freshBundle)(repoRoot);
|
|
90
|
+
let report;
|
|
91
|
+
try {
|
|
92
|
+
const { manifest } = built;
|
|
93
|
+
if (manifest.kitVersion !== targetVersion) {
|
|
94
|
+
throw new Error(`built manifest version ${manifest.kitVersion} != target ${targetVersion}`);
|
|
95
|
+
}
|
|
96
|
+
report = await (options.verifyBundle ?? verifyBundle)({
|
|
97
|
+
bundleRoot: built.bundleRoot, manifest,
|
|
98
|
+
});
|
|
99
|
+
assertVerified(report);
|
|
100
|
+
await writeFile(join(repoRoot, 'agent-workflow-kit.package.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
101
|
+
} finally { await built.cleanup?.(); }
|
|
74
102
|
const run = options.run ?? ((command, args) => defaultRun(command, args, repoRoot));
|
|
75
103
|
await run('npm', ['run', 'release:guard']);
|
|
76
104
|
await run('npm', ['test']);
|
|
77
105
|
await run('npm', ['pack', '--dry-run']);
|
|
78
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
status: resumed ? 'resumed' : 'prepared',
|
|
108
|
+
targetVersion,
|
|
109
|
+
inventoryRevision: report.inventoryRevision,
|
|
110
|
+
};
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
async function main() {
|