@ikon85/agent-workflow-kit 0.26.1 → 0.26.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.26.1",
3
+ "version": "0.26.2",
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": {
@@ -45,7 +45,8 @@ async function shipBundle(repoRoot, distDir, bundleFiles) {
45
45
  const content = await copySource(repoRoot, distDir, file.src, file.dest, file.mode);
46
46
  entries.push({
47
47
  path: file.dest, kind: file.kind, ownerSkill: file.ownerSkill,
48
- surface: file.surface, sha256: sha256(content), mode: file.mode, origin: 'kit',
48
+ surface: file.surface, installRole: file.installRole,
49
+ sha256: sha256(content), mode: file.mode, origin: 'kit',
49
50
  });
50
51
  }
51
52
  return entries.sort((a, b) => a.path.localeCompare(b.path));
@@ -1,12 +1,14 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { createHash } from 'node:crypto';
8
8
  import { execFileSync } from 'node:child_process';
9
9
  import { buildKit } from './build-kit.mjs';
10
+ import { init } from '../src/commands/init.mjs';
11
+ import { CONSUMER_MANIFEST_NAME, readManifest } from '../src/lib/manifest.mjs';
10
12
 
11
13
  const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
12
14
  const PUBLIC_CENSUS_UNIT = JSON.parse(await readFile(
@@ -17,6 +19,7 @@ async function withBuild(fn) {
17
19
  try { return await fn(dist, await buildKit({ repoRoot: REPO, distDir: dist })); }
18
20
  finally { await rm(dist, { recursive: true, force: true }); }
19
21
  }
22
+ const exists = (path) => access(path).then(() => true, () => false);
20
23
 
21
24
  test('historical v0.9.0 manifest remains an immutable golden fixture', async () => {
22
25
  const fixture = await readFile(join(REPO, 'test/fixtures/v0.9.0-agent-workflow-kit.package.json'));
@@ -33,6 +36,46 @@ test('current public SSOT builds deterministically', async () => {
33
36
  assert.equal(first, second);
34
37
  });
35
38
 
39
+ test('current build assigns every file an explicit install role', async () => {
40
+ await withBuild(async (dist) => {
41
+ const manifest = JSON.parse(await readFile(join(dist, 'agent-workflow-kit.package.json'), 'utf8'));
42
+ assert.ok(manifest.files.every(({ installRole }) => ['consumer', 'maintainer'].includes(installRole)));
43
+ assert.deepEqual(
44
+ manifest.files.filter(({ installRole }) => installRole === 'maintainer').map(({ path }) => path),
45
+ [
46
+ '.agents/skills/kit-release/SKILL.md',
47
+ '.claude/skills/kit-release/SKILL.md',
48
+ 'scripts/kit-release.mjs',
49
+ 'scripts/release-delta-guard.mjs',
50
+ ],
51
+ );
52
+ });
53
+ });
54
+
55
+ test('a built kit installs no maintainer-only release surface into a consumer', async () => {
56
+ await withBuild(async (dist) => {
57
+ const consumer = await mkdtemp(join(tmpdir(), 'awkit-role-consumer-'));
58
+ try {
59
+ await init({ kitRoot: dist, consumerRoot: consumer });
60
+ const maintainerPaths = [
61
+ '.agents/skills/kit-release/SKILL.md',
62
+ '.claude/skills/kit-release/SKILL.md',
63
+ 'scripts/kit-release.mjs',
64
+ 'scripts/release-delta-guard.mjs',
65
+ ];
66
+ assert.deepEqual(
67
+ await Promise.all(maintainerPaths.map((path) => exists(join(consumer, path)))),
68
+ [false, false, false, false],
69
+ );
70
+ const manifest = await readManifest(join(consumer, CONSUMER_MANIFEST_NAME));
71
+ assert.equal(manifest.installRole, 'consumer');
72
+ assert.ok(manifest.installed.every(({ installRole }) => installRole === 'consumer'));
73
+ } finally {
74
+ await rm(consumer, { recursive: true, force: true });
75
+ }
76
+ });
77
+ });
78
+
36
79
  test('current build contains post-tag public files and repository metadata', async () => {
37
80
  await withBuild(async (dist) => {
38
81
  await readFile(join(dist, '.agents/skills/codex-adapter-sync/SKILL.md'));
@@ -6,6 +6,8 @@ import { stubSentinel } from '../lib/sentinel.mjs';
6
6
  import { STUB_TARGETS } from '../lib/bundle.mjs';
7
7
  import {
8
8
  readManifest, writeManifest, emptyConsumerManifest,
9
+ filesForInstallRole, CONSUMER_INSTALL_ROLE,
10
+ indexByPath,
9
11
  PACKAGE_MANIFEST_NAME, CONSUMER_MANIFEST_NAME,
10
12
  } from '../lib/manifest.mjs';
11
13
 
@@ -25,11 +27,19 @@ export async function init({ kitRoot, consumerRoot, force = false }) {
25
27
  if (!pkg) throw new Error('kit package manifest not found');
26
28
  const prior = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
27
29
  const tracked = new Set((prior?.installed ?? []).map((e) => e.path));
30
+ const packageIdx = indexByPath(pkg, 'files');
28
31
 
29
32
  const result = { copied: [], skipped: [], seeded: [] };
30
33
  const installed = [];
31
34
 
32
- for (const f of pkg.files) {
35
+ for (const entry of prior?.installed ?? []) {
36
+ const packageEntry = packageIdx.get(entry.path);
37
+ if (packageEntry?.installRole === CONSUMER_INSTALL_ROLE || !packageEntry?.installRole) continue;
38
+ if (!await exists(join(consumerRoot, entry.path))) continue;
39
+ installed.push({ ...entry, installRole: packageEntry.installRole });
40
+ }
41
+
42
+ for (const f of filesForInstallRole(pkg)) {
33
43
  const dest = join(consumerRoot, f.path);
34
44
  if (await exists(dest) && !tracked.has(f.path) && !force) {
35
45
  result.skipped.push(f.path); // pre-existing untracked → never-clobber
@@ -39,6 +49,7 @@ export async function init({ kitRoot, consumerRoot, force = false }) {
39
49
  installed.push({
40
50
  path: f.path, kind: f.kind, ownerSkill: f.ownerSkill, surface: f.surface,
41
51
  installedSha256: await sha256File(dest), origin: 'kit',
52
+ installRole: CONSUMER_INSTALL_ROLE,
42
53
  });
43
54
  result.copied.push(f.path);
44
55
  }
@@ -94,7 +94,8 @@ function verifyRelease(identities, kitVersion) {
94
94
  }
95
95
 
96
96
  function hasUpstreamDelta(result) {
97
- return result.added.length + result.updated.length + result.deleted.length > 0;
97
+ return result.manifestChanged ||
98
+ result.added.length + result.updated.length + result.deleted.length > 0;
98
99
  }
99
100
 
100
101
  async function terminal(result, state, history, transition) {
@@ -40,8 +40,8 @@ export const HELPER_FILES = [
40
40
  { path: 'scripts/wrapup-land.py', kind: 'script', mode: 0o755 },
41
41
  // Deterministic release preparation and its manifest-derived local/CI guard.
42
42
  // Both are invoked through package scripts by /kit-release.
43
- { path: 'scripts/kit-release.mjs', kind: 'script', mode: 0o644 },
44
- { path: 'scripts/release-delta-guard.mjs', kind: 'script', mode: 0o644 },
43
+ { path: 'scripts/kit-release.mjs', kind: 'script', mode: 0o644, installRole: 'maintainer' },
44
+ { path: 'scripts/release-delta-guard.mjs', kind: 'script', mode: 0o644, installRole: 'maintainer' },
45
45
  // Neutral publish/readback parity and externally reconstructable release state.
46
46
  // /kit-release uses these after merge; downstream update/consumer flows reuse
47
47
  // the parity primitive rather than growing a second registry/GitHub comparison.
@@ -161,7 +161,7 @@ const SURFACE_DIR = { claude: '.claude/skills', codex: '.agents/skills' };
161
161
  export function publishableSkills(manifest) {
162
162
  return Object.entries(manifest.skills)
163
163
  .filter(([, e]) => e.publish)
164
- .map(([name, e]) => ({ name, surfaces: e.surfaces }));
164
+ .map(([name, e]) => ({ name, surfaces: e.surfaces, installRole: e.installRole ?? 'consumer' }));
165
165
  }
166
166
 
167
167
  async function walk(dir) {
@@ -188,17 +188,23 @@ async function walk(dir) {
188
188
  */
189
189
  export async function collectBundle(repoRoot, manifest) {
190
190
  const files = [];
191
- for (const { name, surfaces } of publishableSkills(manifest)) {
191
+ for (const { name, surfaces, installRole } of publishableSkills(manifest)) {
192
192
  for (const surface of surfaces) {
193
193
  const base = join(SURFACE_DIR[surface], name);
194
194
  for (const abs of await walk(join(repoRoot, base))) {
195
195
  const rel = relative(repoRoot, abs);
196
- files.push({ src: rel, dest: rel, kind: 'skill', ownerSkill: name, surface, mode: 0o644 });
196
+ files.push({
197
+ src: rel, dest: rel, kind: 'skill', ownerSkill: name, surface,
198
+ installRole, mode: 0o644,
199
+ });
197
200
  }
198
201
  }
199
202
  }
200
203
  for (const h of HELPER_FILES) {
201
- files.push({ src: h.path, dest: h.path, kind: h.kind, mode: h.mode });
204
+ files.push({
205
+ src: h.path, dest: h.path, kind: h.kind,
206
+ installRole: h.installRole ?? 'consumer', mode: h.mode,
207
+ });
202
208
  }
203
209
  return { files, stubs: STUB_TARGETS };
204
210
  }
@@ -4,12 +4,15 @@ import { writeAtomic } from './atomicWrite.mjs';
4
4
  // Two manifests (Codex R1#9 / R3#1):
5
5
  // - package manifest (shipped with the kit): the desired-state file list.
6
6
  // - consumer manifest (agent-workflow-kit.json in the target repo root): installed state.
7
- // Both model every file kind:
7
+ // Package entries model every file kind and install role:
8
8
  // { path, kind: 'skill'|'script'|'hook'|'template'|'doc', ownerSkill?, surface?,
9
- // sha256, mode, origin: 'kit' }
9
+ // installRole: 'consumer'|'maintainer', sha256, mode, origin: 'kit' }
10
+ // Consumer manifests record their top-level installRole and retain the role on
11
+ // every installed entry, including edited legacy maintainer files kept in place.
10
12
 
11
13
  export const CONSUMER_MANIFEST_NAME = 'agent-workflow-kit.json';
12
14
  export const PACKAGE_MANIFEST_NAME = 'agent-workflow-kit.package.json';
15
+ export const CONSUMER_INSTALL_ROLE = 'consumer';
13
16
 
14
17
  /**
15
18
  * Parse a JSON manifest, or null if the file does not exist. A corrupt file
@@ -47,7 +50,14 @@ export async function writeManifest(path, obj) {
47
50
  }
48
51
 
49
52
  export function emptyConsumerManifest(kitVersion) {
50
- return { kitVersion, installed: [] };
53
+ return { kitVersion, installRole: CONSUMER_INSTALL_ROLE, installed: [] };
54
+ }
55
+
56
+ /** Package entries without a role predate role-aware installs and remain consumer-owned. */
57
+ export function filesForInstallRole(manifest, installRole = CONSUMER_INSTALL_ROLE) {
58
+ return (manifest?.files ?? []).filter(
59
+ (entry) => (entry.installRole ?? CONSUMER_INSTALL_ROLE) === installRole,
60
+ );
51
61
  }
52
62
 
53
63
  /** Map a manifest's file list (under `key`) by `path` for quick lookup. */
@@ -5,6 +5,7 @@ import { lineDiff, writeAtomic } from './atomicWrite.mjs';
5
5
  import { hookReferenced } from './settings.mjs';
6
6
  import {
7
7
  CONSUMER_MANIFEST_NAME, PACKAGE_MANIFEST_NAME, emptyConsumerManifest,
8
+ CONSUMER_INSTALL_ROLE, filesForInstallRole,
8
9
  indexByPath, readManifest, writeManifest,
9
10
  } from './manifest.mjs';
10
11
 
@@ -18,11 +19,13 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
18
19
  if (!consumer) throw new Error('not initialised — run `init` first');
19
20
 
20
21
  const installedIdx = indexByPath(consumer, 'installed');
21
- const pkgIdx = indexByPath(pkg, 'files');
22
+ const packageIdx = indexByPath(pkg, 'files');
23
+ const installable = filesForInstallRole(pkg);
24
+ const pkgIdx = indexByPath({ files: installable }, 'files');
22
25
  const result = emptyResult();
23
26
  const nextInstalled = [];
24
27
 
25
- for (const file of pkg.files) {
28
+ for (const file of installable) {
26
29
  const dest = join(consumerRoot, file.path);
27
30
  const prior = installedIdx.get(file.path);
28
31
  const current = (await exists(dest)) ? await sha256File(dest) : null;
@@ -41,12 +44,12 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
41
44
  } else if (userEdited && upstreamChanged) {
42
45
  const incoming = await readFile(join(kitRoot, file.path), 'utf8');
43
46
  result.conflicts.push({ path: file.path, diff: lineDiff(await readFile(dest, 'utf8'), incoming) });
44
- nextInstalled.push(prior);
47
+ nextInstalled.push(withInstallRole(prior));
45
48
  } else if (userEdited) {
46
- nextInstalled.push(prior);
49
+ nextInstalled.push(withInstallRole(prior));
47
50
  result.userModified.push(file.path);
48
51
  } else {
49
- nextInstalled.push(prior);
52
+ nextInstalled.push(withInstallRole(prior));
50
53
  result.unchanged.push(file.path);
51
54
  }
52
55
  }
@@ -58,7 +61,11 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
58
61
  const userEdited = current !== null && current !== prior.installedSha256;
59
62
  const referenced = prior.kind === 'hook' && (await hookReferenced(consumerRoot, prior.path));
60
63
  if (userEdited || referenced || !(await decide('delete', prior.path))) {
61
- nextInstalled.push(prior);
64
+ const packageEntry = packageIdx.get(prior.path);
65
+ nextInstalled.push(withInstallRole(
66
+ prior,
67
+ packageEntry?.installRole ?? prior.installRole ?? CONSUMER_INSTALL_ROLE,
68
+ ));
62
69
  result.keptDeleted.push(prior.path);
63
70
  } else {
64
71
  if (!dryRun && current !== null) await rm(dest);
@@ -66,6 +73,9 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
66
73
  }
67
74
  }
68
75
 
76
+ result.manifestChanged = consumer.installRole !== CONSUMER_INSTALL_ROLE ||
77
+ nextInstalled.some((next) => installedIdx.get(next.path)?.installRole !== next.installRole);
78
+
69
79
  if (!dryRun) {
70
80
  await writeManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME), {
71
81
  ...emptyConsumerManifest(pkg.kitVersion), installed: nextInstalled,
@@ -77,13 +87,17 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
77
87
  function emptyResult() {
78
88
  return {
79
89
  unchanged: [], updated: [], conflicts: [], userModified: [],
80
- added: [], deleted: [], keptDeleted: [],
90
+ added: [], deleted: [], keptDeleted: [], manifestChanged: false,
81
91
  };
82
92
  }
83
93
 
84
94
  function entry(file, installedSha256) {
85
95
  return {
86
96
  path: file.path, kind: file.kind, ownerSkill: file.ownerSkill, surface: file.surface,
87
- installedSha256, origin: 'kit',
97
+ installedSha256, origin: 'kit', installRole: CONSUMER_INSTALL_ROLE,
88
98
  };
89
99
  }
100
+
101
+ function withInstallRole(installed, installRole = CONSUMER_INSTALL_ROLE) {
102
+ return { ...installed, installRole };
103
+ }