@ikon85/agent-workflow-kit 0.34.4 → 0.34.5

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 (29) hide show
  1. package/.agents/skills/kit-update/SKILL.md +6 -3
  2. package/.agents/skills/setup-workflow/SKILL.md +13 -5
  3. package/.agents/skills/setup-workflow/assets/agent-workflow-kit-update.yml +1 -3
  4. package/.claude/skills/kit-update/SKILL.md +6 -3
  5. package/.claude/skills/setup-workflow/SKILL.md +13 -5
  6. package/.claude/skills/setup-workflow/assets/agent-workflow-kit-update.yml +1 -3
  7. package/README.md +10 -0
  8. package/agent-workflow-kit.package.json +8 -8
  9. package/docs/adr/0001-consumer-divergence-policy.md +4 -0
  10. package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +63 -0
  11. package/docs/agents/board-sync.md +1 -1
  12. package/docs/agents/workflow-capabilities.json +17 -0
  13. package/docs/research/benchlm-routing-source.md +198 -0
  14. package/docs/research/consumer-owned-protocol-files.md +238 -0
  15. package/docs/research/frontend-agent-benchmarks.md +282 -0
  16. package/docs/research/model-effort-routing-benchmarks.md +261 -0
  17. package/docs/research/provider-neutral-agent-routing.md +207 -0
  18. package/package.json +1 -1
  19. package/scripts/kit-update-pr.mjs +1 -1
  20. package/scripts/kit-update-pr.test.mjs +2 -0
  21. package/scripts/test_skill_readiness_contract.py +6 -1
  22. package/scripts/test_skill_setup_workflow_seeds.py +18 -17
  23. package/src/commands/update.mjs +52 -20
  24. package/src/lib/bundle.mjs +1 -1
  25. package/src/lib/updateCandidate.mjs +278 -50
  26. package/src/lib/verifyUpdateCandidate.mjs +220 -0
  27. package/src/lib/verifyUpdateCandidateArtifacts.mjs +78 -0
  28. package/src/lib/verifyUpdateCandidateProtocol.mjs +221 -0
  29. package/src/lib/verifyUpdateCandidateTransaction.mjs +152 -0
