@ikon85/agent-workflow-kit 0.32.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.
@@ -72,6 +72,35 @@ proof is still open for state resolution even when Git tracking lets the base
72
72
  scan enumerate it. Overrides are report input only and are never passed into
73
73
  scanning, fingerprinting, verification, or state resolution.
74
74
 
75
+ ## Durable enforcement
76
+
77
+ An activated census is a durable consumer contract, not a one-time activation
78
+ check. Before activating a candidate or reporting an unchanged census as
79
+ `current`, derive the complete set of every `localScanners[].test` from the
80
+ profile and inspect the repository's documented local CI and pre-push paths.
81
+
82
+ Reuse one existing shared project-local census check entry point when the
83
+ repository has one. Otherwise add the smallest such entry point through the
84
+ repository's established script mechanism; do not create a second census
85
+ engine or duplicate scanner logic. It must execute every declared focused test,
86
+ and both local CI and pre-push must transitively reach that same entry point.
87
+ Add or extend an executable wiring test that fails when either durable path or
88
+ any declared scanner test becomes unreachable; matching prose or a bare grep is
89
+ not sufficient proof.
90
+
91
+ Keep enforcement reconciliation idempotent. An already complete wiring graph
92
+ is a no-write result. Mark only the narrow gate block added for this lifecycle
93
+ with the repository's existing managed marker, or with
94
+ `agent-workflow-kit:census` when no marker convention exists; that block is the
95
+ kit-owned census wiring. Preserve surrounding consumer-owned gate logic.
96
+
97
+ Missing or partial durable wiring is a failed candidate verification. If the
98
+ repository has no safe documented gate seam, ask one bounded wiring question
99
+ and stop before activation. On a scanner-test or wiring-proof failure, report
100
+ `failed`, discard the candidate when safe, and confirm that the previous active
101
+ census bytes are unchanged. Never report `current` from activation-time test
102
+ success alone.
103
+
75
104
  ## Workflow
76
105
 
77
106
  1. **Check.** Read the local profile and active census, if present. Report a
@@ -95,8 +124,9 @@ scanning, fingerprinting, verification, or state resolution.
95
124
  and a focused passing test for that pattern. Run that test before rescanning.
96
125
  6. **Verify a candidate.** Build the candidate from the fresh scan and recorded
97
126
  decisions. Fail verification when any surface or behavior remains `offen`,
98
- when a required local scanner test fails, or when the candidate fingerprints
99
- do not describe the current repository.
127
+ when a required local scanner test fails, when durable enforcement or its
128
+ executable wiring test is incomplete, or when the candidate fingerprints do
129
+ not describe the current repository.
100
130
  7. **Activate.** Call `activateCensus` with a real verifier so it stages, verifies,
101
131
  and atomically swaps under its local lock. On `CensusTransactionError`, report
102
132
  `updating` or `failed` and keep the previous active census authoritative.
@@ -137,4 +167,5 @@ Return only the useful audit trail:
137
167
  - surface coverage `X of Y`;
138
168
  - separate behavior overview;
139
169
  - visible `nicht relevant` justifications and active override, if any; <!-- language-census: ok -->
140
- - local scanner tests run and the transaction/no-write result.
170
+ - local scanner tests run, durable local-CI/pre-push reachability, and the
171
+ transaction/no-write result.
@@ -43,11 +43,25 @@ release contain the same artifact.
43
43
  data, infer an external fact, or manufacture `pending`/`not-applicable` to
44
44
  make a capability appear ready.
45
45
 
46
+ Readiness-schema adoption also keeps the Claude and Codex instruction
47
+ surfaces compatible. When exactly one applicable instruction surface has a
48
+ non-empty `## Prod` section and another has no such section, the staged
49
+ candidate mirrors the same section body into the missing surface. This
50
+ narrow migration is previewed as `migrated`, destination-race checked,
51
+ idempotent, and covered by the same verification and rollback transaction.
52
+ It may create a missing instruction file, but it never rewrites an existing
53
+ `## Prod` body. Empty, malformed, or divergent sections are named as
54
+ conflicts and leave every consumer file untouched. This migration belongs
55
+ to `kit update`; do not rerun `setup-workflow` after an ordinary update.
56
+ Readiness JSON diagnoses only the affected path and problem category
57
+ (`missing-file`, `missing-section`, `empty-section`, or
58
+ `divergent-section`); it never echoes consumer content.
59
+
46
60
  3. Read the terminal report. `aktuell` proves a second run found no upstream
