@ikon85/agent-workflow-kit 0.31.0 → 0.32.1

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.
@@ -66,13 +66,20 @@ delegates this route to the shipped `census-update` contract and its focused
66
66
  `scripts/test_census_update_contract.test.mjs` proof; it does not reproduce
67
67
  activation, snapshots, or enforcement. Setup itself never calls `activateCensus`.
68
68
 
69
- Disable follows the ordered contract: remove the kit-owned hook, remove the
70
- kit-owned gate, then atomically replace only the profile's `enabled` value with
71
- `false`, preserving unknown keys, and verify `disabled` through
72
- `resolveCensusState`. Enforcement removal must finish before any profile
73
- mutation. Treat the choice document, local scanners, their tests, the profile,
74
- and the active snapshot as consumer-owned evidence. List those files and ask
75
- for separate deletion approval; without that approval, retain them.
69
+ Activation owns durable enforcement: every focused test declared by the active
70
+ profile must remain transitively reachable through one shared project-local
71
+ census check entry point from both local CI and pre-push. `census-update`
72
+ reconciles that narrow kit-owned census wiring idempotently and requires an
73
+ executable wiring proof before it can report `current`; setup does not pre-wire
74
+ the optional bootstrap state.
75
+
76
+ Disable follows the ordered contract: remove only the kit-owned census wiring
77
+ (including the kit-owned hook and gate block), then atomically replace only the
78
+ profile's `enabled` value with `false`, preserving unknown keys, and verify
79
+ `disabled` through `resolveCensusState`. Enforcement removal must finish before
80
+ any profile mutation. Treat the choice document, local scanners, their tests,
81
+ the profile, and the active snapshot as consumer-owned evidence. List those
82
+ files and ask for separate deletion approval; without that approval, retain them.
76
83
  Setup never deletes consumer-owned files as part of disable.
77
84
 
78
85
  ## Setup report
package/README.md CHANGED
@@ -305,7 +305,10 @@ any language works — map each role once, rename an option with one profile edi
305
305
  `init` records a sha256 of every file it installs. That's the line between the
306
306
  two: **edit any skill or script freely** — `update` detects your edits and backs
307
307
  them up rather than clobbering them. Your **project layer** (`docs/agents/*`, the
308
- board profile, `CLAUDE.md`) is created once and never touched by `update`.
308
+ board profile, `CLAUDE.md`, `AGENTS.md`) remains consumer-owned. `update` may
309
+ only apply a previewed, schema-driven, idempotent compatibility migration that
310
+ fills missing evidence without rewriting an existing value; it verifies and
311
+ rolls that migration back with the rest of the candidate.
309
312
 
310
313
  ## Updating & removing
311
314
 
@@ -348,6 +351,25 @@ concurrency-safe. Do not run manifest-mutating commands concurrently. Flags:
348
351
 
349
352
  ## Release notes
350
353
 
354
+ ### 0.32.1
355
+
356
+ - changed: `.agents/skills/census-update/SKILL.md`
357
+ - changed: `.agents/skills/kit-update/SKILL.md`
358
+ - changed: `.agents/skills/setup-workflow/census.md`
359
+ - changed: `.claude/skills/census-update/SKILL.md`
360
+ - changed: `.claude/skills/kit-update/SKILL.md`
361
+ - changed: `.claude/skills/setup-workflow/census.md`
362
+ - changed: `scripts/readiness.mjs`
363
+
364
+ ### 0.32.0
365
+
366
+ - changed: `.agents/skills/audit-skills/SKILL.md`
367
+ - changed: `.agents/skills/git-worktree-recover/SKILL.md`
368
+ - changed: `.agents/skills/orchestrate-wave/SKILL.md`
369
+ - changed: `.claude/skills/audit-skills/SKILL.md`
370
+ - changed: `.claude/skills/git-worktree-recover/SKILL.md`
371
+ - changed: `.claude/skills/orchestrate-wave/SKILL.md`
372
+
351
373
  ### 0.31.0
352
374
 
