@ikon85/agent-workflow-kit 0.29.1 → 0.31.0

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.
Files changed (52) hide show
  1. package/.agents/skills/board-to-waves/SKILL.md +14 -0
  2. package/.agents/skills/code-review/SKILL.md +22 -3
  3. package/.agents/skills/codex-adapter-sync/SKILL.md +134 -72
  4. package/.agents/skills/kit-update/SKILL.md +21 -1
  5. package/.agents/skills/local-ci/SKILL.md +16 -0
  6. package/.agents/skills/project-release/SKILL.md +18 -0
  7. package/.agents/skills/security-audit/SKILL.md +18 -0
  8. package/.agents/skills/setup-workflow/SKILL.md +39 -2
  9. package/.agents/skills/spec-self-critique/SKILL.md +16 -7
  10. package/.agents/skills/to-issues/SKILL.md +13 -1
  11. package/.agents/skills/to-prd/SKILL.md +14 -0
  12. package/.agents/skills/to-waves/SKILL.md +16 -2
  13. package/.agents/skills/triage/SKILL.md +24 -0
  14. package/.agents/skills/verify-spike/SKILL.md +20 -1
  15. package/.agents/skills/wrapup/SKILL.md +26 -4
  16. package/.claude/skills/board-to-waves/SKILL.md +14 -0
  17. package/.claude/skills/code-review/SKILL.md +22 -3
  18. package/.claude/skills/kit-update/SKILL.md +21 -1
  19. package/.claude/skills/local-ci/SKILL.md +16 -0
  20. package/.claude/skills/project-release/SKILL.md +18 -0
  21. package/.claude/skills/security-audit/SKILL.md +18 -0
  22. package/.claude/skills/setup-workflow/SKILL.md +39 -2
  23. package/.claude/skills/skill-manifest.json +46 -0
  24. package/.claude/skills/spec-self-critique/SKILL.md +16 -7
  25. package/.claude/skills/to-issues/SKILL.md +13 -1
  26. package/.claude/skills/to-prd/SKILL.md +14 -0
  27. package/.claude/skills/to-waves/SKILL.md +16 -2
  28. package/.claude/skills/triage/SKILL.md +24 -0
  29. package/.claude/skills/verify-spike/SKILL.md +20 -1
  30. package/.claude/skills/wrapup/SKILL.md +26 -4
  31. package/README.md +41 -0
  32. package/agent-workflow-kit.package.json +71 -31
  33. package/package.json +1 -1
  34. package/scripts/kit-update-pr.mjs +14 -1
  35. package/scripts/kit-update-pr.test.mjs +21 -0
  36. package/scripts/readiness.mjs +215 -0
  37. package/scripts/test_codex_adapter_sync_contract.py +154 -0
  38. package/scripts/test_retro_wrapup_contract.py +17 -0
  39. package/scripts/test_skill_optional_readiness.py +171 -0
  40. package/scripts/test_skill_portability_lint.py +57 -18
  41. package/scripts/test_skill_readiness_contract.py +139 -0
  42. package/scripts/test_skill_readiness_preflight.py +180 -0
  43. package/scripts/test_skill_required_readiness.py +233 -0
  44. package/scripts/test_skill_setup_workflow_seeds.py +22 -0
  45. package/src/cli.mjs +10 -2
  46. package/src/commands/init.mjs +3 -1
  47. package/src/commands/uninstall.mjs +5 -1
  48. package/src/commands/update.mjs +65 -8
  49. package/src/lib/bundle.mjs +7 -0
  50. package/src/lib/manifest.mjs +26 -2
  51. package/src/lib/updateCandidate.mjs +116 -2
  52. package/src/lib/updateReconcile.mjs +3 -1
@@ -1,7 +1,9 @@
1
1
  import { readFile, rm } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { assertConsumerReleaseParity } from '../../scripts/release-parity.mjs';
4
- import { activateCandidate, stageConsumer, verifyCandidate } from '../lib/updateCandidate.mjs';
4
+ import {
5
+ activateCandidate, adoptReadinessCandidate, readReadinessManifest, stageConsumer, verifyCandidate,
6
+ } from '../lib/updateCandidate.mjs';
5
7
  import { reconcile } from '../lib/updateReconcile.mjs';