47
- delta. A conflict report names and counts every category and leaves every
48
- consumer file untouched. Follow its recommendation and resolve each named
49
- conflict manually; never auto-merge, delete a local edit, or silently choose
50
- the incoming copy.
61
+ delta or pending readiness migration. A conflict report names and counts
62
+ every category and leaves every consumer file untouched. Follow its
63
+ recommendation and resolve each named conflict manually; never auto-merge,
64
+ delete a local edit, or silently choose the incoming copy.
51
65
 
52
66
  Read all four availability categories alongside the file delta: newly
53
67
  available skill core, newly degraded optional blocks, newly blocked skill
@@ -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
@@ -72,6 +72,35 @@ proof is still open for state resolution even when Git tracking lets the base
72
72
  scan enumerate it. Overrides are report input only and are never passed into
73
73
  scanning, fingerprinting, verification, or state resolution.
74
74
 
75
+ ## Durable enforcement
76
+
77
+ An activated census is a durable consumer contract, not a one-time activation
78
+ check. Before activating a candidate or reporting an unchanged census as
79
+ `current`, derive the complete set of every `localScanners[].test` from the
80
+ profile and inspect the repository's documented local CI and pre-push paths.
81
+
82
+ Reuse one existing shared project-local census check entry point when the
83
+ repository has one. Otherwise add the smallest such entry point through the
84
+ repository's established script mechanism; do not create a second census
85
+ engine or duplicate scanner logic. It must execute every declared focused test,
86
+ and both local CI and pre-push must transitively reach that same entry point.
87
+ Add or extend an executable wiring test that fails when either durable path or
88
+ any declared scanner test becomes unreachable; matching prose or a bare grep is
89
+ not sufficient proof.
90
+
91
+ Keep enforcement reconciliation idempotent. An already complete wiring graph
92
+ is a no-write result. Mark only the narrow gate block added for this lifecycle
93
+ with the repository's existing managed marker, or with
94
+ `agent-workflow-kit:census` when no marker convention exists; that block is the
95
+ kit-owned census wiring. Preserve surrounding consumer-owned gate logic.
96
+
97
+ Missing or partial durable wiring is a failed candidate verification. If the
98
+ repository has no safe documented gate seam, ask one bounded wiring question
99
+ and stop before activation. On a scanner-test or wiring-proof failure, report
100
+ `failed`, discard the candidate when safe, and confirm that the previous active
101
+ census bytes are unchanged. Never report `current` from activation-time test
102
+ success alone.
103
+
75
104
  ## Workflow
76
105
 
77
106
  1. **Check.** Read the local profile and active census, if present. Report a
@@ -95,8 +124,9 @@ scanning, fingerprinting, verification, or state resolution.
95
124
  and a focused passing test for that pattern. Run that test before rescanning.
96
125
  6. **Verify a candidate.** Build the candidate from the fresh scan and recorded
97
126
  decisions. Fail verification when any surface or behavior remains `offen`,
98
- when a required local scanner test fails, or when the candidate fingerprints
99
- do not describe the current repository.
127
+ when a required local scanner test fails, when durable enforcement or its
128
+ executable wiring test is incomplete, or when the candidate fingerprints do
129
+ not describe the current repository.
100
130
  7. **Activate.** Call `activateCensus` with a real verifier so it stages, verifies,
101
131
  and atomically swaps under its local lock. On `CensusTransactionError`, report
102
132
  `updating` or `failed` and keep the previous active census authoritative.
@@ -137,4 +167,5 @@ Return only the useful audit trail:
137
167
  - surface coverage `X of Y`;
138
168
  - separate behavior overview;
139
169
  - visible `nicht relevant` justifications and active override, if any; <!-- language-census: ok -->
140
- - local scanner tests run and the transaction/no-write result.
170
+ - local scanner tests run, durable local-CI/pre-push reachability, and the
171
+ transaction/no-write result.
@@ -43,11 +43,25 @@ release contain the same artifact.
43
43
  data, infer an external fact, or manufacture `pending`/`not-applicable` to
44
44
  make a capability appear ready.
45
45
 
46
+ Readiness-schema adoption also keeps the Claude and Codex instruction
47
+ surfaces compatible. When exactly one applicable instruction surface has a
48
+ non-empty `## Prod` section and another has no such section, the staged
49
+ candidate mirrors the same section body into the missing surface. This
50
+ narrow migration is previewed as `migrated`, destination-race checked,
51
+ idempotent, and covered by the same verification and rollback transaction.
52
+ It may create a missing instruction file, but it never rewrites an existing
53
+ `## Prod` body. Empty, malformed, or divergent sections are named as
54
+ conflicts and leave every consumer file untouched. This migration belongs
55
+ to `kit update`; do not rerun `setup-workflow` after an ordinary update.
56
+ Readiness JSON diagnoses only the affected path and problem category
57
+ (`missing-file`, `missing-section`, `empty-section`, or
58
+ `divergent-section`); it never echoes consumer content.
59
+
46
60
  3. Read the terminal report. `aktuell` proves a second run found no upstream
