@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
@@ -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
- if (start === -1) return null;
34
- const endStart = content.indexOf(CLAUDE_END, start + CLAUDE_START.length);
35
- if (endStart === -1) return null;
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
- const block = managedClaudeBlock(template);
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
  };
@@ -8,6 +8,7 @@ const { report, treeAction } = require('./report');
8
8
  const { copyRulesDirectory } = require('./claude-runtime');
9
9
  const {
10
10
  normalizeCodexBody,
11
+ managedRange,
11
12
  upsertManagedCodexBlock
12
13
  } = require('../lib/codex-install');
13
14
 
@@ -20,6 +21,33 @@ const CODEX_OWN_RUNTIME = [
20
21
  ['hooks.json', 'hooks.json']
21
22
  ];
22
23
 
24
+ const WINDOWS_HOOK_COMMAND = /^node "\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
25
+
26
+ function materializeWindowsHookCommands(content, projectRoot = process.cwd()) {
27
+ const config = JSON.parse(content);
28
+ const canonicalRoot = fs.realpathSync(projectRoot);
29
+
30
+ for (const groups of Object.values(config.hooks || {})) {
31
+ for (const group of groups) {
32
+ for (const handler of group.hooks || []) {
33
+ if (typeof handler.commandWindows !== 'string') continue;
34
+ const match = handler.commandWindows.match(WINDOWS_HOOK_COMMAND);
35
+ if (!match) {
36
+ throw new Error(`Unsupported Codex Windows hook command: ${handler.commandWindows}`);
37
+ }
38
+ const hookPath = path.join(canonicalRoot, '.codex', 'hooks', match[1]);
39
+ const encodedPath = Buffer.from(hookPath, 'utf8').toString('base64url');
40
+ handler.commandWindows = (
41
+ 'node -e "require(Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\'))" ' +
42
+ encodedPath
43
+ );
44
+ }
45
+ }
46
+ }
47
+
48
+ return `${JSON.stringify(config, null, 2)}\n`;
49
+ }
50
+
23
51
  function writeSourceFile(ctx, platformKey, src, dest, label, transform) {
24
52
  const platform = PLATFORMS[platformKey];
25
53
  const { action } = writeManagedFile({
@@ -41,7 +69,10 @@ function installRuntimeFiles(ctx, platformKey) {
41
69
  platformKey,
42
70
  path.join(CODEX_SRC, sourceRel),
43
71
  path.join(platform.folder, targetRel),
44
- `Codex runtime: ${targetRel}`
72
+ `Codex runtime: ${targetRel}`,
73
+ sourceRel === 'hooks.json'
74
+ ? (content) => materializeWindowsHookCommands(content, process.cwd())
75
+ : undefined
45
76
  );
46
77
  }
47
78
  writeSourceFile(
@@ -77,8 +108,8 @@ function installNativeRuleOverrides(ctx, platformKey) {
77
108
  }
78
109
 
79
110
  function installManagedAgentsMd(ctx) {
80
- const source = path.join(CODEX_SRC, 'AGENTS.md');
81
- if (!fs.existsSync(source)) {
111
+ const runtimeSource = path.join(CODEX_SRC, 'AGENTS.md');
112
+ if (!fs.existsSync(runtimeSource)) {
82
113
  report(ctx, 'missing', 'Codex AGENTS.md template');
83
114
  return;
84
115
  }
@@ -86,9 +117,12 @@ function installManagedAgentsMd(ctx) {
86
117
  const destination = 'AGENTS.md';
87
118
  const exists = fs.existsSync(destination);
88
119
  const existing = exists ? fs.readFileSync(destination, 'utf8') : '';
89
- const block = normalizeCodexBody(fs.readFileSync(source, 'utf8'))
90
- .replace(/^# CLAUDE\.md\s*$/m, '# CafeKit for Codex CLI')
91
- .trim();
120
+ if (managedRange(existing) === false) {
121
+ ctx.ui.warn(`AGENTS.md: malformed CafeKit CODEX marker topology; preserved ${destination} without writing`);
122
+ ctx.results.errors++;
123
+ return;
124
+ }
125
+ const block = normalizeCodexBody(fs.readFileSync(runtimeSource, 'utf8'));
92
126
  const next = upsertManagedCodexBlock(existing, block);
93
127
  const action = !exists ? 'created' : next === existing ? 'unchanged' : 'updated';
94
128
 
@@ -107,4 +141,4 @@ function installCodexRuntime(ctx, platformKey) {
107
141
  return ctx;
108
142
  }
109
143
 
110
- module.exports = { installCodexRuntime };
144
+ module.exports = { installCodexRuntime, materializeWindowsHookCommands };
@@ -15,9 +15,10 @@ const fs = require('fs');
15
15
  const path = require('path');
16
16
  const { PLATFORMS } = require('../lib/context');
17
17
  const { LANGUAGE_LABELS } = require('../lib/i18n');
18
- const { setupOpenCodeModel } = require('../lib/opencode-install');
18
+ const { setupOpenCodeModel, transformManagedOpenCodeContent } = require('../lib/opencode-install');
19
19
  const { transformManagedCodexContent } = require('../lib/codex-install');
20
20
  const { transformManagedClaudeContent } = require('./claude-runtime');
21
+ const { transformManagedCoreContent } = require('../lib/instruction-blocks');
21
22
  const ASSISTANT_NAMES = {
22
23
  claude: 'Claude Code',
23
24
  codex: 'Codex CLI',
@@ -48,6 +49,10 @@ function instructionTargets(ctx) {
48
49
  codex: {
49
50
  file: path.join(process.cwd(), 'AGENTS.md'),
50
51
  transform: transformManagedCodexContent
52
+ },
53
+ opencode: {
54
+ file: path.join(process.cwd(), 'AGENTS.md'),
55
+ transform: transformManagedOpenCodeContent
51
56
  }
52
57
  };
53
58
  return Object.keys(targets)
@@ -84,44 +89,44 @@ function patchSettingsLanguage(ctx) {
84
89
  }
85
90
 
86
91
  // Use locale (freeform label) so "Korean" shows correctly, not just "en".
87
- const label = ctx.locale || LANGUAGE_LABELS[ctx.lang] || ctx.lang;
88
- if (settings.language === label) return;
92
+ const label = ctx.locale;
93
+ if (!label) return;
89
94
  settings.language = label;
90
95
  fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
91
96
  }
92
97
 
93
98
  /**
94
- * Patch the "## Language Consistency" section in CLAUDE.md with the chosen
95
- * language so every AI session knows which language to respond in — without
96
- * relying on hooks reading runtime.json.
97
- *
98
- * The section is identified by a stable marker comment so subsequent installs
99
- * can update it idempotently.
99
+ * Patch the "## Language Consistency" section in the shared AGENTS.md core
100
+ * block so every installed platform uses the chosen response language.
100
101
  */
101
102
  function patchLanguageSection(ctx) {
102
- // Skip when locale is English (default — no override needed in CLAUDE.md).
103
- const locale = ctx.locale || ctx.lang;
103
+ // No explicit/saved locale means follow the user's language; do not inject an
104
+ // English override into the shared core block.
105
+ const locale = ctx.locale;
104
106
  if (!locale || locale === 'en' || locale === 'English') return;
105
107
 
106
108
  // Use locale directly when set (e.g. "Korean"), or map from lang code.
107
109
  const label = locale in LANGUAGE_LABEL_BY_CODE ? LANGUAGE_LABEL_BY_CODE[locale] : locale;
108
-
109
110
  const newSection = `## Language Consistency <!-- cafekit:lang -->
110
111
 
111
- Always respond in **${label}**. Technical terms, code identifiers, and file paths may remain in English, but all explanations, comments directed at the user, and structured output (specs, docs, reports) must be in ${label}.
112
+ Always respond in **${label}**. Technical terms, code identifiers, and file paths may remain in English, but explanations, comments directed at the user, and structured output must be in ${label}.
112
113
 
113
114
  `;
114
-
115
- // Replace only inside the installer-owned block.
116
115
  const markerRe = /## Language Consistency <!-- cafekit:lang -->[\s\S]*?(?=\n##|\n*$)/;
117
116
  const genericRe = /## Language Consistency\n[\s\S]*?(?=\n##|\n*$)/;
118
- for (const target of instructionTargets(ctx)) {
119
- updateInstructionTarget(target, (managed) => {
117
+ const target = path.join(process.cwd(), 'AGENTS.md');
118
+
119
+ updateInstructionTarget(
120
+ {
121
+ file: target,
122
+ transform: transformManagedCoreContent
123
+ },
124
+ (managed) => {
120
125
  if (markerRe.test(managed)) return managed.replace(markerRe, newSection);
121
126
  if (genericRe.test(managed)) return managed.replace(genericRe, newSection);
122
127
  return `${managed.trimEnd()}\n\n${newSection}\n`;
123
- });
124
- }
128
+ }
129
+ );
125
130
  }
126
131
 
127
132
  function patchRuntimeLocale(ctx) {
@@ -136,7 +141,9 @@ function patchRuntimeLocale(ctx) {
136
141
  }
137
142
  data.locale = data.locale || {};
138
143
  // Use locale (freeform label) so custom languages propagate to the AI hook.
139
- const locale = ctx.locale || ctx.lang;
144
+ // A null locale intentionally leaves the runtime's current value untouched.
145
+ const locale = ctx.locale;
146
+ if (!locale) continue;
140
147
  if (data.locale.responseLanguage === locale) continue;
141
148
  // Hardening: never downgrade an existing configured label to a bare default
142
149
  // code when this run never made an explicit language choice (ctx.locale
@@ -181,7 +188,7 @@ ${name} always addresses the user as "${userAddress}" throughout the conversatio
181
188
  }
182
189
 
183
190
  async function setupAddressing(ctx) {
184
- if (!ctx.platforms.some((key) => key === 'claude' || key === 'codex')) return;
191
+ if (!ctx.platforms.some((key) => key === 'claude' || key === 'codex' || key === 'opencode')) return;
185
192
 
186
193
  let existingName = null;
187
194
  for (const target of instructionTargets(ctx)) {
@@ -243,7 +250,7 @@ async function runPostInstall(ctx) {
243
250
 
244
251
  await setupAddressing(ctx);
245
252
 
246
- // Patch Language Consistency section in CLAUDE.md so AI responds in the chosen language.
253
+ // Patch Language Consistency section in the shared AGENTS.md core block.
247
254
  patchLanguageSection(ctx);
248
255
 
249
256
  // Persist chosen language into each platform's runtime.json (records in tracker).
@@ -24,13 +24,87 @@ function hasPattern(lines, pattern) {
24
24
  return lines.includes(pattern) || lines.includes(bare) || lines.includes(withSlash);
25
25
  }
26
26
 
27
+ const PLAN_PATTERNS = new Set([
28
+ 'plans',
29
+ 'plans/',
30
+ 'plans/*',
31
+ 'plans/**',
32
+ 'plans/**/*',
33
+ '!plans/*.md',
34
+ '!plans/templates',
35
+ '!plans/templates/',
36
+ '!plans/templates/*',
37
+ '!plans/templates/**',
38
+ '!plans/templates/**/*'
39
+ ]);
40
+
41
+ function isManagedPattern(line, patterns) {
42
+ return patterns.has(line.trim());
43
+ }
44
+
45
+ function isTrivia(line) {
46
+ const trimmed = line.trim();
47
+ return trimmed === '' || trimmed.startsWith('#');
48
+ }
49
+
50
+ function migrateManagedPlanPatterns(lines, header, patterns) {
51
+ const headerIndex = lines.findIndex((line) => line.trim() === header);
52
+ if (headerIndex < 0) return false;
53
+
54
+ const managedPatterns = new Set([...patterns, ...PLAN_PATTERNS]);
55
+ const blockStart = headerIndex + 1;
56
+ let blockEnd = blockStart;
57
+ let pendingTriviaStart = null;
58
+ let sawManagedPattern = false;
59
+
60
+ for (let index = blockStart; index < lines.length; index++) {
61
+ if (isManagedPattern(lines[index], managedPatterns)) {
62
+ sawManagedPattern = true;
63
+ pendingTriviaStart = null;
64
+ blockEnd = index + 1;
65
+ continue;
66
+ }
67
+ if (isTrivia(lines[index])) {
68
+ if (pendingTriviaStart === null) pendingTriviaStart = index;
69
+ continue;
70
+ }
71
+ blockEnd = pendingTriviaStart ?? index;
72
+ break;
73
+ }
74
+
75
+ if (!sawManagedPattern) return false;
76
+ if (blockEnd === blockStart && pendingTriviaStart !== null) return false;
77
+
78
+ const block = lines.slice(blockStart, blockEnd);
79
+ const existingPlanPatterns = block
80
+ .filter((line) => PLAN_PATTERNS.has(line.trim()))
81
+ .map((line) => line.trim());
82
+ const canonicalPlanPatterns = patterns.slice(1, 5);
83
+ if (
84
+ existingPlanPatterns.length === canonicalPlanPatterns.length &&
85
+ existingPlanPatterns.every((pattern, index) => pattern === canonicalPlanPatterns[index])
86
+ ) {
87
+ return false;
88
+ }
89
+
90
+ const firstPlanIndex = block.findIndex((line) => PLAN_PATTERNS.has(line.trim()));
91
+ if (firstPlanIndex < 0) return false;
92
+
93
+ const withoutPlans = block.filter((line) => !PLAN_PATTERNS.has(line.trim()));
94
+ withoutPlans.splice(firstPlanIndex, 0, ...canonicalPlanPatterns);
95
+ lines.splice(blockStart, block.length, ...withoutPlans);
96
+ return true;
97
+ }
98
+
27
99
  function ensureGitignore(ctx) {
28
100
  const gitignorePath = path.join(process.cwd(), '.gitignore');
29
101
  const header = '# CafeKit / Ecosystem';
30
102
  const patterns = [
31
103
  'specs/_shared/',
32
- 'plans/',
104
+ 'plans/*',
105
+ '!plans/*.md',
33
106
  '!plans/templates/',
107
+ '!plans/templates/**',
34
108
  '.cafekit-backup/',
35
109
  '.cafekit.lock',
36
110
  // Local runtime payload — reinstall with npx; keep out of git
@@ -50,16 +124,21 @@ function ensureGitignore(ctx) {
50
124
  }
51
125
 
52
126
  const content = fs.readFileSync(gitignorePath, 'utf8');
53
- const lines = content.split('\n').map((l) => l.trim());
54
- const missing = patterns.filter((p) => !hasPattern(lines, p));
55
-
56
- if (missing.length > 0) {
57
- let newContent = content;
58
- if (!newContent.endsWith('\n')) newContent += '\n';
59
- if (!content.includes(header)) newContent += `\n${header}\n`;
60
- newContent += missing.join('\n') + '\n';
127
+ const lines = content.split('\n');
128
+ const migrated = migrateManagedPlanPatterns(lines, header, patterns);
129
+ const normalizedLines = lines.map((line) => line.trim());
130
+ const missing = patterns.filter((p) => !hasPattern(normalizedLines, p));
131
+
132
+ if (migrated || missing.length > 0) {
133
+ let newContent = lines.join('\n');
134
+ if (missing.length > 0) {
135
+ if (!newContent.endsWith('\n')) newContent += '\n';
136
+ if (!content.includes(header)) newContent += `\n${header}\n`;
137
+ newContent += missing.join('\n') + '\n';
138
+ }
61
139
  if (!ctx.dryRun) fs.writeFileSync(gitignorePath, newContent, 'utf8');
62
- ctx.ui.detail(` ↻ ${prefix}.gitignore updated: added ${missing.join(', ')}`);
140
+ const details = migrated ? 'normalized managed plans patterns' : `added ${missing.join(', ')}`;
141
+ ctx.ui.detail(` ↻ ${prefix}.gitignore updated: ${details}`);
63
142
  ctx.results.updated++;
64
143
  } else {
65
144
  ctx.ui.detail(` → ${prefix}.gitignore already up to date`);
@@ -128,6 +128,19 @@ function cancelSelection(ctx) {
128
128
  return [];
129
129
  }
130
130
 
131
+ async function promptAddMorePlatforms(ctx, savedPlatforms) {
132
+ const allKeys = getPlatformKeys();
133
+ const available = allKeys.filter((key) => !savedPlatforms.includes(key));
134
+ if (available.length === 0) return false;
135
+
136
+ const r = await ctx.ui.confirm(
137
+ { message: ctx.t('addPlatformsPrompt', { names: platformNames(savedPlatforms) }), initialValue: false },
138
+ false
139
+ );
140
+ if (ctx.ui.isCancel(r)) return false;
141
+ return r;
142
+ }
143
+
131
144
  async function promptPlatforms(ctx) {
132
145
  const platforms = await promptPlatformSelection(ctx);
133
146
  return platforms.length > 0 ? platforms : cancelSelection(ctx);
@@ -164,6 +177,16 @@ async function resolvePlatforms(ctx) {
164
177
  platforms = await promptPlatforms(ctx);
165
178
  if (ctx.cancelled) return ctx;
166
179
  }
180
+ } else if (ctx.interactive) {
181
+ // No newly detected platforms, but user might want to ADD more platforms
182
+ const wantAdd = await promptAddMorePlatforms(ctx, savedPlatforms);
183
+ if (wantAdd) {
184
+ const added = await promptPlatformSelection(ctx);
185
+ if (ctx.cancelled) return ctx;
186
+ if (added.length > 0) {
187
+ platforms = [...new Set([...platforms, ...added])];
188
+ }
189
+ }
167
190
  }
168
191
  ctx.platforms = platforms;
169
192
  ctx.ui.info(ctx.t('platformKept', { names: platformNames(platforms) }));
@@ -79,12 +79,16 @@ async function setupNode(ctx, skillsDir) {
79
79
  const extraEnv = (skill === 'chrome-devtools' && systemChrome)
80
80
  ? { PUPPETEER_SKIP_DOWNLOAD: 'true' }
81
81
  : {};
82
- const ok = await dep.npmInstall(dir, hasNodeModules, extraEnv);
82
+ const result = await dep.npmInstall(dir, hasNodeModules, extraEnv);
83
83
 
84
- if (ok) {
84
+ if (result.ok) {
85
85
  ctx.ui.stopSpinner(ctx.t('npmInstalled', { skill }));
86
86
  } else {
87
- ctx.ui.stopSpinner(ctx.t('npmFailed', { skill, dir }));
87
+ ctx.ui.stopSpinner(ctx.t('npmFailed', {
88
+ skill,
89
+ dir,
90
+ error: dep.commandFailureReason(result)
91
+ }));
88
92
  continue;
89
93
  }
90
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.15.1",
3
+ "version": "0.16.0-rc.1",
4
4
  "description": "Native spec-driven workflows for Claude Code, Codex CLI, and OpenCode. Bundles CafeKit skills, runtime hooks, agents, and installer scaffolding.",
5
5
  "author": "Haposoft <nghialt@haposoft.com>",
6
6
  "license": "MIT",
@@ -9,15 +9,25 @@
9
9
  "cafekit": "bin/install.js"
10
10
  },
11
11
  "scripts": {
12
- "test": "node scripts/run-skill-self-tests.mjs"
12
+ "test": "node scripts/run-skill-self-tests.mjs",
13
+ "test:package": "node --test bin/__tests__/package-inventory.test.js",
14
+ "test:secrets": "node --test bin/__tests__/secret-scanner.test.js",
15
+ "test:policy": "node --test bin/__tests__/develop-contract.test.js"
13
16
  },
14
17
  "files": [
15
18
  "bin",
16
19
  "src",
17
20
  "README.md",
21
+ "!src/**/.logs/**",
22
+ "!src/**/*.log",
18
23
  "!src/**/.coverage",
19
- "!src/**/__pycache__",
20
- "!src/**/*.pyc"
24
+ "!src/**/.coverage/**",
25
+ "!src/**/__pycache__/**",
26
+ "!src/**/*.pyc",
27
+ "!src/**/.cache/**",
28
+ "!src/**/.state/**",
29
+ "!src/**/.tmp/**",
30
+ "!src/.codex/**"
21
31
  ],
22
32
  "repository": {
23
33
  "type": "git",
@@ -49,7 +59,7 @@
49
59
  "@opencode-ai/plugin": "1.15.11"
50
60
  },
51
61
  "dependencies": {
52
- "@clack/prompts": "^1.5.0",
62
+ "@clack/prompts": "0.11.0",
53
63
  "picocolors": "^1.1.1"
54
64
  }
55
65
  }