@haposoft/cafekit 0.15.1 → 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.
Files changed (99) hide show
  1. package/README.md +6 -2
  2. package/bin/__tests__/codex-hooks.test.js +140 -0
  3. package/bin/__tests__/codex-native.test.js +230 -2
  4. package/bin/__tests__/develop-contract.test.js +1120 -0
  5. package/bin/__tests__/installer-safety.test.js +553 -2
  6. package/bin/__tests__/package-inventory.test.js +258 -0
  7. package/bin/__tests__/secret-scanner.test.js +157 -0
  8. package/bin/__tests__/validator-grounding.test.js +713 -0
  9. package/bin/install.js +5 -1
  10. package/bin/lib/codex-install.js +21 -6
  11. package/bin/lib/context.js +17 -6
  12. package/bin/lib/copy-utils.js +31 -0
  13. package/bin/lib/i18n.js +12 -3
  14. package/bin/lib/instruction-blocks.js +85 -0
  15. package/bin/lib/managed-writer.js +4 -1
  16. package/bin/lib/opencode-install.js +75 -30
  17. package/bin/lib/skill-deps.js +45 -7
  18. package/bin/phases/claude-runtime.js +103 -4
  19. package/bin/phases/codex-runtime.js +41 -7
  20. package/bin/phases/post-install.js +29 -22
  21. package/bin/phases/root-config.js +89 -10
  22. package/bin/phases/select-platform.js +23 -0
  23. package/bin/phases/skills-setup.js +7 -3
  24. package/package.json +15 -5
  25. package/src/claude/CLAUDE.md +6 -122
  26. package/src/claude/agents/code-auditor.md +21 -22
  27. package/src/claude/agents/debugger.md +3 -3
  28. package/src/claude/agents/deployer.md +1 -1
  29. package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
  30. package/src/claude/agents/project-manager.md +1 -1
  31. package/src/claude/agents/spec-maker.md +2 -2
  32. package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
  33. package/src/claude/hooks/agent.cjs +7 -14
  34. package/src/claude/hooks/rules.cjs +33 -53
  35. package/src/claude/hooks/spec-gate.cjs +66 -21
  36. package/src/claude/hooks/spec-state.cjs +5 -0
  37. package/src/claude/migration-manifest.json +9 -2
  38. package/src/claude/runtime.json +0 -3
  39. package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
  40. package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
  41. package/src/claude/scripts/spec-ground.cjs +189 -83
  42. package/src/claude/scripts/validate-spec-output.cjs +294 -30
  43. package/src/claude/scripts/workflow-policy.cjs +609 -0
  44. package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
  45. package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
  46. package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
  47. package/src/claude/skills/backend-development/SKILL.md +4 -4
  48. package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
  49. package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
  50. package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
  51. package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
  52. package/src/claude/skills/backend-development/references/backend-security.md +3 -3
  53. package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
  54. package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
  55. package/src/claude/skills/brainstorm/SKILL.md +2 -3
  56. package/src/claude/skills/chrome-devtools/scripts/package-lock.json +261 -150
  57. package/src/claude/skills/chrome-devtools/scripts/package.json +1 -1
  58. package/src/claude/skills/code-review/SKILL.md +12 -8
  59. package/src/claude/skills/code-review/references/verification-gate.md +4 -4
  60. package/src/claude/skills/debug/SKILL.md +2 -0
  61. package/src/claude/skills/develop/SKILL.md +100 -52
  62. package/src/claude/skills/develop/references/parallel-waves.md +105 -26
  63. package/src/claude/skills/develop/references/quality-gate.md +58 -38
  64. package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
  65. package/src/claude/skills/git/SKILL.md +44 -50
  66. package/src/claude/skills/hotfix/SKILL.md +10 -8
  67. package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
  68. package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
  69. package/src/claude/skills/inspect/SKILL.md +12 -30
  70. package/src/claude/skills/mobile-development/SKILL.md +12 -12
  71. package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
  72. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
  73. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
  74. package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
  75. package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
  76. package/src/claude/skills/pptx/scripts/package-lock.json +868 -0
  77. package/src/claude/skills/pptx/scripts/package.json +1 -1
  78. package/src/claude/skills/question/SKILL.md +13 -13
  79. package/src/claude/skills/research/SKILL.md +2 -2
  80. package/src/claude/skills/specs/SKILL.md +32 -15
  81. package/src/claude/skills/specs/references/review.md +3 -3
  82. package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
  83. package/src/claude/skills/specs/templates/spec-state.json +7 -3
  84. package/src/claude/skills/sync/SKILL.md +26 -1
  85. package/src/claude/skills/sync/references/sync-protocols.md +3 -3
  86. package/src/claude/skills/test/SKILL.md +12 -4
  87. package/src/claude/skills/test/references/failure-triage.md +17 -17
  88. package/src/claude/skills/test/references/test-memory.md +2 -2
  89. package/src/codex/AGENTS.md +16 -24
  90. package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
  91. package/src/codex/hooks/rules.cjs +27 -26
  92. package/src/codex/hooks/spec-gate.cjs +19 -8
  93. package/src/codex/hooks/spec-state.cjs +10 -1
  94. package/src/codex/hooks.json +14 -14
  95. package/src/common/AGENTS.md +43 -0
  96. package/src/opencode/AGENTS.md +21 -121
  97. package/src/opencode/plugins/rules.ts +26 -34
  98. package/src/opencode/plugins/spec-gate.ts +283 -0
  99. 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
