@haposoft/cafekit 0.16.0 → 0.16.2

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 (66) hide show
  1. package/README.md +5 -3
  2. package/bin/__tests__/codex-hooks.test.js +125 -0
  3. package/bin/__tests__/codex-native.test.js +95 -0
  4. package/bin/__tests__/develop-contract.test.js +43 -1
  5. package/bin/__tests__/grok-envelope.test.js +271 -0
  6. package/bin/__tests__/grok-platform.test.js +133 -0
  7. package/bin/__tests__/hook-payload.test.js +194 -0
  8. package/bin/__tests__/omp-bridge.test.js +168 -0
  9. package/bin/__tests__/omp-hooks.test.js +178 -0
  10. package/bin/__tests__/omp-runtime.test.js +150 -0
  11. package/bin/__tests__/orca-session.test.js +225 -0
  12. package/bin/__tests__/orca-skill.test.js +210 -0
  13. package/bin/__tests__/package-inventory.test.js +1 -0
  14. package/bin/__tests__/runtime-dir.test.js +158 -0
  15. package/bin/__tests__/runtime-schema.test.js +5 -3
  16. package/bin/__tests__/specs-v2-execution-closeout.test.js +28 -1
  17. package/bin/install.js +5 -5
  18. package/bin/lib/context.js +50 -3
  19. package/bin/lib/i18n.js +3 -30
  20. package/bin/phases/codex-hooks.js +90 -17
  21. package/bin/phases/copy-payload.js +7 -0
  22. package/bin/phases/omp-runtime.js +140 -0
  23. package/bin/phases/root-config.js +3 -0
  24. package/bin/phases/select-platform.js +22 -1
  25. package/bin/phases/summary.js +0 -4
  26. package/package.json +1 -1
  27. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +2 -0
  28. package/src/claude/hooks/__tests__/spec-gate.test.js +338 -0
  29. package/src/claude/hooks/__tests__/state.test.js +2 -0
  30. package/src/claude/hooks/agent.cjs +12 -7
  31. package/src/claude/hooks/completion-authority.cjs +10 -4
  32. package/src/claude/hooks/docs-sync.cjs +2 -1
  33. package/src/claude/hooks/inspect-block.cjs +27 -7
  34. package/src/claude/hooks/lib/config.cjs +5 -2
  35. package/src/claude/hooks/lib/counter.cjs +7 -5
  36. package/src/claude/hooks/lib/hook-payload.cjs +160 -0
  37. package/src/claude/hooks/lib/hook-state-dir.cjs +2 -2
  38. package/src/claude/hooks/lib/runtime-dir.cjs +49 -0
  39. package/src/claude/hooks/precompact.cjs +2 -1
  40. package/src/claude/hooks/privacy-block.cjs +4 -2
  41. package/src/claude/hooks/rules.cjs +4 -2
  42. package/src/claude/hooks/secret-output-guardrail.cjs +2 -1
  43. package/src/claude/hooks/semantic-review-authority.cjs +6 -4
  44. package/src/claude/hooks/session.cjs +16 -8
  45. package/src/claude/hooks/spec-gate.cjs +18 -8
  46. package/src/claude/hooks/spec-state.cjs +9 -5
  47. package/src/claude/hooks/state.cjs +6 -4
  48. package/src/claude/hooks/task-scaffold-guard.cjs +29 -8
  49. package/src/claude/hooks/usage.cjs +5 -4
  50. package/src/claude/migration-manifest.json +3 -0
  51. package/src/claude/rules/skill-domain-routing.md +1 -0
  52. package/src/claude/rules/state-sync.md +7 -1
  53. package/src/claude/rules/workflow.md +2 -0
  54. package/src/claude/scripts/provenance.cjs +11 -4
  55. package/src/claude/scripts/spec-receipt.cjs +65 -4
  56. package/src/claude/skills/orca/SKILL.md +51 -0
  57. package/src/claude/skills/specs/SKILL.md +10 -11
  58. package/src/codex/hooks/session.cjs +6 -1
  59. package/src/codex/hooks/spec-state.cjs +1 -1
  60. package/src/codex/hooks.json +18 -18
  61. package/src/codex/rules/state-sync.md +7 -1
  62. package/src/omp/extensions/cafekit-bridge.mjs +233 -0
  63. package/src/omp/hooks/privacy-block.cjs +250 -0
  64. package/src/omp/runtime.json +33 -0
  65. package/bin/lib/rtk-run.js +0 -28
  66. package/bin/phases/setup-rtk.js +0 -159