47
- delta. A conflict report names and counts every category and leaves every
48
- consumer file untouched. Follow its recommendation and resolve each named
49
- conflict manually; never auto-merge, delete a local edit, or silently choose
50
- the incoming copy.
61
+ delta or pending readiness migration. A conflict report names and counts
62
+ every category and leaves every consumer file untouched. Follow its
63
+ recommendation and resolve each named conflict manually; never auto-merge,
64
+ delete a local edit, or silently choose the incoming copy.
51
65
 
52
66
  Read all four availability categories alongside the file delta: newly
53
67
  available skill core, newly degraded optional blocks, newly blocked skill
@@ -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,16 @@ 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
+
351
364
  ### 0.32.0
352
365
 
353
366
  - changed: `.agents/skills/audit-skills/SKILL.md`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.32.0",
2
+ "kitVersion": "0.32.1",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -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
  },
@@ -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
  },
@@ -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
  },
@@ -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
  },
@@ -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
  },
@@ -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.32.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 () => {
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
  }
@@ -1,5 +1,5 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { access, cp, mkdtemp, readFile, rm, stat, symlink } from 'node:fs/promises';
2
+ import { access, cp, lstat, mkdtemp, readFile, rm, stat, symlink } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join, relative } from 'node:path';
5
5
  import { promisify } from 'node:util';
@@ -12,10 +12,15 @@ import {
12
12
  CONSUMER_MANIFEST_NAME, CONSUMER_ORIGIN, READINESS_MANIFEST_PATH,
13
13
  indexByPath, readManifest, writeManifest,
14
14
  } from './manifest.mjs';
15
- import { checkSkill, evaluateCapability } from '../../scripts/readiness.mjs';
15
+ import { checkSkill, evaluateCapability, inspectProdSections } from '../../scripts/readiness.mjs';
16
16
 
17
17
  const run = promisify(execFile);
18
18
  const exists = (path) => access(path).then(() => true, () => false);
19
+ const pathEntryExists = (path) => lstat(path).then(() => true, (error) => {
20
+ if (error.code === 'ENOENT') return false;
21
+ throw error;
22
+ });
23
+ const MIGRATABLE_INSTRUCTION_PATHS = new Set(['CLAUDE.md', 'AGENTS.md']);
19
24
 
20
25
  /** Copy a verification candidate without duplicating git metadata or dependencies. */
21
26
  export async function stageConsumer(consumerRoot) {
@@ -40,7 +45,11 @@ export async function activateCandidate({
40
45
  }) {
41
46
  const changed = [...preview.added, ...preview.updated];
42
47
  const generated = preview.generated ?? [];
43
- const touched = [...changed, ...generated, ...preview.deleted, CONSUMER_MANIFEST_NAME];
48
+ const migrations = preview.migrations ?? [];
49
+ const touched = [
50
+ ...changed, ...generated, ...migrations.map(({ path }) => path),
51
+ ...preview.deleted, CONSUMER_MANIFEST_NAME,
52
+ ];
44
53
  const currentManifest = await readFile(join(consumerRoot, CONSUMER_MANIFEST_NAME));
45
54
  if (!currentManifest.equals(consumerManifestBefore)) {
46
55
  throw new Error('consumer manifest changed during verification');
@@ -58,6 +67,11 @@ export async function activateCandidate({
58
67
  throw new Error(`generated candidate hash mismatch: ${path}`);
59
68
  }
60
69
  }
70
+ for (const migration of migrations) {
71
+ if (await sha256File(join(candidateRoot, migration.path)) !== migration.afterSha256) {
72
+ throw new Error(`migrated candidate hash mismatch: ${migration.path}`);
73
+ }
74
+ }
61
75
  await assertConsumerStillMatchesPreview(consumerRoot, preview);
62
76
  const rollback = new Map();
63
77
  for (const path of touched) rollback.set(path, await snapshot(join(consumerRoot, path)));
@@ -68,6 +82,9 @@ export async function activateCandidate({
68
82
  for (const path of generated) {
69
83
  await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)));
70
84
  }
85
+ for (const { path } of migrations) {
86
+ await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)));
87
+ }
71
88
  await afterGenerated();