353
375
  - changed: `.agents/skills/board-to-waves/SKILL.md`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.31.0",
2
+ "kitVersion": "0.32.1",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -27,7 +27,7 @@
27
27
  "ownerSkill": "audit-skills",
28
28
  "surface": "codex",
29
29
  "installRole": "consumer",
30
- "sha256": "f99bfc88bf23ad8279583653bdd7b63dcdcff010227258e4adf9e8df69384f7f",
30
+ "sha256": "662c4dec0c03716c468f202f73f52140ceba84e092873e6619692dde6d7ed0f2",
31
31
  "mode": 420,
32
32
  "origin": "kit"
33
33
  },
@@ -47,7 +47,7 @@
47
47
  "ownerSkill": "census-update",
48
48
  "surface": "codex",
49
49
  "installRole": "consumer",
50
- "sha256": "3a04a2667005cae51cfad7952ff85fe43ba1ae390458b4ce746e23f6e872b3fb",
50
+ "sha256": "b122fe2eb46c070696e38660b021411458b455b7a4e977174f943f2ce4c00819",
51
51
  "mode": 420,
52
52
  "origin": "kit"
53
53
  },
@@ -197,7 +197,7 @@
197
197
  "ownerSkill": "git-worktree-recover",
198
198
  "surface": "codex",
199
199
  "installRole": "consumer",
200
- "sha256": "71e043456586a59ddb83fb3fb1f98c8c269cd7575ade8616238bdb7e07fba4ba",
200
+ "sha256": "1628f47a5b5f71b7596ec070c8f038a3ce98abe007fce1b7f5c21ea40858124d",
201
201
  "mode": 420,
202
202
  "origin": "kit"
203
203
  },
@@ -377,7 +377,7 @@
377
377
  "ownerSkill": "kit-update",
378
378
  "surface": "codex",
379
379
  "installRole": "consumer",
380
- "sha256": "880766b7aef88760f6d79dd31c3333356eef060623fc053fc61f5d5611765bb8",
380
+ "sha256": "249a45761bcb95148b1567d1bc4d1b622b9b8c41519de4e67b1ef4e33c9ee9fd",
381
381
  "mode": 420,
382
382
  "origin": "kit"
383
383
  },
@@ -417,7 +417,7 @@
417
417
  "ownerSkill": "orchestrate-wave",
418
418
  "surface": "codex",
419
419
  "installRole": "consumer",
420
- "sha256": "39c0dd4d6f7663ab9a3602aab2869a881497604ca12f53f6ba0ca7debb0a4157",
420
+ "sha256": "7a94293b7db49f42497d34e6ae0fe9992ab1c770ae0766557618818d6d671c50",
421
421
  "mode": 420,
422
422
  "origin": "kit"
423
423
  },
@@ -567,7 +567,7 @@
567
567
  "ownerSkill": "setup-workflow",
568
568
  "surface": "codex",
569
569
  "installRole": "consumer",
570
- "sha256": "ec80f4ad36073e0c7c903434fa098d24f31b9d9a46ce55623782173ddba2e5f9",
570
+ "sha256": "d495758ae52bebd11c81bbe28fc5a1f9b614f972afdba55d1b728630d12cfee8",
571
571
  "mode": 420,
572
572
  "origin": "kit"
573
573
  },
@@ -1189,7 +1189,7 @@
1189
1189
  "ownerSkill": "audit-skills",
1190
1190
  "surface": "claude",
1191
1191
  "installRole": "consumer",
1192
- "sha256": "80ab0f1910914f4bcb375bf63aa66a1297a0b873082d7cc4f386d0d931e6015f",
1192
+ "sha256": "dce7c13e9b9c2eb6fe9028f69fe2fe2d48ad50546327c9d9c1232191425f1b7c",
1193
1193
  "mode": 420,
1194
1194
  "origin": "kit"
1195
1195
  },
@@ -1209,7 +1209,7 @@
1209
1209
  "ownerSkill": "census-update",
1210
1210
  "surface": "claude",
1211
1211
  "installRole": "consumer",