package/bin/lib/i18n.js CHANGED
@@ -62,16 +62,6 @@ const MESSAGES = {
62
62
  documentSkillsMetadataRecovery: 'Could not read {path}; preserving document skills to avoid destructive cleanup.',
63
63
  skillsSkipped: 'Skill dependencies skipped.\n Run later: npx @haposoft/cafekit --with-skills-deps',
64
64
 
65
- // rtk token-saver setup
66
- rtkConfirm: 'Install the rtk token-saver? (compacts git/grep/ls/build/test output for Claude Code Bash commands)',
67
- rtkInstalling: 'Installing rtk via {method}',
68
- rtkInstalled: 'rtk installed',
69
- rtkInstallFailed: 'Could not install rtk — skipping (non-fatal). Install manually later: https://github.com/rtk-ai/rtk',
70
- rtkInitFailed: 'rtk init -g failed: {reason} — hook not registered (non-fatal)',
71
- rtkNeedsJq: 'rtk hook needs jq to run. Install jq, then the hook activates automatically.',
72
- rtkFailed: 'rtk setup error: {reason} — skipped (non-fatal)',
73
- rtkSkipped: 'rtk token-saver skipped.\n Run later: npx @haposoft/cafekit --with-rtk',
74
- rtkSummary: 'rtk token-saver: hook registered for Claude Code Bash commands',
75
65
  venvCreating: 'Setting up Python environment',
76
66
  venvReady: 'Python environment ready',
77
67
  venvFailed: 'Could not create Python environment — check your Python 3 installation.',
@@ -130,6 +120,7 @@ const MESSAGES = {
130
120
 
131
121
  // Platform/language auto-skip when already installed
132
122
  installingFor: 'Installing for: {names}',
123
+ grokCompatNotice: 'Grok CLI reads this install through its Claude compatibility. Trust the project first: run `grok --trust` or `/hooks-trust`, or its project hooks are skipped.',
133
124
  langKept: 'Language: {lang} (saved — skipping prompt)',
134
125
  platformKept: 'Platform: {names} (saved — skipping prompt)',
135
126
 
@@ -186,16 +177,6 @@ const MESSAGES = {
186
177
  documentSkillsMetadataRecovery: '{path} を読み取れないため、破壊的な削除を避けて document skills を保持します。',
187
178
  skillsSkipped: 'スキルの依存関係をスキップしました。\n 後で実行: npx @haposoft/cafekit --with-skills-deps',
188
179
 
189
- // rtk token-saver setup
190
- rtkConfirm: 'rtk トークンセーバーをインストールしますか?(Claude Code の Bash コマンドの git/grep/ls/build/test 出力を圧縮します)',
191
- rtkInstalling: '{method} で rtk をインストール中',
192
- rtkInstalled: 'rtk をインストールしました',
193
- rtkInstallFailed: 'rtk をインストールできませんでした — スキップします(致命的ではありません)。後で手動でインストール: https://github.com/rtk-ai/rtk',
194
- rtkInitFailed: 'rtk init -g が失敗しました: {reason} — フックは未登録です(致命的ではありません)',
195
- rtkNeedsJq: 'rtk フックの実行には jq が必要です。jq をインストールするとフックが自動的に有効になります。',
196
- rtkFailed: 'rtk セットアップエラー: {reason} — スキップしました(致命的ではありません)',
197
- rtkSkipped: 'rtk トークンセーバーをスキップしました。\n 後で実行: npx @haposoft/cafekit --with-rtk',
198
- rtkSummary: 'rtk トークンセーバー: Claude Code の Bash コマンド用にフックを登録しました',
199
180
  venvCreating: 'Python 環境を準備しています',
200
181
  venvReady: 'Python 環境の準備ができました',
201
182
  venvFailed: 'Python 環境を作成できませんでした — Python 3 のインストールを確認してください。',
@@ -251,6 +232,7 @@ const MESSAGES = {
251
232
 
252
233
  // Platform/language auto-skip when already installed
253
234
  installingFor: 'インストール先: {names}',
235
+ grokCompatNotice: 'Grok CLI はこのインストールを Claude 互換として読み込みます。先にプロジェクトを信頼してください: `grok --trust` または `/hooks-trust`。信頼しないとプロジェクトフックはスキップされます。',
254
236
  langKept: '言語: {lang} (保存済み — スキップ)',
255
237
  platformKept: 'プラットフォーム: {names} (保存済み — スキップ)',
256
238
 
@@ -307,16 +289,6 @@ const MESSAGES = {
307
289
  documentSkillsMetadataRecovery: 'Không đọc được {path}; giữ nguyên skill tài liệu để tránh dọn nhầm dữ liệu.',
308
290
  skillsSkipped: 'Đã bỏ qua dependencies skill.\n Cài sau bằng lệnh: npx @haposoft/cafekit --with-skills-deps',
309
291
 
310
- // rtk token-saver setup
311
- rtkConfirm: 'Cài rtk token-saver? (rút gọn output git/grep/ls/build/test cho các lệnh Bash của Claude Code)',
312
- rtkInstalling: 'Đang cài rtk qua {method}',
313
- rtkInstalled: 'Đã cài rtk',
314
- rtkInstallFailed: 'Không thể cài rtk — bỏ qua (không nghiêm trọng). Cài thủ công sau: https://github.com/rtk-ai/rtk',
315
- rtkInitFailed: 'rtk init -g thất bại: {reason} — hook chưa được đăng ký (không nghiêm trọng)',
316
- rtkNeedsJq: 'Hook của rtk cần jq để chạy. Cài jq thì hook sẽ tự kích hoạt.',
317
- rtkFailed: 'Lỗi cài rtk: {reason} — đã bỏ qua (không nghiêm trọng)',
318
- rtkSkipped: 'Đã bỏ qua rtk token-saver.\n Cài sau bằng lệnh: npx @haposoft/cafekit --with-rtk',
319
- rtkSummary: 'rtk token-saver: đã đăng ký hook cho các lệnh Bash của Claude Code',
320
292
  venvCreating: 'Đang thiết lập môi trường Python',
321
293
  venvReady: 'Môi trường Python đã sẵn sàng',
322
294
  venvFailed: 'Không thể tạo môi trường Python — hãy kiểm tra cài đặt Python 3.',
@@ -372,6 +344,7 @@ const MESSAGES = {
372
344
 
373
345
  // Platform/language auto-skip when already installed
374
346
  installingFor: 'Cài cho: {names}',
347
+ grokCompatNotice: 'Grok CLI đọc bản cài này qua lớp tương thích Claude. Hãy trust project trước: chạy `grok --trust` hoặc `/hooks-trust`, nếu không project hooks sẽ bị bỏ qua.',
375
348
  langKept: 'Ngôn ngữ: {lang} (đã lưu — bỏ qua chọn lại)',
376
349
  platformKept: 'Nền tảng: {names} (đã lưu — bỏ qua chọn lại)',
377
350
 
@@ -16,7 +16,9 @@ const path = require('path');
16
16
  const { PLATFORMS } = require('../lib/context');
17
17
 
18
18
  const CODEX_SRC = path.join(__dirname, '../../src/codex');
19
- const WINDOWS_HOOK_COMMAND = /^node "\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
19
+ const HOOK_COMMAND_TEMPLATE = /^node "\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
20
+ // What an installer before 0.16.2 wrote for POSIX. Kept only to recognize and repair it.
21
+ const LEGACY_GIT_COMMAND = /^node "\$\(git rev-parse --show-toplevel\)\/\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
20
22
 
21
23
  /** The hook script a command runs, which is stable across installs and platforms. */
22
24
  function hookScript(command) {
@@ -25,21 +27,81 @@ function hookScript(command) {
25
27
  return match ? match[1] : null;
26
28
  }
27
29
 
30
+ /** Single quotes suppress every shell expansion, so only a literal quote needs escaping. */
31
+ function shellQuote(value) {
32
+ return `'${value.replace(/'/g, `'\\''`)}'`;
33
+ }
34
+
35
+ function hookPathFor(script, projectRoot) {
36
+ return path.join(fs.realpathSync(projectRoot), '.codex', 'hooks', script);
37
+ }
38
+
28
39
  /**
29
- * Rewrite a managed `commandWindows` into a path-encoded invocation. Absolute Windows
30
- * paths cannot be quoted safely inside the Codex command string, so the path travels
31
- * base64url and is decoded by the launcher.
40
+ * The POSIX launcher carries the installed path literally.
41
+ *
42
+ * It used to resolve its own root with `$(git rev-parse --show-toplevel)`, which returns
43
+ * nothing outside a repository and the *outer* root for a project nested inside one. Both
44
+ * cases produced a path that does not exist, so Codex started every hook, every hook died,
45
+ * and the gates were silently absent. The path is known at install time, so it is written
46
+ * directly. The literal `/hooks/<script>.cjs` segment must survive, because hookScript()
47
+ * reads it as the identity that lets a reinstall recognize CafeKit's own entries.
32
48
  */
33
- function materializeWindowsCommand(handler, projectRoot) {
34
- if (typeof handler.commandWindows !== 'string') return handler;
35
- const match = handler.commandWindows.match(WINDOWS_HOOK_COMMAND);
36
- if (!match) throw new Error(`Unsupported Codex Windows hook command: ${handler.commandWindows}`);
37
- const hookPath = path.join(fs.realpathSync(projectRoot), '.codex', 'hooks', match[1]);
38
- const encoded = Buffer.from(hookPath, 'utf8').toString('base64url');
39
- return {
40
- ...handler,
41
- commandWindows: 'node -e "process.argv[1]=Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\');require(\'module\').runMain()" ' + encoded,
42
- };
49
+ function posixHookCommand(script, projectRoot) {
50
+ return `node ${shellQuote(hookPathFor(script, projectRoot))}`;
51
+ }
52
+
53
+ /**
54
+ * Absolute Windows paths cannot be quoted safely inside the Codex command string, so that
55
+ * side travels base64url and is decoded by the launcher.
56
+ */
57
+ function windowsHookCommand(script, projectRoot) {
58
+ const encoded = Buffer.from(hookPathFor(script, projectRoot), 'utf8').toString('base64url');
59
+ return 'node -e "process.argv[1]=Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\');require(\'module\').runMain()" ' + encoded;
60
+ }
61
+
62
+ /** Rewrite a managed template handler into absolute launchers for both platforms. */
63
+ function materializeHookCommands(handler, projectRoot) {
64
+ const next = { ...handler };
65
+ for (const [field, build] of [['command', posixHookCommand], ['commandWindows', windowsHookCommand]]) {
66
+ if (typeof handler[field] !== 'string') continue;
67
+ const match = handler[field].match(HOOK_COMMAND_TEMPLATE);
68
+ if (!match) throw new Error(`Unsupported Codex hook ${field}: ${handler[field]}`);
69
+ next[field] = build(match[1], projectRoot);
70
+ }
71
+ return next;
72
+ }
73
+
74
+ /**
75
+ * Repair launchers an older installer wrote. A reinstall treats an already-registered
76
+ * script as the user's placement and leaves it alone, so without this pass every existing
77
+ * project would keep its broken git-derived command forever. Only the exact byte pattern
78
+ * CafeKit itself emitted is rewritten; anything else stays untouched.
79
+ */
80
+ function repairLegacyCodexLaunchers(config, projectRoot) {
81
+ if (!config.hooks) return { config, repaired: 0 };
82
+ let repaired = 0;
83
+ const hooks = {};
84
+ for (const [eventName, groups] of Object.entries(config.hooks)) {
85
+ if (!Array.isArray(groups)) { hooks[eventName] = groups; continue; }
86
+ hooks[eventName] = groups.map((group) => {
87
+ if (!Array.isArray(group?.hooks)) return group;
88
+ return {
89
+ ...group,
90
+ hooks: group.hooks.map((handler) => {
91
+ const legacy = typeof handler?.command === 'string' && handler.command.match(LEGACY_GIT_COMMAND);
92
+ const template = typeof handler?.commandWindows === 'string'
93
+ && handler.commandWindows.match(HOOK_COMMAND_TEMPLATE);
94
+ if (!legacy && !template) return handler;
95
+ const next = { ...handler };
96
+ if (legacy) next.command = posixHookCommand(legacy[1], projectRoot);
97
+ if (template) next.commandWindows = windowsHookCommand(template[1], projectRoot);
98
+ repaired += 1;
99
+ return next;
100
+ }),
101
+ };
102
+ });
103
+ }
104
+ return { config: repaired > 0 ? { ...config, hooks } : config, repaired };
43
105
  }
44
106
 
45
107
  /** Drop CafeKit hooks the manifest has retired, leaving foreign entries untouched. */
@@ -96,12 +158,18 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
96
158
  }
97
159
  }
98
160
 
99
- const { config: base, removed } = pruneObsoleteCodexHooks({ ...existing }, ctx);
161
+ const { config: pruned, removed } = pruneObsoleteCodexHooks({ ...existing }, ctx);
100
162
  if (removed > 0) {
101
163
  ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Codex hooks: removed ${removed} obsolete hook(s)`);
102
164
  ctx.results.updated++;
103
165
  }
104
166
 
167
+ const { config: base, repaired } = repairLegacyCodexLaunchers(pruned, projectRoot);
168
+ if (repaired > 0) {
169
+ ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Codex hooks: repaired ${repaired} launcher(s) that resolved their path through Git`);
170
+ ctx.results.updated++;
171
+ }
172
+
105
173
  const merged = { ...base };
106
174
  if (typeof managed.description === 'string' && typeof merged.description !== 'string') {
107
175
  merged.description = managed.description;
@@ -124,7 +192,7 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
124
192
  const script = hookScript(handler?.command);
125
193
  return script && !registered.has(script);
126
194
  })
127
- .map((handler) => materializeWindowsCommand(handler, projectRoot));
195
+ .map((handler) => materializeHookCommands(handler, projectRoot));
128
196
  if (wanted.length === 0) continue;
129
197
 
130
198
  const matcher = managedGroup.matcher || '';
@@ -153,4 +221,9 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
153
221
  }
154
222
  }
155
223
 
156
- module.exports = { mergeCodexHooks, pruneObsoleteCodexHooks, hookScript };
224
+ module.exports = {
225
+ mergeCodexHooks,
226
+ pruneObsoleteCodexHooks,
227
+ repairLegacyCodexLaunchers,
228
+ hookScript,
229
+ };
@@ -189,7 +189,14 @@ function copyPlatformFiles(ctx, platformKey) {
189
189
  } else {
190
190
  report(ctx, 'missing', `agents: ${platform.agentsDir}`);
191
191
  }
192
+ }
192
193
 
194
+ // ── Scripts ─────────────────────────────────────────────
195
+ // Gated on its own capability. This block used to sit inside the agents guard,
196
+ // so a platform with agents: false and scripts: true (omp) never received
197
+ // .omp/scripts/, and every Stop-time gate that requires ../scripts/*.cjs answered
198
+ // "Completion gate unavailable" on each session_stop.
199
+ if (hasPlatformCapability(platformKey, 'scripts')) {
193
200
  const scriptsSourceDir = path.join(SRC, 'claude/scripts');
194
201
  if (fs.existsSync(scriptsSourceDir)) {
195
202
  const scriptsDest = getRuntimeSupportTargetDir(platformKey, 'scripts');
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Phase: Oh My Pi runtime.
5
+ *
6
+ * omp discovers `.claude/skills` and `.agents/skills` on its own, so CafeKit copies no
7
+ * skill payload here. What omp does not have is CafeKit's enforcement chain, so this
8
+ * phase provisions what carries it:
9
+ *
10
+ * .omp/hooks/ the gate scripts: the Claude set, with omp's overlay on top
11
+ * .omp/extensions/ the bridge omp auto-loads, which dispatches those scripts
12
+ * .omp/runtime.json the hooks' configuration, beside the schema that describes it
13
+ * .omp/rules/ the rules the hooks inject
14
+ *
15
+ * The hooks are portable: they derive their runtime directory from their own location,
16
+ * so the Claude bytes run unchanged under `.omp/`. Only the files that genuinely differ
17
+ * for omp live in `src/omp/hooks/` and are written over the Claude set afterwards, so a
18
+ * change to a shared hook reaches omp without anyone remembering to re-fork it.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { PLATFORMS } = require('../lib/context');
24
+ const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
25
+ const { report, treeAction } = require('./report');
26
+ const { copyRulesDirectory } = require('./claude-runtime');
27
+
28
+ const SRC = path.join(__dirname, '../../src');
29
+ const CLAUDE_SRC = path.join(SRC, 'claude');
30
+ const OMP_SRC = path.join(SRC, 'omp');
31
+
32
+ /** Files under `src/omp/hooks/` that replace their Claude counterpart, relative to the hooks dir. */
33
+ function overlayFiles(dir = path.join(OMP_SRC, 'hooks'), prefix = '') {
34
+ if (!fs.existsSync(dir)) return [];
35
+ const found = [];
36
+ for (const name of fs.readdirSync(dir)) {
37
+ const rel = prefix ? `${prefix}/${name}` : name;
38
+ if (fs.statSync(path.join(dir, name)).isDirectory()) found.push(...overlayFiles(path.join(dir, name), rel));
39
+ else found.push(rel);
40
+ }
41
+ return found.sort();
42
+ }
43
+
44
+ /**
45
+ * The Claude hook set is the list the migration manifest ships to `.claude/`, not a
46
+ * directory walk: the source tree also holds `__tests__/`, which must never install.
47
+ */
48
+ function claudeHookFiles(ctx) {
49
+ const files = ctx.manifest?.runtime?.files || [];
50
+ return files.filter((rel) => rel.startsWith('hooks/')).map((rel) => rel.slice('hooks/'.length));
51
+ }
52
+
53
+ function writeSourceFile(ctx, platformKey, src, dest, label) {
54
+ const platform = PLATFORMS[platformKey];
55
+ const { action } = writeManagedFile({
56
+ src,
57
+ dest,
58
+ platformFolder: platform.folder,
59
+ ctx,
60
+ tracker: ctx.trackers[platformKey]
61
+ });
62
+ report(ctx, action, label);
63
+ }
64
+
65
+ function installHooks(ctx, platformKey) {
66
+ const platform = PLATFORMS[platformKey];
67
+ const overlay = new Set(overlayFiles());
68
+ // Claude set first, skipping anything the overlay is about to replace so a file is
69
+ // written once and the manifest records the bytes that actually land.
70
+ for (const rel of claudeHookFiles(ctx)) {
71
+ if (overlay.has(rel)) continue;
72
+ writeSourceFile(
73
+ ctx,
74
+ platformKey,
75
+ path.join(CLAUDE_SRC, 'hooks', rel),
76
+ path.join(platform.folder, 'hooks', rel),
77
+ `omp hooks: ${rel}`
78
+ );
79
+ }
80
+ for (const rel of overlay) {
81
+ writeSourceFile(
82
+ ctx,
83
+ platformKey,
84
+ path.join(OMP_SRC, 'hooks', rel),
85
+ path.join(platform.folder, 'hooks', rel),
86
+ `omp hooks (overlay): ${rel}`
87
+ );
88
+ }
89
+ }
90
+
91
+ function installRuntimeFiles(ctx, platformKey) {
92
+ const platform = PLATFORMS[platformKey];
93
+ writeSourceFile(
94
+ ctx,
95
+ platformKey,
96
+ path.join(OMP_SRC, 'runtime.json'),
97
+ path.join(platform.folder, 'runtime.json'),
98
+ 'omp runtime: runtime.json'
99
+ );
100
+ // One canonical runtime schema lives in the Claude source tree; every installed
101
+ // runtime references it as ./runtime.schema.json beside its runtime.json.
102
+ writeSourceFile(
103
+ ctx,
104
+ platformKey,
105
+ path.join(CLAUDE_SRC, 'runtime.schema.json'),
106
+ path.join(platform.folder, 'runtime.schema.json'),
107
+ 'omp runtime schema'
108
+ );
109
+ }
110
+
111
+ function installBridge(ctx, platformKey) {
112
+ const platform = PLATFORMS[platformKey];
113
+ const bridgeSource = path.join(OMP_SRC, 'extensions');
114
+ const bridgeDest = path.join(platform.folder, 'extensions');
115
+ if (fs.existsSync(bridgeSource)) {
116
+ const bridge = copyManagedTree({
117
+ src: bridgeSource,
118
+ dest: bridgeDest,
119
+ platformFolder: platform.folder,
120
+ ctx,
121
+ tracker: ctx.trackers[platformKey]
122
+ });
123
+ report(ctx, treeAction(bridge), 'omp bridge extension');
124
+ } else if (!ctx.dryRun) {
125
+ // A half-provisioned install is visibly empty rather than silently absent.
126
+ fs.mkdirSync(bridgeDest, { recursive: true });
127
+ ctx.ui.detail(' ✓ omp extensions directory created (bridge not present)');
128
+ }
129
+ }
130
+
131
+ function installOmpRuntime(ctx, platformKey) {
132
+ if (platformKey !== 'omp') return;
133
+ installHooks(ctx, platformKey);
134
+ installRuntimeFiles(ctx, platformKey);
135
+ installBridge(ctx, platformKey);
136
+ copyRulesDirectory(ctx, platformKey);
137
+ return ctx;
138
+ }
139
+
140
+ module.exports = { installOmpRuntime, overlayFiles, claudeHookFiles };
@@ -110,6 +110,9 @@ function ensureGitignore(ctx) {
110
110
  // Local runtime payload — reinstall with npx; keep out of git
111
111
  '.claude/',
112
112
  '.codex/',
113
+ // omp auto-executes every file in .omp/extensions/, so a committed copy
114
+ // would run arbitrary code on clone. Keep it out of git like the rest.
115
+ '.omp/',
113
116
  '.agents/'
114
117
  ];
115
118
  const prefix = ctx.dryRun ? '[dry-run] ' : '';
@@ -11,7 +11,8 @@ const path = require('path');
11
11
  const {
12
12
  PLATFORMS,
13
13
  detectPlatforms,
14
- getPlatformKeys
14
+ getPlatformKeys,
15
+ resolvePlatformAliases
15
16
  } = require('../lib/context');
16
17
  const { SUPPORTED, LANGUAGE_LABELS, OTHER_LABEL } = require('../lib/i18n');
17
18
 
@@ -36,7 +37,25 @@ function getInstalledLocale(platformKeys = getPlatformKeys()) {
36
37
  }
37
38
 
38
39
  /** First step: pick the installer/UI language (interactive only). */
40
+ /**
41
+ * Resolve host aliases on the requested platform list, once, before anything reads it.
42
+ *
43
+ * selectLanguage() filters this list through PLATFORMS to decide which installed runtime
44
+ * to take a saved locale from, so an unresolved alias would empty that list and fall back
45
+ * to scanning every platform folder — `--platform grok` in a project that also has
46
+ * `.codex/` would then silently take Codex's language.
47
+ */
48
+ function applyPlatformAliases(ctx) {
49
+ const requested = ctx.options.platforms || [];
50
+ if (requested.length === 0) return ctx;
51
+ const { platforms, aliased } = resolvePlatformAliases(requested);
52
+ ctx.options.platforms = platforms;
53
+ ctx.aliasedHost = aliased;
54
+ return ctx;
55
+ }
56
+
39
57
  async function selectLanguage(ctx) {
58
+ applyPlatformAliases(ctx);
40
59
  // Restore the saved locale BEFORE the interactivity check: a non-interactive
41
60
  // upgrade (--yes) must not forget the configured language. Skipping this left
42
61
  // ctx at the 'en' default, and patchRuntimeLocale then clobbered the user's
@@ -161,6 +180,7 @@ async function resolvePlatforms(ctx) {
161
180
  ctx.ui.info(ctx.t('installingFor', {
162
181
  names: platformNames(requested)
163
182
  }));
183
+ if (ctx.aliasedHost) ctx.ui.info(ctx.t('grokCompatNotice'));
164
184
  reportInstallMode(ctx);
165
185
  return ctx;
166
186
  }
@@ -244,6 +264,7 @@ function getInstalledPlatform() {
244
264
 
245
265
  module.exports = {
246
266
  selectLanguage,
267
+ applyPlatformAliases,
247
268
  promptPlatformSelection,
248
269
  resolvePlatforms,
249
270
  getInstalledPlatform,
@@ -59,10 +59,6 @@ function printSummary(ctx) {
59
59
  }[key];
60
60
  if (messageKey) lines.push(t(messageKey));
61
61
  }
62
- // rtk token-saver result (set by phases/setup-rtk.js when setup completed).
63
- if (ctx.rtkSetupRan) {
64
- lines.push(t('rtkSummary'));
65
- }
66
62
  const keySkills = skillsNeedingKeys(platforms);
67
63
  if (keySkills.length > 0) {
68
64
  lines.push(t('nsKeys', { skills: keySkills.join(', ') }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Native spec-driven workflows for Claude Code and Codex CLI. Bundles CafeKit skills, runtime hooks, agents, and installer scaffolding.",
5
5
  "author": "Haposoft <nghialt@haposoft.com>",
6
6
  "license": "MIT",
@@ -30,6 +30,8 @@ function installSemanticRuntime(target, runtime) {
30
30
  for (const name of ['semantic-review-authority.cjs', 'completion-authority-state.cjs']) {
31
31
  fs.copyFileSync(path.join(PACKAGE_ROOT, `src/${runtime}/hooks`, name), path.join(hooks, name));
32
32
  }
33
+ fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-dir.cjs'), path.join(hooks, 'lib/runtime-dir.cjs'));
34
+ fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/hook-payload.cjs'), path.join(hooks, 'lib/hook-payload.cjs'));
33
35
  fs.copyFileSync(
34
36
  path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
35
37
  path.join(hooks, 'lib/runtime-path-safety.cjs'),