6
8
  import {
7
9
  CONSUMER_MANIFEST_NAME, PACKAGE_MANIFEST_NAME, readManifest,
@@ -9,6 +11,12 @@ import {
9
11
 
10
12
  const RELEASE_NAME = '@ikon85/agent-workflow-kit';
11
13
 
14
+ export function renderUpdateFailure(result) {
15
+ const failure = result.failure ?? { phase: 'unknown', consumerState: 'unknown' };
16
+ return `candidate update failed · phase: ${failure.phase} · ` +
17
+ `consumerState: ${failure.consumerState} · ${result.error}`;
18
+ }
19
+
12
20
  /**
13
21
  * Transactionally reconcile a consumer with a parity-proven kit release.
14
22
  * checking -> preview/awaiting_decision -> staging -> verifying -> terminal state.
@@ -16,7 +24,8 @@ const RELEASE_NAME = '@ikon85/agent-workflow-kit';
16
24
  export async function update(options) {
17
25
  const {
18
26
  kitRoot, consumerRoot, decide = () => false, dryRun = false,
19
- releaseIdentities, verify = verifyCandidate, signal, onState = () => {}, resumeFrom,
27
+ releaseIdentities, verify = verifyCandidate, activate = activateCandidate,
28
+ signal, onState = () => {}, resumeFrom,
20
29
  } = options;
21
30
  const history = [];
22
31
  const transition = async (state) => { history.push(state); await onState(state); };
@@ -30,6 +39,8 @@ export async function update(options) {
30
39
  throw new Error('not initialised — run `init` first');
31
40
  }
32
41
  const consumerManifestBefore = await readFile(consumerManifestPath);
42
+ const priorReadinessManifest = await readReadinessManifest(consumerRoot);
43
+ const nextReadinessManifest = await readReadinessManifest(kitRoot);
33
44
 
34
45
  const decisions = new Map();
35
46
  const choosePreview = async (action, path) => {
@@ -39,7 +50,21 @@ export async function update(options) {
39
50
  return decisions.get(key);
40
51
  };
41
52
  const preview = await reconcile({ kitRoot, consumerRoot, decide: choosePreview, dryRun: true });
53
+ let previewFailure;
54
+ try {
55
+ Object.assign(preview, await previewReadinessAdoption({
56
+ kitRoot, consumerRoot, priorReadinessManifest, nextReadinessManifest,
57
+ }));
58
+ } catch (error) {
59
+ previewFailure = error;
60
+ }
42
61
  await transition('preview');
62
+ if (previewFailure) {
63
+ return {
64
+ ...await terminal(preview, 'failed', history, transition), error: previewFailure.message,
65
+ failure: { phase: 'staging', consumerState: 'unchanged' },
66
+ };
67
+ }
43
68
  if (dryRun) return { ...preview, state: 'preview', history };
44
69
  if (preview.conflicts.length) return terminal(preview, 'conflicted', history, transition);
45
70
  const resolvedPreview = await resolvePreview({
@@ -52,11 +77,28 @@ export async function update(options) {
52
77
  return { ...await terminal(resolvedPreview, 'applied', history, transition), status: 'current' };
53
78
  }
54
79
  return applyTransaction({
55
- kitRoot, consumerRoot, pkg, preview: resolvedPreview, decisions, verify, signal, resumeFrom,
56
- consumerManifestBefore, history, transition,
80
+ kitRoot, consumerRoot, pkg, preview: resolvedPreview, decisions, verify, activate, signal, resumeFrom,
81
+ consumerManifestBefore, priorReadinessManifest, nextReadinessManifest, history, transition,
57
82
  });
58
83
  }
59
84
 
85
+ async function previewReadinessAdoption(context) {
86
+ const { kitRoot, consumerRoot, priorReadinessManifest, nextReadinessManifest } = context;
87
+ const candidateRoot = await stageConsumer(consumerRoot);
88
+ try {
89
+ await reconcile({
90
+ kitRoot, consumerRoot: candidateRoot,
91
+ decide: (action) => action === 'collision' ? 'keep-as-owned' : false,
92
+ });
93
+ return await adoptReadinessCandidate({
94
+ candidateRoot, consumerRoot, priorManifest: priorReadinessManifest,
95
+ nextManifest: nextReadinessManifest,
96
+ });
97
+ } finally {
98
+ await rm(candidateRoot, { recursive: true, force: true });
99
+ }
100
+ }
101
+
60
102
  async function resolvePreview({ kitRoot, consumerRoot, preview, decisions, decide, transition }) {
61
103
  if (preview.deleted.length || preview.keptDeleted.length || preview.collisions.length) {
62
104
  await transition('awaiting_decision');
@@ -73,11 +115,12 @@ async function resolvePreview({ kitRoot, consumerRoot, preview, decisions, decid
73
115
 
74
116
  async function applyTransaction(context) {
75
117
  const {
76
- kitRoot, consumerRoot, pkg, preview, decisions, verify, signal, resumeFrom,
77
- consumerManifestBefore, history, transition,
118
+ kitRoot, consumerRoot, pkg, preview, decisions, verify, activate, signal, resumeFrom,
119
+ consumerManifestBefore, priorReadinessManifest, nextReadinessManifest, history, transition,
78
120
  } = context;
79
121
  let candidateRoot = resumeFrom;
80
122
  let keepCandidate = false;
123
+ let phase = 'staging';
81
124
  try {
82
125
  await transition('staging');
83
126
  if (candidateRoot && preview.collisionResolutions.length) {
@@ -90,6 +133,16 @@ async function applyTransaction(context) {
90
133
  decide: (action, path) => decisions.get(decisionKey(action, path)),
91
134
  });
92
135
  }
136
+ const readiness = await adoptReadinessCandidate({
137
+ candidateRoot, consumerRoot, priorManifest: priorReadinessManifest,
138
+ nextManifest: nextReadinessManifest,
139
+ });
140
+ preview.generated = readiness.generated;
141
+ preview.availability = readiness.availability;
142
+ if (readiness.incompatible.length) {
143
+ throw new Error(`monotonic compatibility would block existing skill core: ${readiness.incompatible.join(', ')}`);
144
+ }
145
+ phase = 'verification';
93
146
  await transition('verifying');
94
147
  const abort = async () => {
95
148
  keepCandidate = true;
@@ -98,12 +151,16 @@ async function applyTransaction(context) {
98
151
  if (signal?.aborted) return abort();
99
152
  await verify(candidateRoot);
100
153
  if (signal?.aborted) return abort();
101
- await activateCandidate({
154
+ phase = 'activation';
155
+ await activate({
102
156
  candidateRoot, consumerRoot, pkg, preview, consumerManifestBefore,
103
157
  });
104
158
  return { ...await terminal(preview, 'applied', history, transition), status: 'updated' };
105
159
  } catch (error) {
106
- return { ...await terminal(preview, 'failed', history, transition), error: error.message };
160
+ return {
161
+ ...await terminal(preview, 'failed', history, transition), error: error.message,
162
+ failure: { phase, consumerState: error.consumerState ?? 'unchanged' },
163
+ };
107
164
  } finally {
108
165
  if (candidateRoot && !keepCandidate) await rm(candidateRoot, { recursive: true, force: true });
109
166
  }
@@ -4,6 +4,13 @@ import { join, relative } from 'node:path';
4
4
  // The planning skills are not usable without their helper ecosystem (Codex R2#1).
5
5
  // These ship alongside the skills. Paths are relative to the bundle/consumer root.
6
6
  export const HELPER_FILES = [
7
+ // Readiness declarations and their deterministic consumer-side command ship
8
+ // together; the manifest remains the only capability/dependency registry.
9
+ { path: '.claude/skills/skill-manifest.json', kind: 'doc', mode: 0o644 },
10
+ { path: 'scripts/readiness.mjs', kind: 'script', mode: 0o755 },
11
+ { path: 'src/lib/sentinel.mjs', kind: 'script', mode: 0o644 },
12
+ { path: 'src/lib/manifest.mjs', kind: 'script', mode: 0o644 },
13
+ { path: 'src/lib/atomicWrite.mjs', kind: 'script', mode: 0o644 },
7
14
  // Shared profile loader imported by the three planning scripts — they read
8
15
  // every board-specific value from docs/agents/board-sync.md through it, so it
9
16
  // MUST ship or they are broken-on-arrival. Library (imported, not run) → 0o644.
@@ -1,4 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
2
3
  import { writeAtomic } from './atomicWrite.mjs';
3
4
 
4
5
  // Two manifests (Codex R1#9 / R3#1):
@@ -15,6 +16,8 @@ export const PACKAGE_MANIFEST_NAME = 'agent-workflow-kit.package.json';
15
16
  export const CONSUMER_INSTALL_ROLE = 'consumer';
16
17
  export const KIT_ORIGIN = 'kit';
17
18
  export const CONSUMER_ORIGIN = 'consumer';
19
+ export const READINESS_CONTRACT_VERSION = 1;
20
+ export const READINESS_MANIFEST_PATH = '.claude/skills/skill-manifest.json';
18
21
 
19
22
  /**
20
23
  * Parse a JSON manifest, or null if the file does not exist. A corrupt file
@@ -51,8 +54,29 @@ export async function writeManifest(path, obj) {
51
54
  await writeAtomic(path, JSON.stringify(obj, null, 2) + '\n');
52
55
  }
53
56
 
54
- export function emptyConsumerManifest(kitVersion) {
55
- return { kitVersion, installRole: CONSUMER_INSTALL_ROLE, installed: [] };
57
+ export async function readReadinessContract(root) {
58
+ const manifest = await readManifest(join(root, READINESS_MANIFEST_PATH));
59
+ return manifest?.readiness ?? null;
60
+ }
61
+
62
+ function retainedDecisions(prior, readiness) {
63
+ const allowed = readiness?.capabilities ? new Set(Object.keys(readiness.capabilities)) : null;
64
+ return Object.fromEntries(Object.entries(prior.readinessDecisions ?? {}).filter(
65
+ ([name, value]) => (!allowed || allowed.has(name)) && (value === 'pending'
66
+ || (value === 'not-applicable' && readiness?.capabilities?.[name]?.allowNotApplicable)),
67
+ ));
68
+ }
69
+
70
+ export function emptyConsumerManifest(kitVersion, prior = {}, readiness = null) {
71
+ prior ??= {};
72
+ return {
73
+ ...prior,
74
+ kitVersion,
75
+ installRole: CONSUMER_INSTALL_ROLE,
76
+ readinessContractVersion: readiness?.contractVersion ?? READINESS_CONTRACT_VERSION,
77
+ readinessDecisions: retainedDecisions(prior, readiness),
78
+ installed: [],
79
+ };
56
80
  }
57
81
 
58
82
  /** Package entries without a role predate role-aware installs and remain consumer-owned. */
@@ -6,7 +6,13 @@ import { promisify } from 'node:util';
6
6
  import { writeAtomic } from './atomicWrite.mjs';
7
7
  import { validateConsumerFile } from './consumerPath.mjs';
8
8
  import { sha256File } from './hash.mjs';
9
- import { CONSUMER_MANIFEST_NAME, indexByPath, readManifest } from './manifest.mjs';
9
+ import { stubSentinel } from './sentinel.mjs';
10
+ import { STUB_TARGETS } from './bundle.mjs';
11
+ import {
12
+ CONSUMER_MANIFEST_NAME, CONSUMER_ORIGIN, READINESS_MANIFEST_PATH,
13
+ indexByPath, readManifest, writeManifest,
14
+ } from './manifest.mjs';
15
+ import { checkSkill, evaluateCapability } from '../../scripts/readiness.mjs';
10
16
 
11
17
  const run = promisify(execFile);
12
18
  const exists = (path) => access(path).then(() => true, () => false);
@@ -30,9 +36,11 @@ export async function stageConsumer(consumerRoot) {
30
36
  /** Activate only verified kit-owned deltas, rolling every touched path back on failure. */
31
37
  export async function activateCandidate({
32
38
  candidateRoot, consumerRoot, pkg, preview, consumerManifestBefore,
39
+ afterGenerated = async () => {},
33
40
  }) {
34
41
  const changed = [...preview.added, ...preview.updated];
35
- const touched = [...changed, ...preview.deleted, CONSUMER_MANIFEST_NAME];
42
+ const generated = preview.generated ?? [];
43
+ const touched = [...changed, ...generated, ...preview.deleted, CONSUMER_MANIFEST_NAME];
36
44
  const currentManifest = await readFile(join(consumerRoot, CONSUMER_MANIFEST_NAME));
37
45
  if (!currentManifest.equals(consumerManifestBefore)) {
38
46
  throw new Error('consumer manifest changed during verification');
@@ -43,6 +51,13 @@ export async function activateCandidate({
43
51
  throw new Error(`candidate hash mismatch: ${path}`);
44
52
  }
45
53
  }
54
+ const candidateManifest = await readManifest(join(candidateRoot, CONSUMER_MANIFEST_NAME));
55
+ const candidateInstalled = indexByPath(candidateManifest, 'installed');
56
+ for (const path of generated) {
57
+ if (await sha256File(join(candidateRoot, path)) !== candidateInstalled.get(path)?.installedSha256) {
58
+ throw new Error(`generated candidate hash mismatch: ${path}`);
59
+ }
60
+ }
46
61
  await assertConsumerStillMatchesPreview(consumerRoot, preview);
47
62
  const rollback = new Map();
48
63
  for (const path of touched) rollback.set(path, await snapshot(join(consumerRoot, path)));
@@ -50,6 +65,10 @@ export async function activateCandidate({
50
65
  for (const path of changed) {
51
66
  await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)), pkgIdx.get(path)?.mode);
52
67
  }
68
+ for (const path of generated) {
69
+ await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)));
70
+ }
71
+ await afterGenerated();
53
72
  for (const path of preview.deleted) await rm(join(consumerRoot, path), { force: true });
54
73
  await writeAtomic(
55
74
  join(consumerRoot, CONSUMER_MANIFEST_NAME),
@@ -57,6 +76,7 @@ export async function activateCandidate({
57
76
  );
58
77
  } catch (error) {
59
78
  for (const path of touched.reverse()) await restore(join(consumerRoot, path), rollback.get(path));
79
+ error.consumerState = 'rolled-back';
60
80
  throw error;
61
81
  }
62
82
  }
@@ -80,6 +100,11 @@ async function assertConsumerStillMatchesPreview(consumerRoot, preview) {
80
100
  if (replacements.has(path)) continue;
81
101
  if (await exists(join(consumerRoot, path))) throw new Error(`consumer changed during verification: ${path}`);
82
102
  }
103
+ for (const path of preview.generated ?? []) {
104
+ if (await exists(join(consumerRoot, path))) {
105
+ throw new Error(`consumer changed during verification: ${path}`);
106
+ }
107
+ }
83
108
  for (const path of [...preview.updated, ...preview.deleted]) {
84
109
  const prior = installed.get(path);
85
110
  const current = await exists(join(consumerRoot, path))
@@ -90,6 +115,95 @@ async function assertConsumerStillMatchesPreview(consumerRoot, preview) {
90
115
  }
91
116
  }
92
117
 
118
+ /** Seed only newly declared, decision-free project-layer stubs in a staged candidate. */
119
+ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, priorManifest, nextManifest }) {
120
+ const priorPaths = readinessStubPaths(priorManifest);
121
+ const manifestPath = join(candidateRoot, CONSUMER_MANIFEST_NAME);
122
+ const manifest = await readManifest(manifestPath);
123
+ const candidateInstalled = indexByPath(manifest, 'installed');
124
+ const generated = [];
125
+ for (const path of readinessStubPaths(nextManifest)) {
126
+ if (priorPaths.has(path)) continue;
127
+ if (await exists(join(candidateRoot, path))) {
128
+ if (candidateInstalled.get(path)?.origin === CONSUMER_ORIGIN
129
+ && !await exists(join(consumerRoot, path))) generated.push(path);
130
+ continue;
131
+ }
132
+ if (await exists(join(consumerRoot, path))) continue;
133
+ await writeAtomic(join(candidateRoot, path), `${stubSentinel()}\n`);
134
+ generated.push(path);
135
+ }
136
+ if (generated.length) {
137
+ const installed = [...manifest.installed];
138
+ for (const generatedPath of generated) {
139
+ if (candidateInstalled.has(generatedPath)) continue;
140
+ installed.push({
141
+ path: generatedPath, kind: 'doc', installedSha256: await sha256File(join(candidateRoot, generatedPath)),
142
+ origin: CONSUMER_ORIGIN, installRole: 'consumer',
143
+ });
144
+ }
145
+ await writeManifest(manifestPath, { ...manifest, installed });
146
+ }
147
+ const before = await readinessSnapshot(consumerRoot, priorManifest);
148
+ const after = await readinessSnapshot(candidateRoot, nextManifest);
149
+ const incompatible = Object.entries(after.skills)
150
+ .filter(([skill, current]) => before.skills[skill]?.verdict !== 'blocked'
151
+ && before.skills[skill] && current.verdict === 'blocked')
152
+ .map(([skill]) => skill)
153
+ .sort();
154
+ return { generated, availability: readinessDiff(before, after), incompatible };
155
+ }
156
+
157
+ function readinessStubPaths(manifest) {
158
+ const safe = new Set(STUB_TARGETS);
159
+ return new Set(Object.values(manifest?.readiness?.capabilities ?? {}).flatMap((capability) => {
160
+ if (capability.evidence?.type !== 'sentinel') return [];
161
+ return (capability.evidence.paths ?? []).filter((path) => safe.has(path));
162
+ }));
163
+ }
164
+
165
+ async function readinessSnapshot(root, manifest) {
166
+ const skills = {};
167
+ for (const [name, declaration] of Object.entries(manifest?.skills ?? {})) {
168
+ if (!declaration.readiness) continue;
169
+ skills[name] = await checkSkill({ root, skill: name, manifest });
170
+ }
171
+ const consumer = await readManifest(join(root, CONSUMER_MANIFEST_NAME));
172
+ const capabilities = {};
173
+ for (const [name, capability] of Object.entries(manifest?.readiness?.capabilities ?? {})) {
174
+ capabilities[name] = await evaluateCapability({
175
+ root, capability, decision: consumer?.readinessDecisions?.[name],
176
+ });
177
+ }
178
+ return { skills, capabilities };
179
+ }
180
+
181
+ function readinessDiff(before, after) {
182
+ const newlyAvailable = [];
183
+ const newlyDegraded = [];
184
+ const newlyBlocked = [];
185
+ const unresolved = new Set();
186
+ for (const [skill, current] of Object.entries(after.skills)) {
187
+ const prior = before.skills[skill];
188
+ if (current.verdict === 'blocked' && prior?.verdict !== 'blocked') newlyBlocked.push(skill);
189
+ if (current.verdict !== 'blocked' && (!prior || prior.verdict === 'blocked')) newlyAvailable.push(skill);
190
+ for (const block of current.inactiveBlocks) {
191
+ if (!prior || !prior.inactiveBlocks.includes(block)) newlyDegraded.push(`${skill}.${block}`);
192
+ }
193
+ }
194
+ for (const [capability, result] of Object.entries(after.capabilities)) {
195
+ if (result.state !== 'ready') unresolved.add(`${capability}:${result.state}`);
196
+ }
197
+ return {
198
+ newlyAvailable: newlyAvailable.sort(), newlyDegraded: newlyDegraded.sort(),
199
+ newlyBlocked: newlyBlocked.sort(), stillUnresolved: [...unresolved].sort(),
200
+ };
201
+ }
202
+
203
+ export async function readReadinessManifest(root) {
204
+ return readManifest(join(root, READINESS_MANIFEST_PATH));
205
+ }
206
+
93
207
  async function snapshot(path) {
94
208
  if (!await exists(path)) return null;
95
209
  const info = await stat(path);
@@ -8,6 +8,7 @@ import {
8
8
  CONSUMER_MANIFEST_NAME, PACKAGE_MANIFEST_NAME, emptyConsumerManifest,
9
9
  CONSUMER_INSTALL_ROLE, CONSUMER_ORIGIN, KIT_ORIGIN, filesForInstallRole,
10
10
  indexByPath, readManifest, writeManifest,
11
+ readReadinessContract,
11
12
  } from './manifest.mjs';
12
13
 
13
14
  const exists = (path) => access(path).then(() => true, () => false);
@@ -18,6 +19,7 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
18
19
  if (!pkg) throw new Error('kit package manifest not found');
19
20
  const consumer = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
20
21
  if (!consumer) throw new Error('not initialised — run `init` first');
22
+ const readiness = await readReadinessContract(kitRoot);
21
23
 
22
24
  const installedIdx = indexByPath(consumer, 'installed');
23
25
  const packageIdx = indexByPath(pkg, 'files');
@@ -117,7 +119,7 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
117
119
 
118
120
  if (!dryRun) {
119
121
  await writeManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME), {
120
- ...emptyConsumerManifest(pkg.kitVersion), installed: nextInstalled,
122
+ ...emptyConsumerManifest(pkg.kitVersion, consumer, readiness), installed: nextInstalled,
121
123
  });
122
124
  }
123
125
  return result;