- copyRulesDirectory
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
  }
@@ -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
- 'god-developer',
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
- .replace(/\.claude\/skills\b/g, '.agents/skills')
68
- .replace(/\.claude\\skills\b/g, '.agents\\skills')
69
- .replace(/\.claude(?=[/\\])/g, '.codex')
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
- const block = `${CODEX_BLOCK_START}\n${String(blockContent).trim()}\n${CODEX_BLOCK_END}`;
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
  };
@@ -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 (an interactive prompt may change it).
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
- // ctx.locale = the string that gets written to CLAUDE.md / runtime.json as the AI's response language.
311
- // For known codes it mirrors lang; for "other", it's set later by setLang.
312
- const locale = options.lang || 'en';
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,
@@ -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
  };
package/bin/lib/i18n.js CHANGED
@@ -79,7 +79,7 @@ const MESSAGES = {
79
79
  pipFailed: '{skill} — some packages failed. Retry:\n {cmd}',
80
80
  npmInstalling: 'Installing Node packages: {skill}',
81
81
  npmInstalled: '{skill} — Node packages ready',
82
- npmFailed: '{skill} — npm install failed. Check the log and retry in: {dir}',
82
+ npmFailed: '{skill} — npm install failed ({error}). Retry in: {dir}',
83
83
  playwrightInstalling: 'Downloading Playwright browser for {skill}',
84
84
  playwrightReady: '{skill} — Playwright browser ready',
85
85
  playwrightSkipped: '{skill} — Playwright browser download skipped',
@@ -132,6 +132,9 @@ const MESSAGES = {
132
132
  langKept: 'Language: {lang} (saved — skipping prompt)',
133
133
  platformKept: 'Platform: {names} (saved — skipping prompt)',
134
134
 
135
+ // Add more platforms prompt (interactive, prior install exists)
136
+ addPlatformsPrompt: 'Existing platforms: {names}. Add more?',
137
+
135
138
  // Version picker
136
139
  versionPickPrompt: 'Select CafeKit version to install:',
137
140
  versionPickCurrent: '(installed)',
@@ -199,7 +202,7 @@ const MESSAGES = {
199
202
  pipFailed: '{skill} — 一部のパッケージが失敗しました。再試行:\n {cmd}',
200
203
  npmInstalling: 'Node パッケージをインストール中: {skill}',
201
204
  npmInstalled: '{skill} — Node パッケージの準備ができました',
202
- npmFailed: '{skill} — npm install に失敗しました。{dir} で手動再試行してください。',
205
+ npmFailed: '{skill} — npm install に失敗しました ({error})。{dir} で手動再試行してください。',
203
206
  playwrightInstalling: '{skill} 用の Playwright ブラウザをダウンロード中',
204
207
  playwrightReady: '{skill} — Playwright ブラウザの準備ができました',
205
208
  playwrightSkipped: '{skill} — Playwright ブラウザのダウンロードをスキップしました',
@@ -249,6 +252,9 @@ const MESSAGES = {
249
252
  langKept: '言語: {lang} (保存済み — スキップ)',
250
253
  platformKept: 'プラットフォーム: {names} (保存済み — スキップ)',
251
254
 
255
+ // Add more platforms prompt (interactive, prior install exists)
256
+ addPlatformsPrompt: '既存プラットフォーム: {names}。さらに追加しますか?',
257
+
252
258
  // Version picker
253
259
  versionPickPrompt: 'インストールする CafeKit バージョンを選択:',
254
260
  versionPickCurrent: '(インストール済み)',
@@ -316,7 +322,7 @@ const MESSAGES = {
316
322
  pipFailed: '{skill} — một số gói cài thất bại. Chạy lại thủ công:\n {cmd}',
317
323
  npmInstalling: 'Đang cài gói Node: {skill}',
318
324
  npmInstalled: '{skill} — gói Node đã sẵn sàng',
319
- npmFailed: '{skill} — npm install thất bại. Hãy thử lại trong: {dir}',
325
+ npmFailed: '{skill} — npm install thất bại ({error}). Hãy thử lại trong: {dir}',
320
326
  playwrightInstalling: 'Đang tải Playwright browser cho {skill}',
321
327
  playwrightReady: '{skill} — Playwright browser đã sẵn sàng',
322
328
  playwrightSkipped: '{skill} — đã bỏ qua tải Playwright browser',
@@ -366,6 +372,9 @@ const MESSAGES = {
366
372
  langKept: 'Ngôn ngữ: {lang} (đã lưu — bỏ qua chọn lại)',
367
373
  platformKept: 'Nền tảng: {names} (đã lưu — bỏ qua chọn lại)',
368
374
 
375
+ // Add more platforms prompt (interactive, prior install exists)
376
+ addPlatformsPrompt: 'Nền tảng hiện có: {names}. Thêm nền tảng khác?',
377
+
369
378
  // Version picker
370
379
  versionPickPrompt: 'Chọn phiên bản CafeKit để cài:',
371
380
  versionPickCurrent: '(đang cài)',
@@ -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: 'god-developer',
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: 'god-developer',
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: 'god-developer',
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: 'god-developer',
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: 'god-developer',
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
- .replace(/\.claude\/(agents|commands|skills|rules|scripts|references|hooks)\b/g, '.opencode/$1')
273
- .replace(/\.claude\\(agents|commands|skills|rules|scripts|references|hooks)\b/g, '.opencode\\$1')
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
- const managedBlock = `${start}\n${blockContent.trim()}\n${end}`;
416
- const blockPattern = new RegExp(`${start}[\\s\\S]*?${end}`);
417
-
418
- if (blockPattern.test(existingContent)) {
419
- return existingContent.replace(blockPattern, managedBlock);
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
- if (fs.existsSync(openCodeTemplate)) {
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
- const sourceContent = fs.readFileSync(sourcePath, 'utf8');
559
- fs.writeFileSync(destPath, sourceContent, 'utf8');
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,
@@ -17,6 +17,19 @@ const { spawnSync, spawn } = require('child_process');
17
17
 
18
18
  const isWindows = process.platform === 'win32';
19
19
 
20
+ /**
21
+ * npm and npx are `.cmd` launchers on Windows, so Node cannot spawn them
22
+ * directly without a command interpreter. Keep the shell boundary explicit;
23
+ * all arguments passed here are installer-owned constants.
24
+ */
25
+ function resolvePackageCommand(command, args, platform = process.platform, env = process.env) {
26
+ if (platform !== 'win32') return { command, args };
27
+ return {
28
+ command: env.ComSpec || env.COMSPEC || 'cmd.exe',
29
+ args: ['/d', '/s', '/c', `${command}.cmd`, ...args]
30
+ };
31
+ }
32
+
20
33
  /** Run a command synchronously, return { ok, stdout, stderr, status }. Never throws. */
21
34
  function run(cmd, args, opts = {}) {
22
35
  const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts });
@@ -48,11 +61,32 @@ function runAsync(cmd, args, opts = {}) {
48
61
  });
49
62
  });
50
63
  child.on('error', (err) => {
51
- resolve({ ok: false, status: null, stdout, stderr: stderr || err.message });
64
+ resolve({
65
+ ok: false,
66
+ status: null,
67
+ stdout,
68
+ stderr: stderr || err.message,
69
+ errorCode: err.code || ''
70
+ });
52
71
  });
53
72
  });
54
73
  }
55
74
 
75
+ /** Return one actionable line without flooding the installer UI with npm output. */
76
+ function commandFailureReason(result) {
77
+ const lines = `${result.stderr || ''}\n${result.stdout || ''}`
78
+ .split(/\r?\n/)
79
+ .map((line) => line.trim())
80
+ .filter(Boolean);
81
+ return lines.find((line) => (
82
+ /\bE[A-Z0-9_]{2,}\b/.test(line) || /\bnpm error code\b/i.test(line)
83
+ ))
84
+ || lines.find((line) => /^npm error\b/i.test(line))
85
+ || lines.at(-1)
86
+ || result.errorCode
87
+ || `exit ${result.status ?? 'unknown'}`;
88
+ }
89
+
56
90
  /** Is a command available on PATH? */
57
91
  function hasCmd(name) {
58
92
  const probe = isWindows ? run('where', [name]) : run('which', [name]);
@@ -104,12 +138,12 @@ async function pipInstall(skillsDir, requirementsPath, upgrade = false) {
104
138
  return r.ok;
105
139
  }
106
140
 
107
- /** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. Async — keeps spinner alive. */
141
+ /** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. */
108
142
  async function npmInstall(dir, upgrade = false, extraEnv = {}) {
109
143
  const args = upgrade ? ['update', '--no-audit', '--no-fund'] : ['install', '--no-audit', '--no-fund'];
110
144
  const env = { ...process.env, ...extraEnv };
111
- const r = await runAsync('npm', args, { cwd: dir, env });
112
- return r.ok;
145
+ const npm = resolvePackageCommand('npm', args);
146
+ return runAsync(npm.command, npm.args, { cwd: dir, env });
113
147
  }
114
148
 
115
149
  /**
@@ -130,10 +164,11 @@ async function installChromium(scriptsDir, interactive = false) {
130
164
  const r = await runAsync('node', [installMjs], { cwd: scriptsDir });
131
165
  return r.ok;
132
166
  }
167
+ const npx = resolvePackageCommand('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome']);
133
168
  if (interactive) {
134
- return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir, stdio: 'inherit' }).ok;
169
+ return run(npx.command, npx.args, { cwd: scriptsDir, stdio: 'inherit' }).ok;
135
170
  }
136
- const r = await runAsync('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir });
171
+ const r = await runAsync(npx.command, npx.args, { cwd: scriptsDir });
137
172
  return r.ok;
138
173
  }
139
174
 
@@ -149,7 +184,8 @@ function pkgHasDep(scriptsDir, name) {
149
184
 
150
185
  /** Download the Playwright Chromium browser (for the pptx html2pptx workflow). Async — keeps spinner alive. */
151
186
  async function installPlaywrightBrowser(scriptsDir) {
152
- const r = await runAsync('npx', ['--yes', 'playwright', 'install', 'chromium'], { cwd: scriptsDir });
187
+ const npx = resolvePackageCommand('npx', ['--yes', 'playwright', 'install', 'chromium']);
188
+ const r = await runAsync(npx.command, npx.args, { cwd: scriptsDir });
153
189
  return r.ok;
154
190
  }
155
191
 
@@ -255,6 +291,8 @@ function systemHint(tool) {
255
291
 
256
292
  module.exports = {
257
293
  isWindows,
294
+ resolvePackageCommand,
295
+ commandFailureReason,
258
296
  hasCmd,
259
297
  findPython,
260
298
  venvDir,