@ikon85/agent-workflow-kit 0.36.2 → 0.36.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/README.md CHANGED
@@ -445,6 +445,16 @@ the old way. Decision record:
445
445
 
446
446
  ## Release notes
447
447
 
448
+ ### 0.36.4
449
+
450
+ - changed: `scripts/release-delta-guard.mjs`
451
+ - changed: `scripts/release-delta-guard.test.mjs`
452
+ - changed: `src/lib/updateReconcile.mjs`
453
+
454
+ ### 0.36.3
455
+
456
+ - Metadata-only release.
457
+
448
458
  ### 0.36.2
449
459
 
450
460
  - changed: `.claude/skills/to-issues/SKILL.md`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.36.2",
2
+ "kitVersion": "0.36.4",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -2683,7 +2683,7 @@
2683
2683
  "path": "scripts/release-delta-guard.mjs",
2684
2684
  "kind": "script",
2685
2685
  "installRole": "maintainer",
2686
- "sha256": "1d2aab5f15bca25167b166b37232139916cf814e94de873fa74a653ecf9a4619",
2686
+ "sha256": "566d7bae5526af2f8863d7a06dfa3016c9383b241b616cd43757eb7b46555895",
2687
2687
  "mode": 420,
2688
2688
  "origin": "kit"
2689
2689
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.36.2",
3
+ "version": "0.36.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": {
@@ -1,6 +1,6 @@
1
1
  /** Block shipped changes whose version or checked manifest does not match a fresh build. */
2
2
  import { execFileSync } from 'node:child_process';
3
- import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import { mkdir, mkdtemp, readFile, readdir, rm } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
@@ -24,6 +24,12 @@ const describe = (delta) => ['added', 'removed', 'changed']
24
24
  .filter((kind) => delta[kind].length)
25
25
  .map((kind) => `${kind}: ${delta[kind].join(', ')}`).join('; ');
26
26
 
27
+ function mergeDeltas(...deltas) {
28
+ return Object.fromEntries(['added', 'removed', 'changed'].map((kind) => [
29
+ kind, [...new Set(deltas.flatMap((delta) => delta[kind]))].sort(),
30
+ ]));
31
+ }
32
+
27
33
  export function recommendBump(delta) {
28
34
  if (delta.removed.length) return 'major';
29
35
  if (delta.added.length) return 'minor';
@@ -40,7 +46,11 @@ function bumpKind(before, after) {
40
46
  }
41
47
 
42
48
  export function assessRelease(input) {
43
- const delta = manifestDelta(input.baseManifest, input.builtManifest);
49
+ const bundleDelta = manifestDelta(input.baseManifest, input.builtManifest);
50
+ const packageDelta = input.basePackagePayload && input.currentPackagePayload
51
+ ? manifestDelta(input.basePackagePayload, input.currentPackagePayload)
52
+ : { added: [], removed: [], changed: [] };
53
+ const delta = mergeDeltas(bundleDelta, packageDelta);
44
54
  const checkedDrift = manifestDelta(input.checkedManifest, input.builtManifest);
45
55
  const payloadDrift = manifestDelta(input.builtManifest, input.payloadManifest);
46
56
  const errors = [];
@@ -99,6 +109,18 @@ export function resolveBaseTag({ repoRoot, baseVersion, tagPrefix = 'v' }) {
99
109
  }
100
110
 
101
111
  export async function packedPayloadManifest({ repoRoot, manifest }) {
112
+ const payload = await packedPackagePayload({ repoRoot });
113
+ const indexed = byPath(payload);
114
+ return {
115
+ kitVersion: manifest.kitVersion,
116
+ files: manifest.files.map((entry) => ({
117
+ ...entry,
118
+ sha256: indexed.get(entry.path),
119
+ })),
120
+ };
121
+ }
122
+
123
+ export async function packedPackagePayload({ repoRoot }) {
102
124
  const tempRoot = await mkdtemp(join(tmpdir(), 'awkit-package-payload-'));
103
125
  try {
104
126
  const packOutput = execFileSync(
@@ -110,17 +132,38 @@ export async function packedPayloadManifest({ repoRoot, manifest }) {
110
132
  await mkdir(unpackDir);
111
133
  execFileSync('tar', ['-xzf', join(tempRoot, filename), '-C', unpackDir]);
112
134
  return {
113
- kitVersion: manifest.kitVersion,
114
- files: await Promise.all(manifest.files.map(async (entry) => ({
115
- ...entry,
116
- sha256: sha256(await readFile(join(unpackDir, 'package', entry.path))),
117
- }))),
135
+ files: await packageFiles(join(unpackDir, 'package')),
118
136
  };
119
137
  } finally {
120
138
  await rm(tempRoot, { recursive: true, force: true });
121
139
  }
122
140
  }
123
141
 
142
+ async function packageFiles(root, relative = '') {
143
+ const files = [];
144
+ for (const entry of await readdir(join(root, relative), { withFileTypes: true })) {
145
+ const path = relative ? `${relative}/${entry.name}` : entry.name;
146
+ if (entry.isDirectory()) files.push(...await packageFiles(root, path));
147
+ else if (entry.isFile()) {
148
+ files.push({ path, sha256: sha256(await readFile(join(root, path))) });
149
+ }
150
+ }
151
+ return files.sort((a, b) => a.path.localeCompare(b.path));
152
+ }
153
+
154
+ async function packagePayloadAtRef(repoRoot, ref) {
155
+ const archiveRoot = await mkdtemp(join(tmpdir(), 'awkit-package-base-'));
156
+ try {
157
+ const archive = execFileSync('git', ['archive', '--format=tar', ref], {
158
+ cwd: repoRoot, maxBuffer: 50 * 1024 * 1024,
159
+ });
160
+ execFileSync('tar', ['-xf', '-', '-C', archiveRoot], { input: archive });
161
+ return await packedPackagePayload({ repoRoot: archiveRoot });
162
+ } finally {
163
+ await rm(archiveRoot, { recursive: true, force: true });
164
+ }
165
+ }
166
+
124
167
  export async function checkReleaseDelta({ repoRoot, baseRef = 'origin/main' } = {}) {
125
168
  repoRoot ??= join(dirname(fileURLToPath(import.meta.url)), '..');
126
169
  const distDir = await mkdtemp(join(tmpdir(), 'awkit-release-guard-'));
@@ -129,6 +172,7 @@ export async function checkReleaseDelta({ repoRoot, baseRef = 'origin/main' } =
129
172
  const currentPackage = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8'));
130
173
  const builtManifest = JSON.parse(await readFile(join(distDir, 'agent-workflow-kit.package.json'), 'utf8'));
131
174
  const baseVersion = gitShowJson(repoRoot, baseRef, 'package.json').version;
175
+ const currentPackagePayload = await packedPackagePayload({ repoRoot });
132
176
  return assessRelease({
133
177
  baseVersion,
134
178
  baseTag: resolveBaseTag({ repoRoot, baseVersion }),
@@ -136,7 +180,15 @@ export async function checkReleaseDelta({ repoRoot, baseRef = 'origin/main' } =
136
180
  baseManifest: gitShowJson(repoRoot, baseRef, 'agent-workflow-kit.package.json'),
137
181
  checkedManifest: JSON.parse(await readFile(join(repoRoot, 'agent-workflow-kit.package.json'), 'utf8')),
138
182
  builtManifest,
139
- payloadManifest: await packedPayloadManifest({ repoRoot, manifest: builtManifest }),
183
+ payloadManifest: {
184
+ kitVersion: builtManifest.kitVersion,
185
+ files: builtManifest.files.map((entry) => ({
186
+ ...entry,
187
+ sha256: byPath(currentPackagePayload).get(entry.path),
188
+ })),
189
+ },
190
+ basePackagePayload: await packagePayloadAtRef(repoRoot, baseRef),
191
+ currentPackagePayload,
140
192
  });
141
193
  } finally { await rm(distDir, { recursive: true, force: true }); }
142
194
  }
@@ -39,6 +39,25 @@ test('an unbumped shipped change is blocked with its concrete delta', () => {
39
39
  assert.match(result.errors.join('\n'), /version remains 1\.2\.3/);
40
40
  });
41
41
 
42
+ test('an npm runtime change outside the Consumer manifest still requires a release', () => {
43
+ const consumer = { kitVersion: '1.2.3', files: [file('skill.md', 'same')] };
44
+ const result = assessRelease({
45
+ baseVersion: '1.2.3',
46
+ currentVersion: '1.2.3',
47
+ baseManifest: consumer,
48
+ builtManifest: consumer,
49
+ checkedManifest: consumer,
50
+ payloadManifest: consumer,
51
+ basePackagePayload: { files: [file('src/cli.mjs', 'old')] },
52
+ currentPackagePayload: { files: [file('src/cli.mjs', 'new')] },
53
+ });
54
+
55
+ assert.equal(result.ok, false);
56
+ assert.deepEqual(result.delta.changed, ['src/cli.mjs']);
57
+ assert.equal(result.recommendedBump, 'patch');
58
+ assert.match(result.errors.join('\n'), /version remains 1\.2\.3/);
59
+ });
60
+
42
61
  test('dead checked-manifest entries are rejected', () => {
43
62
  const result = assessRelease({
44
63
  baseVersion: '1.2.3', currentVersion: '1.3.0',
@@ -135,7 +135,7 @@ async function previewReadinessAdoption(context) {
135
135
  candidatePreview.generated = readiness.generated;
136
136
  candidatePreview.migrations = readiness.migrations;
137
137
  candidatePreview.migrated = readiness.migrated;
138
- if (!readiness.migrationConflicts.length) {
138
+ if (!readiness.migrationConflicts.length && !candidatePreview.conflicts.length) {
139
139
  await verifyCandidateSchema(candidateRoot, {
140
140
  pkg, preview: candidatePreview, priorReadinessManifest, nextReadinessManifest,
141
141
  });
@@ -143,7 +143,8 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
143
143
  const userEdited = current !== prior.installedSha256;
144
144
  const currentMode = (await lstat(dest)).mode & 0o777;
145
145
  const upstreamChanged = file.sha256 !== prior.installedSha256
146
- || currentMode !== file.mode;
146
+ || currentMode !== file.mode
147
+ || packageMetadataChanged(file, prior);
147
148
  if (!userEdited && upstreamChanged) {
148
149
  if (!dryRun) await writeAtomic(dest, await readFile(join(kitRoot, file.path)), file.mode);
149
150
  nextInstalled.push(entry(file, file.sha256));
@@ -185,7 +186,8 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
185
186
  }
186
187
  }
187
188
 
188
- result.manifestChanged = consumer.installRole !== CONSUMER_INSTALL_ROLE ||
189
+ result.manifestChanged = consumer.kitVersion !== pkg.kitVersion ||
190
+ consumer.installRole !== CONSUMER_INSTALL_ROLE ||
189
191
  result.bridgeRetired.length > 0 ||
190
192
  nextInstalled.some((next) => installedIdx.get(next.path)?.installRole !== next.installRole);
191
193
 
@@ -227,6 +229,12 @@ function withInstallRole(installed, installRole = CONSUMER_INSTALL_ROLE) {
227
229
  return { ...installed, installRole };
228
230
  }
229
231
 
232
+ function packageMetadataChanged(file, installed) {
233
+ return ['kind', 'ownerSkill', 'surface'].some(
234
+ (key) => (file[key] ?? null) !== (installed[key] ?? null),
235
+ );
236
+ }
237
+
230
238
  async function projectExtensionEvidence(consumerRoot, path) {
231
239
  const match = /^docs\/agents\/skills\/([a-z0-9-]+)\.md$/.exec(path);
232
240
  if (!match) return null;