@ikon85/agent-workflow-kit 0.34.5 → 0.35.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.
- package/.agents/skills/ask-matt/SKILL.md +8 -0
- package/.agents/skills/audit-skills/SKILL.md +8 -0
- package/.agents/skills/board-to-waves/SKILL.md +8 -0
- package/.agents/skills/census-update/SKILL.md +8 -0
- package/.agents/skills/code-review/SKILL.md +8 -0
- package/.agents/skills/codebase-design/SKILL.md +8 -0
- package/.agents/skills/codex-adapter-sync/SKILL.md +8 -0
- package/.agents/skills/decision-gate/SKILL.md +8 -0
- package/.agents/skills/diagnose/SKILL.md +8 -0
- package/.agents/skills/domain-modeling/SKILL.md +8 -0
- package/.agents/skills/git-worktree-recover/SKILL.md +8 -0
- package/.agents/skills/grill-me/SKILL.md +8 -0
- package/.agents/skills/grill-with-docs/SKILL.md +8 -0
- package/.agents/skills/implement/SKILL.md +8 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +8 -0
- package/.agents/skills/kit-release/SKILL.md +37 -21
- package/.agents/skills/kit-update/SKILL.md +18 -4
- package/.agents/skills/local-ci/SKILL.md +8 -0
- package/.agents/skills/memory-lifecycle/SKILL.md +8 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +2 -2
- package/.agents/skills/project-release/SKILL.md +8 -0
- package/.agents/skills/prototype/SKILL.md +8 -0
- package/.agents/skills/research/SKILL.md +8 -0
- package/.agents/skills/resolving-merge-conflicts/SKILL.md +8 -0
- package/.agents/skills/retro/SKILL.md +8 -0
- package/.agents/skills/scale-check/SKILL.md +8 -0
- package/.agents/skills/security-audit/SKILL.md +8 -0
- package/.agents/skills/setup-workflow/SKILL.md +30 -0
- package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
- package/.agents/skills/setup-workflow/spec-self-critique-seed.md +1 -0
- package/.agents/skills/spec-self-critique/SKILL.md +8 -0
- package/.agents/skills/tdd/SKILL.md +8 -0
- package/.agents/skills/to-issues/SKILL.md +8 -0
- package/.agents/skills/to-prd/SKILL.md +8 -0
- package/.agents/skills/to-waves/SKILL.md +8 -0
- package/.agents/skills/triage/SKILL.md +8 -0
- package/.agents/skills/verify-spike/SKILL.md +8 -0
- package/.agents/skills/wayfinder/SKILL.md +8 -0
- package/.agents/skills/wrapup/SKILL.md +8 -0
- package/.claude/skills/ask-matt/SKILL.md +8 -0
- package/.claude/skills/audit-skills/SKILL.md +8 -0
- package/.claude/skills/board-to-waves/SKILL.md +8 -0
- package/.claude/skills/census-update/SKILL.md +8 -0
- package/.claude/skills/code-review/SKILL.md +8 -0
- package/.claude/skills/codebase-design/SKILL.md +8 -0
- package/.claude/skills/codex-build/SKILL.md +8 -0
- package/.claude/skills/codex-review/SKILL.md +8 -0
- package/.claude/skills/decision-gate/SKILL.md +8 -0
- package/.claude/skills/diagnose/SKILL.md +8 -0
- package/.claude/skills/domain-modeling/SKILL.md +8 -0
- package/.claude/skills/git-guardrails-claude-code/SKILL.md +8 -0
- package/.claude/skills/git-worktree-recover/SKILL.md +8 -0
- package/.claude/skills/grill-me/SKILL.md +8 -0
- package/.claude/skills/grill-me-codex/SKILL.md +8 -0
- package/.claude/skills/grill-with-docs/SKILL.md +8 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +8 -0
- package/.claude/skills/implement/SKILL.md +8 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +8 -0
- package/.claude/skills/kit-release/SKILL.md +37 -21
- package/.claude/skills/kit-update/SKILL.md +18 -4
- package/.claude/skills/local-ci/SKILL.md +8 -0
- package/.claude/skills/memory-lifecycle/SKILL.md +8 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +2 -2
- package/.claude/skills/project-release/SKILL.md +8 -0
- package/.claude/skills/prototype/SKILL.md +8 -0
- package/.claude/skills/research/SKILL.md +8 -0
- package/.claude/skills/resolving-merge-conflicts/SKILL.md +8 -0
- package/.claude/skills/retro/SKILL.md +8 -0
- package/.claude/skills/scale-check/SKILL.md +8 -0
- package/.claude/skills/security-audit/SKILL.md +8 -0
- package/.claude/skills/setup-pre-commit/SKILL.md +8 -0
- package/.claude/skills/setup-workflow/SKILL.md +30 -0
- package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
- package/.claude/skills/setup-workflow/spec-self-critique-seed.md +1 -0
- package/.claude/skills/spec-self-critique/SKILL.md +8 -0
- package/.claude/skills/tdd/SKILL.md +8 -0
- package/.claude/skills/to-issues/SKILL.md +8 -0
- package/.claude/skills/to-prd/SKILL.md +8 -0
- package/.claude/skills/to-waves/SKILL.md +8 -0
- package/.claude/skills/triage/SKILL.md +8 -0
- package/.claude/skills/verify-spike/SKILL.md +8 -0
- package/.claude/skills/wayfinder/SKILL.md +8 -0
- package/.claude/skills/wrapup/SKILL.md +8 -0
- package/.claude/skills/write-a-skill/SKILL.md +8 -0
- package/README.md +123 -4
- package/agent-workflow-kit.package.json +127 -87
- package/docs/adr/0004-release-intent-is-a-version-tag.md +18 -5
- package/docs/agents/skills/local-ci.md +1 -0
- package/docs/agents/skills/orchestrate-wave.md +1 -1
- package/docs/agents/skills/spec-self-critique.md +1 -1
- package/package.json +1 -1
- package/scripts/kit-release.test.mjs +10 -4
- package/scripts/project-skill-extension.mjs +37 -0
- package/scripts/readiness.mjs +2 -1
- package/scripts/test_release_authorization_contract.py +101 -0
- package/scripts/test_skill_publish_audit.py +9 -1
- package/src/cli.mjs +29 -12
- package/src/commands/init.mjs +24 -0
- package/src/commands/own.mjs +4 -3
- package/src/commands/update.mjs +86 -21
- package/src/lib/bundle.mjs +5 -0
- package/src/lib/manifest.mjs +13 -2
- package/src/lib/ownershipClassifier.mjs +54 -0
- package/src/lib/projectSkillExtension.mjs +60 -0
- package/src/lib/skillRegistry.mjs +167 -0
- package/src/lib/updateCandidate.mjs +105 -4
- package/src/lib/updateDecisions.mjs +5 -0
- package/src/lib/updateReconcile.mjs +64 -8
- package/src/lib/verifyUpdateCandidate.mjs +13 -1
- package/src/lib/verifyUpdateCandidateProtocol.mjs +2 -0
- package/src/lib/verifyUpdateCandidateTransaction.mjs +9 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const OwnershipState = Object.freeze({
|
|
2
|
+
CLEAN_CORE: 'clean-core',
|
|
3
|
+
PROJECT_EXTENSION: 'project-extension',
|
|
4
|
+
CONTRIBUTION_BRIDGE: 'contribution-bridge',
|
|
5
|
+
EXPLICIT_FORK: 'explicit-fork',
|
|
6
|
+
AMBIGUOUS_COLLISION: 'ambiguous-collision',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const ROUTES = Object.freeze([
|
|
10
|
+
{ id: 'project-extension', action: 'move Project data to docs/agents/skills/<skill>.md' },
|
|
11
|
+
{ id: 'contribution-bridge', action: 'register a temporary contribution-bridge' },
|
|
12
|
+
{ id: 'explicit-fork', action: 'register an explicit-fork with its own update line' },
|
|
13
|
+
{ id: 'clean-core', action: 'explicitly replace the destination with Kit Core' },
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function classifyOwnershipEvidence({
|
|
17
|
+
path, packageEntry, installedEntry, destinationPresent, projectExtension,
|
|
18
|
+
}) {
|
|
19
|
+
const evidence = {
|
|
20
|
+
packageDeclared: Boolean(packageEntry),
|
|
21
|
+
ledgerOrigin: installedEntry?.origin ?? 'absent',
|
|
22
|
+
destination: destinationPresent ? 'present' : 'absent',
|
|
23
|
+
projectExtension: projectExtension
|
|
24
|
+
? (projectExtension.invalid ? 'invalid' : `schema-v${projectExtension.schemaVersion}`)
|
|
25
|
+
: 'absent',
|
|
26
|
+
};
|
|
27
|
+
if (projectExtension?.invalid) evidence.extensionDiagnostic = projectExtension.invalid;
|
|
28
|
+
if (installedEntry?.origin === 'kit') {
|
|
29
|
+
return { path, state: OwnershipState.CLEAN_CORE, evidence, routes: [] };
|
|
30
|
+
}
|
|
31
|
+
if (installedEntry?.origin === 'consumer') {
|
|
32
|
+
const state = installedEntry.ownershipState ?? OwnershipState.EXPLICIT_FORK;
|
|
33
|
+
if (![
|
|
34
|
+
OwnershipState.PROJECT_EXTENSION,
|
|
35
|
+
OwnershipState.CONTRIBUTION_BRIDGE,
|
|
36
|
+
OwnershipState.EXPLICIT_FORK,
|
|
37
|
+
].includes(state)) {
|
|
38
|
+
return { path, state: OwnershipState.AMBIGUOUS_COLLISION, evidence, routes: [...ROUTES] };
|
|
39
|
+
}
|
|
40
|
+
if (state === OwnershipState.PROJECT_EXTENSION
|
|
41
|
+
&& (!/^docs\/agents\/skills\/[a-z0-9-]+\.md$/.test(path)
|
|
42
|
+
|| !projectExtension || projectExtension.invalid)) {
|
|
43
|
+
return { path, state: OwnershipState.AMBIGUOUS_COLLISION, evidence, routes: [...ROUTES] };
|
|
44
|
+
}
|
|
45
|
+
return { path, state, evidence, routes: [] };
|
|
46
|
+
}
|
|
47
|
+
if (projectExtension && !projectExtension.invalid) {
|
|
48
|
+
return { path, state: OwnershipState.PROJECT_EXTENSION, evidence, routes: [
|
|
49
|
+
{ id: 'project-extension', action: 'keep the declared Project extension' },
|
|
50
|
+
{ id: 'clean-core', action: 'explicitly replace the destination with Kit Core' },
|
|
51
|
+
] };
|
|
52
|
+
}
|
|
53
|
+
return { path, state: OwnershipState.AMBIGUOUS_COLLISION, evidence, routes: [...ROUTES] };
|
|
54
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { firstLineState } from './sentinel.mjs';
|
|
4
|
+
|
|
5
|
+
const SKILL_NAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
6
|
+
const MARKER_PREFIX = '<!-- agent-workflow-kit: project-extension/';
|
|
7
|
+
const MARKER = /^<!-- agent-workflow-kit: project-extension\/([^;]+); skill=([a-z0-9-]+) -->$/;
|
|
8
|
+
|
|
9
|
+
export function projectSkillExtensionPath(skill) {
|
|
10
|
+
if (typeof skill !== 'string' || !SKILL_NAME.test(skill)) {
|
|
11
|
+
throw new Error(`invalid Project extension skill identity: ${skill}`);
|
|
12
|
+
}
|
|
13
|
+
return `docs/agents/skills/${skill}.md`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function inspectProjectSkillExtension({ root, skill }) {
|
|
17
|
+
const path = projectSkillExtensionPath(skill);
|
|
18
|
+
const absolute = join(root, path);
|
|
19
|
+
let state;
|
|
20
|
+
try {
|
|
21
|
+
state = await lstat(absolute);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT') return { state: 'inactive', reason: 'absent' };
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
if (!state.isFile()) {
|
|
27
|
+
throw new Error(`Project extension is not a regular file: ${path}`);
|
|
28
|
+
}
|
|
29
|
+
const body = await readFile(absolute, 'utf8');
|
|
30
|
+
const setupState = firstLineState(body);
|
|
31
|
+
if (setupState === 'stub' || setupState === 'not-applicable') {
|
|
32
|
+
return { state: 'inactive', reason: setupState };
|
|
33
|
+
}
|
|
34
|
+
const lines = body.split('\n').map((line) => line.trim());
|
|
35
|
+
const markerLine = lines.find((line) => line.startsWith(MARKER_PREFIX));
|
|
36
|
+
if (!markerLine) {
|
|
37
|
+
if (!body.trim()) return { state: 'inactive', reason: 'empty' };
|
|
38
|
+
return { state: 'active', schemaVersion: 0, path };
|
|
39
|
+
}
|
|
40
|
+
const match = MARKER.exec(markerLine);
|
|
41
|
+
if (!match || match[1] !== 'v1') {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Project extension has unsupported schema at ${path}; ` +
|
|
44
|
+
`expected project-extension/v1 for skill=${skill}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (match[2] !== skill) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Project extension identity mismatch at ${path}; expected skill=${skill}, ` +
|
|
50
|
+
`found skill=${match[2]}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const content = lines.filter(
|
|
54
|
+
(line) => line && line !== markerLine && !line.startsWith('<!-- setup-workflow:'),
|
|
55
|
+
);
|
|
56
|
+
if (!content.length) {
|
|
57
|
+
throw new Error(`Project extension has no instructions after its v1 marker: ${path}`);
|
|
58
|
+
}
|
|
59
|
+
return { state: 'active', schemaVersion: 1, path };
|
|
60
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
|
|
5
|
+
export const PROJECT_SKILL_REGISTRY_PATH = 'docs/agents/skill-registry.json';
|
|
6
|
+
export const PROJECT_SKILL_REGISTRY_SCHEMA_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
const CORE_ANNOTATION_FIELDS = new Set(['note']);
|
|
9
|
+
const SURFACES = new Set(['claude', 'codex']);
|
|
10
|
+
const SKILL_CLASSES = new Set(['generic', 'vendored', 'adapter', 'project-private']);
|
|
11
|
+
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function compatibleSubset(prior, current) {
|
|
17
|
+
if (isRecord(prior)) {
|
|
18
|
+
return isRecord(current) && Object.entries(prior).every(
|
|
19
|
+
([key, value]) => key in current && compatibleSubset(value, current[key]),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return isDeepStrictEqual(prior, current);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function corePart(declaration) {
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
Object.entries(declaration).filter(([key]) => !CORE_ANNOTATION_FIELDS.has(key)),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function annotations(declaration) {
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
Object.entries(declaration).filter(([key]) => CORE_ANNOTATION_FIELDS.has(key)),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateCore(core, label = 'Kit Core skill registry') {
|
|
38
|
+
if (!isRecord(core) || core.schema_version !== 1
|
|
39
|
+
|| !isRecord(core.readiness) || !isRecord(core.readiness.capabilities)
|
|
40
|
+
|| !isRecord(core.skills)) {
|
|
41
|
+
throw new Error(`${label} has an unsupported schema`);
|
|
42
|
+
}
|
|
43
|
+
for (const [name, declaration] of Object.entries(core.skills)) {
|
|
44
|
+
for (const capability of [
|
|
45
|
+
...(declaration?.readiness?.required ?? []),
|
|
46
|
+
...Object.values(declaration?.readiness?.optionalBlocks ?? {}),
|
|
47
|
+
]) {
|
|
48
|
+
if (!core.readiness.capabilities[capability]) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`candidate invariant schema: unknown readiness reference ${name}.${capability}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function emptyProjectSkillRegistry(core) {
|
|
58
|
+
validateCore(core);
|
|
59
|
+
return {
|
|
60
|
+
schemaVersion: PROJECT_SKILL_REGISTRY_SCHEMA_VERSION,
|
|
61
|
+
coreSchemaVersion: core.schema_version,
|
|
62
|
+
skills: {},
|
|
63
|
+
annotations: {},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateProjectSkill(name, declaration, capabilities) {
|
|
68
|
+
if (!isRecord(declaration)
|
|
69
|
+
|| !SKILL_CLASSES.has(declaration.class)
|
|
70
|
+
|| typeof declaration.publish !== 'boolean'
|
|
71
|
+
|| !Array.isArray(declaration.surfaces)
|
|
72
|
+
|| declaration.surfaces.some((surface) => !SURFACES.has(surface))
|
|
73
|
+
|| new Set(declaration.surfaces).size !== declaration.surfaces.length) {
|
|
74
|
+
throw new Error(`Project skill registry has an invalid local skill: ${name}`);
|
|
75
|
+
}
|
|
76
|
+
const readiness = declaration.readiness ?? {};
|
|
77
|
+
if (!isRecord(readiness)
|
|
78
|
+
|| !Array.isArray(readiness.required ?? [])
|
|
79
|
+
|| !isRecord(readiness.optionalBlocks ?? {})) {
|
|
80
|
+
throw new Error(`Project skill registry has invalid readiness: ${name}`);
|
|
81
|
+
}
|
|
82
|
+
for (const capability of [
|
|
83
|
+
...(readiness.required ?? []),
|
|
84
|
+
...Object.values(readiness.optionalBlocks ?? {}),
|
|
85
|
+
]) {
|
|
86
|
+
if (!capabilities[capability]) {
|
|
87
|
+
throw new Error(`Project skill registry references unknown capability: ${name}.${capability}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function validateProjectSkillRegistry(core, registry) {
|
|
93
|
+
validateCore(core);
|
|
94
|
+
if (!isRecord(registry)
|
|
95
|
+
|| registry.schemaVersion !== PROJECT_SKILL_REGISTRY_SCHEMA_VERSION
|
|
96
|
+
|| registry.coreSchemaVersion !== core.schema_version
|
|
97
|
+
|| !isRecord(registry.skills)
|
|
98
|
+
|| !isRecord(registry.annotations)) {
|
|
99
|
+
throw new Error('Project skill registry has an unsupported schema');
|
|
100
|
+
}
|
|
101
|
+
for (const [name, declaration] of Object.entries(registry.skills)) {
|
|
102
|
+
if (core.skills[name]) {
|
|
103
|
+
throw new Error(`Project skill registry collides with Kit Core: ${name}`);
|
|
104
|
+
}
|
|
105
|
+
validateProjectSkill(name, declaration, core.readiness.capabilities ?? {});
|
|
106
|
+
}
|
|
107
|
+
for (const [name, annotation] of Object.entries(registry.annotations)) {
|
|
108
|
+
if (!core.skills[name] || !isRecord(annotation)
|
|
109
|
+
|| Object.keys(annotation).some((key) => !CORE_ANNOTATION_FIELDS.has(key))
|
|
110
|
+
|| Object.values(annotation).some((value) => typeof value !== 'string' || !value.trim())) {
|
|
111
|
+
throw new Error(`Project skill registry has an invalid Core annotation: ${name}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return registry;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function composeSkillRegistry(core, registry) {
|
|
118
|
+
validateProjectSkillRegistry(core, registry);
|
|
119
|
+
const skills = Object.fromEntries(Object.entries(core.skills).map(([name, declaration]) => [
|
|
120
|
+
name, { ...declaration, ...(registry.annotations[name] ?? {}) },
|
|
121
|
+
]));
|
|
122
|
+
for (const [name, declaration] of Object.entries(registry.skills)) {
|
|
123
|
+
skills[name] = declaration;
|
|
124
|
+
}
|
|
125
|
+
return { ...core, skills };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function migrateLegacySkillRegistry({ legacyCore, nextCore }) {
|
|
129
|
+
validateCore(legacyCore, 'Legacy mixed skill registry');
|
|
130
|
+
validateCore(nextCore);
|
|
131
|
+
if (!compatibleSubset(legacyCore.readiness, nextCore.readiness)) {
|
|
132
|
+
throw new Error('Legacy mixed skill registry has ambiguous Core readiness changes');
|
|
133
|
+
}
|
|
134
|
+
const skills = {};
|
|
135
|
+
const coreAnnotations = {};
|
|
136
|
+
for (const [name, declaration] of Object.entries(legacyCore.skills)) {
|
|
137
|
+
if (!nextCore.skills[name]) {
|
|
138
|
+
skills[name] = declaration;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!compatibleSubset(corePart(declaration), nextCore.skills[name])) {
|
|
142
|
+
throw new Error(`Legacy mixed skill registry has ambiguous Kit Core changes: ${name}`);
|
|
143
|
+
}
|
|
144
|
+
const local = annotations(declaration);
|
|
145
|
+
if (Object.keys(local).length) coreAnnotations[name] = local;
|
|
146
|
+
}
|
|
147
|
+
const registry = {
|
|
148
|
+
schemaVersion: PROJECT_SKILL_REGISTRY_SCHEMA_VERSION,
|
|
149
|
+
coreSchemaVersion: nextCore.schema_version,
|
|
150
|
+
skills,
|
|
151
|
+
annotations: coreAnnotations,
|
|
152
|
+
};
|
|
153
|
+
validateProjectSkillRegistry(nextCore, registry);
|
|
154
|
+
return registry;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function readComposedSkillRegistry(root, core) {
|
|
158
|
+
core ??= JSON.parse(await readFile(join(root, '.claude/skills/skill-manifest.json'), 'utf8'));
|
|
159
|
+
let registry;
|
|
160
|
+
try {
|
|
161
|
+
registry = JSON.parse(await readFile(join(root, PROJECT_SKILL_REGISTRY_PATH), 'utf8'));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error.code === 'ENOENT') return core;
|
|
164
|
+
throw new Error(`Project skill registry is invalid: ${error.message}`, { cause: error });
|
|
165
|
+
}
|
|
166
|
+
return composeSkillRegistry(core, registry);
|
|
167
|
+
}
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
CONSUMER_MANIFEST_NAME, CONSUMER_ORIGIN, READINESS_MANIFEST_PATH,
|
|
16
16
|
filesForInstallRole, indexByPath, readManifest, writeManifest,
|
|
17
17
|
} from './manifest.mjs';
|
|
18
|
+
import {
|
|
19
|
+
PROJECT_SKILL_REGISTRY_PATH, emptyProjectSkillRegistry, migrateLegacySkillRegistry,
|
|
20
|
+
readComposedSkillRegistry,
|
|
21
|
+
} from './skillRegistry.mjs';
|
|
18
22
|
import { checkSkill, evaluateCapability, inspectProdSections } from '../../scripts/readiness.mjs';
|
|
19
23
|
|
|
20
24
|
const exists = (path) => access(path).then(() => true, () => false);
|
|
@@ -56,6 +60,7 @@ export async function materializeUpdateCandidate({
|
|
|
56
60
|
export function candidateInputPaths({ pkg, manifests }) {
|
|
57
61
|
const candidates = [
|
|
58
62
|
CONSUMER_MANIFEST_NAME,
|
|
63
|
+
PROJECT_SKILL_REGISTRY_PATH,
|
|
59
64
|
...INTEGRATION_INPUTS,
|
|
60
65
|
...filesForInstallRole(pkg).map(({ path }) => path),
|
|
61
66
|
];
|
|
@@ -349,12 +354,18 @@ async function assertConsumerStillMatchesPreview(consumerRoot, preview) {
|
|
|
349
354
|
}
|
|
350
355
|
|
|
351
356
|
/** Seed only newly declared, decision-free project-layer stubs in a staged candidate. */
|
|
352
|
-
export async function adoptReadinessCandidate({
|
|
357
|
+
export async function adoptReadinessCandidate({
|
|
358
|
+
candidateRoot, consumerRoot, kitRoot, priorManifest, nextManifest,
|
|
359
|
+
}) {
|
|
353
360
|
const priorPaths = readinessStubPaths(priorManifest);
|
|
354
361
|
const manifestPath = join(candidateRoot, CONSUMER_MANIFEST_NAME);
|
|
355
|
-
|
|
362
|
+
let manifest = await readManifest(manifestPath);
|
|
363
|
+
const registry = await adoptProjectSkillRegistry({
|
|
364
|
+
candidateRoot, consumerRoot, kitRoot, manifest, priorManifest, nextManifest,
|
|
365
|
+
});
|
|
366
|
+
manifest = await readManifest(manifestPath);
|
|
356
367
|
const candidateInstalled = indexByPath(manifest, 'installed');
|
|
357
|
-
const generated = [];
|
|
368
|
+
const generated = [...registry.generated];
|
|
358
369
|
for (const path of readinessStubPaths(nextManifest)) {
|
|
359
370
|
if (priorPaths.has(path)) continue;
|
|
360
371
|
if (await exists(join(candidateRoot, path))) {
|
|
@@ -377,9 +388,11 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
|
|
|
377
388
|
}
|
|
378
389
|
await writeManifest(manifestPath, { ...manifest, installed });
|
|
379
390
|
}
|
|
380
|
-
const
|
|
391
|
+
const prod = await migrateProdSections({
|
|
381
392
|
candidateRoot, consumerRoot, nextManifest,
|
|
382
393
|
});
|
|
394
|
+
const migrations = [...registry.migrations, ...prod.migrations];
|
|
395
|
+
const migrationConflicts = [...registry.migrationConflicts, ...prod.migrationConflicts];
|
|
383
396
|
const before = await readinessSnapshot(consumerRoot, priorManifest);
|
|
384
397
|
const after = await readinessSnapshot(candidateRoot, nextManifest);
|
|
385
398
|
const incompatible = Object.entries(after.skills)
|
|
@@ -397,6 +410,94 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
|
|
|
397
410
|
};
|
|
398
411
|
}
|
|
399
412
|
|
|
413
|
+
async function adoptProjectSkillRegistry({
|
|
414
|
+
candidateRoot, consumerRoot, kitRoot, manifest, priorManifest, nextManifest,
|
|
415
|
+
}) {
|
|
416
|
+
if (!nextManifest) {
|
|
417
|
+
return { generated: [], migrations: [], migrationConflicts: [] };
|
|
418
|
+
}
|
|
419
|
+
const tracked = manifest.installed.find(({ path }) => path === READINESS_MANIFEST_PATH);
|
|
420
|
+
if (tracked?.origin !== CONSUMER_ORIGIN) {
|
|
421
|
+
const registryPath = join(candidateRoot, PROJECT_SKILL_REGISTRY_PATH);
|
|
422
|
+
if (!await exists(registryPath)) {
|
|
423
|
+
await writeAtomic(
|
|
424
|
+
registryPath,
|
|
425
|
+
`${JSON.stringify(emptyProjectSkillRegistry(nextManifest), null, 2)}\n`,
|
|
426
|
+
);
|
|
427
|
+
const installedSha256 = await sha256File(registryPath);
|
|
428
|
+
await writeManifest(join(candidateRoot, CONSUMER_MANIFEST_NAME), {
|
|
429
|
+
...manifest,
|
|
430
|
+
installed: [...manifest.installed, {
|
|
431
|
+
path: PROJECT_SKILL_REGISTRY_PATH,
|
|
432
|
+
kind: 'doc',
|
|
433
|
+
installedSha256,
|
|
434
|
+
origin: CONSUMER_ORIGIN,
|
|
435
|
+
installRole: 'consumer',
|
|
436
|
+
}],
|
|
437
|
+
});
|
|
438
|
+
await readComposedSkillRegistry(candidateRoot, nextManifest);
|
|
439
|
+
return {
|
|
440
|
+
generated: [PROJECT_SKILL_REGISTRY_PATH],
|
|
441
|
+
migrations: [],
|
|
442
|
+
migrationConflicts: [],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
await readComposedSkillRegistry(candidateRoot, nextManifest);
|
|
446
|
+
return { generated: [], migrations: [], migrationConflicts: [] };
|
|
447
|
+
}
|
|
448
|
+
const registryPath = join(candidateRoot, PROJECT_SKILL_REGISTRY_PATH);
|
|
449
|
+
if (await exists(registryPath)) {
|
|
450
|
+
return {
|
|
451
|
+
generated: [],
|
|
452
|
+
migrations: [],
|
|
453
|
+
migrationConflicts: [
|
|
454
|
+
`${PROJECT_SKILL_REGISTRY_PATH}: legacy mixed Core already has a Project registry`,
|
|
455
|
+
],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
let registry;
|
|
459
|
+
try {
|
|
460
|
+
registry = migrateLegacySkillRegistry({ legacyCore: priorManifest, nextCore: nextManifest });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return {
|
|
463
|
+
generated: [], migrations: [],
|
|
464
|
+
migrationConflicts: [`${READINESS_MANIFEST_PATH}: ${error.message}`],
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const corePath = join(candidateRoot, READINESS_MANIFEST_PATH);
|
|
468
|
+
await writeAtomic(corePath, await readFile(join(kitRoot, READINESS_MANIFEST_PATH)));
|
|
469
|
+
await writeAtomic(registryPath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
470
|
+
const coreHash = await sha256File(corePath);
|
|
471
|
+
const registryHash = await sha256File(registryPath);
|
|
472
|
+
const installed = manifest.installed.map((entry) => {
|
|
473
|
+
if (entry.path !== READINESS_MANIFEST_PATH) return entry;
|
|
474
|
+
const { ownershipState: _legacyLifecycle, ...coreEntry } = entry;
|
|
475
|
+
return { ...coreEntry, installedSha256: coreHash, origin: 'kit' };
|
|
476
|
+
});
|
|
477
|
+
if (!installed.some(({ path }) => path === PROJECT_SKILL_REGISTRY_PATH)) {
|
|
478
|
+
installed.push({
|
|
479
|
+
path: PROJECT_SKILL_REGISTRY_PATH,
|
|
480
|
+
kind: 'doc',
|
|
481
|
+
installedSha256: registryHash,
|
|
482
|
+
origin: CONSUMER_ORIGIN,
|
|
483
|
+
installRole: 'consumer',
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
await writeManifest(join(candidateRoot, CONSUMER_MANIFEST_NAME), {
|
|
487
|
+
...manifest, installed,
|
|
488
|
+
});
|
|
489
|
+
return {
|
|
490
|
+
generated: [PROJECT_SKILL_REGISTRY_PATH],
|
|
491
|
+
migrations: [{
|
|
492
|
+
path: READINESS_MANIFEST_PATH,
|
|
493
|
+
beforeSha256: await sha256File(join(consumerRoot, READINESS_MANIFEST_PATH)),
|
|
494
|
+
afterSha256: coreHash,
|
|
495
|
+
ownership: 'kit-core',
|
|
496
|
+
}],
|
|
497
|
+
migrationConflicts: [],
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
400
501
|
async function migrateProdSections({ candidateRoot, consumerRoot, nextManifest }) {
|
|
401
502
|
const paths = [...new Set(Object.values(nextManifest?.readiness?.capabilities ?? {})
|
|
402
503
|
.flatMap(({ evidence }) => evidence?.type === 'prod-section' ? evidence.paths ?? [] : []))];
|
|
@@ -4,6 +4,10 @@ import { sha256File } from './hash.mjs';
|
|
|
4
4
|
import { lineDiff, writeAtomic } from './atomicWrite.mjs';
|
|
5
5
|
import { hookReferenced } from './settings.mjs';
|
|
6
6
|
import { validateConsumerFile } from './consumerPath.mjs';
|
|
7
|
+
import { inspectProjectSkillExtension } from './projectSkillExtension.mjs';
|
|
8
|
+
import {
|
|
9
|
+
OwnershipState, classifyOwnershipEvidence,
|
|
10
|
+
} from './ownershipClassifier.mjs';
|
|
7
11
|
import {
|
|
8
12
|
CONSUMER_MANIFEST_NAME, PACKAGE_MANIFEST_NAME, emptyConsumerManifest,
|
|
9
13
|
CONSUMER_INSTALL_ROLE, CONSUMER_ORIGIN, KIT_ORIGIN, filesForInstallRole,
|
|
@@ -32,6 +36,22 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
|
|
|
32
36
|
const dest = join(consumerRoot, file.path);
|
|
33
37
|
const prior = installedIdx.get(file.path);
|
|
34
38
|
if (prior?.origin === CONSUMER_ORIGIN) {
|
|
39
|
+
const classification = classifyOwnershipEvidence({
|
|
40
|
+
path: file.path,
|
|
41
|
+
packageEntry: file,
|
|
42
|
+
installedEntry: prior,
|
|
43
|
+
destinationPresent: await exists(dest),
|
|
44
|
+
projectExtension: prior.ownershipState === OwnershipState.PROJECT_EXTENSION
|
|
45
|
+
? await projectExtensionEvidence(consumerRoot, file.path) : null,
|
|
46
|
+
});
|
|
47
|
+
result.ownershipStates.push(classification);
|
|
48
|
+
if (classification.state === OwnershipState.AMBIGUOUS_COLLISION) {
|
|
49
|
+
result.conflicts.push({
|
|
50
|
+
path: file.path,
|
|
51
|
+
kind: 'ownership-lifecycle',
|
|
52
|
+
diff: 'Consumer lifecycle metadata does not match its declared path/schema.',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
35
55
|
nextInstalled.push(withInstallRole(prior));
|
|
36
56
|
result.consumerOwned.push(file.path);
|
|
37
57
|
continue;
|
|
@@ -39,6 +59,13 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
|
|
|
39
59
|
const present = await exists(dest);
|
|
40
60
|
if (!prior && present) {
|
|
41
61
|
await validateConsumerFile(consumerRoot, file.path);
|
|
62
|
+
const classification = classifyOwnershipEvidence({
|
|
63
|
+
path: file.path,
|
|
64
|
+
packageEntry: file,
|
|
65
|
+
destinationPresent: true,
|
|
66
|
+
projectExtension: await projectExtensionEvidence(consumerRoot, file.path),
|
|
67
|
+
});
|
|
68
|
+
result.ownershipStates.push(classification);
|
|
42
69
|
const decision = await decide('collision', file.path);
|
|
43
70
|
if (decision === false || decision === null || decision === undefined) {
|
|
44
71
|
if (dryRun) {
|
|
@@ -47,15 +74,30 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
|
|
|
47
74
|
}
|
|
48
75
|
throw new Error(`collision decision required for ${file.path}`);
|
|
49
76
|
}
|
|
50
|
-
|
|
51
|
-
|
|
77
|
+
const allowed = [
|
|
78
|
+
'keep-as-owned', 'project-extension', 'contribution-bridge', 'explicit-fork', 'replace',
|
|
79
|
+
];
|
|
80
|
+
if (!allowed.includes(decision)
|
|
81
|
+
|| (decision === 'project-extension'
|
|
82
|
+
&& classification.state !== OwnershipState.PROJECT_EXTENSION)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`collision decision for ${file.path} must select a valid explicit ownership route`,
|
|
85
|
+
);
|
|
52
86
|
}
|
|
53
87
|
const destinationSha256 = await sha256File(dest);
|
|
54
|
-
|
|
88
|
+
const resolvedState = decision === 'replace' ? OwnershipState.CLEAN_CORE
|
|
89
|
+
: (decision === 'keep-as-owned' ? OwnershipState.EXPLICIT_FORK : decision);
|
|
90
|
+
const resolution = {
|
|
55
91
|
path: file.path, outcome: decision, destinationSha256,
|
|
56
|
-
}
|
|
57
|
-
if (
|
|
58
|
-
|
|
92
|
+
};
|
|
93
|
+
if (!['replace', 'keep-as-owned'].includes(decision)) {
|
|
94
|
+
resolution.ownershipState = resolvedState;
|
|
95
|
+
}
|
|
96
|
+
result.collisionResolutions.push(resolution);
|
|
97
|
+
if (decision !== 'replace') {
|
|
98
|
+
nextInstalled.push(entry(
|
|
99
|
+
file, destinationSha256, CONSUMER_ORIGIN, resolvedState,
|
|
100
|
+
));
|
|
59
101
|
result.consumerOwned.push(file.path);
|
|
60
102
|
} else {
|
|
61
103
|
if (!dryRun) await writeAtomic(dest, await readFile(join(kitRoot, file.path)), file.mode);
|
|
@@ -128,17 +170,31 @@ export async function reconcile({ kitRoot, consumerRoot, decide = () => false, d
|
|
|
128
170
|
function emptyResult() {
|
|
129
171
|
return {
|
|
130
172
|
unchanged: [], updated: [], conflicts: [], collisions: [], collisionResolutions: [], userModified: [],
|
|
173
|
+
ownershipStates: [],
|
|
131
174
|
added: [], deleted: [], keptDeleted: [], consumerOwned: [], manifestChanged: false,
|
|
132
175
|
};
|
|
133
176
|
}
|
|
134
177
|
|
|
135
|
-
function entry(file, installedSha256, origin = KIT_ORIGIN) {
|
|
136
|
-
|
|
178
|
+
function entry(file, installedSha256, origin = KIT_ORIGIN, ownershipState) {
|
|
179
|
+
const result = {
|
|
137
180
|
path: file.path, kind: file.kind, ownerSkill: file.ownerSkill, surface: file.surface,
|
|
138
181
|
installedSha256, origin, installRole: CONSUMER_INSTALL_ROLE,
|
|
139
182
|
};
|
|
183
|
+
if (ownershipState) result.ownershipState = ownershipState;
|
|
184
|
+
return result;
|
|
140
185
|
}
|
|
141
186
|
|
|
142
187
|
function withInstallRole(installed, installRole = CONSUMER_INSTALL_ROLE) {
|
|
143
188
|
return { ...installed, installRole };
|
|
144
189
|
}
|
|
190
|
+
|
|
191
|
+
async function projectExtensionEvidence(consumerRoot, path) {
|
|
192
|
+
const match = /^docs\/agents\/skills\/([a-z0-9-]+)\.md$/.exec(path);
|
|
193
|
+
if (!match) return null;
|
|
194
|
+
try {
|
|
195
|
+
const result = await inspectProjectSkillExtension({ root: consumerRoot, skill: match[1] });
|
|
196
|
+
return result.state === 'active' ? result : null;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return { invalid: error.message };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -189,6 +189,14 @@ function validateInstalledEntry(tracked, desired, origin) {
|
|
|
189
189
|
if (tracked.origin === KIT_ORIGIN && tracked.installedSha256 !== desired.sha256) {
|
|
190
190
|
throw new Error(`candidate invariant ownership: Kit hash identity mismatch ${tracked.path}`);
|
|
191
191
|
}
|
|
192
|
+
if (tracked.origin === KIT_ORIGIN && tracked.ownershipState !== undefined) {
|
|
193
|
+
throw new Error(`candidate invariant ownership: Kit path has Consumer lifecycle ${tracked.path}`);
|
|
194
|
+
}
|
|
195
|
+
if (tracked.origin === CONSUMER_ORIGIN && ![
|
|
196
|
+
undefined, 'project-extension', 'contribution-bridge', 'explicit-fork',
|
|
197
|
+
].includes(tracked.ownershipState)) {
|
|
198
|
+
throw new Error(`candidate invariant ownership: invalid lifecycle ${tracked.path}`);
|
|
199
|
+
}
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
function verifyLedgerMetadata(ledger, context) {
|
|
@@ -207,8 +215,12 @@ function verifyLedgerMetadata(ledger, context) {
|
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
function expectedOrigin(path, priorInstalled, preview) {
|
|
218
|
+
const transferredToCore = (preview.migrations ?? []).some(
|
|
219
|
+
(migration) => migration.path === path && migration.ownership === 'kit-core',
|
|
220
|
+
);
|
|
221
|
+
if (transferredToCore) return KIT_ORIGIN;
|
|
210
222
|
const keptOwned = (preview.collisionResolutions ?? []).some(
|
|
211
|
-
(resolution) => resolution.path === path && resolution.outcome
|
|
223
|
+
(resolution) => resolution.path === path && resolution.outcome !== 'replace',
|
|
212
224
|
);
|
|
213
225
|
return keptOwned || (priorInstalled.get(path)?.origin ?? KIT_ORIGIN) === CONSUMER_ORIGIN
|
|
214
226
|
? CONSUMER_ORIGIN
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { CONSUMER_INSTALL_ROLE, CONSUMER_ORIGIN, filesForInstallRole } from './manifest.mjs';
|
|
4
4
|
import { validateCandidateManifestPath } from './updateCandidate.mjs';
|
|
5
|
+
import { readComposedSkillRegistry } from './skillRegistry.mjs';
|
|
5
6
|
|
|
6
7
|
const READINESS_MANIFEST_PATH = '.claude/skills/skill-manifest.json';
|
|
7
8
|
const SURFACE_ROOT = {
|
|
@@ -18,6 +19,7 @@ export async function verifyCandidateProtocol(candidateRoot, pkg, installed) {
|
|
|
18
19
|
throw new Error('candidate invariant schema: readiness manifest is invalid JSON');
|
|
19
20
|
}
|
|
20
21
|
validateReadinessManifest(manifest);
|
|
22
|
+
await readComposedSkillRegistry(candidateRoot, manifest);
|
|
21
23
|
validateSkillArtifactReferences(manifest, pkg);
|
|
22
24
|
for (const [name, declaration] of Object.entries(manifest.skills)) {
|
|
23
25
|
if (!declaration.publish
|
|
@@ -109,15 +109,21 @@ function verifyCollisionRecords(preview, installablePaths, installed, actionSets
|
|
|
109
109
|
for (const resolution of preview.collisionResolutions ?? []) {
|
|
110
110
|
const path = resolution?.path;
|
|
111
111
|
claimTransactionPath(path, 'collision resolution', resolutions);
|
|
112
|
+
const nonReplace = resolution?.outcome !== 'replace';
|
|
113
|
+
const expectedOwnershipState = resolution?.outcome === 'keep-as-owned'
|
|
114
|
+
? 'explicit-fork' : resolution?.ownershipState;
|
|
112
115
|
if (!installablePaths.has(path)
|
|
113
|
-
|| ![
|
|
116
|
+
|| ![
|
|
117
|
+
'keep-as-owned', 'project-extension', 'contribution-bridge', 'explicit-fork', 'replace',
|
|
118
|
+
].includes(resolution?.outcome)
|
|
114
119
|
|| !HASH.test(resolution?.destinationSha256 ?? '')) {
|
|
115
120
|
throw new Error(`candidate invariant transaction: invalid collision resolution ${path}`);
|
|
116
121
|
}
|
|
117
|
-
const expectedOrigin =
|
|
122
|
+
const expectedOrigin = nonReplace ? CONSUMER_ORIGIN : KIT_ORIGIN;
|
|
118
123
|
if (installed.get(path)?.origin !== expectedOrigin
|
|
124
|
+
|| (nonReplace && installed.get(path)?.ownershipState !== expectedOwnershipState)
|
|
119
125
|
|| (resolution.outcome === 'replace' && !actionSets.get('added').has(path))
|
|
120
|
-
|| (
|
|
126
|
+
|| (nonReplace && owners.has(path))) {
|
|
121
127
|
throw new Error(`candidate invariant transaction: incoherent collision resolution ${path}`);
|
|
122
128
|
}
|
|
123
129
|
}
|