imhcode 2.0.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/imhcode.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * imhcode agent list → List all discovered agents
13
13
  * imhcode agent inspect <id> → Show full manifest + loaded skills
14
14
  * imhcode agent run <id> "<task>" → Run agent (dry-run by default)
15
- * --engine <cli> → Override engine (claude|opencode|codex|agy|qwen|mimo)
15
+ * --engine <cli> → Override engine (claude|opencode|codex|codex-fugu|agy|qwen|mimo)
16
16
  * -m, --model <model> → Override model
17
17
  * --live → Call real LLM
18
18
  * --output <path> → Custom session output directory
@@ -40,6 +40,88 @@ const BRAINSTORM_MD = path.join(DOCS_DIR, 'brainstorming.md');
40
40
  const BRIEF_MD = path.join(DOCS_DIR, 'PROJECT_BRIEF.md');
41
41
  const CONTEXT_MD = path.join(LOCAL_DIR_NAME, 'context.md');
42
42
 
43
+ /**
44
+ * Ranked priority lists per category.
45
+ * The algorithm scores each available model against these lists
46
+ * and selects the highest-ranked match found in the installed engines.
47
+ */
48
+ const MODEL_PRIORITY_RANKS = {
49
+ frontend: [
50
+ // Engine Model substring to match (lowercase, no hyphens/dots)
51
+ ['mimo', 'mimov25pro'],
52
+ ['mimo', 'mimov25'],
53
+ ['opencode', 'mimov25pro'],
54
+ ['opencode', 'mimov25'],
55
+ ['codex', 'gpt55'],
56
+ ['codex', 'gpt5'],
57
+ ['agy', 'claudeopus46'],
58
+ ['agy', 'claudeopus'],
59
+ ['opencode', 'gemini35flash'],
60
+ ['opencode', 'gemini25pro'],
61
+ ['claude', 'claudeopus46'],
62
+ ['claude', 'opus'],
63
+ ],
64
+ backend: [
65
+ ['opencode', 'deepseekv4pro'],
66
+ ['opencode', 'deepseekv4'],
67
+ ['opencode', 'kimik27code'],
68
+ ['opencode', 'kimik2'],
69
+ ['opencode', 'qwen3coder480b'],
70
+ ['opencode', 'qwen3coder'],
71
+ ['codex', 'gpt55'],
72
+ ['codex', 'gpt5'],
73
+ ['opencode', 'gptoss120b'],
74
+ ['qwen', 'qwen3codermax'],
75
+ ['qwen', 'qwen3coder'],
76
+ ['claude', 'claudesonnet46'],
77
+ ['claude', 'sonnet'],
78
+ ],
79
+ planning: [
80
+ ['agy', 'claudeopus46'],
81
+ ['agy', 'claudeopus'],
82
+ ['claude', 'claudeopus46'],
83
+ ['claude', 'opus'],
84
+ ['codex', 'gpt55'],
85
+ ['codex', 'gpt5'],
86
+ ['opencode', 'gemini31propreview'],
87
+ ['opencode', 'gemini31pro'],
88
+ ['opencode', 'deepseekv4pro'],
89
+ ],
90
+ testing: [
91
+ ['codex-fugu', 'fuguultra'],
92
+ ['codex-fugu', 'fugu'],
93
+ ['codex', 'gpt55'],
94
+ ['codex', 'gpt5'],
95
+ ['agy', 'gptoss120bmedium'],
96
+ ['agy', 'gptoss120b'],
97
+ ['agy', 'claudeopus46'],
98
+ ['agy', 'claudeopus'],
99
+ ['opencode', 'deepseekv4pro'],
100
+ ['claude', 'claudeopus46'],
101
+ ],
102
+ review: [
103
+ ['codex-fugu', 'fuguultra'],
104
+ ['codex-fugu', 'fugu'],
105
+ ['codex', 'gpt55'],
106
+ ['codex', 'gpt5'],
107
+ ['agy', 'claudesonnet46'],
108
+ ['agy', 'claudesonnet'],
109
+ ['opencode', 'gptoss120b'],
110
+ ['claude', 'claudesonnet46'],
111
+ ['opencode', 'deepseekv4pro'],
112
+ ],
113
+ fast: [
114
+ ['opencode', 'deepseekv4flash'],
115
+ ['opencode', 'deepseekv4flash'],
116
+ ['opencode', 'gemini35flash'],
117
+ ['codex', 'gpt54mini'],
118
+ ['codex', 'gptmini'],
119
+ ['qwen', 'qwen3coderflash'],
120
+ ['claude', 'claudehaiku35'],
121
+ ['claude', 'haiku'],
122
+ ],
123
+ };
124
+
43
125
  // ─── Entry Point ───────────────────────────────────────────────────────────────