1212
- "sha256": "3a04a2667005cae51cfad7952ff85fe43ba1ae390458b4ce746e23f6e872b3fb",
1212
+ "sha256": "b122fe2eb46c070696e38660b021411458b455b7a4e977174f943f2ce4c00819",
1213
1213
  "mode": 420,
1214
1214
  "origin": "kit"
1215
1215
  },
@@ -1419,7 +1419,7 @@
1419
1419
  "ownerSkill": "git-worktree-recover",
1420
1420
  "surface": "claude",
1421
1421
  "installRole": "consumer",
1422
- "sha256": "435c3a32a0c31dbefc774d8f6caa30cae9f23026930292ea0c364d2115ed3e52",
1422
+ "sha256": "3251c8db671286330e48748719e05ee3fdc0555d93d61d7fee3fc0e0d2868e92",
1423
1423
  "mode": 420,
1424
1424
  "origin": "kit"
1425
1425
  },
@@ -1659,7 +1659,7 @@
1659
1659
  "ownerSkill": "kit-update",
1660
1660
  "surface": "claude",
1661
1661
  "installRole": "consumer",
1662
- "sha256": "880766b7aef88760f6d79dd31c3333356eef060623fc053fc61f5d5611765bb8",
1662
+ "sha256": "249a45761bcb95148b1567d1bc4d1b622b9b8c41519de4e67b1ef4e33c9ee9fd",
1663
1663
  "mode": 420,
1664
1664
  "origin": "kit"
1665
1665
  },
@@ -1699,7 +1699,7 @@
1699
1699
  "ownerSkill": "orchestrate-wave",
1700
1700
  "surface": "claude",
1701
1701
  "installRole": "consumer",
1702
- "sha256": "20a972d7c13f059b1fda4daaeb77da777c898620bf9ec6bf6dd19c31b7e41d90",
1702
+ "sha256": "9690ec772a99131c225037dd4bca884e899c133a8e460c36b26a25d6c6c797e1",
1703
1703
  "mode": 420,
1704
1704
  "origin": "kit"
1705
1705
  },
@@ -1869,7 +1869,7 @@
1869
1869
  "ownerSkill": "setup-workflow",
1870
1870
  "surface": "claude",
1871
1871
  "installRole": "consumer",
1872
- "sha256": "ec80f4ad36073e0c7c903434fa098d24f31b9d9a46ce55623782173ddba2e5f9",
1872
+ "sha256": "d495758ae52bebd11c81bbe28fc5a1f9b614f972afdba55d1b728630d12cfee8",
1873
1873
  "mode": 420,
1874
1874
  "origin": "kit"
1875
1875
  },
@@ -2587,7 +2587,7 @@
2587
2587
  "path": "scripts/readiness.mjs",
2588
2588
  "kind": "script",
2589
2589
  "installRole": "consumer",
2590
- "sha256": "f5e8bc2e724f72d1aa9f94c4f8d87b8f4d3f9843797cd6ae59f48e563dc7c7fb",
2590
+ "sha256": "9e7a9c6882e372f38383fe92d98ff2f818bae0be61a62301b7e9a6a2842dd21b",
2591
2591
  "mode": 493,
2592
2592
  "origin": "kit"
2593
2593
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.31.0",
3
+ "version": "0.32.1",
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": {
@@ -99,22 +99,53 @@ function section(text, heading) {
99
99
  return body.join('\n').trim();
100
100
  }
101
101
 
