@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.
@@ -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.3",
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": "7e08d5115d34f36da08491fa3bb8d5f4ca45e2908560e8f8c924cf3fd619d7f9",
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": "7e08d5115d34f36da08491fa3bb8d5f4ca45e2908560e8f8c924cf3fd619d7f9",
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": "a51f30e7261e4efb0b5c5002f39353ef8e8be18a2bd39ad7139f1ce7a2e7e18d",
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": "23b4ffc77e9b499587fa563d067b1df9ae81b87a654d636281c7d139e5842687",
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",
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: gitShowJson(repoRoot, baseRef, 'package.json').version,
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 });