@haposoft/cafekit 0.15.3 → 0.16.0-rc.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.
- package/README.md +4 -2
- package/bin/__tests__/codex-hooks.test.js +140 -0
- package/bin/__tests__/codex-native.test.js +2 -2
- package/bin/__tests__/develop-contract.test.js +1120 -0
- package/bin/__tests__/installer-safety.test.js +510 -2
- package/bin/__tests__/package-inventory.test.js +258 -0
- package/bin/__tests__/secret-scanner.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +713 -0
- package/bin/install.js +5 -1
- package/bin/lib/codex-install.js +21 -6
- package/bin/lib/context.js +17 -6
- package/bin/lib/copy-utils.js +31 -0
- package/bin/lib/instruction-blocks.js +85 -0
- package/bin/lib/managed-writer.js +4 -1
- package/bin/lib/opencode-install.js +75 -30
- package/bin/phases/claude-runtime.js +103 -4
- package/bin/phases/codex-runtime.js +9 -5
- package/bin/phases/post-install.js +29 -22
- package/bin/phases/root-config.js +89 -10
- package/package.json +14 -4
- package/src/claude/CLAUDE.md +6 -122
- package/src/claude/agents/code-auditor.md +21 -22
- package/src/claude/agents/debugger.md +3 -3
- package/src/claude/agents/deployer.md +1 -1
- package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/spec-maker.md +2 -2
- package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
- package/src/claude/hooks/agent.cjs +7 -14
- package/src/claude/hooks/rules.cjs +33 -53
- package/src/claude/hooks/spec-gate.cjs +66 -21
- package/src/claude/hooks/spec-state.cjs +5 -0
- package/src/claude/migration-manifest.json +9 -2
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
- package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
- package/src/claude/scripts/spec-ground.cjs +189 -83
- package/src/claude/scripts/validate-spec-output.cjs +294 -30
- package/src/claude/scripts/workflow-policy.cjs +609 -0
- package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
- package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
- package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
- package/src/claude/skills/backend-development/SKILL.md +4 -4
- package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
- package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
- package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
- package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
- package/src/claude/skills/backend-development/references/backend-security.md +3 -3
- package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
- package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
- package/src/claude/skills/brainstorm/SKILL.md +2 -3
- package/src/claude/skills/code-review/SKILL.md +12 -8
- package/src/claude/skills/code-review/references/verification-gate.md +4 -4
- package/src/claude/skills/debug/SKILL.md +2 -0
- package/src/claude/skills/develop/SKILL.md +100 -52
- package/src/claude/skills/develop/references/parallel-waves.md +105 -26
- package/src/claude/skills/develop/references/quality-gate.md +58 -38
- package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
- package/src/claude/skills/git/SKILL.md +44 -50
- package/src/claude/skills/hotfix/SKILL.md +10 -8
- package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
- package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
- package/src/claude/skills/inspect/SKILL.md +12 -30
- package/src/claude/skills/mobile-development/SKILL.md +12 -12
- package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
- package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
- package/src/claude/skills/question/SKILL.md +13 -13
- package/src/claude/skills/research/SKILL.md +2 -2
- package/src/claude/skills/specs/SKILL.md +32 -15
- package/src/claude/skills/specs/references/review.md +3 -3
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/spec-state.json +7 -3
- package/src/claude/skills/sync/SKILL.md +26 -1
- package/src/claude/skills/sync/references/sync-protocols.md +3 -3
- package/src/claude/skills/test/SKILL.md +12 -4
- package/src/claude/skills/test/references/failure-triage.md +17 -17
- package/src/claude/skills/test/references/test-memory.md +2 -2
- package/src/codex/AGENTS.md +16 -24
- package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
- package/src/codex/hooks/rules.cjs +27 -26
- package/src/codex/hooks/spec-gate.cjs +19 -8
- package/src/codex/hooks/spec-state.cjs +10 -1
- package/src/common/AGENTS.md +43 -0
- package/src/opencode/AGENTS.md +21 -121
- package/src/opencode/plugins/rules.ts +26 -34
- package/src/opencode/plugins/spec-gate.ts +283 -0
- package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
package/bin/install.js
CHANGED
|
@@ -28,7 +28,9 @@ const {
|
|
|
28
28
|
copyClaudeRuntimeFiles,
|
|
29
29
|
removeObsoleteClaudeRuntimeFiles,
|
|
30
30
|
copyClaudeMdFile,
|
|
31
|
-
|
|
31
|
+
ensureSharedAgentsMdCore,
|
|
32
|
+
copyRulesDirectory,
|
|
33
|
+
removeObsoleteAgents
|
|
32
34
|
} = require('./phases/claude-runtime');
|
|
33
35
|
const { mergeClaudeSettings } = require('./phases/claude-settings');
|
|
34
36
|
const { installOpenCodeRuntime } = require('./phases/opencode-runtime');
|
|
@@ -57,6 +59,7 @@ function installPlatform(ctx, platformKey) {
|
|
|
57
59
|
ctx.ui.startSpinner(ctx.t('installingPlatform', { name: platform.name }));
|
|
58
60
|
|
|
59
61
|
copyPlatformFiles(ctx, platformKey);
|
|
62
|
+
removeObsoleteAgents(ctx, platformKey);
|
|
60
63
|
|
|
61
64
|
if (platformKey === 'claude') {
|
|
62
65
|
copyRoutingFile(ctx, platformKey);
|
|
@@ -174,6 +177,7 @@ async function main() {
|
|
|
174
177
|
ctx.backupDir = backup.snapshot([...targets, '.gitignore'], ctx.runId);
|
|
175
178
|
}
|
|
176
179
|
|
|
180
|
+
ensureSharedAgentsMdCore(ctx);
|
|
177
181
|
for (const platformKey of ctx.platforms) {
|
|
178
182
|
installPlatform(ctx, platformKey);
|
|
179
183
|
}
|
package/bin/lib/codex-install.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { normalizeSourcePaths } = require('./copy-utils');
|
|
4
5
|
const { splitFrontmatter } = require('./codex-frontmatter');
|
|
6
|
+
const { preserveAddressingSection } = require('./instruction-blocks');
|
|
5
7
|
|
|
6
8
|
const CODEX_BLOCK_START = '<!-- CAFEKIT CODEX START -->';
|
|
7
9
|
const CODEX_BLOCK_END = '<!-- CAFEKIT CODEX END -->';
|
|
@@ -14,7 +16,7 @@ const AGENT_NAMES = [
|
|
|
14
16
|
'deployer',
|
|
15
17
|
'docs-keeper',
|
|
16
18
|
'git-ops',
|
|
17
|
-
'
|
|
19
|
+
'implementer',
|
|
18
20
|
'inspector',
|
|
19
21
|
'project-manager',
|
|
20
22
|
'researcher',
|
|
@@ -63,10 +65,13 @@ function applyReplacements(content, replacements) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function normalizeRuntimePaths(content) {
|
|
66
|
-
return content
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
return normalizeSourcePaths(content, {
|
|
69
|
+
runtimeRoot: '.codex',
|
|
70
|
+
skillsRoot: '.agents/skills'
|
|
71
|
+
})
|
|
72
|
+
.replace(/(?<!~\/)\.claude\/skills\b/g, '.agents/skills')
|
|
73
|
+
.replace(/(?<!~\\)\.claude\\skills\b/g, '.agents\\skills')
|
|
74
|
+
.replace(/(?<!~\/)\.claude(?=[/\\])/g, '.codex')
|
|
70
75
|
.replace(/(['"])\.claude\1/g, '$1.codex$1')
|
|
71
76
|
.replace(/`\.claude`/g, '`.codex`');
|
|
72
77
|
}
|
|
@@ -163,8 +168,17 @@ function managedRange(content) {
|
|
|
163
168
|
|
|
164
169
|
function upsertManagedCodexBlock(existingContent, blockContent) {
|
|
165
170
|
const existing = String(existingContent || '');
|
|
166
|
-
|
|
171
|
+
let body = String(blockContent).trim();
|
|
172
|
+
// Reinstall replaces the managed block in place. If the new template dropped
|
|
173
|
+
// its Addressing section, carry the exact section over from the existing
|
|
174
|
+
// managed block so the saved address survives for setupAddressing. Only reads
|
|
175
|
+
// the managed body — never user-owned sections outside the block.
|
|
167
176
|
const range = managedRange(existing);
|
|
177
|
+
if (range) {
|
|
178
|
+
const existingBody = existing.slice(range.start + CODEX_BLOCK_START.length, range.end);
|
|
179
|
+
body = preserveAddressingSection(body, existingBody);
|
|
180
|
+
}
|
|
181
|
+
const block = `${CODEX_BLOCK_START}\n${body}\n${CODEX_BLOCK_END}`;
|
|
168
182
|
if (range === false) return existing;
|
|
169
183
|
if (range) {
|
|
170
184
|
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end + CODEX_BLOCK_END.length)}`;
|
|
@@ -190,6 +204,7 @@ module.exports = {
|
|
|
190
204
|
getCodexCopyOptions,
|
|
191
205
|
codexAgentName,
|
|
192
206
|
convertCodexAgentContent,
|
|
207
|
+
managedRange,
|
|
193
208
|
upsertManagedCodexBlock,
|
|
194
209
|
transformManagedCodexContent
|
|
195
210
|
};
|
package/bin/lib/context.js
CHANGED
|
@@ -16,6 +16,7 @@ const packageJson = require('../../package.json');
|
|
|
16
16
|
const { getOpenCodeCopyOptions } = require('./opencode-install');
|
|
17
17
|
const { isInteractive, createUI } = require('./ui');
|
|
18
18
|
const { resolveLang, createTranslator } = require('./i18n');
|
|
19
|
+
const { normalizeSourcePaths } = require('./copy-utils');
|
|
19
20
|
|
|
20
21
|
const INSTALL_COMMAND = `npx ${packageJson.name}@${packageJson.version}`;
|
|
21
22
|
|
|
@@ -82,7 +83,7 @@ const PLATFORMS = {
|
|
|
82
83
|
commandPrefix: '/',
|
|
83
84
|
sourceDir: 'claude', // Maps to src/claude/
|
|
84
85
|
sourceSubdir: 'commands', // Source subfolder within src/claude/
|
|
85
|
-
backupTargets: ['.claude', 'CLAUDE.md'],
|
|
86
|
+
backupTargets: ['.claude', 'CLAUDE.md', 'AGENTS.md'],
|
|
86
87
|
capabilities: {
|
|
87
88
|
skills: true,
|
|
88
89
|
agents: true,
|
|
@@ -216,7 +217,16 @@ function getCopyOptions(platformKey, baseOptions = {}) {
|
|
|
216
217
|
// during module initialization.
|
|
217
218
|
return require('./codex-install').getCodexCopyOptions(baseOptions);
|
|
218
219
|
}
|
|
219
|
-
return baseOptions;
|
|
220
|
+
if (platformKey !== 'claude') return baseOptions;
|
|
221
|
+
return {
|
|
222
|
+
...baseOptions,
|
|
223
|
+
transform: (content, sourcePath) => normalizeSourcePaths(
|
|
224
|
+
typeof baseOptions.transform === 'function'
|
|
225
|
+
? baseOptions.transform(content, sourcePath)
|
|
226
|
+
: content,
|
|
227
|
+
{ runtimeRoot: '.claude', skillsRoot: '.claude/skills' }
|
|
228
|
+
)
|
|
229
|
+
};
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -305,11 +315,12 @@ function buildContext(argv, runId) {
|
|
|
305
315
|
const options = parseInstallerArgs(argv);
|
|
306
316
|
// Interactive only on a real TTY and not when --yes/CI forces non-interactive.
|
|
307
317
|
const interactive = isInteractive() && !options.yes;
|
|
308
|
-
// Language: --lang wins; otherwise English
|
|
318
|
+
// Language: --lang wins; otherwise English for installer UI only (an interactive
|
|
319
|
+
// prompt or saved runtime locale may change both values).
|
|
309
320
|
const lang = options.lang ? resolveLang(options.lang) : 'en';
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
const locale = options.lang
|
|
321
|
+
// A missing locale means "follow the user's language". Never persist the UI
|
|
322
|
+
// fallback as an AI response-language override on fresh installs/upgrades.
|
|
323
|
+
const locale = options.lang ? options.lang : null;
|
|
313
324
|
return {
|
|
314
325
|
argv,
|
|
315
326
|
runId,
|
package/bin/lib/copy-utils.js
CHANGED
|
@@ -11,6 +11,33 @@ function isTextAsset(filePath) {
|
|
|
11
11
|
return TEXT_REWRITE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Generated artifacts never ship as runtime payload: the coverage db, the
|
|
15
|
+
// Python bytecode cache directory, and bytecode files. Skipping them here keeps
|
|
16
|
+
// both copyRecursive and copyManagedTree (and the OpenCode plugin copy path
|
|
17
|
+
// built on copyRecursive) from ever copying build residue.
|
|
18
|
+
function isGeneratedArtifact(name) {
|
|
19
|
+
const base = String(name || '');
|
|
20
|
+
return (
|
|
21
|
+
base === '.coverage'
|
|
22
|
+
|| base === '__pycache__'
|
|
23
|
+
|| base.endsWith('.pyc')
|
|
24
|
+
|| base.endsWith('.pyo')
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeSourcePaths(content, options = {}) {
|
|
29
|
+
const runtimeRoot = String(options.runtimeRoot || '.claude');
|
|
30
|
+
const skillsRoot = String(options.skillsRoot || `${runtimeRoot}/skills`);
|
|
31
|
+
const runtimeRootWindows = runtimeRoot.replace(/\//g, '\\');
|
|
32
|
+
const skillsRootWindows = skillsRoot.replace(/\//g, '\\');
|
|
33
|
+
|
|
34
|
+
return String(content)
|
|
35
|
+
.replace(/packages\/spec\/src\/claude\/skills(?=\/|$)/g, skillsRoot)
|
|
36
|
+
.replace(/packages\\spec\\src\\claude\\skills(?=\\|$)/g, skillsRootWindows)
|
|
37
|
+
.replace(/packages\/spec\/src\/claude(?=\/|$)/g, runtimeRoot)
|
|
38
|
+
.replace(/packages\\spec\\src\\claude(?=\\|$)/g, runtimeRootWindows);
|
|
39
|
+
}
|
|
40
|
+
|
|
14
41
|
function copyRecursive(src, dest, options = {}) {
|
|
15
42
|
const exists = fs.existsSync(src);
|
|
16
43
|
const stats = exists && fs.statSync(src);
|
|
@@ -22,10 +49,12 @@ function copyRecursive(src, dest, options = {}) {
|
|
|
22
49
|
fs.mkdirSync(dest, { recursive: true });
|
|
23
50
|
}
|
|
24
51
|
fs.readdirSync(src).forEach(childItemName => {
|
|
52
|
+
if (isGeneratedArtifact(childItemName)) return;
|
|
25
53
|
const destItemName = childItemName === 'gitignore' ? '.gitignore' : childItemName;
|
|
26
54
|
copyRecursive(path.join(src, childItemName), path.join(dest, destItemName), options);
|
|
27
55
|
});
|
|
28
56
|
} else {
|
|
57
|
+
if (isGeneratedArtifact(path.basename(src))) return;
|
|
29
58
|
if (fs.existsSync(dest) && !shouldOverwriteManagedFiles) {
|
|
30
59
|
return;
|
|
31
60
|
}
|
|
@@ -56,6 +85,8 @@ function readJsonFile(filePath) {
|
|
|
56
85
|
module.exports = {
|
|
57
86
|
TEXT_REWRITE_EXTENSIONS,
|
|
58
87
|
isTextAsset,
|
|
88
|
+
isGeneratedArtifact,
|
|
89
|
+
normalizeSourcePaths,
|
|
59
90
|
copyRecursive,
|
|
60
91
|
readJsonFile
|
|
61
92
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CORE_BLOCK_START = '<!-- CAFEKIT CORE START -->';
|
|
4
|
+
const CORE_BLOCK_END = '<!-- CAFEKIT CORE END -->';
|
|
5
|
+
|
|
6
|
+
function managedRange(content, startMarker = CORE_BLOCK_START, endMarker = CORE_BLOCK_END) {
|
|
7
|
+
const existing = String(content || '');
|
|
8
|
+
const start = existing.indexOf(startMarker);
|
|
9
|
+
const end = existing.indexOf(endMarker);
|
|
10
|
+
if (start < 0 && end < 0) return null;
|
|
11
|
+
const duplicateStart = existing.indexOf(startMarker, start + startMarker.length);
|
|
12
|
+
const duplicateEnd = existing.indexOf(endMarker, end + endMarker.length);
|
|
13
|
+
if (start < 0 || end <= start || duplicateStart >= 0 || duplicateEnd >= 0) return false;
|
|
14
|
+
return {
|
|
15
|
+
start,
|
|
16
|
+
end: end + endMarker.length,
|
|
17
|
+
bodyStart: start + startMarker.length,
|
|
18
|
+
bodyEnd: end
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function migrateExactLegacyBlock(existingContent, startMarker, endMarker, replacementContent) {
|
|
23
|
+
const existing = String(existingContent || '');
|
|
24
|
+
const range = managedRange(existing, startMarker, endMarker);
|
|
25
|
+
if (!range) return existing;
|
|
26
|
+
const body = existing.slice(range.bodyStart, range.bodyEnd).trim();
|
|
27
|
+
if (body !== String(replacementContent).trim()) return existing;
|
|
28
|
+
const block = `${CORE_BLOCK_START}\n${String(replacementContent).trim()}\n${CORE_BLOCK_END}`;
|
|
29
|
+
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function upsertManagedCoreBlock(existingContent, coreContent) {
|
|
33
|
+
const existing = String(existingContent || '');
|
|
34
|
+
const block = `${CORE_BLOCK_START}\n${String(coreContent).trim()}\n${CORE_BLOCK_END}`;
|
|
35
|
+
const range = managedRange(existing);
|
|
36
|
+
if (range === false) return existing;
|
|
37
|
+
if (range) {
|
|
38
|
+
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end)}`;
|
|
39
|
+
}
|
|
40
|
+
if (!existing) return `${block}\n`;
|
|
41
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
42
|
+
return `${existing}${separator}${block}\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function transformManagedCoreContent(content, transform) {
|
|
46
|
+
const existing = String(content || '');
|
|
47
|
+
const range = managedRange(existing);
|
|
48
|
+
if (!range || typeof transform !== 'function') return existing;
|
|
49
|
+
const body = existing.slice(range.bodyStart, range.bodyEnd);
|
|
50
|
+
return `${existing.slice(0, range.bodyStart)}${transform(body)}${existing.slice(range.bodyEnd)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Mirrors the section regex used by post-install configureAddressing so an
|
|
54
|
+
// extracted section round-trips exactly back into the managed body.
|
|
55
|
+
const ADDRESSING_SECTION_RE = /##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?(?=\n##|\n*$)/;
|
|
56
|
+
|
|
57
|
+
/** Extract the exact Addressing section (heading through next `##`) from a body, or null. */
|
|
58
|
+
function extractAddressingSection(body) {
|
|
59
|
+
const match = String(body || '').match(ADDRESSING_SECTION_RE);
|
|
60
|
+
return match ? match[0].replace(/\n+$/, '') : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* When a reinstall template drops the Addressing section, carry the exact one
|
|
65
|
+
* from the existing CafeKit-managed block so the saved address survives for
|
|
66
|
+
* setupAddressing. Only reads the managed block body — never user-owned
|
|
67
|
+
* sections outside the block, never the shared CORE block.
|
|
68
|
+
*/
|
|
69
|
+
function preserveAddressingSection(newBody, existingManagedBody) {
|
|
70
|
+
if (extractAddressingSection(newBody) !== null) return newBody;
|
|
71
|
+
const section = extractAddressingSection(existingManagedBody);
|
|
72
|
+
if (section === null) return newBody;
|
|
73
|
+
return `${String(newBody).trimEnd()}\n\n${section}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
CORE_BLOCK_START,
|
|
78
|
+
CORE_BLOCK_END,
|
|
79
|
+
managedRange,
|
|
80
|
+
migrateExactLegacyBlock,
|
|
81
|
+
upsertManagedCoreBlock,
|
|
82
|
+
transformManagedCoreContent,
|
|
83
|
+
extractAddressingSection,
|
|
84
|
+
preserveAddressingSection
|
|
85
|
+
};
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const path = require('path');
|
|
23
|
-
const { isTextAsset } = require('./copy-utils');
|
|
23
|
+
const { isTextAsset, isGeneratedArtifact } = require('./copy-utils');
|
|
24
24
|
const manifest = require('./manifest');
|
|
25
25
|
const { assertNoSymlinkPath } = require('./path-safety');
|
|
26
26
|
|
|
@@ -151,6 +151,9 @@ function copyManagedTree(opts) {
|
|
|
151
151
|
const stats = fs.statSync(src);
|
|
152
152
|
if (stats.isDirectory()) {
|
|
153
153
|
for (const childName of fs.readdirSync(src)) {
|
|
154
|
+
// Generated artifacts (coverage, Python bytecode) are not runtime
|
|
155
|
+
// payload — skip so they never ship.
|
|
156
|
+
if (isGeneratedArtifact(childName)) continue;
|
|
154
157
|
const destName = childName === 'gitignore' ? '.gitignore' : childName;
|
|
155
158
|
const childAgg = copyManagedTree({
|
|
156
159
|
...opts,
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
-
const { copyRecursive, isTextAsset } = require('./copy-utils');
|
|
6
|
+
const { copyRecursive, isTextAsset, isGeneratedArtifact, normalizeSourcePaths } = require('./copy-utils');
|
|
7
|
+
const { preserveAddressingSection } = require('./instruction-blocks');
|
|
7
8
|
|
|
8
9
|
const SPEC_ROOT = path.resolve(__dirname, '..', '..');
|
|
9
10
|
const OPENCODE_FOLDER = '.opencode';
|
|
@@ -48,7 +49,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
48
49
|
{
|
|
49
50
|
fileName: 'develop.md',
|
|
50
51
|
skillName: 'develop',
|
|
51
|
-
agent: '
|
|
52
|
+
agent: 'implementer',
|
|
52
53
|
subtask: true,
|
|
53
54
|
description: 'Implement approved CafeKit spec tasks with scope fidelity.'
|
|
54
55
|
},
|
|
@@ -76,7 +77,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
76
77
|
{
|
|
77
78
|
fileName: 'hotfix.md',
|
|
78
79
|
skillName: 'hotfix',
|
|
79
|
-
agent: '
|
|
80
|
+
agent: 'implementer',
|
|
80
81
|
subtask: true,
|
|
81
82
|
description: 'Apply a scout-first, narrow production hotfix.'
|
|
82
83
|
},
|
|
@@ -113,7 +114,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
113
114
|
{
|
|
114
115
|
fileName: 'backend-development.md',
|
|
115
116
|
skillName: 'backend-development',
|
|
116
|
-
agent: '
|
|
117
|
+
agent: 'implementer',
|
|
117
118
|
subtask: true,
|
|
118
119
|
description: 'Implement backend services, APIs, data layers, and integrations.'
|
|
119
120
|
},
|
|
@@ -148,7 +149,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
148
149
|
{
|
|
149
150
|
fileName: 'frontend-development.md',
|
|
150
151
|
skillName: 'frontend-development',
|
|
151
|
-
agent: '
|
|
152
|
+
agent: 'implementer',
|
|
152
153
|
subtask: true,
|
|
153
154
|
description: 'Build React/TypeScript UI components and frontend features.'
|
|
154
155
|
},
|
|
@@ -162,7 +163,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
162
163
|
{
|
|
163
164
|
fileName: 'mobile-development.md',
|
|
164
165
|
skillName: 'mobile-development',
|
|
165
|
-
agent: '
|
|
166
|
+
agent: 'implementer',
|
|
166
167
|
subtask: true,
|
|
167
168
|
description: 'Implement iOS, Android, React Native, or Flutter mobile features.'
|
|
168
169
|
},
|
|
@@ -268,9 +269,12 @@ function formatOpenCodeValue(value) {
|
|
|
268
269
|
// Applied to every text asset copied into .opencode/ so the installed
|
|
269
270
|
// content points at the self-contained OpenCode runtime layout.
|
|
270
271
|
function normalizeOpenCodeBody(content) {
|
|
271
|
-
return content
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
return normalizeSourcePaths(content, {
|
|
273
|
+
runtimeRoot: '.opencode',
|
|
274
|
+
skillsRoot: '.opencode/skills'
|
|
275
|
+
})
|
|
276
|
+
.replace(/(?<!~\/)\.claude\/(agents|commands|skills|rules|scripts|references|hooks)\b/g, '.opencode/$1')
|
|
277
|
+
.replace(/(?<!~\\)\.claude\\(agents|commands|skills|rules|scripts|references|hooks)\b/g, '.opencode\\$1')
|
|
274
278
|
.replace(/\.claude\/runtime\.json/g, '.opencode/runtime.json')
|
|
275
279
|
.replace(/\.claude\/ROUTING\.md/g, '.opencode/ROUTING.md')
|
|
276
280
|
.replace(/\.claude\/\.env/g, '.opencode/.env')
|
|
@@ -409,16 +413,44 @@ function createOpenCodeAgentsContent(sourceContent) {
|
|
|
409
413
|
return adaptedContent.replace('## Core Objective', `${openCodeRuntimeBlock}\n## Core Objective`);
|
|
410
414
|
}
|
|
411
415
|
|
|
416
|
+
function managedOpenCodeRange(content) {
|
|
417
|
+
const existing = String(content || '');
|
|
418
|
+
const startMarker = '<!-- CAFEKIT OPENCODE START -->';
|
|
419
|
+
const endMarker = '<!-- CAFEKIT OPENCODE END -->';
|
|
420
|
+
const start = existing.indexOf(startMarker);
|
|
421
|
+
const end = existing.indexOf(endMarker);
|
|
422
|
+
if (start < 0 && end < 0) return null;
|
|
423
|
+
const duplicateStart = existing.indexOf(startMarker, start + startMarker.length);
|
|
424
|
+
const duplicateEnd = existing.indexOf(endMarker, end + endMarker.length);
|
|
425
|
+
if (start < 0 || end <= start || duplicateStart >= 0 || duplicateEnd >= 0) return false;
|
|
426
|
+
return {
|
|
427
|
+
start,
|
|
428
|
+
end: end + endMarker.length,
|
|
429
|
+
bodyStart: start + startMarker.length,
|
|
430
|
+
bodyEnd: end
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
412
435
|
function upsertCafeKitAgentsBlock(existingContent, blockContent) {
|
|
413
436
|
const start = '<!-- CAFEKIT OPENCODE START -->';
|
|
414
437
|
const end = '<!-- CAFEKIT OPENCODE END -->';
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
438
|
+
let body = String(blockContent).trim();
|
|
439
|
+
// Reinstall replaces the managed block in place. If the new template dropped
|
|
440
|
+
// its Addressing section, carry the exact section over from the existing
|
|
441
|
+
// managed block so the saved address survives for setupAddressing. Only reads
|
|
442
|
+
// the managed body — never user-owned sections outside the block.
|
|
443
|
+
const range = managedOpenCodeRange(existingContent);
|
|
444
|
+
if (range) {
|
|
445
|
+
const existingBody = existingContent.slice(range.bodyStart, range.bodyEnd);
|
|
446
|
+
body = preserveAddressingSection(body, existingBody);
|
|
447
|
+
}
|
|
448
|
+
const managedBlock = `${start}\n${body}\n${end}`;
|
|
449
|
+
if (range === false) return existingContent;
|
|
450
|
+
if (range) {
|
|
451
|
+
return `${existingContent.slice(0, range.start)}${managedBlock}${existingContent.slice(range.end)}`;
|
|
420
452
|
}
|
|
421
|
-
|
|
453
|
+
if (!existingContent) return `${managedBlock}\n`;
|
|
422
454
|
// Pre-managed-block releases wrote the shipped template as the whole file.
|
|
423
455
|
// Wrap only an exact shipped prefix and preserve any user/Codex suffix.
|
|
424
456
|
if (existingContent.startsWith(blockContent)) {
|
|
@@ -431,25 +463,23 @@ function upsertCafeKitAgentsBlock(existingContent, blockContent) {
|
|
|
431
463
|
) {
|
|
432
464
|
return `${managedBlock}${existingContent.slice(trimmedLegacy.length)}`;
|
|
433
465
|
}
|
|
434
|
-
if (!existingContent) return `${managedBlock}\n`;
|
|
435
|
-
|
|
436
466
|
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
437
467
|
return `${existingContent}${separator}${managedBlock}\n`;
|
|
438
468
|
}
|
|
439
469
|
|
|
470
|
+
/** Transform only the CafeKit-owned OpenCode body, preserving surrounding bytes. */
|
|
471
|
+
function transformManagedOpenCodeContent(content, transform) {
|
|
472
|
+
const existing = String(content || '');
|
|
473
|
+
const range = managedOpenCodeRange(existing);
|
|
474
|
+
if (!range || typeof transform !== 'function') return existing;
|
|
475
|
+
const body = existing.slice(range.bodyStart, range.bodyEnd);
|
|
476
|
+
return `${existing.slice(0, range.bodyStart)}${transform(body)}${existing.slice(range.bodyEnd)}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
440
479
|
function getOpenCodeAgentsTemplateContent() {
|
|
441
480
|
const openCodeTemplate = path.join(SPEC_ROOT, 'src/opencode/AGENTS.md');
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return normalizeOpenCodeBody(fs.readFileSync(openCodeTemplate, 'utf8'));
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const claudeTemplate = path.join(SPEC_ROOT, 'src/claude/CLAUDE.md');
|
|
448
|
-
if (!fs.existsSync(claudeTemplate)) {
|
|
449
|
-
return null;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return createOpenCodeAgentsContent(fs.readFileSync(claudeTemplate, 'utf8'));
|
|
481
|
+
if (!fs.existsSync(openCodeTemplate)) return null;
|
|
482
|
+
return normalizeOpenCodeBody(fs.readFileSync(openCodeTemplate, 'utf8'));
|
|
453
483
|
}
|
|
454
484
|
|
|
455
485
|
// Copy OpenCode project instructions to AGENTS.md at the project root.
|
|
@@ -462,6 +492,11 @@ function copyOpenCodeAgentsMdFile(platformKey, results, options = {}) {
|
|
|
462
492
|
if (content) {
|
|
463
493
|
const destinationExists = fs.existsSync(dest);
|
|
464
494
|
const existingContent = destinationExists ? fs.readFileSync(dest, 'utf8') : '';
|
|
495
|
+
if (managedOpenCodeRange(existingContent) === false) {
|
|
496
|
+
console.log(` ⚠ AGENTS.md: malformed CafeKit OPENCODE marker topology; preserved ${dest} without writing`);
|
|
497
|
+
results.errors++;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
465
500
|
const nextContent = upsertCafeKitAgentsBlock(existingContent, content);
|
|
466
501
|
|
|
467
502
|
if (nextContent === existingContent) {
|
|
@@ -537,6 +572,10 @@ function copyOpenCodePlugins(platformKey, results, options = {}) {
|
|
|
537
572
|
|
|
538
573
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
539
574
|
entries.forEach(entry => {
|
|
575
|
+
// Skip generated artifacts before any directory/file handling, matching
|
|
576
|
+
// the filter copyRecursive applies to directory children.
|
|
577
|
+
if (isGeneratedArtifact(entry.name)) return;
|
|
578
|
+
|
|
540
579
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
541
580
|
const destPath = path.join(targetDir, entry.name);
|
|
542
581
|
|
|
@@ -555,8 +594,12 @@ function copyOpenCodePlugins(platformKey, results, options = {}) {
|
|
|
555
594
|
return;
|
|
556
595
|
}
|
|
557
596
|
|
|
558
|
-
|
|
559
|
-
|
|
597
|
+
if (isTextAsset(sourcePath)) {
|
|
598
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
599
|
+
fs.writeFileSync(destPath, normalizeOpenCodeBody(sourceContent), 'utf8');
|
|
600
|
+
} else {
|
|
601
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
602
|
+
}
|
|
560
603
|
|
|
561
604
|
if (destExists) {
|
|
562
605
|
console.log(` ↻ Plugin updated: ${entry.name}`);
|
|
@@ -846,6 +889,8 @@ module.exports = {
|
|
|
846
889
|
convertOpenCodeAgentContent,
|
|
847
890
|
convertOpenCodeCommandContent,
|
|
848
891
|
copyOpenCodeAgentsMdFile,
|
|
892
|
+
managedOpenCodeRange,
|
|
893
|
+
transformManagedOpenCodeContent,
|
|
849
894
|
copyOpenCodeSharedRuntimeFiles,
|
|
850
895
|
copyOpenCodePlugins,
|
|
851
896
|
copyOpenCodeCommandTemplates,
|
|
@@ -19,10 +19,18 @@ const { sha256 } = require('../lib/manifest');
|
|
|
19
19
|
const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
|
|
20
20
|
const { report, treeAction } = require('./report');
|
|
21
21
|
const { normalizeOpenCodeBody } = require('../lib/opencode-install');
|
|
22
|
+
const {
|
|
23
|
+
upsertManagedCoreBlock,
|
|
24
|
+
managedRange,
|
|
25
|
+
migrateExactLegacyBlock,
|
|
26
|
+
preserveAddressingSection
|
|
27
|
+
} = require('../lib/instruction-blocks');
|
|
22
28
|
|
|
23
29
|
const SRC = path.join(__dirname, '../../src');
|
|
24
30
|
const CLAUDE_START = '<!-- CAFEKIT CLAUDE START -->';
|
|
25
31
|
const CLAUDE_END = '<!-- CAFEKIT CLAUDE END -->';
|
|
32
|
+
const LEGACY_CORE_START = '<!-- CAFEKIT CLAUDE AGENTS START -->';
|
|
33
|
+
const LEGACY_CORE_END = '<!-- CAFEKIT CLAUDE AGENTS END -->';
|
|
26
34
|
|
|
27
35
|
function managedClaudeBlock(template) {
|
|
28
36
|
return `${CLAUDE_START}\n${template.trimEnd()}\n${CLAUDE_END}`;
|
|
@@ -30,9 +38,11 @@ function managedClaudeBlock(template) {
|
|
|
30
38
|
|
|
31
39
|
function managedClaudeRange(content) {
|
|
32
40
|
const start = content.indexOf(CLAUDE_START);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
const endStart = content.indexOf(CLAUDE_END);
|
|
42
|
+
if (start === -1 && endStart === -1) return null;
|
|
43
|
+
const duplicateStart = content.indexOf(CLAUDE_START, start + CLAUDE_START.length);
|
|
44
|
+
const duplicateEnd = content.indexOf(CLAUDE_END, endStart + CLAUDE_END.length);
|
|
45
|
+
if (start === -1 || endStart <= start || duplicateStart >= 0 || duplicateEnd >= 0) return false;
|
|
36
46
|
return { start, end: endStart + CLAUDE_END.length, bodyStart: start + CLAUDE_START.length, bodyEnd: endStart };
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -45,7 +55,19 @@ function transformManagedClaudeContent(content, transform) {
|
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
function upsertManagedClaudeBlock(existing, template, legacyOwned) {
|
|
48
|
-
|
|
58
|
+
// When an existing managed block present, reinstall replaces it. If the new
|
|
59
|
+
// template dropped its Addressing section, carry the saved section over from
|
|
60
|
+
// the existing managed block so the user's address survives for setupAddressing.
|
|
61
|
+
const existingRange = managedClaudeRange(existing);
|
|
62
|
+
if (existingRange === false) return existing;
|
|
63
|
+
const existingManagedBody = existingRange
|
|
64
|
+
? existing.slice(existingRange.bodyStart, existingRange.bodyEnd)
|
|
65
|
+
: '';
|
|
66
|
+
let body = template;
|
|
67
|
+
if (existingRange) {
|
|
68
|
+
body = preserveAddressingSection(template, existingManagedBody);
|
|
69
|
+
}
|
|
70
|
+
const block = managedClaudeBlock(body);
|
|
49
71
|
const range = managedClaudeRange(existing);
|
|
50
72
|
if (range) {
|
|
51
73
|
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end)}`;
|
|
@@ -60,6 +82,44 @@ function upsertManagedClaudeBlock(existing, template, legacyOwned) {
|
|
|
60
82
|
return `${existing}${separator}${block}\n`;
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
function ensureSharedAgentsMdCore(ctx) {
|
|
86
|
+
const src = path.join(SRC, 'common/AGENTS.md');
|
|
87
|
+
const dest = 'AGENTS.md';
|
|
88
|
+
if (!fs.existsSync(src)) {
|
|
89
|
+
ctx.ui.warn('Shared AGENTS.md template not found');
|
|
90
|
+
ctx.results.missingDependencies++;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const template = fs.readFileSync(src, 'utf8');
|
|
95
|
+
const exists = fs.existsSync(dest);
|
|
96
|
+
const existing = exists ? fs.readFileSync(dest, 'utf8') : '';
|
|
97
|
+
if (managedRange(existing) === false) {
|
|
98
|
+
ctx.ui.warn(`AGENTS.md: malformed CafeKit CORE marker topology; preserved ${dest} without writing`);
|
|
99
|
+
ctx.results.errors++;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const migrated = migrateExactLegacyBlock(
|
|
103
|
+
existing,
|
|
104
|
+
LEGACY_CORE_START,
|
|
105
|
+
LEGACY_CORE_END,
|
|
106
|
+
template
|
|
107
|
+
);
|
|
108
|
+
const next = upsertManagedCoreBlock(migrated, template);
|
|
109
|
+
const action = !exists ? 'created' : next === existing ? 'unchanged' : 'updated';
|
|
110
|
+
|
|
111
|
+
if (!ctx.dryRun && action !== 'unchanged') {
|
|
112
|
+
fs.writeFileSync(dest, next, 'utf8');
|
|
113
|
+
}
|
|
114
|
+
report(ctx, action, 'AGENTS.md (shared CafeKit core)');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Install the shared AGENTS.md core used by Claude's @AGENTS.md import. */
|
|
118
|
+
function copyClaudeAgentsMdFile(ctx, platformKey) {
|
|
119
|
+
if (platformKey !== 'claude') return;
|
|
120
|
+
ensureSharedAgentsMdCore(ctx);
|
|
121
|
+
}
|
|
122
|
+
|
|
63
123
|
/** Copy ROUTING.md (SKILLS_DIR substituted; OpenCode body normalized). */
|
|
64
124
|
function copyRoutingFile(ctx, platformKey) {
|
|
65
125
|
const platform = PLATFORMS[platformKey];
|
|
@@ -132,6 +192,37 @@ function removeObsoleteClaudeRuntimeFiles(ctx, platformKey) {
|
|
|
132
192
|
});
|
|
133
193
|
}
|
|
134
194
|
|
|
195
|
+
/** Remove renamed agents only when ownership manifest proves pristine bytes. */
|
|
196
|
+
function removeObsoleteAgents(ctx, platformKey) {
|
|
197
|
+
const platform = PLATFORMS[platformKey];
|
|
198
|
+
const obsolete = ctx.manifest?.obsolete?.agents?.[platformKey] || [];
|
|
199
|
+
const tracker = ctx.trackers?.[platformKey];
|
|
200
|
+
if (!platform || !tracker || obsolete.length === 0) return;
|
|
201
|
+
|
|
202
|
+
const ownership = ctx.ownership?.[platform.folder] || { files: {} };
|
|
203
|
+
for (const relPath of obsolete) {
|
|
204
|
+
const targetPath = path.join(platform.folder, relPath);
|
|
205
|
+
if (!fs.existsSync(targetPath)) continue;
|
|
206
|
+
|
|
207
|
+
const key = tracker.keyFor(targetPath);
|
|
208
|
+
const recorded = ownership.files?.[key];
|
|
209
|
+
const currentHash = sha256(fs.readFileSync(targetPath));
|
|
210
|
+
if (recorded?.sha256 === currentHash) {
|
|
211
|
+
if (!ctx.dryRun) {
|
|
212
|
+
fs.rmSync(targetPath, { force: true });
|
|
213
|
+
tracker.prune(key);
|
|
214
|
+
}
|
|
215
|
+
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete agent: ${targetPath}`);
|
|
216
|
+
ctx.results.updated++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
ctx.ui.warn(`Preserved user-owned obsolete agent: ${targetPath}`);
|
|
221
|
+
ctx.results.preserved++;
|
|
222
|
+
ctx.results.preservedFiles.push(targetPath);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
135
226
|
/** Install CLAUDE.md at the project root (ownership-aware). Claude only. */
|
|
136
227
|
function copyClaudeMdFile(ctx, platformKey) {
|
|
137
228
|
if (platformKey !== 'claude') return;
|
|
@@ -152,6 +243,11 @@ function copyClaudeMdFile(ctx, platformKey) {
|
|
|
152
243
|
const template = fs.readFileSync(src, 'utf8');
|
|
153
244
|
const exists = fs.existsSync(dest);
|
|
154
245
|
const existing = exists ? fs.readFileSync(dest, 'utf8') : '';
|
|
246
|
+
if (managedClaudeRange(existing) === false) {
|
|
247
|
+
ctx.ui.warn(`CLAUDE.md: malformed CafeKit CLAUDE marker topology; preserved ${dest} without writing`);
|
|
248
|
+
ctx.results.errors++;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
155
251
|
const legacyRecord = ctx.ownership?.[PLATFORMS.claude.folder]?.files?.['../CLAUDE.md'];
|
|
156
252
|
const legacyOwned = Boolean(legacyRecord && legacyRecord.sha256 === sha256(existing));
|
|
157
253
|
const next = upsertManagedClaudeBlock(existing, template, legacyOwned);
|
|
@@ -187,6 +283,9 @@ module.exports = {
|
|
|
187
283
|
copyClaudeRuntimeFiles,
|
|
188
284
|
removeObsoleteClaudeRuntimeFiles,
|
|
189
285
|
copyClaudeMdFile,
|
|
286
|
+
copyClaudeAgentsMdFile,
|
|
287
|
+
ensureSharedAgentsMdCore,
|
|
190
288
|
copyRulesDirectory,
|
|
289
|
+
removeObsoleteAgents,
|
|
191
290
|
transformManagedClaudeContent
|
|
192
291
|
};
|