@ikon85/agent-workflow-kit 0.34.3 → 0.34.4
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/kit-release/SKILL.md +12 -0
- package/.claude/skills/kit-release/SKILL.md +12 -0
- package/README.md +7 -0
- package/agent-workflow-kit.package.json +5 -5
- package/package.json +1 -1
- package/scripts/release-delta-guard.mjs +28 -1
- package/scripts/release-delta-guard.test.mjs +45 -0
- package/scripts/release-state.mjs +7 -3
- package/scripts/release-state.test.mjs +22 -0
|
@@ -110,3 +110,15 @@ manifest and compares it with both the base and the checked-in manifest. It
|
|
|
110
110
|
blocks an unbumped shipped delta, a stale manifest, a too-small Semver bump, or
|
|
111
111
|
a dead manifest entry and prints the concrete paths. CI runs the same command;
|
|
112
112
|
there is no separately remembered shipped-file list.
|
|
113
|
+
|
|
114
|
+
It also blocks a **version bump stacked on an untagged previous release**: if
|
|
115
|
+
the base version carries no matching annotated tag, that release is still
|
|
116
|
+
`awaiting-tag` and never became its own artifact. Tag and publish it first —
|
|
117
|
+
never bury it under the next bump. A repository with no matching tag at all is
|
|
118
|
+
bootstrapping its first release and is not blocked.
|
|
119
|
+
|
|
120
|
+
A red release run does not prove nothing was published. Reconstruct the real
|
|
121
|
+
state from the registry and the release before reacting; a stale local
|
|
122
|
+
packument cache can answer "not published" for a package that is live, and the
|
|
123
|
+
recovery route is the idempotent reconciler on the existing tag, never a new
|
|
124
|
+
version.
|
|
@@ -110,3 +110,15 @@ manifest and compares it with both the base and the checked-in manifest. It
|
|
|
110
110
|
blocks an unbumped shipped delta, a stale manifest, a too-small Semver bump, or
|
|
111
111
|
a dead manifest entry and prints the concrete paths. CI runs the same command;
|
|
112
112
|
there is no separately remembered shipped-file list.
|
|
113
|
+
|
|
114
|
+
It also blocks a **version bump stacked on an untagged previous release**: if
|
|
115
|
+
the base version carries no matching annotated tag, that release is still
|
|
116
|
+
`awaiting-tag` and never became its own artifact. Tag and publish it first —
|
|
117
|
+
never bury it under the next bump. A repository with no matching tag at all is
|
|
118
|
+
bootstrapping its first release and is not blocked.
|
|
119
|
+
|
|
120
|
+
A red release run does not prove nothing was published. Reconstruct the real
|
|
121
|
+
state from the registry and the release before reacting; a stale local
|
|
122
|
+
packument cache can answer "not published" for a package that is live, and the
|
|
123
|
+
recovery route is the idempotent reconciler on the existing tag, never a new
|
|
124
|
+
version.
|
package/README.md
CHANGED
|
@@ -387,6 +387,13 @@ the old way. Decision record:
|
|
|
387
387
|
|
|
388
388
|
## Release notes
|
|
389
389
|
|
|
390
|
+
### 0.34.4
|
|
391
|
+
|
|
392
|
+
- changed: `.agents/skills/kit-release/SKILL.md`
|
|
393
|
+
- changed: `.claude/skills/kit-release/SKILL.md`
|
|
394
|
+
- changed: `scripts/release-delta-guard.mjs`
|
|
395
|
+
- changed: `scripts/release-state.mjs`
|
|
396
|
+
|
|
390
397
|
### 0.34.3
|
|
391
398
|
|
|
392
399
|
- changed: `.agents/skills/diagnose/SKILL.md`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "0.34.
|
|
2
|
+
"kitVersion": "0.34.4",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": ".agents/skills/ask-matt/SKILL.md",
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
"ownerSkill": "kit-release",
|
|
368
368
|
"surface": "codex",
|
|
369
369
|
"installRole": "maintainer",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "f584caa1753ed34de584418a4176015cc3e2382db06efae488bb6d785eab71b7",
|
|
371
371
|
"mode": 420,
|
|
372
372
|
"origin": "kit"
|
|
373
373
|
},
|
|
@@ -1679,7 +1679,7 @@
|
|
|
1679
1679
|
"ownerSkill": "kit-release",
|
|
1680
1680
|
"surface": "claude",
|
|
1681
1681
|
"installRole": "maintainer",
|
|
1682
|
-
"sha256": "
|
|
1682
|
+
"sha256": "f584caa1753ed34de584418a4176015cc3e2382db06efae488bb6d785eab71b7",
|
|
1683
1683
|
"mode": 420,
|
|
1684
1684
|
"origin": "kit"
|
|
1685
1685
|
},
|
|
@@ -2655,7 +2655,7 @@
|
|
|
2655
2655
|
"path": "scripts/release-delta-guard.mjs",
|
|
2656
2656
|
"kind": "script",
|
|
2657
2657
|
"installRole": "maintainer",
|
|
2658
|
-
"sha256": "
|
|
2658
|
+
"sha256": "1d2aab5f15bca25167b166b37232139916cf814e94de873fa74a653ecf9a4619",
|
|
2659
2659
|
"mode": 420,
|
|
2660
2660
|
"origin": "kit"
|
|
2661
2661
|
},
|
|
@@ -2671,7 +2671,7 @@
|
|
|
2671
2671
|
"path": "scripts/release-state.mjs",
|
|
2672
2672
|
"kind": "script",
|
|
2673
2673
|
"installRole": "consumer",
|
|
2674
|
-
"sha256": "
|
|
2674
|
+
"sha256": "fd9f7f86e26ab3faba819e0c46e5e87c2dd3a84dc9a6111b4eceb4ebe6a3a344",
|
|
2675
2675
|
"mode": 420,
|
|
2676
2676
|
"origin": "kit"
|
|
2677
2677
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.4",
|
|
4
4
|
"description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -57,6 +57,18 @@ export function assessRelease(input) {
|
|
|
57
57
|
if (hasDelta(payloadDrift)) {
|
|
58
58
|
errors.push(`npm package payload does not match built manifest (${describe(payloadDrift)})`);
|
|
59
59
|
}
|
|
60
|
+
// Merging integrates a prepared version; only its annotated tag publishes it.
|
|
61
|
+
// Nothing else notices when that tag never arrives, so the next release PR
|
|
62
|
+
// would silently stack on top and the skipped version would never exist as
|
|
63
|
+
// its own artifact. Block that here, where a human is present anyway. A
|
|
64
|
+
// repository without any matching tag is bootstrapping, not stacking.
|
|
65
|
+
const { baseTag } = input;
|
|
66
|
+
if (baseTag?.repoHasTags && !baseTag.exists && input.currentVersion !== input.baseVersion) {
|
|
67
|
+
errors.push(
|
|
68
|
+
`previous release ${input.baseVersion} is still awaiting-tag (no ${baseTag.name} tag); `
|
|
69
|
+
+ `tag and publish it before preparing ${input.currentVersion}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
60
72
|
const recommendedBump = recommendBump(delta);
|
|
61
73
|
const actual = bumpKind(input.baseVersion, input.currentVersion);
|
|
62
74
|
const rank = { patch: 1, minor: 2, major: 3 };
|
|
@@ -73,6 +85,19 @@ function gitShowJson(repoRoot, ref, path) {
|
|
|
73
85
|
return JSON.parse(execFileSync('git', ['show', `${ref}:${path}`], { cwd: repoRoot, encoding: 'utf8' }));
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
function git(repoRoot, args) {
|
|
89
|
+
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8' }).trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveBaseTag({ repoRoot, baseVersion, tagPrefix = 'v' }) {
|
|
93
|
+
const name = `${tagPrefix}${baseVersion}`;
|
|
94
|
+
let exists = true;
|
|
95
|
+
try {
|
|
96
|
+
git(repoRoot, ['rev-parse', '-q', '--verify', `refs/tags/${name}`]);
|
|
97
|
+
} catch { exists = false; }
|
|
98
|
+
return { name, exists, repoHasTags: git(repoRoot, ['tag', '-l', `${tagPrefix}*`]) !== '' };
|
|
99
|
+
}
|
|
100
|
+
|
|
76
101
|
export async function packedPayloadManifest({ repoRoot, manifest }) {
|
|
77
102
|
const tempRoot = await mkdtemp(join(tmpdir(), 'awkit-package-payload-'));
|
|
78
103
|
try {
|
|
@@ -103,8 +128,10 @@ export async function checkReleaseDelta({ repoRoot, baseRef = 'origin/main' } =
|
|
|
103
128
|
await buildKit({ repoRoot, distDir });
|
|
104
129
|
const currentPackage = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8'));
|
|
105
130
|
const builtManifest = JSON.parse(await readFile(join(distDir, 'agent-workflow-kit.package.json'), 'utf8'));
|
|
131
|
+
const baseVersion = gitShowJson(repoRoot, baseRef, 'package.json').version;
|
|
106
132
|
return assessRelease({
|
|
107
|
-
baseVersion
|
|
133
|
+
baseVersion,
|
|
134
|
+
baseTag: resolveBaseTag({ repoRoot, baseVersion }),
|
|
108
135
|
currentVersion: currentPackage.version,
|
|
109
136
|
baseManifest: gitShowJson(repoRoot, baseRef, 'agent-workflow-kit.package.json'),
|
|
110
137
|
checkedManifest: JSON.parse(await readFile(join(repoRoot, 'agent-workflow-kit.package.json'), 'utf8')),
|
|
@@ -75,6 +75,51 @@ test('shipped content rejects downgrade and malformed version transitions', () =
|
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test('a bump on top of an untagged previous release is blocked', () => {
|
|
79
|
+
const current = { kitVersion: '1.3.0', files: [file('new.md', 'one')] };
|
|
80
|
+
const result = assessRelease({
|
|
81
|
+
baseVersion: '1.2.3', currentVersion: '1.3.0',
|
|
82
|
+
baseManifest: { kitVersion: '1.2.3', files: [] }, builtManifest: current,
|
|
83
|
+
checkedManifest: current, payloadManifest: current,
|
|
84
|
+
baseTag: { name: 'v1.2.3', exists: false, repoHasTags: true },
|
|
85
|
+
});
|
|
86
|
+
assert.equal(result.ok, false);
|
|
87
|
+
assert.match(result.errors.join('\n'), /1\.2\.3 is still awaiting-tag/);
|
|
88
|
+
assert.match(result.errors.join('\n'), /v1\.2\.3/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('a bump passes once the previous release carries its tag', () => {
|
|
92
|
+
const current = { kitVersion: '1.3.0', files: [file('new.md', 'one')] };
|
|
93
|
+
const result = assessRelease({
|
|
94
|
+
baseVersion: '1.2.3', currentVersion: '1.3.0',
|
|
95
|
+
baseManifest: { kitVersion: '1.2.3', files: [] }, builtManifest: current,
|
|
96
|
+
checkedManifest: current, payloadManifest: current,
|
|
97
|
+
baseTag: { name: 'v1.2.3', exists: true, repoHasTags: true },
|
|
98
|
+
});
|
|
99
|
+
assert.equal(result.ok, true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a never-tagged repository is not blocked from its first release', () => {
|
|
103
|
+
const current = { kitVersion: '0.1.0', files: [file('new.md', 'one')] };
|
|
104
|
+
const result = assessRelease({
|
|
105
|
+
baseVersion: '0.0.0', currentVersion: '0.1.0',
|
|
106
|
+
baseManifest: { kitVersion: '0.0.0', files: [] }, builtManifest: current,
|
|
107
|
+
checkedManifest: current, payloadManifest: current,
|
|
108
|
+
baseTag: { name: 'v0.0.0', exists: false, repoHasTags: false },
|
|
109
|
+
});
|
|
110
|
+
assert.equal(result.ok, true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('an untagged base blocks nothing while the version stays put', () => {
|
|
114
|
+
const same = { kitVersion: '1.2.3', files: [file('skill.md', 'one')] };
|
|
115
|
+
const result = assessRelease({
|
|
116
|
+
baseVersion: '1.2.3', currentVersion: '1.2.3',
|
|
117
|
+
baseManifest: same, builtManifest: same, checkedManifest: same, payloadManifest: same,
|
|
118
|
+
baseTag: { name: 'v1.2.3', exists: false, repoHasTags: true },
|
|
119
|
+
});
|
|
120
|
+
assert.equal(result.ok, true);
|
|
121
|
+
});
|
|
122
|
+
|
|
78
123
|
test('actual npm payload drift is blocked even when checked and built manifests match', () => {
|
|
79
124
|
const scrubbed = { kitVersion: '1.2.3', files: [file('skill.md', 'scrubbed')] };
|
|
80
125
|
const result = assessRelease({
|
|
@@ -99,9 +99,13 @@ export function npmTarballFilename(name, version) {
|
|
|
99
99
|
return `${name.replace(/^@/, '').replaceAll('/', '-')}-${version}.tgz`;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
async function packedTarball(run, spec, directory, repoRoot) {
|
|
102
|
+
async function packedTarball(run, spec, directory, repoRoot, { preferOnline = false } = {}) {
|
|
103
|
+
// A stale local packument answers ETARGET for a version that IS published.
|
|
104
|
+
// Read through it for registry specs: the status would otherwise report
|
|
105
|
+
// `awaiting-npm` for a released package and invite a second publish.
|
|
106
|
+
const cachePolicy = preferOnline ? ['--prefer-online'] : [];
|
|
103
107
|
const { stdout } = await run(
|
|
104
|
-
'npm', ['pack', spec, '--json', '--pack-destination', directory], { cwd: repoRoot },
|
|
108
|
+
'npm', ['pack', spec, '--json', '--pack-destination', directory, ...cachePolicy], { cwd: repoRoot },
|
|
105
109
|
);
|
|
106
110
|
const result = JSON.parse(stdout);
|
|
107
111
|
if (!Array.isArray(result) || result.length !== 1 || !result[0].filename) {
|
|
@@ -120,7 +124,7 @@ function releaseReaders(context) {
|
|
|
120
124
|
async function npm(identity) {
|
|
121
125
|
try {
|
|
122
126
|
state.npmTarball = await packedTarball(
|
|
123
|
-
run, `${identity.name}@${identity.version}`, scratch, repoRoot,
|
|
127
|
+
run, `${identity.name}@${identity.version}`, scratch, repoRoot, { preferOnline: true },
|
|
124
128
|
);
|
|
125
129
|
return await releaseIdentityFromTarball(state.npmTarball);
|
|
126
130
|
} catch (error) {
|
|
@@ -170,6 +170,28 @@ test('post-merge status inspection is read-only and reports the reconstructable
|
|
|
170
170
|
assert.deepEqual(fixture.events, ['read npm', 'read github']);
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
test('the registry read bypasses the local packument cache, the local pack does not', async () => {
|
|
174
|
+
const calls = [];
|
|
175
|
+
const run = async (command, args) => {
|
|
176
|
+
calls.push({ command, args });
|
|
177
|
+
throw Object.assign(new Error('stubbed pack'), { stderr: 'stubbed pack' });
|
|
178
|
+
};
|
|
179
|
+
const probe = await createCommandAdapter({ repoRoot: REPO, run });
|
|
180
|
+
try {
|
|
181
|
+
await assert.rejects(() => probe.local());
|
|
182
|
+
await assert.rejects(() => probe.npm(identity));
|
|
183
|
+
} finally { await probe.dispose(); }
|
|
184
|
+
|
|
185
|
+
const [localPack, registryPack] = calls;
|
|
186
|
+
// A stale packument makes `npm pack <name>@<version>` answer ETARGET for a
|
|
187
|
+
// version that IS published — the status would then claim awaiting-npm and
|
|
188
|
+
// invite a second publish. The local pack must NOT reach for the registry.
|
|
189
|
+
assert.ok(registryPack.args.includes('--prefer-online'), 'registry read must bypass the cache');
|
|
190
|
+
assert.ok(registryPack.args.includes(`${identity.name}@${identity.version}`));
|
|
191
|
+
assert.ok(!localPack.args.includes('--prefer-online'), 'local pack must stay offline');
|
|
192
|
+
assert.ok(localPack.args.includes('.'));
|
|
193
|
+
});
|
|
194
|
+
|
|
173
195
|
test('local Claude overrides cannot change the packed release identity or enter the tarball', async () => {
|
|
174
196
|
const fixture = await mkdtemp(join(tmpdir(), 'awkit-local-overrides-'));
|
|
175
197
|
await mkdir(join(fixture, '.claude'), { recursive: true });
|