@@ -0,0 +1,220 @@
1
+ import { lstat, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { sha256File } from './hash.mjs';
5
+ import {
6
+ CONSUMER_INSTALL_ROLE, CONSUMER_MANIFEST_NAME, CONSUMER_ORIGIN, KIT_ORIGIN,
7
+ emptyConsumerManifest, filesForInstallRole,
8
+ } from './manifest.mjs';
9
+ import { validateCandidateManifestPath } from './updateCandidate.mjs';
10
+ import {
11
+ verifyCandidateMembership, verifyChangedSyntax,
12
+ } from './verifyUpdateCandidateArtifacts.mjs';
13
+ import { verifyCandidateProtocol } from './verifyUpdateCandidateProtocol.mjs';
14
+ import {
15
+ verifyDeletionState, verifyDerivedArtifacts, verifyTransactionPreview,
16
+ } from './verifyUpdateCandidateTransaction.mjs';
17
+
18
+ const HASH = /^[a-f0-9]{64}$/;
19
+ const PACKAGE_KINDS = new Set(['skill', 'hook', 'doc', 'template', 'script']);
20
+
21
+ /**
22
+ * Verify the Kit-owned staged end state without executing Consumer behavior.
23
+ *
24
+ * The trusted package manifest and transaction preview are supplied by the
25
+ * updater; candidate-owned metadata never establishes its own authority.
26
+ */
27
+ export async function verifyUpdateCandidate(candidateRoot, context) {
28
+ await verifyCandidateMetadata(candidateRoot, context);
29
+ await verifyChangedSyntax(candidateRoot, context.preview);
30
+ }
31
+
32
+ export async function verifyCandidateMetadata(candidateRoot, context) {
33
+ if (!candidateRoot || !context?.pkg) {
34
+ throw new Error('candidate invariant transaction: trusted package manifest is required');
35
+ }
36
+ const { installable, installed, ledger } = await verifyCandidateSchema(candidateRoot, context);
37
+ if (!Array.isArray(context.priorConsumerManifest?.installed)) {
38
+ throw new Error('candidate invariant ownership: trusted prior Consumer ledger is required');
39
+ }
40
+ verifyLedgerMetadata(ledger, context);
41
+ const priorInstalled = uniqueEntries(context.priorConsumerManifest.installed, 'prior ledger');
42
+ verifyTransactionPreview(context.preview, installable, installed);
43
+ verifyDeletionState(installed, context.pkg, installable, context.preview);
44
+ await verifyCandidateMembership(candidateRoot, context);
45
+ for (const entry of installable) {
46
+ const tracked = installed.get(entry.path);
47
+ if (!tracked) {
48
+ throw new Error(`candidate invariant ownership: untracked package path ${entry.path}`);
49
+ }
50
+ validateInstalledEntry(tracked, entry, expectedOrigin(entry.path, priorInstalled, context.preview));
51
+ let state;
52
+ try {
53
+ state = await lstat(join(candidateRoot, entry.path));
54
+ } catch (error) {
55
+ if (error.code === 'ENOENT') {
56
+ throw new Error(`candidate invariant artifact: missing ${entry.path}`);
57
+ }
58
+ throw error;
59
+ }
60
+ if (!state.isFile()) {
61
+ throw new Error(`candidate invariant artifact: not a regular file ${entry.path}`);
62
+ }
63
+ if (tracked.origin === KIT_ORIGIN && (state.mode & 0o777) !== entry.mode) {
64
+ throw new Error(`candidate invariant artifact: mode mismatch ${entry.path}`);
65
+ }
66
+ const expectedHash = tracked.origin === CONSUMER_ORIGIN
67
+ ? tracked.installedSha256 : entry.sha256;
68
+ if (await sha256File(join(candidateRoot, entry.path)) !== expectedHash) {
69
+ throw new Error(`candidate invariant artifact: hash mismatch ${entry.path}`);
70
+ }
71
+ }
72
+ await verifyDerivedArtifacts(candidateRoot, installed, context.preview);
73
+ }
74
+
75
+ export async function verifyCandidateSchema(candidateRoot, context) {
76
+ if (!candidateRoot || !context?.pkg) {
77
+ throw new Error('candidate invariant transaction: trusted package manifest is required');
78
+ }
79
+ const installable = packageArtifacts(context.pkg);
80
+ const ledger = await candidateLedger(candidateRoot, context);
81
+ const installed = uniqueEntries(ledger.installed, 'ledger');
82
+ await verifyCandidateProtocol(candidateRoot, context.pkg, installed);
83
+ return { installable, installed, ledger };
84
+ }
85
+
86
+ export { verifyUpdateCandidate as verifyCandidate };
87
+
88
+ function packageArtifacts(pkg) {
89
+ if (typeof pkg.kitVersion !== 'string' || !pkg.kitVersion) {
90
+ throw new Error('candidate invariant manifest: package kitVersion is required');
91
+ }
92
+ if (!Array.isArray(pkg.files)) {
93
+ throw new Error('candidate invariant manifest: package files must be an array');
94
+ }
95
+ const seen = new Set();
96
+ for (const entry of pkg.files) {
97
+ if (!entry || typeof entry.path !== 'string') {
98
+ throw new Error('candidate invariant manifest: package entry path is required');
99
+ }
100
+ try {
101
+ validateCandidateManifestPath(entry.path);
102
+ } catch {
103
+ throw new Error(`candidate invariant manifest: unsafe path ${entry.path}`);
104
+ }
105
+ if (seen.has(entry.path)) {
106
+ throw new Error(`candidate invariant manifest: duplicate path ${entry.path}`);
107
+ }
108
+ if (!HASH.test(entry.sha256 ?? '')) {
109
+ throw new Error(`candidate invariant manifest: invalid hash ${entry.path}`);
110
+ }
111
+ if (!Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o777) {
112
+ throw new Error(`candidate invariant manifest: invalid mode ${entry.path}`);
113
+ }
114
+ if (!PACKAGE_KINDS.has(entry.kind)) {
115
+ throw new Error(`candidate invariant manifest: invalid kind ${entry.path}`);
116
+ }
117
+ if ((entry.origin ?? KIT_ORIGIN) !== KIT_ORIGIN) {
118
+ throw new Error(`candidate invariant manifest: invalid origin ${entry.path}`);
119
+ }
120
+ if (![CONSUMER_INSTALL_ROLE, 'maintainer'].includes(
121
+ entry.installRole ?? CONSUMER_INSTALL_ROLE,
122
+ )) {
123
+ throw new Error(`candidate invariant manifest: invalid role ${entry.path}`);
124
+ }
125
+ seen.add(entry.path);
126
+ }
127
+ return filesForInstallRole(pkg);
128
+ }
129
+
130
+ async function candidateLedger(candidateRoot, context) {
131
+ const { pkg, nextReadinessManifest } = context;
132
+ let ledger;
133
+ try {
134
+ ledger = JSON.parse(await readFile(join(candidateRoot, CONSUMER_MANIFEST_NAME), 'utf8'));
135
+ } catch {
136
+ throw new Error('candidate invariant schema: Consumer ledger is missing or invalid JSON');
137
+ }
138
+ if (!ledger || ledger.kitVersion !== pkg.kitVersion
139
+ || ledger.installRole !== CONSUMER_INSTALL_ROLE
140
+ || !Array.isArray(ledger.installed)
141
+ || !Number.isInteger(ledger.readinessContractVersion)
142
+ || !isRecord(ledger.readinessDecisions)
143
+ || Object.values(ledger.readinessDecisions).some(
144
+ (value) => !['pending', 'not-applicable'].includes(value),
145
+ )) {
146
+ throw new Error('candidate invariant schema: Consumer ledger identity is invalid');
147
+ }
148
+ const readiness = nextReadinessManifest?.readiness;
149
+ if (readiness && (ledger.readinessContractVersion !== readiness.contractVersion
150
+ || Object.entries(ledger.readinessDecisions).some(([name, value]) => (
151
+ !readiness.capabilities?.[name]
152
+ || (value === 'not-applicable' && !readiness.capabilities[name].allowNotApplicable)
153
+ )))) {
154
+ throw new Error('candidate invariant schema: Consumer ledger readiness references are invalid');
155
+ }
156
+ return ledger;
157
+ }
158
+
159
+ function uniqueEntries(entries, source) {
160
+ const indexed = new Map();
161
+ for (const entry of entries) {
162
+ if (!entry || typeof entry.path !== 'string') {
163
+ throw new Error(`candidate invariant schema: ${source} entry path is required`);
164
+ }
165
+ try {
166
+ validateCandidateManifestPath(entry.path);
167
+ } catch {
168
+ throw new Error(`candidate invariant schema: unsafe ${source} path ${entry.path}`);
169
+ }
170
+ if (indexed.has(entry.path)) {
171
+ throw new Error(`candidate invariant schema: duplicate ${source} path ${entry.path}`);
172
+ }
173
+ indexed.set(entry.path, entry);
174
+ }
175
+ return indexed;
176
+ }
177
+
178
+ function validateInstalledEntry(tracked, desired, origin) {
179
+ if (tracked.origin !== origin
180
+ || (tracked.installRole ?? CONSUMER_INSTALL_ROLE) !== CONSUMER_INSTALL_ROLE
181
+ || !HASH.test(tracked.installedSha256 ?? '')) {
182
+ throw new Error(`candidate invariant ownership: invalid ledger entry ${tracked.path}`);
183
+ }
184
+ for (const key of ['kind', 'ownerSkill', 'surface']) {
185
+ if ((tracked[key] ?? null) !== (desired[key] ?? null)) {
186
+ throw new Error(`candidate invariant ownership: ${key} mismatch ${tracked.path}`);
187
+ }
188
+ }
189
+ if (tracked.origin === KIT_ORIGIN && tracked.installedSha256 !== desired.sha256) {
190
+ throw new Error(`candidate invariant ownership: Kit hash identity mismatch ${tracked.path}`);
191
+ }
192
+ }
193
+
194
+ function verifyLedgerMetadata(ledger, context) {
195
+ const expected = emptyConsumerManifest(
196
+ context.pkg.kitVersion,
197
+ context.priorConsumerManifest,
198
+ context.nextReadinessManifest?.readiness ?? null,
199
+ );
200
+ const { installed: _actualInstalled, ...actualMetadata } = ledger;
201
+ const { installed: _expectedInstalled, ...expectedMetadata } = expected;
202
+ if (!isDeepStrictEqual(actualMetadata, expectedMetadata)) {
203
+ throw new Error(
204
+ 'candidate invariant schema: Consumer ledger metadata differs from trusted prior state',
205
+ );
206
+ }
207
+ }
208
+
209
+ function expectedOrigin(path, priorInstalled, preview) {
210
+ const keptOwned = (preview.collisionResolutions ?? []).some(
211
+ (resolution) => resolution.path === path && resolution.outcome === 'keep-as-owned',
212
+ );
213
+ return keptOwned || (priorInstalled.get(path)?.origin ?? KIT_ORIGIN) === CONSUMER_ORIGIN
214
+ ? CONSUMER_ORIGIN
215
+ : KIT_ORIGIN;
216
+ }
217
+
218
+ function isRecord(value) {
219
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
220
+ }
@@ -0,0 +1,78 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { lstat, readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import {
6
+ candidateInputPaths, declaredCandidateRunbookPaths, validateCandidateManifestPath,
7
+ } from './updateCandidate.mjs';
8
+
9
+ const run = promisify(execFile);
10
+
11
+ export async function verifyCandidateMembership(candidateRoot, context) {
12
+ const manifests = [
13
+ context.priorReadinessManifest, context.nextReadinessManifest,
14
+ ];
15
+ const allowed = new Set(candidateInputPaths({ pkg: context.pkg, manifests }));
16
+ for (const path of await declaredCandidateRunbookPaths({ candidateRoot, manifests })) {
17
+ allowed.add(path);
18
+ }
19
+ for (const path of context.preview?.generated ?? []) allowed.add(path);
20
+ for (const { path } of context.preview?.migrations ?? []) allowed.add(path);
21
+ for (const path of await regularCandidateFiles(candidateRoot)) {
22
+ if (!allowed.has(path)) {
23
+ throw new Error(`candidate invariant artifact: extra ${path}`);
24
+ }
25
+ }
26
+ }
27
+
28
+ export async function verifyChangedSyntax(candidateRoot, preview) {
29
+ const changed = new Set([
30
+ ...(preview?.added ?? []),
31
+ ...(preview?.updated ?? []),
32
+ ...(preview?.generated ?? []),
33
+ ...(preview?.migrations ?? []).map(({ path }) => path),
34
+ ]);
35
+ for (const path of [...changed].sort()) {
36
+ const absolute = join(candidateRoot, path);
37
+ let command;
38
+ if (/\.(?:mjs|cjs|js)$/.test(path)) {
39
+ command = [process.execPath, ['--check', absolute], 'Node'];
40
+ } else if (path.endsWith('.py')) {
41
+ command = [
42
+ 'python3',
43
+ [
44
+ '-c',
45
+ 'import ast,pathlib,sys; ast.parse(pathlib.Path(sys.argv[1]).read_bytes())',
46
+ absolute,
47
+ ],
48
+ 'Python',
49
+ ];
50
+ } else if (path.endsWith('.sh')) {
51
+ command = ['bash', ['-n', absolute], 'shell'];
52
+ } else {
53
+ continue;
54
+ }
55
+ try {
56
+ await run(command[0], command[1]);
57
+ } catch {
58
+ throw new Error(`candidate invariant syntax: ${command[2]} parse failed ${path}`);
59
+ }
60
+ }
61
+ }
62
+
63
+ async function regularCandidateFiles(root, relative = '') {
64
+ const files = [];
65
+ for (const entry of await readdir(join(root, relative), { withFileTypes: true })) {
66
+ const path = relative ? `${relative}/${entry.name}` : entry.name;
67
+ if (entry.isDirectory()) {
68
+ files.push(...await regularCandidateFiles(root, path));
69
+ continue;
70
+ }
71
+ const state = await lstat(join(root, path));
72
+ if (!state.isFile()) {
73
+ throw new Error(`candidate invariant artifact: not a regular file ${path}`);
74
+ }
75
+ files.push(path);
76
+ }
77
+ return files;
78
+ }
@@ -0,0 +1,221 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { CONSUMER_INSTALL_ROLE, CONSUMER_ORIGIN, filesForInstallRole } from './manifest.mjs';
4
+ import { validateCandidateManifestPath } from './updateCandidate.mjs';
5
+
6
+ const READINESS_MANIFEST_PATH = '.claude/skills/skill-manifest.json';
7
+ const SURFACE_ROOT = {
8
+ claude: '.claude/skills/',
9
+ codex: '.agents/skills/',
10
+ };
11
+
12
+ export async function verifyCandidateProtocol(candidateRoot, pkg, installed) {
13
+ if (!filesForInstallRole(pkg).some(({ path }) => path === READINESS_MANIFEST_PATH)) return;
14
+ let manifest;
15
+ try {
16
+ manifest = JSON.parse(await readFile(join(candidateRoot, READINESS_MANIFEST_PATH), 'utf8'));
17
+ } catch {
18
+ throw new Error('candidate invariant schema: readiness manifest is invalid JSON');
19
+ }
20
+ validateReadinessManifest(manifest);
21
+ validateSkillArtifactReferences(manifest, pkg);
22
+ for (const [name, declaration] of Object.entries(manifest.skills)) {
23
+ if (!declaration.publish
24
+ || (declaration.installRole ?? CONSUMER_INSTALL_ROLE) !== CONSUMER_INSTALL_ROLE
25
+ || !declaration.surfaces.includes('claude')
26
+ || !declaration.surfaces.includes('codex')) continue;
27
+ await verifyMirrorGroup(candidateRoot, pkg, installed, name, declaration.class);
28
+ }
29
+ }
30
+
31
+ function validateReadinessManifest(manifest) {
32
+ if (!manifest || manifest.schema_version !== 1
33
+ || !isRecord(manifest.skills)
34
+ || !isRecord(manifest.readiness)
35
+ || !Number.isInteger(manifest.readiness.contractVersion)
36
+ || !isRecord(manifest.readiness.capabilities)) {
37
+ throw new Error('candidate invariant schema: readiness manifest identity is invalid');
38
+ }
39
+ const capabilities = manifest.readiness.capabilities;
40
+ for (const [name, capability] of Object.entries(capabilities)) {
41
+ const evidence = capability?.evidence;
42
+ if (!isRecord(evidence) || typeof evidence.type !== 'string'
43
+ || !Array.isArray(evidence.paths) || !evidence.paths.length) {
44
+ throw new Error(`candidate invariant schema: invalid capability evidence ${name}`);
45
+ }
46
+ for (const path of evidence.paths) {
47
+ try {
48
+ validateCandidateManifestPath(path);
49
+ } catch {
50
+ throw new Error(`candidate invariant schema: unsafe capability path ${name}`);
51
+ }
52
+ }
53
+ }
54
+ for (const [name, skill] of Object.entries(manifest.skills)) {
55
+ const readiness = skill?.readiness ?? {};
56
+ if (!isRecord(skill) || typeof skill.publish !== 'boolean'
57
+ || !['generic', 'vendored', 'adapter'].includes(skill.class)
58
+ || ![CONSUMER_INSTALL_ROLE, 'maintainer'].includes(
59
+ skill.installRole ?? CONSUMER_INSTALL_ROLE,
60
+ )
61
+ || !Array.isArray(skill.surfaces)
62
+ || (skill.publish && !skill.surfaces.length)
63
+ || skill.surfaces.some((surface) => !SURFACE_ROOT[surface])
64
+ || new Set(skill.surfaces).size !== skill.surfaces.length
65
+ || !isRecord(readiness)
66
+ || !Array.isArray(readiness.required ?? [])
67
+ || !isRecord(readiness.optionalBlocks ?? {})
68
+ || [...(readiness.required ?? []), ...Object.values(readiness.optionalBlocks ?? {})]
69
+ .some((capability) => typeof capability !== 'string' || !capability)) {
70
+ throw new Error(`candidate invariant schema: invalid skill declaration ${name}`);
71
+ }
72
+ for (const capability of [
73
+ ...(readiness.required ?? []),
74
+ ...Object.values(readiness.optionalBlocks ?? {}),
75
+ ]) {
76
+ if (!capabilities[capability]) {
77
+ throw new Error(`candidate invariant schema: unknown readiness reference ${name}.${capability}`);
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ function validateSkillArtifactReferences(manifest, pkg) {
84
+ const installable = filesForInstallRole(pkg);
85
+ for (const entry of installable.filter(({ kind }) => kind === 'skill')) {
86
+ const name = entry.ownerSkill ?? skillNameFromPath(entry.path);
87
+ const surface = entry.surface ?? skillSurfaceFromPath(entry.path);
88
+ const declaration = manifest.skills[name];
89
+ if (!declaration?.publish || !declaration.surfaces.includes(surface)) {
90
+ throw new Error(`candidate invariant schema: undeclared skill artifact ${entry.path}`);
91
+ }
92
+ }
93
+ for (const [name, declaration] of Object.entries(manifest.skills)) {
94
+ if (!declaration.publish
95
+ || (declaration.installRole ?? CONSUMER_INSTALL_ROLE) !== CONSUMER_INSTALL_ROLE) continue;
96
+ for (const surface of declaration.surfaces) {
97
+ const path = `${SURFACE_ROOT[surface]}${name}/SKILL.md`;
98
+ if (!installable.some((entry) => entry.path === path)) {
99
+ throw new Error(`candidate invariant schema: missing skill entrypoint ${path}`);
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ async function verifyMirrorGroup(candidateRoot, pkg, installed, name, skillClass) {
106
+ const relativeBySurface = {};
107
+ for (const [surface, root] of Object.entries(SURFACE_ROOT)) {
108
+ const prefix = `${root}${name}/`;
109
+ relativeBySurface[surface] = new Map(filesForInstallRole(pkg)
110
+ .filter(({ path }) => path.startsWith(prefix))
111
+ .map((entry) => [entry.path.slice(prefix.length), entry.path]));
112
+ }
113
+ const claude = relativeBySurface.claude;
114
+ const codex = relativeBySurface.codex;
115
+ const union = new Set([...claude.keys(), ...codex.keys()]);
116
+ for (const relative of union) {
117
+ const sourcePath = claude.get(relative);
118
+ const mirrorPath = codex.get(relative);
119
+ if (!sourcePath || !mirrorPath) {
120
+ throw new Error(`candidate invariant protocol: mirror file-set mismatch ${name}/${relative}`);
121
+ }
122
+ if (installed.get(sourcePath)?.origin === CONSUMER_ORIGIN
123
+ || installed.get(mirrorPath)?.origin === CONSUMER_ORIGIN) continue;
124
+ const source = await readFile(join(candidateRoot, sourcePath));
125
+ const mirror = await readFile(join(candidateRoot, mirrorPath));
126
+ if (relative.endsWith('.md') && ['generic', 'vendored'].includes(skillClass)) {
127
+ const left = normalizedMirrorMarkdown(source.toString('utf8'));
128
+ const right = normalizedMirrorMarkdown(mirror.toString('utf8'));
129
+ if (left.sequence.join('\0') !== right.sequence.join('\0')
130
+ || left.body !== right.body) {
131
+ throw new Error(`candidate invariant protocol: mirror content mismatch ${name}/${relative}`);
132
+ }
133
+ } else if (!source.equals(mirror)) {
134
+ throw new Error(`candidate invariant protocol: mirror content mismatch ${name}/${relative}`);
135
+ }
136
+ }
137
+ }
138
+
139
+ function normalizedMirrorMarkdown(text) {
140
+ const lines = text.split(/\r?\n/);
141
+ let body = lines;
142
+ let frontmatter = [];
143
+ if (lines[0] === '---') {
144
+ const end = lines.indexOf('---', 1);
145
+ if (end < 0) throw new Error('candidate invariant schema: unterminated skill frontmatter');
146
+ frontmatter = normalizedFrontmatter(lines.slice(1, end));
147
+ body = lines.slice(end + 1);
148
+ }
149
+ const kept = [];
150
+ const sequence = [];
151
+ let inTransform = false;
152
+ for (const line of body) {
153
+ const start = /^\s*<!--\s*mirror-xform:start(?:\s+([^>]*?))?\s*-->\s*$/.exec(line);
154
+ if (start) {
155
+ if (inTransform) throw new Error('candidate invariant protocol: nested mirror transform');
156
+ inTransform = true;
157
+ sequence.push((start[1] ?? '').trim());
158
+ continue;
159
+ }
160
+ if (/^\s*<!--\s*mirror-xform:end\s*-->\s*$/.test(line)) {
161
+ if (!inTransform) throw new Error('candidate invariant protocol: unmatched mirror transform');
162
+ inTransform = false;
163
+ continue;
164
+ }
165
+ if (!inTransform) kept.push(line.trimEnd());
166
+ }
167
+ if (inTransform) throw new Error('candidate invariant protocol: unterminated mirror transform');
168
+ return { body: [...frontmatter, ...kept].join('\n').trim(), sequence };
169
+ }
170
+
171
+ function normalizedFrontmatter(lines) {
172
+ const fields = new Map();
173
+ for (let index = 0; index < lines.length; index += 1) {
174
+ const match = /^(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_-]+)):\s*(.*)$/.exec(
175
+ lines[index].trimEnd(),
176
+ );
177
+ if (!match) throw new Error('candidate invariant schema: invalid skill frontmatter');
178
+ const key = match[1] ?? match[2] ?? match[3];
179
+ if (fields.has(key)) throw new Error(`candidate invariant schema: duplicate frontmatter key ${key}`);
180
+ let value = match[4];
181
+ if (/^[>|]-?$/.test(value)) {
182
+ const folded = value.startsWith('>');
183
+ const continuation = [];
184
+ while (index + 1 < lines.length && /^\s+/.test(lines[index + 1])) {
185
+ index += 1;
186
+ continuation.push(lines[index].trim());
187
+ }
188
+ value = continuation.join(folded ? ' ' : '\n');
189
+ } else if (value.startsWith('"') && value.endsWith('"')) {
190
+ try { value = JSON.parse(value); } catch {
191
+ throw new Error(`candidate invariant schema: invalid frontmatter value ${key}`);
192
+ }
193
+ } else if (value.startsWith("'") && value.endsWith("'")) {
194
+ value = value.slice(1, -1).replaceAll("''", "'");
195
+ } else if (/^(?:true|false|null|-?\d+(?:\.\d+)?)$/.test(value)) {
196
+ value = JSON.parse(value);
197
+ }
198
+ fields.set(key, value);
199
+ }
200
+ return [
201
+ '---',
202
+ ...[...fields].filter(([key]) => key !== 'description')
203
+ .sort(([left], [right]) => left.localeCompare(right))
204
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`),
205
+ '---',
206
+ ];
207
+ }
208
+
209
+ function skillNameFromPath(path) {
210
+ return path.split('/')[2];
211
+ }
212
+
213
+ function skillSurfaceFromPath(path) {
214
+ if (path.startsWith(SURFACE_ROOT.claude)) return 'claude';
215
+ if (path.startsWith(SURFACE_ROOT.codex)) return 'codex';
216
+ return null;
217
+ }
218
+
219
+ function isRecord(value) {
220
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
221
+ }
@@ -0,0 +1,152 @@
1
+ import { join } from 'node:path';
2
+ import { sha256File } from './hash.mjs';
3
+ import { CONSUMER_INSTALL_ROLE, CONSUMER_ORIGIN, KIT_ORIGIN } from './manifest.mjs';
4
+ import { validateCandidateManifestPath } from './updateCandidate.mjs';
5
+
6
+ const HASH = /^[a-f0-9]{64}$/;
7
+
8
+ export function verifyDeletionState(installed, pkg, installable, preview) {
9
+ const installablePaths = new Set(installable.map(({ path }) => path));
10
+ const packageEntries = new Map(pkg.files.map((entry) => [entry.path, entry]));
11
+ const deleted = new Set(preview?.deleted ?? []);
12
+ const preserved = new Set(preview?.keptDeleted ?? []);
13
+ for (const [path, tracked] of installed) {
14
+ if (installablePaths.has(path)) continue;
15
+ if (deleted.has(path) && tracked.origin === KIT_ORIGIN) {
16
+ throw new Error(`candidate invariant deletion: stale Kit ledger path ${path}`);
17
+ }
18
+ if (tracked.origin === CONSUMER_ORIGIN) continue;
19
+ if (!preserved.has(path)) {
20
+ throw new Error(`candidate invariant deletion: undeclared legacy Kit path ${path}`);
21
+ }
22
+ const packaged = packageEntries.get(path);
23
+ const expectedRole = packaged?.installRole ?? tracked.installRole ?? CONSUMER_INSTALL_ROLE;
24
+ if ((tracked.installRole ?? CONSUMER_INSTALL_ROLE) !== expectedRole) {
25
+ throw new Error(`candidate invariant deletion: role mismatch ${path}`);
26
+ }
27
+ }
28
+ for (const path of deleted) {
29
+ if (installed.has(path)) {
30
+ throw new Error(`candidate invariant deletion: stale Kit ledger path ${path}`);
31
+ }
32
+ }
33
+ for (const path of preserved) {
34
+ if (!installed.has(path)) {
35
+ throw new Error(`candidate invariant deletion: missing preserved ledger path ${path}`);
36
+ }
37
+ }
38
+ }
39
+
40
+ export function verifyTransactionPreview(preview, installable, installed) {
41
+ if (!preview || typeof preview !== 'object') {
42
+ throw new Error('candidate invariant transaction: preview is required');
43
+ }
44
+ const installablePaths = new Set(installable.map(({ path }) => path));
45
+ const owners = new Map();
46
+ const actionSets = new Map();
47
+ for (const key of ['added', 'updated', 'deleted', 'generated', 'keptDeleted']) {
48
+ if (!Array.isArray(preview[key] ?? [])) {
49
+ throw new Error(`candidate invariant transaction: ${key} must be an array`);
50
+ }
51
+ const local = new Set();
52
+ for (const path of preview[key] ?? []) {
53
+ claimTransactionPath(path, key, local, owners);
54
+ if (['added', 'updated'].includes(key) && !installablePaths.has(path)) {
55
+ throw new Error(`candidate invariant transaction: unmanaged ${key} path ${path}`);
56
+ }
57
+ if (key === 'deleted' && installablePaths.has(path)) {
58
+ throw new Error(`candidate invariant transaction: deletes current package path ${path}`);
59
+ }
60
+ }
61
+ actionSets.set(key, local);
62
+ }
63
+ if (!Array.isArray(preview.migrations ?? [])) {
64
+ throw new Error('candidate invariant transaction: migrations must be an array');
65
+ }
66
+ const migrationPaths = new Set();
67
+ for (const migration of preview.migrations ?? []) {
68
+ claimTransactionPath(migration?.path, 'migrations', migrationPaths, owners);
69
+ }
70
+ verifyCollisionRecords(preview, installablePaths, installed, actionSets, owners);
71
+ }
72
+
73
+ export async function verifyDerivedArtifacts(candidateRoot, installed, preview) {
74
+ for (const path of preview.generated ?? []) {
75
+ const tracked = installed.get(path);
76
+ if (!tracked || tracked.origin !== CONSUMER_ORIGIN
77
+ || !HASH.test(tracked.installedSha256 ?? '')) {
78
+ throw new Error(`candidate invariant transaction: invalid generated ledger ${path}`);
79
+ }
80
+ if (await transactionHash(candidateRoot, path) !== tracked.installedSha256) {
81
+ throw new Error(`candidate invariant transaction: generated hash mismatch ${path}`);
82
+ }
83
+ }
84
+ const migrations = new Set();
85
+ for (const migration of preview.migrations ?? []) {
86
+ const path = migration?.path;
87
+ claimTransactionPath(path, 'migration', migrations);
88
+ if (!(migration.beforeSha256 === null || HASH.test(migration.beforeSha256 ?? ''))
89
+ || !HASH.test(migration.afterSha256 ?? '')
90
+ || await transactionHash(candidateRoot, path) !== migration.afterSha256) {
91
+ throw new Error(`candidate invariant transaction: migration hash mismatch ${path}`);
92
+ }
93
+ }
94
+ }
95
+
96
+ function verifyCollisionRecords(preview, installablePaths, installed, actionSets, owners) {
97
+ if (!Array.isArray(preview.collisions ?? [])
98
+ || !Array.isArray(preview.collisionResolutions ?? [])) {
99
+ throw new Error('candidate invariant transaction: collision records must be arrays');
100
+ }
101
+ const unresolved = new Set();
102
+ for (const path of preview.collisions ?? []) {
103
+ claimTransactionPath(path, 'collisions', unresolved);
104
+ }
105
+ if (unresolved.size) {
106
+ throw new Error('candidate invariant transaction: unresolved collision');
107
+ }
108
+ const resolutions = new Set();
109
+ for (const resolution of preview.collisionResolutions ?? []) {
110
+ const path = resolution?.path;
111
+ claimTransactionPath(path, 'collision resolution', resolutions);
112
+ if (!installablePaths.has(path)
113
+ || !['keep-as-owned', 'replace'].includes(resolution?.outcome)
114
+ || !HASH.test(resolution?.destinationSha256 ?? '')) {
115
+ throw new Error(`candidate invariant transaction: invalid collision resolution ${path}`);
116
+ }
117
+ const expectedOrigin = resolution.outcome === 'keep-as-owned' ? CONSUMER_ORIGIN : KIT_ORIGIN;
118
+ if (installed.get(path)?.origin !== expectedOrigin
119
+ || (resolution.outcome === 'replace' && !actionSets.get('added').has(path))
120
+ || (resolution.outcome === 'keep-as-owned' && owners.has(path))) {
121
+ throw new Error(`candidate invariant transaction: incoherent collision resolution ${path}`);
122
+ }
123
+ }
124
+ }
125
+
126
+ function claimTransactionPath(path, key, local, owners = null) {
127
+ try {
128
+ validateCandidateManifestPath(path);
129
+ } catch {
130
+ throw new Error(`candidate invariant transaction: unsafe ${key} path ${path}`);
131
+ }
132
+ if (local.has(path)) {
133
+ throw new Error(`candidate invariant transaction: duplicate ${key} path ${path}`);
134
+ }
135
+ local.add(path);
136
+ const owner = owners?.get(path);
137
+ if (owner && owner !== key) {
138
+ throw new Error(`candidate invariant transaction: overlapping action ${path}`);
139
+ }
140
+ owners?.set(path, key);
141
+ }
142
+
143
+ async function transactionHash(candidateRoot, path) {
144
+ try {
145
+ return await sha256File(join(candidateRoot, path));
146
+ } catch (error) {
147
+ if (error.code === 'ENOENT') {
148
+ throw new Error(`candidate invariant transaction: missing derived artifact ${path}`);
149
+ }
150
+ throw error;
151
+ }
152
+ }