72
89
  for (const path of preview.deleted) await rm(join(consumerRoot, path), { force: true });
73
90
  await writeAtomic(
@@ -105,6 +122,17 @@ async function assertConsumerStillMatchesPreview(consumerRoot, preview) {
105
122
  throw new Error(`consumer changed during verification: ${path}`);
106
123
  }
107
124
  }
125
+ for (const migration of preview.migrations ?? []) {
126
+ const present = await pathEntryExists(join(consumerRoot, migration.path));
127
+ if (present) await validateConsumerFile(consumerRoot, migration.path);
128
+ else if (!MIGRATABLE_INSTRUCTION_PATHS.has(migration.path)) {
129
+ throw new Error(`unsafe consumer path: ${migration.path}`);
130
+ }
131
+ const current = present ? await sha256File(join(consumerRoot, migration.path)) : null;
132
+ if (current !== migration.beforeSha256) {
133
+ throw new Error(`consumer changed during verification: ${migration.path}`);
134
+ }
135
+ }
108
136
  for (const path of [...preview.updated, ...preview.deleted]) {
109
137
  const prior = installed.get(path);
110
138
  const current = await exists(join(consumerRoot, path))
@@ -144,6 +172,9 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
144
172
  }
145
173
  await writeManifest(manifestPath, { ...manifest, installed });
146
174
  }
175
+ const { migrations, migrationConflicts } = await migrateProdSections({
176
+ candidateRoot, consumerRoot, nextManifest,
177
+ });
147
178
  const before = await readinessSnapshot(consumerRoot, priorManifest);
148
179
  const after = await readinessSnapshot(candidateRoot, nextManifest);
149
180
  const incompatible = Object.entries(after.skills)
@@ -151,7 +182,50 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
151
182
  && before.skills[skill] && current.verdict === 'blocked')
152
183
  .map(([skill]) => skill)
153
184
  .sort();
154
- return { generated, availability: readinessDiff(before, after), incompatible };
185
+ return {
186
+ generated,
187
+ migrations,
188
+ migrated: migrations.map(({ path }) => path),
189
+ migrationConflicts,
190
+ availability: readinessDiff(before, after),
191
+ incompatible,
192
+ };
193
+ }
194
+
195
+ async function migrateProdSections({ candidateRoot, consumerRoot, nextManifest }) {
196
+ const paths = [...new Set(Object.values(nextManifest?.readiness?.capabilities ?? {})
197
+ .flatMap(({ evidence }) => evidence?.type === 'prod-section' ? evidence.paths ?? [] : []))];
198
+ if (paths.length < 2) return { migrations: [], migrationConflicts: [] };
199
+ if (paths.some((path) => !MIGRATABLE_INSTRUCTION_PATHS.has(path))) {
200
+ return { migrations: [], migrationConflicts: paths };
201
+ }
202
+ const sections = await inspectProdSections(candidateRoot, paths);
203
+ const invalid = sections.filter(({ state }) => state === 'invalid');
204
+ const validBodies = [...new Set(
205
+ sections.filter(({ state }) => state === 'valid').map(({ body }) => body),
206
+ )];
207
+ if (invalid.length || validBodies.length > 1) {
208
+ return { migrations: [], migrationConflicts: paths };
209
+ }
210
+ if (validBodies.length !== 1) return { migrations: [], migrationConflicts: [] };
211
+
212
+ const body = validBodies[0];
213
+ const migrations = [];
214
+ for (const entry of sections.filter(({ state }) => state === 'missing')) {
215
+ const candidatePath = join(candidateRoot, entry.path);
216
+ const present = await pathEntryExists(candidatePath);
217
+ if (present) await validateConsumerFile(candidateRoot, entry.path);
218
+ const before = present ? await readFile(candidatePath, 'utf8') : '';
219
+ const separator = before && !before.endsWith('\n') ? '\n\n' : (before ? '\n' : '');
220
+ await writeAtomic(candidatePath, `${before}${separator}## Prod\n\n${body}\n`);
221
+ migrations.push({
222
+ path: entry.path,
223
+ beforeSha256: await exists(join(consumerRoot, entry.path))
224
+ ? await sha256File(join(consumerRoot, entry.path)) : null,
225
+ afterSha256: await sha256File(candidatePath),
226
+ });
227
+ }
228
+ return { migrations, migrationConflicts: [] };
155
229
  }
156
230
 
157
231
  function readinessStubPaths(manifest) {