102
- async function prodVerdict(root, paths) {
103
- const bodies = [];
102
+ export async function inspectProdSections(root, paths) {
103
+ const sections = [];
104
104
  for (const path of paths) {
105
105
  const text = await readText(root, path);
106
- if (text === null) continue;
107
106
  const body = section(text, '## Prod');
108
- bodies.push(body);
107
+ const problem = text === null ? 'missing-file'
108
+ : (body === null ? 'missing-section' : (body ? null : 'empty-section'));
109
+ sections.push({
110
+ path,
111
+ state: body === null ? 'missing' : (body ? 'valid' : 'invalid'),
112
+ body,
113
+ problem,
114
+ });
115
+ }
116
+ return sections;
117
+ }
118
+
119
+ async function prodEvidence(root, paths) {
120
+ const sections = await inspectProdSections(root, paths);
121
+ const present = sections.filter(({ problem }) => problem !== 'missing-file');
122
+ const valid = present.filter(({ state }) => state === 'valid');
123
+ const empty = present.filter(({ problem }) => problem === 'empty-section');
124
+ if (!valid.length && !empty.length) {
125
+ return {
126
+ verdict: 'absent',
127
+ diagnostics: sections.map(({ path, problem }) => ({ path, problem })),
128
+ };
129
+ }
130
+ const malformed = present.filter(({ state }) => state !== 'valid');
131
+ if (malformed.length) {
132
+ return {
133
+ verdict: 'invalid',
134
+ diagnostics: malformed.map(({ path, problem }) => ({ path, problem })),
135
+ };
136
+ }
137
+ const bodies = [...new Set(present.map(({ body }) => body))];
138
+ if (bodies.length > 1) {
139
+ return {
140
+ verdict: 'invalid',
141
+ diagnostics: present.map(({ path }) => ({ path, problem: 'divergent-section' })),
142
+ };
109
143
  }
110
- if (!bodies.length) return 'absent';
111
- if (bodies.every((body) => body === null)) return 'absent';
112
- if (bodies.some((body) => !body)) return 'invalid';
113
- return bodies.every((body) => body === bodies[0]) ? 'valid' : 'invalid';
144
+ return { verdict: 'valid', diagnostics: [] };
114
145
  }
115
146
 
116
147
  async function evidenceVerdict(root, evidence) {
117
- if (evidence.type === 'prod-section') return prodVerdict(root, evidence.paths);
148
+ if (evidence.type === 'prod-section') return (await prodEvidence(root, evidence.paths)).verdict;
118
149
  if (evidence.type === 'runbook-reference') return runbookVerdict(root, evidence);
119
150
  const verdicts = await Promise.all((evidence.paths ?? []).map(async (path) => {
120
151
  const text = await readText(root, path);
@@ -129,14 +160,17 @@ async function evidenceVerdict(root, evidence) {
129
160
  }
130
161
 
131
162
  export async function evaluateCapability({ root, capability, decision }) {
132
- const evidence = await evidenceVerdict(root, capability.evidence);
133
- if (evidence === 'invalid') return { state: 'invalid', clearDecision: false };
163
+ const prod = capability.evidence.type === 'prod-section'
164
+ ? await prodEvidence(root, capability.evidence.paths) : null;
165
+ const evidence = prod?.verdict ?? await evidenceVerdict(root, capability.evidence);
166
+ const diagnostics = prod?.diagnostics?.length ? { diagnostics: prod.diagnostics } : {};
167
+ if (evidence === 'invalid') return { state: 'invalid', clearDecision: false, ...diagnostics };
134
168
  if (evidence === 'valid') return { state: 'ready', clearDecision: Boolean(decision) };
135
- if (decision === 'pending') return { state: 'pending', clearDecision: false };
169
+ if (decision === 'pending') return { state: 'pending', clearDecision: false, ...diagnostics };
136
170
  if (decision === 'not-applicable' && capability.allowNotApplicable) {
137
- return { state: 'not-applicable', clearDecision: false };
171
+ return { state: 'not-applicable', clearDecision: false, ...diagnostics };
138
172
  }
139
- return { state: 'missing', clearDecision: false };
173
+ return { state: 'missing', clearDecision: false, ...diagnostics };
140
174
  }
141
175
 
142
176
  async function loadManifest(root) {
@@ -169,6 +169,20 @@ test('census-update coordinates the stable foundation API without a second engin
169
169
  assert.match(skill, /\.census\/profile\.json/);
170
170
  assert.match(skill, /\.census\/active\.json/);
171
171
  assert.match(skill, /schemaVersion/);
172
+ assert.match(skill, /every `localScanners\[\]\.test`/);
173
+ assert.match(skill, /shared project-local census check entry point/i);
174
+ assert.match(
175
+ skill,
176
+ /It must execute every declared focused test,\s+and both local CI and pre-push must transitively reach that same entry point\./i,
177
+ );
178
+ assert.match(skill, /executable wiring test/i);
179
+ assert.match(skill, /missing or partial[\s\S]*failed/i);
180
+ assert.match(skill, /previous active\s+census bytes are unchanged/i);
181
+ assert.match(skill, /idempotent/i);
182
+
183
+ const setupCensus = await text('.claude/skills/setup-workflow/census.md');
184
+ assert.match(setupCensus, /activation[\s\S]*durable[\s\S]*local CI[\s\S]*pre-push/i);
185
+ assert.match(setupCensus, /remove only[\s\S]*kit-owned census wiring/i);
172
186
  });
173
187
 
174
188
  test('brownfield bootstrap activates real surface coverage with a separate behavior overview', async () => {
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import re
7
+ import subprocess
7
8
  import unittest
8
9
  from pathlib import Path
9
10
 
@@ -12,7 +13,8 @@ MANIFEST_PATH = ROOT / ".claude/skills/skill-manifest.json"
12
13
  START = re.compile(r"^<!-- readiness:block ([a-z][A-Za-z0-9]*) -->$")
13
14
  END = "<!-- readiness:end -->"
14
15
  SURFACE = {"claude": ".claude/skills", "codex": ".agents/skills"}
15
- REQUIRED = {
16
+ FIXTURES = ROOT / "test/fixtures/readiness"
17
+ REQUIRED_V1 = {
16
18
  "to-prd": ["issueTracker", "managedBoard"],
17
19
  "to-issues": ["issueTracker", "managedBoard", "specCompleteness"],
18
20
  "to-waves": ["issueTracker", "managedBoard", "specCompleteness"],
@@ -23,7 +25,7 @@ REQUIRED = {
23
25
  "project-release": ["projectReleaseProfile"],
24
26
  "security-audit": ["securityAuditRunbook"],
25
27
  }
26
- OPTIONAL = {
28
+ OPTIONAL_V1 = {
27
29
  "wrapup": {"deployReport": "prodTarget"},
28
30
  "orchestrate-wave": {"projectRecipe": "orchestrateWaveRecipe"},
29
31
  "spec-self-critique": {"projectEnrichment": "specCritiqueLayer"},
@@ -32,6 +34,74 @@ OPTIONAL = {
32
34
  "audit-skills": {"projectChecks": "auditSkillsLayer"},
33
35
  "git-worktree-recover": {"projectRecovery": "worktreeRecoveryLayer"},
34
36
  }
37
+ COMPAT_UPDATE_SCRIPT = r"""
38
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
39
+ import { join } from 'node:path';
40
+ import { pathToFileURL } from 'node:url';
41
+ const [repo, fixturePath] = process.argv.slice(-2);
42
+ const load = async (path) => import(pathToFileURL(join(repo, path)));
43
+ const { init } = await load('src/commands/init.mjs');
44
+ const { update } = await load('src/commands/update.mjs');
45
+ const { checkSkill } = await load('scripts/readiness.mjs');
46
+ const { makeKit, makeEmptyDir, cleanup } = await load('test/helpers.mjs');
47
+ const { PACKAGE_MANIFEST_NAME, readManifest, writeManifest } = await load('src/lib/manifest.mjs');
48
+ const { sha256 } = await load('src/lib/hash.mjs');
49
+ const readinessPath = '.claude/skills/skill-manifest.json';
50
+ const fixture = JSON.parse(await readFile(fixturePath, 'utf8'));
51
+ const current = JSON.parse(await readFile(join(repo, readinessPath), 'utf8'));
52
+ const previous = structuredClone(current);
53
+ for (const skill of Object.keys(fixture.readinessFixture.skills)) {
54
+ previous.skills[skill].readiness = {};
55
+ }
56
+ const kit = await makeKit({ '.claude/skills/to-prd/SKILL.md': 'fixture\n' });
57
+ const consumer = await makeEmptyDir();
58
+ async function setKitReadiness(manifest) {
59
+ const content = `${JSON.stringify(manifest, null, 2)}\n`;
60
+ const path = join(kit, readinessPath);
61
+ await mkdir(join(kit, '.claude/skills'), { recursive: true });
62
+ await writeFile(path, content);
63
+ const pkg = await readManifest(join(kit, PACKAGE_MANIFEST_NAME));
64
+ const entry = pkg.files.find(({ path: candidate }) => candidate === readinessPath);
65
+ if (entry) entry.sha256 = sha256(content);
66
+ else pkg.files.push({ path: readinessPath, kind: 'doc', sha256: sha256(content), mode: 0o644, origin: 'kit' });
67
+ await writeManifest(join(kit, PACKAGE_MANIFEST_NAME), pkg);
68
+ }
69
+ try {
70
+ await setKitReadiness(previous);
71
+ await init({ kitRoot: kit, consumerRoot: consumer });
72
+ for (const [relative, body] of Object.entries(fixture.readinessFixture.evidence)) {
73
+ const path = join(consumer, relative);
74
+ await mkdir(join(path, '..'), { recursive: true });
75
+ await writeFile(path, body);
76
+ }
77
+ const manifestPath = join(consumer, 'agent-workflow-kit.json');
78
+ const installed = await readManifest(manifestPath);
79
+ await writeManifest(manifestPath, {
80
+ ...installed,
81
+ readinessDecisions: fixture.readinessDecisions ?? {},
82
+ readinessFixture: fixture.readinessFixture,
83
+ });
84
+ const before = {};
85
+ for (const skill of Object.keys(fixture.readinessFixture.skills)) {
86
+ before[skill] = await checkSkill({ root: consumer, skill, manifest: previous });
87
+ }
88
+ await setKitReadiness(current);
89
+ const identity = { name: '@ikon85/agent-workflow-kit', version: '0.1.0', tarballIntegrity: 'sha512-fixture', manifestSha256: 'fixture-manifest' };
90
+ const result = await update({
91
+ kitRoot: kit, consumerRoot: consumer,
92
+ releaseIdentities: { installed: { name: identity.name, version: identity.version, manifestSha256: identity.manifestSha256 }, npm: identity, github: identity },
93
+ verify: async () => {},
94
+ });
95
+ const afterManifest = await readManifest(manifestPath);
96
+ const after = {};
97
+ for (const skill of Object.keys(fixture.readinessFixture.skills)) {
98
+ after[skill] = await checkSkill({ root: consumer, skill, manifest: current });
99
+ }
100
+ console.log(JSON.stringify({ before, result, afterManifest, after }));
101
+ } finally {
102
+ await cleanup(kit, consumer);
103
+ }
104
+ """
35
105
 
36
106
 
37
107
  def load_manifest() -> dict:
@@ -100,11 +170,32 @@ class ReadinessContractTests(unittest.TestCase):
100
170
  blocks = list(declaration.get("optionalBlocks", {}))
101
171
  self.assertEqual(len(blocks), len(set(blocks)), skill)
102
172
 
103
- def test_initial_enforcement_mapping_is_exact(self) -> None:
104
- for skill, required in REQUIRED.items():
105
- self.assertEqual(self.published[skill]["readiness"]["required"], required)
106
- for skill, blocks in OPTIONAL.items():
107
- self.assertEqual(self.published[skill]["readiness"]["optionalBlocks"], blocks)
173
+ def test_locked_v1_policy_cannot_disappear_from_the_manifest(self) -> None:
174
+ actual_required = {
175
+ skill: entry["readiness"]["required"]
176
+ for skill, entry in self.published.items()
177
+ if entry.get("readiness", {}).get("required")
178
+ }
179
+ actual_optional = {
180
+ skill: entry["readiness"]["optionalBlocks"]
181
+ for skill, entry in self.published.items()
182
+ if entry.get("readiness", {}).get("optionalBlocks")
183
+ }
184
+ self.assertEqual(actual_required, REQUIRED_V1)
185
+ self.assertEqual(actual_optional, OPTIONAL_V1)
186
+
187
+ def test_every_declared_consumer_has_the_standard_preflight(self) -> None:
188
+ consumers = {
189
+ skill: entry for skill, entry in self.published.items()
190
+ if entry.get("readiness")
191
+ }
192
+ self.assertGreater(len(consumers), 0)
193
+ for skill, entry in consumers.items():
194
+ expected = f"node scripts/readiness.mjs check --skill {skill} --json"
195
+ for surface in entry["surfaces"]:
196
+ path = ROOT / SURFACE[surface] / skill / "SKILL.md"
197
+ body = path.read_text(encoding="utf-8")
198
+ self.assertEqual(body.count(expected), 1, f"{surface}:{skill}")
108
199
 
109
200
  def test_markers_are_balanced_declared_and_surface_equal_when_activated(self) -> None:
110
201
  for skill, entry in self.published.items():
@@ -114,6 +205,7 @@ class ReadinessContractTests(unittest.TestCase):
114
205
  path = ROOT / SURFACE[surface] / skill / "SKILL.md"
115
206
  self.assertTrue(path.is_file(), f"missing published surface: {path}")
116
207
  sequence = marker_sequence(path.read_text(encoding="utf-8"), declared)
208
+ self.assertEqual(set(sequence), declared, f"undeclared marker coverage: {surface}:{skill}")
117
209
  sequences.append((surface, sequence))
118
210
  if len(sequences) > 1:
119
211
  validate_surface_sequences([sequence for _, sequence in sequences])
@@ -134,6 +226,34 @@ class ReadinessContractTests(unittest.TestCase):
134
226
  with self.assertRaises(AssertionError):
135
227
  validate_surface_sequences([["projectEnrichment"], []])
136
228
 
229
+ def test_legacy_and_current_consumers_keep_previously_ready_skill_cores(self) -> None:
230
+ fixtures = sorted(FIXTURES.glob("*/agent-workflow-kit.json"))
231
+ self.assertEqual([path.parent.name for path in fixtures], ["current", "legacy"])
232
+ for fixture_path in fixtures:
233
+ fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
234
+ scenario = fixture["readinessFixture"]
235
+ with self.subTest(fixture=fixture_path.parent.name):
236
+ run = subprocess.run(
237
+ ["node", "--input-type=module", "-e", COMPAT_UPDATE_SCRIPT,
238
+ str(ROOT), str(fixture_path)],
239
+ check=True, capture_output=True, text=True,
240
+ )
241
+ proof = json.loads(run.stdout)
242
+ self.assertEqual(proof["result"]["state"], "applied")
243
+ self.assertEqual(proof["result"]["availability"]["newlyBlocked"], [])
244
+ for skill, expected in scenario["skills"].items():
245
+ self.assertEqual(proof["before"][skill]["verdict"], "ready", skill)
246
+ verdict = proof["after"][skill]
247
+ self.assertNotEqual(verdict["verdict"], "blocked", skill)
248
+ self.assertEqual(verdict["verdict"], expected["verdict"], skill)
249
+ self.assertEqual(verdict["inactiveBlocks"], expected["inactiveBlocks"], skill)
250
+ self.assertEqual(
251
+ proof["afterManifest"].get("readinessDecisions", {}),
252
+ scenario["expectedDecisions"],
253
+ "unavailable behavior must not manufacture a decision",
254
+ )
255
+ self.assertEqual(proof["afterManifest"]["readinessFixture"], scenario)
256
+
137
257
 
138
258
  if __name__ == "__main__":
139
259
  unittest.main()
package/src/cli.mjs CHANGED
@@ -57,7 +57,7 @@ try {
57
57
  } else if (r.status === 'current') {
58
58
  p.outro(`aktuell · unchanged ${r.unchanged.length} · local modifications ${r.userModified.length}`);
59
59
  } else {
60
- p.outro(`updated ${r.updated.length} · added ${r.added.length} · deleted ${r.deleted.length}`);
60
+ p.outro(`updated ${r.updated.length} · added ${r.added.length} · migrated ${r.migrated?.length ?? 0} · deleted ${r.deleted.length}`);
61
61
  }
62
62
  } else if (cmd === 'uninstall') {
63
63
  const ok = yes || (await p.confirm({ message: 'Remove kit-installed files?' })) === true;
@@ -82,7 +82,7 @@ function printPlan(r) {
82
82
  const lines = [];
83
83
  for (const k of [
84
84
  'added', 'updated', 'userModified', 'consumerOwned', 'unchanged',
85
- 'deleted', 'keptDeleted', 'collisions',
85
+ 'deleted', 'keptDeleted', 'collisions', 'migrated',
86
86
  ])
87
87
  if (r[k]?.length) lines.push(`${k}: ${r[k].length}`);
88
88
  if (r.conflicts?.length) lines.push(`conflicts: ${r.conflicts.length}`);
@@ -55,6 +55,11 @@ export async function update(options) {
55
55
  Object.assign(preview, await previewReadinessAdoption({
56
56
  kitRoot, consumerRoot, priorReadinessManifest, nextReadinessManifest,
57
57
  }));
58
+ preview.conflicts.push(...(preview.migrationConflicts ?? []).map((path) => ({
59
+ path,
60
+ kind: 'prod-section',
61
+ diff: 'Prod section differs from the other instruction surface or is malformed.',
62
+ })));
58
63
  } catch (error) {
59
64
  previewFailure = error;
60
65
  }
@@ -66,6 +71,9 @@ export async function update(options) {
66
71
  };
67
72
  }
68
73
  if (dryRun) return { ...preview, state: 'preview', history };
74
+ if (preview.migrationConflicts?.length) {
75
+ return terminal(preview, 'conflicted', history, transition);
76
+ }
69
77
  if (preview.conflicts.length) return terminal(preview, 'conflicted', history, transition);
70
78
  const resolvedPreview = await resolvePreview({
71
79
  kitRoot, consumerRoot, preview, decisions, decide, transition,
@@ -138,7 +146,13 @@ async function applyTransaction(context) {
138
146
  nextManifest: nextReadinessManifest,
139
147
  });
140
148
  preview.generated = readiness.generated;
149
+ preview.migrations = readiness.migrations;
150
+ preview.migrated = readiness.migrated;
151
+ preview.migrationConflicts = readiness.migrationConflicts;
141
152
  preview.availability = readiness.availability;
153
+ if (readiness.migrationConflicts.length) {
154
+ throw new Error(`Prod section migration conflict: ${readiness.migrationConflicts.join(', ')}`);
155
+ }
142
156
  if (readiness.incompatible.length) {
143
157
  throw new Error(`monotonic compatibility would block existing skill core: ${readiness.incompatible.join(', ')}`);
144
158
  }
@@ -180,6 +194,7 @@ function verifyRelease(identities, kitVersion) {
180
194
 
181
195
  function hasUpstreamDelta(result) {
182
196
  return result.manifestChanged ||
197
+ (result.migrations?.length ?? 0) > 0 ||
183
198
  result.added.length + result.updated.length + result.deleted.length > 0;
184
199
  }
185
200
 
@@ -205,9 +220,12 @@ async function terminal(result, state, history, transition) {
205
220
  conflicts: result.conflicts.map(({ path }) => path),
206
221
  keptDeleted: result.keptDeleted,
207
222
  },
208
- recommendation: result.conflicts.length
209
- ? 'Review each named conflict; keep the local file or apply the incoming diff manually.'
210
- : null,
223
+ recommendation: result.migrationConflicts?.length
224
+ ? `Prod sections differ or are malformed in: ${result.migrationConflicts.join(', ')}. ` +
225
+ 'Resolve them manually; no consumer file was changed.'
226
+ : (result.conflicts.length
227
+ ? 'Review each named conflict; keep the local file or apply the incoming diff manually.'
228
+ : null),
211
229
  },
212
230
  };
213
231
  }