44
126
 
45
127
  const args = process.argv.slice(2);
@@ -155,7 +237,7 @@ function printGeneralHelp() {
155
237
  ${CLI_CMD} agent inspect <id> → Show agent manifest + loaded skills
156
238
  ${CLI_CMD} agent run <id> "<task>" → Run agent (dry-run by default)
157
239
  --live → Call real LLM
158
- --engine <cli> → Override engine (claude|opencode|codex|agy|qwen|mimo)
240
+ --engine <cli> → Override engine (claude|opencode|codex|codex-fugu|agy|qwen|mimo)
159
241
  -m, --model <model> → Override model
160
242
  --output <path> → Save session to custom path
161
243
 
@@ -1017,6 +1099,132 @@ async function runInit() {
1017
1099
  console.log(` Created docs/start.md (write your project here)`);
1018
1100
  }
1019
1101
 
1102
+ const globalConfigPath = path.join(GLOBAL_DIR, 'global.config.json');
1103
+ let useGlobal = false;
1104
+ let globalConfig = null;
1105
+
1106
+ if (fs.existsSync(globalConfigPath)) {
1107
+ try {
1108
+ globalConfig = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
1109
+ } catch {}
1110
+ }
1111
+
1112
+ const isInteractive = process.stdout.isTTY;
1113
+
1114
+ if (globalConfig && isInteractive) {
1115
+ console.log(`\n🕌 Found saved global default configuration:`);
1116
+ console.log(` Primary Engine: ${globalConfig.primary_engine} (${globalConfig.default_model || 'auto'})`);
1117
+ console.log(` Testing Route: ${globalConfig.model_routing?.testing?.model || 'auto'} via ${globalConfig.model_routing?.testing?.engine || 'auto'}`);
1118
+ console.log(` Review Route: ${globalConfig.model_routing?.review?.model || 'auto'} via ${globalConfig.model_routing?.review?.engine || 'auto'}`);
1119
+
1120
+ const ans = await askQuestion('\nUse default global engine and model routing? [Y/n] ');
1121
+ if (ans.toLowerCase() !== 'n' && ans.toLowerCase() !== 'no') {
1122
+ useGlobal = true;
1123
+ }
1124
+ } else if (globalConfig) {
1125
+ useGlobal = true;
1126
+ }
1127
+
1128
+ if (useGlobal && globalConfig) {
1129
+ console.log('\n✅ Applying global default configuration...');
1130
+
1131
+ const dirsToCreate = [
1132
+ DOCS_DIR,
1133
+ LOCAL_DIR_NAME,
1134
+ path.join(LOCAL_DIR_NAME, 'commands'),
1135
+ path.join(LOCAL_DIR_NAME, 'sessions'),
1136
+ ];
1137
+ for (const dir of dirsToCreate) {
1138
+ const dirPath = path.join(cwd, dir);
1139
+ if (!fs.existsSync(dirPath)) {
1140
+ fs.mkdirSync(dirPath, { recursive: true });
1141
+ console.log(` Created local directory: ${dir}`);
1142
+ }
1143
+ }
1144
+
1145
+ const startMdPath = path.join(cwd, START_MD);
1146
+ if (!fs.existsSync(startMdPath)) {
1147
+ const startMdContent = [
1148
+ '# 🕌 IMH-Code — Project Start',
1149
+ '',
1150
+ '> **Imam Hussain Coding Harness Platform**',
1151
+ '> Answer the scope questions below, write your description, then run `imhcode plan`.',
1152
+ '',
1153
+ '---',
1154
+ '',
1155
+ '## ⚡ Quick Scope Check',
1156
+ '',
1157
+ '**Do you need a backend API / server-side logic?**',
1158
+ '> **Answer:** yes *(change to: yes / no / unsure)*',
1159
+ '',
1160
+ '**Do you need a mobile app (iOS/Android)?**',
1161
+ '> **Answer:** no *(change to: yes / no)*',
1162
+ '',
1163
+ '---',
1164
+ '',
1165
+ '## 📝 Your Project Description',
1166
+ '',
1167
+ 'Write your complete project idea below. Be as detailed as possible.',
1168
+ 'Include: what you\'re building, who it\'s for, key features, preferred stack,',
1169
+ 'design preferences, integrations needed, business constraints.',
1170
+ '',
1171
+ '<!-- WRITE_PROMPT_HERE -->',
1172
+ 'I want to build a SaaS dashboard for managing hotel room bookings with real-time availability,',
1173
+ 'a Next.js frontend, Laravel API backend, and PostgreSQL database. Target users are hotel managers.',
1174
+ 'Key features: room calendar, booking CRUD, guest management, reporting, email notifications.',
1175
+ '<!-- END_PROMPT -->',
1176
+ '',
1177
+ '---',
1178
+ '',
1179
+ '## 🚀 Next Step',
1180
+ '',
1181
+ 'After filling in the scope and your description, run:',
1182
+ '',
1183
+ '```bash',
1184
+ 'imhcode plan',
1185
+ '```',
1186
+ '',
1187
+ 'IMH-Code will invoke your configured **planning AI model** (e.g. Claude Opus, GPT-5.5)',
1188
+ 'to generate `docs/brainstorming.md` with smart, project-specific questions and answers.',
1189
+ ].join('\n');
1190
+ fs.writeFileSync(startMdPath, startMdContent, 'utf8');
1191
+ console.log(` Created docs/start.md (write your project here)`);
1192
+ }
1193
+
1194
+ const localGitignore = path.join(cwd, '.gitignore');
1195
+ if (!fs.existsSync(localGitignore)) {
1196
+ const gitignoreTemplate = path.join(packageRoot, '.gitignore.template');
1197
+ if (fs.existsSync(gitignoreTemplate)) {
1198
+ fs.copyFileSync(gitignoreTemplate, localGitignore);
1199
+ console.log(` Created local .gitignore`);
1200
+ }
1201
+ }
1202
+
1203
+ registerCliGlobally(imhcodeScriptPath);
1204
+
1205
+ const configPath = path.join(cwd, CONFIG_FILE);
1206
+ fs.writeFileSync(configPath, JSON.stringify(globalConfig, null, 2), 'utf8');
1207
+ console.log(`\n💾 Configuration saved: ${configPath}`);
1208
+
1209
+ console.log(`\n✅ ${PLATFORM_NAME} initialized successfully!`);
1210
+ console.log(`─`.repeat(60));
1211
+ console.log(`\n🕌 HOW TO BUILD WITH IMH-CODE:\n`);
1212
+ console.log(` 1. Open docs/start.md → Answer scope questions + write your description`);
1213
+ console.log(` 2. Run: imhcode plan`);
1214
+ console.log(` → Your planning AI generates brainstorming.md`);
1215
+ console.log(` 3. Open docs/brainstorming.md → Review/edit AI-recommended answers`);
1216
+ console.log(` 4. Run: imhcode plan`);
1217
+ console.log(` → Your planning AI generates sprint plans with correct agent routing`);
1218
+ console.log(` 5. Run: imhcode execute 1 → Sprint 1 (frontend tasks)`);
1219
+ console.log(` 6. Run: imhcode execute 2 → Sprint 2 (backend tasks)`);
1220
+ console.log(` 7. Run: imhcode test → Final testing + security + SEO`);
1221
+ console.log(` 8. Run: imhcode report → Generate PROJECT_REPORT.md`);
1222
+ console.log(`\n Run "imhcode --help" for all commands.`);
1223
+ console.log(`─`.repeat(60));
1224
+ console.log('');
1225
+ return;
1226
+ }
1227
+
1020
1228
  // Create .gitignore
1021
1229
  const localGitignore = path.join(cwd, '.gitignore');
1022
1230
  if (!fs.existsSync(localGitignore)) {
@@ -1028,7 +1236,7 @@ async function runInit() {
1028
1236
  }
1029
1237
 
1030
1238
  registerCliGlobally(imhcodeScriptPath);
1031
- ensureCavemanAndGraphify();
1239
+ ensureCavemanAndGraphify(globalConfig?.skills_configured);
1032
1240
 
1033
1241
  // ── Interactive Engine & Model Setup ────────────────────────────────────────
1034
1242
  console.log('\n🔍 Scanning for local coding assistant CLIs...');
@@ -1037,12 +1245,11 @@ async function runInit() {
1037
1245
 
1038
1246
  if (foundEngines.length === 0) {
1039
1247
  console.warn('\n⚠️ No local coding assistant CLIs detected.');
1040
- console.warn(' Supported: claude, opencode, codex, agy (Antigravity), qwen (QwenCode), mimo (MimoCode)');
1248
+ console.warn(' Supported: claude, opencode, codex, codex-fugu, agy (Antigravity), qwen (QwenCode), mimo (MimoCode)');
1041
1249
  console.warn(' Install one and re-run "imhcode" to configure model routing.\n');
1042
1250
  }
1043
1251
 
1044
1252
  let primaryEngine = 'claude';
1045
- const isInteractive = process.stdout.isTTY;
1046
1253
 
1047
1254
  if (isInteractive && foundEngines.length > 0) {
1048
1255
  console.log('\nFound the following local coding assistant CLIs:');
@@ -1114,6 +1321,18 @@ async function runInit() {
1114
1321
  fs.writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf8');
1115
1322
  console.log(`\n💾 Configuration saved: ${configPath}`);
1116
1323
 
1324
+ // Save to global config defaults as well
1325
+ const globalConfigData = {
1326
+ ...configData,
1327
+ skills_configured: true,
1328
+ };
1329
+ try {
1330
+ fs.writeFileSync(globalConfigPath, JSON.stringify(globalConfigData, null, 2), 'utf8');
1331
+ console.log(`💾 Global defaults saved: ${globalConfigPath}`);
1332
+ } catch (e) {
1333
+ console.warn(`⚠️ Could not save global default configuration: ${e.message}`);
1334
+ }
1335
+
1117
1336
  // Print final guide
1118
1337
  console.log(`\n✅ ${PLATFORM_NAME} initialized successfully!`);
1119
1338
  console.log(`─`.repeat(60));
@@ -1132,87 +1351,8 @@ async function runInit() {
1132
1351
  console.log(`─`.repeat(60));
1133
1352
  console.log('');
1134
1353
  }
1135
-
1136
1354
  // ─── Fix 4: Model Routing Setup Wizard (Ranked Scoring Algorithm) ─────────────
1137
1355
 
1138
- /**
1139
- * Ranked priority lists per category.
1140
- * The algorithm scores each available model against these lists
1141
- * and selects the highest-ranked match found in the installed engines.
1142
- */
1143
- const MODEL_PRIORITY_RANKS = {
1144
- frontend: [
1145
- // Engine Model substring to match (lowercase, no hyphens/dots)
1146
- ['mimo', 'mimov25pro'],
1147
- ['mimo', 'mimov25'],
1148
- ['opencode', 'mimov25pro'],
1149
- ['opencode', 'mimov25'],
1150
- ['codex', 'gpt55'],
1151
- ['codex', 'gpt5'],
1152
- ['agy', 'claudeopus46'],
1153
- ['agy', 'claudeopus'],
1154
- ['opencode', 'gemini35flash'],
1155
- ['opencode', 'gemini25pro'],
1156
- ['claude', 'claudeopus46'],
1157
- ['claude', 'opus'],
1158
- ],
1159
- backend: [
1160
- ['opencode', 'deepseekv4pro'],
1161
- ['opencode', 'deepseekv4'],
1162
- ['opencode', 'kimik27code'],
1163
- ['opencode', 'kimik2'],
1164
- ['opencode', 'qwen3coder480b'],
1165
- ['opencode', 'qwen3coder'],
1166
- ['codex', 'gpt55'],
1167
- ['codex', 'gpt5'],
1168
- ['opencode', 'gptoss120b'],
1169
- ['qwen', 'qwen3codermax'],
1170
- ['qwen', 'qwen3coder'],
1171
- ['claude', 'claudesonnet46'],
1172
- ['claude', 'sonnet'],
1173
- ],
1174
- planning: [
1175
- ['agy', 'claudeopus46'],
1176
- ['agy', 'claudeopus'],
1177
- ['claude', 'claudeopus46'],
1178
- ['claude', 'opus'],
1179
- ['codex', 'gpt55'],
1180
- ['codex', 'gpt5'],
1181
- ['opencode', 'gemini31propreview'],
1182
- ['opencode', 'gemini31pro'],
1183
- ['opencode', 'deepseekv4pro'],
1184
- ],
1185
- testing: [
1186
- ['codex', 'gpt55'],
1187
- ['codex', 'gpt5'],
1188
- ['agy', 'gptoss120bmedium'],
1189
- ['agy', 'gptoss120b'],
1190
- ['agy', 'claudeopus46'],
1191
- ['agy', 'claudeopus'],
1192
- ['opencode', 'deepseekv4pro'],
1193
- ['claude', 'claudeopus46'],
1194
- ],
1195
- review: [
1196
- ['codex', 'gpt55'],
1197
- ['codex', 'gpt5'],
1198
- ['agy', 'claudesonnet46'],
1199
- ['agy', 'claudesonnet'],
1200
- ['opencode', 'gptoss120b'],
1201
- ['claude', 'claudesonnet46'],
1202
- ['opencode', 'deepseekv4pro'],
1203
- ],
1204
- fast: [
1205
- ['opencode', 'deepseekv4flash'],
1206
- ['opencode', 'deepseekv4flash'],
1207
- ['opencode', 'gemini35flash'],
1208
- ['codex', 'gpt54mini'],
1209
- ['codex', 'gptmini'],
1210
- ['qwen', 'qwen3coderflash'],
1211
- ['claude', 'claudehaiku35'],
1212
- ['claude', 'haiku'],
1213
- ],
1214
- };
1215
-
1216
1356
  /**
1217
1357
  * Normalize a model/engine string for fuzzy matching:
1218
1358
  * lowercase, remove hyphens, dots, underscores, spaces.
@@ -2377,15 +2517,37 @@ function detectSprintDocs(cwd) {
2377
2517
  return fs.readdirSync(docsDir).some(f => /^sprint-\d+$/i.test(f));
2378
2518
  }
2379
2519
 
2520
+ function isSprintComplete(cwd, sprintNum) {
2521
+ const progressPath = path.join(cwd, DOCS_DIR, `sprint-${sprintNum}`, 'progress.md');
2522
+ if (!fs.existsSync(progressPath)) return false;
2523
+ try {
2524
+ const content = fs.readFileSync(progressPath, 'utf8');
2525
+ return !/-\s*\[\s*\]/i.test(content);
2526
+ } catch {
2527
+ return false;
2528
+ }
2529
+ }
2530
+
2380
2531
  function detectCurrentSprint(cwd) {
2381
2532
  const docsDir = path.join(cwd, DOCS_DIR);
2382
2533
  if (!fs.existsSync(docsDir)) return 1;
2383
- let max = 0;
2534
+
2535
+ const sprintNums = [];
2384
2536
  for (const f of fs.readdirSync(docsDir)) {
2385
2537
  const m = f.match(/^sprint-(\d+)$/i);
2386
- if (m) max = Math.max(max, parseInt(m[1], 10));
2538
+ if (m) sprintNums.push(parseInt(m[1], 10));
2539
+ }
2540
+
2541
+ if (sprintNums.length === 0) return 1;
2542
+ sprintNums.sort((a, b) => a - b);
2543
+
2544
+ for (const num of sprintNums) {
2545
+ if (!isSprintComplete(cwd, num)) {
2546
+ return num;
2547
+ }
2387
2548
  }
2388
- return max || 1;
2549
+
2550
+ return sprintNums[sprintNums.length - 1];
2389
2551
  }
2390
2552
 
2391
2553
  function detectLastSprint(cwd) {
@@ -2611,11 +2773,40 @@ function scanAssistantCLIs() {
2611
2773
  claude: { name: 'Claude Code', path: resolveBinary('claude', ['~/.local/bin/claude', '/usr/local/bin/claude', '/opt/homebrew/bin/claude']), models: [], modelsCmd: b => `"${b}" --list-models 2>/dev/null || echo ''` },
2612
2774
  opencode: { name: 'OpenCode', path: resolveBinary('opencode', ['~/.opencode/bin/opencode', '/usr/local/bin/opencode']), models: [], modelsCmd: b => `"${b}" models 2>/dev/null` },
2613
2775
  codex: { name: 'Codex (OpenAI)', path: resolveBinary('codex', ['~/Library/PhpWebStudy/env/node/bin/codex', '/usr/local/bin/codex']), models: [], modelsCmd: b => `"${b}" debug models 2>/dev/null` },
2776
+ 'codex-fugu': { name: 'Codex Fugu', path: resolveBinary('codex-fugu', ['~/.local/bin/codex-fugu', '/usr/local/bin/codex-fugu', '/opt/homebrew/bin/codex-fugu']), models: [], modelsCmd: b => `"${b}" debug models 2>/dev/null || echo ''` },
2614
2777
  agy: { name: 'Antigravity CLI (agy)', path: resolveBinary('agy', ['~/.local/bin/agy', '/usr/local/bin/agy', '/opt/homebrew/bin/agy']), models: [], modelsCmd: b => `"${b}" models 2>/dev/null || echo ''` },
2615
2778
  qwen: { name: 'QwenCode (qwen)', path: resolveBinary('qwen', ['~/.local/bin/qwen', '/usr/local/bin/qwen', '/opt/homebrew/bin/qwen']), models: [], modelsCmd: b => `"${b}" models 2>/dev/null || echo ''` },
2616
2779
  mimo: { name: 'MimoCode (mimo)', path: resolveBinary('mimo', ['~/.local/bin/mimo', '/usr/local/bin/mimo', '/opt/homebrew/bin/mimo', '~/.mimo/bin/mimo']), models: [], modelsCmd: b => `"${b}" models 2>/dev/null || echo ''` },
2617
2780
  };
2618
2781
 
2782
+ // Scan for agy2, agy3, agy4, etc.
2783
+ for (let i = 2; i <= 10; i++) {
2784
+ const key = `agy${i}`;
2785
+ const binaryPath = resolveBinary(key, [
2786
+ `~/.local/bin/${key}`,
2787
+ `/usr/local/bin/${key}`,
2788
+ `/opt/homebrew/bin/${key}`,
2789
+ ]);
2790
+ if (binaryPath) {
2791
+ engines[key] = {
2792
+ name: `Antigravity CLI (${key})`,
2793
+ path: binaryPath,
2794
+ models: [],
2795
+ modelsCmd: b => `"${b}" models 2>/dev/null || echo ''`
2796
+ };
2797
+ }
2798
+ }
2799
+
2800
+ const fallbackModels = {
2801
+ claude: ['claude-3-5-sonnet', 'claude-3-opus', 'claude-3-5-haiku', 'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-3-5', 'claude-sonnet-5', 'claude-fable-5', 'claude-opus-4-8', 'opus', 'sonnet', 'haiku'],
2802
+ opencode: ['mimo-vl-v2.5-pro', 'deepseek-v4-pro', 'deepseek-v4-flash', 'gemini-3.5-flash', 'kimi-k2.7-code', 'qwen-3-coder-480b', 'gpt-oss-120b'],
2803
+ codex: ['gpt-5.5', 'gpt-5.4-mini', 'gpt-mini'],
2804
+ 'codex-fugu': ['fugu', 'fugu-ultra'],
2805
+ agy: ['claude-opus-4-6', 'gemini-3.5-flash'],
2806
+ qwen: ['qwen3-coder-max', 'qwen3-coder-flash'],
2807
+ mimo: ['mimo-vl-v2.5-pro', 'mimo-vl-v2.5']
2808
+ };
2809
+
2619
2810
  for (const [key, eng] of Object.entries(engines)) {
2620
2811
  if (!eng.path) continue;
2621
2812
  try {
@@ -2630,6 +2821,11 @@ function scanAssistantCLIs() {
2630
2821
  }
2631
2822
  if (models.length > 0) eng.models = models;
2632
2823
  } catch { eng.models = []; }
2824
+
2825
+ const baseKey = key.startsWith('agy') ? 'agy' : key;
2826
+ if (eng.models.length === 0 && fallbackModels[baseKey]) {
2827
+ eng.models = fallbackModels[baseKey];
2828
+ }
2633
2829
  }
2634
2830
 
2635
2831
  return engines;
@@ -2703,7 +2899,11 @@ function registerCliGlobally(imhcodeScriptPath) {
2703
2899
  }
2704
2900
  }
2705
2901
 
2706
- function ensureCavemanAndGraphify() {
2902
+ function ensureCavemanAndGraphify(skillsAlreadyConfigured) {
2903
+ if (skillsAlreadyConfigured) {
2904
+ console.log('\n🔍 Token-saving skills already configured globally. Skipping installation checks.');
2905
+ return;
2906
+ }
2707
2907
  console.log('\n🔍 Checking token-saving skills...');
2708
2908
  let skillsInstalled = false;
2709
2909
  try { execSync('npx --no-install skills --version', { stdio: 'ignore' }); skillsInstalled = true; } catch { /* not installed */ }
@@ -3063,9 +3263,9 @@ async function runTuiCommand() {
3063
3263
 
3064
3264
  // Step 2: Requirements
3065
3265
  if (state.hasConfig && !state.hasStart) {
3066
- items.push({ id: 'write', name: 'Write Requirements', desc: '⚠️ Edit docs/start.md (Required)', highlight: true });
3266
+ items.push({ id: 'write', name: 'Write Requirements', desc: '⚠️ Input requirements & scope (Required)', highlight: true });
3067
3267
  } else if (state.hasConfig) {
3068
- items.push({ id: 'write', name: 'Write Requirements', desc: 'Edit docs/start.md requirements' });
3268
+ items.push({ id: 'write', name: 'Write Requirements', desc: 'Input requirements & scope answers' });
3069
3269
  }
3070
3270
 
3071
3271
  // Step 3: Plan & Brainstorm
@@ -3107,7 +3307,17 @@ async function runTuiCommand() {
3107
3307
 
3108
3308
  function detectProjectState(dir) {
3109
3309
  const hasConfig = fs.existsSync(path.join(dir, CONFIG_FILE));
3110
- const hasStart = fs.existsSync(path.join(dir, START_MD));
3310
+ let hasStart = fs.existsSync(path.join(dir, START_MD));
3311
+ if (hasStart) {
3312
+ try {
3313
+ const content = fs.readFileSync(path.join(dir, START_MD), 'utf8');
3314
+ if (content.includes('hotel room bookings with real-time availability')) {
3315
+ hasStart = false; // default template hasn't been edited/replaced yet
3316
+ }
3317
+ } catch {
3318
+ hasStart = false;
3319
+ }
3320
+ }
3111
3321
  const hasBrainstorm = fs.existsSync(path.join(dir, BRAINSTORM_MD));
3112
3322
  const hasSprints = detectSprintDocs(dir);
3113
3323
 
@@ -3346,69 +3556,104 @@ async function runTuiCommand() {
3346
3556
  process.stdin.pause();
3347
3557
  process.stdout.write(ANSI.showCursor + '\n');
3348
3558
 
3349
- // Let user choose how they want to edit
3350
- console.log('Requirements Editor Options:');
3351
- console.log(' [1] Open docs/start.md in default terminal editor (nano, vi, notepad)');
3352
- console.log(' [2] Open docs/start.md in VS Code (code)');
3353
- console.log(' [3] Edit project description directly in terminal');
3559
+ console.log(ANSI.cyan + '\n🕌 IMH-Code Write Requirements' + ANSI.reset);
3560
+ console.log('Enter your project description below. When done, type /submit or a single . on a new line to finish:\n');
3354
3561
 
3355
- const rlEdit = readline.createInterface({ input: process.stdin, output: process.stdout });
3356
- const choice = await new Promise(res => rlEdit.question('\nSelect option [1-3] (default: 1): ', res));
3357
- rlEdit.close();
3358
-
3359
- const startMdPath = path.join(cwd, START_MD);
3562
+ const lines = [];
3563
+ const rlInput = readline.createInterface({ input: process.stdin, output: process.stdout });
3360
3564
 
3361
- if (choice === '2') {
3362
- try {
3363
- execSync(`code "${startMdPath}"`, { stdio: 'inherit' });
3364
- console.log('📂 Opened docs/start.md in VS Code.');
3365
- } catch {
3366
- console.log('⚠️ Failed to launch VS Code ("code" CLI not in path). Fallback to standard editor.');
3367
- const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'nano');
3368
- execSync(`${editor} "${startMdPath}"`, { stdio: 'inherit' });
3369
- }
3370
- } else if (choice === '3') {
3371
- console.log('\nEnter your new project description below (press Enter on empty line to finish):\n');
3372
- const lines = [];
3373
- const rlInput = readline.createInterface({ input: process.stdin, output: process.stdout });
3374
- while (true) {
3375
- const line = await new Promise(res => rlInput.question('> ', res));
3376
- if (line.trim() === '') break;
3377
- lines.push(line);
3378
- }
3379
- rlInput.close();
3380
-
3381
- if (lines.length > 0) {
3382
- // Write to docs/start.md within markers
3383
- let startContent = '';
3384
- if (fs.existsSync(startMdPath)) {
3385
- startContent = fs.readFileSync(startMdPath, 'utf8');
3565
+ await new Promise(resolve => {
3566
+ rlInput.on('line', (line) => {
3567
+ if (line.trim() === '/submit' || line.trim() === '.') {
3568
+ resolve();
3569
+ return;
3386
3570
  }
3387
-
3388
- const desc = lines.join('\n');
3389
- const markerStart = '<!-- WRITE_PROMPT_HERE -->';
3390
- const markerEnd = '<!-- END_PROMPT -->';
3391
-
3392
- if (startContent.includes(markerStart) && startContent.includes(markerEnd)) {
3393
- const before = startContent.split(markerStart)[0];
3394
- const after = startContent.split(markerEnd)[1];
3395
- const newContent = `${before}${markerStart}\n${desc}\n${markerEnd}${after}`;
3396
- fs.writeFileSync(startMdPath, newContent, 'utf8');
3397
- } else {
3398
- // Write a simple template with markers
3399
- const newContent = `# 🕌 IMH-Code — Project Start\n\n${markerStart}\n${desc}\n${markerEnd}\n`;
3400
- fs.writeFileSync(startMdPath, newContent, 'utf8');
3401
- }
3402
- console.log('✅ Requirements updated successfully in docs/start.md!');
3403
- }
3404
- } else {
3405
- const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'nano');
3406
- try {
3407
- execSync(`${editor} "${startMdPath}"`, { stdio: 'inherit' });
3408
- } catch (err) {
3409
- console.error(`❌ Failed to launch editor: ${err.message}`);
3410
- }
3571
+ lines.push(line);
3572
+ });
3573
+ rlInput.on('close', () => {
3574
+ resolve();
3575
+ });
3576
+ });
3577
+
3578
+ const desc = lines.join('\n');
3579
+ rlInput.close();
3580
+
3581
+ if (desc.trim() === '') {
3582
+ console.log('⚠️ No requirements entered. Aborting.');
3583
+ inSubMode = false;
3584
+ if (isRaw) process.stdin.setRawMode(true);
3585
+ process.stdin.resume();
3586
+ renderMenu();
3587
+ break;
3588
+ }
3589
+
3590
+ const rlQuestions = readline.createInterface({ input: process.stdin, output: process.stdout });
3591
+
3592
+ // Ask for backend
3593
+ let backend = 'yes';
3594
+ while (true) {
3595
+ const ans = await new Promise(res => rlQuestions.question('\nDo you need a backend API / server-side logic? (yes/no/unsure) [default: yes]: ', res));
3596
+ const cleanAns = ans.trim().toLowerCase();
3597
+ if (cleanAns === '') { backend = 'yes'; break; }
3598
+ if (['yes', 'no', 'unsure'].includes(cleanAns)) { backend = cleanAns; break; }
3599
+ console.log('❌ Invalid input. Please enter yes, no, or unsure.');
3600
+ }
3601
+
3602
+ // Ask for mobile
3603
+ let mobile = 'no';
3604
+ while (true) {
3605
+ const ans = await new Promise(res => rlQuestions.question('Do you need a mobile app (iOS/Android)? (yes/no) [default: no]: ', res));
3606
+ const cleanAns = ans.trim().toLowerCase();
3607
+ if (cleanAns === '') { mobile = 'no'; break; }
3608
+ if (['yes', 'no'].includes(cleanAns)) { mobile = cleanAns; break; }
3609
+ console.log('❌ Invalid input. Please enter yes or no.');
3610
+ }
3611
+
3612
+ rlQuestions.close();
3613
+
3614
+ // Write start.md template
3615
+ const startMdPath = path.join(cwd, START_MD);
3616
+ const startMdContent = [
3617
+ '# 🕌 IMH-Code — Project Start',
3618
+ '',
3619
+ '> **Imam Hussain Coding Harness Platform**',
3620
+ '',
3621
+ '---',
3622
+ '',
3623
+ '## ⚡ Quick Scope Check',
3624
+ '',
3625
+ '**Do you need a backend API / server-side logic?**',
3626
+ `> **Answer:** ${backend}`,
3627
+ '',
3628
+ '**Do you need a mobile app (iOS/Android)?**',
3629
+ `> **Answer:** ${mobile}`,
3630
+ '',
3631
+ '---',
3632
+ '',
3633
+ '## 📝 Your Project Description',
3634
+ '',
3635
+ '<!-- WRITE_PROMPT_HERE -->',
3636
+ desc,
3637
+ '<!-- END_PROMPT -->',
3638
+ '',
3639
+ '---',
3640
+ '',
3641
+ '## 🚀 Next Step',
3642
+ '',
3643
+ 'After filling in the scope and your description, run:',
3644
+ '',
3645
+ '```bash',
3646
+ 'imhcode plan',
3647
+ '```'
3648
+ ].join('\n');
3649
+
3650
+ // Ensure directories exist
3651
+ const docsDir = path.dirname(startMdPath);
3652
+ if (!fs.existsSync(docsDir)) {
3653
+ fs.mkdirSync(docsDir, { recursive: true });
3411
3654
  }
3655
+ fs.writeFileSync(startMdPath, startMdContent, 'utf8');
3656
+ console.log('\n✅ Requirements updated successfully in docs/start.md!');
3412
3657
 
3413
3658
  inSubMode = false;
3414
3659
  if (isRaw) process.stdin.setRawMode(true);