@boyingliu01/xp-gate 0.15.1 → 0.15.3

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/xp-gate.js CHANGED
@@ -211,6 +211,30 @@ const COMMANDS = {
211
211
  handleSprintAudit(subargs).then(code => process.exit(code));
212
212
  },
213
213
  usage: 'xp-gate sprint-audit [--dir <path>] [--json]'
214
+ },
215
+ 'detect-languages': {
216
+ description: 'Detect project languages and generate .xp-gate-config.json',
217
+ run: subargs => {
218
+ const { handleDetectLanguages } = require('../lib/language-tools.js');
219
+ handleDetectLanguages(subargs).then(code => process.exit(code));
220
+ },
221
+ usage: 'xp-gate detect-languages [--json] [--write]'
222
+ },
223
+ 'check-tools': {
224
+ description: 'Check quality gate tool availability per language',
225
+ run: subargs => {
226
+ const { handleCheckTools } = require('../lib/language-tools.js');
227
+ handleCheckTools(subargs).then(code => process.exit(code));
228
+ },
229
+ usage: 'xp-gate check-tools [--languages <langs>] [--json]'
230
+ },
231
+ 'install-tools': {
232
+ description: 'Install missing quality gate tools for detected languages',
233
+ run: subargs => {
234
+ const { handleInstallTools } = require('../lib/language-tools.js');
235
+ handleInstallTools(subargs).then(code => process.exit(code));
236
+ },
237
+ usage: 'xp-gate install-tools [--languages <langs>] [--dry-run] [--yes]'
214
238
  }
215
239
  };
216
240
 
package/hooks/pre-commit CHANGED
@@ -375,7 +375,29 @@ if [ -n "$_RESOLVED_LANG_OVERRIDE" ]; then
375
375
  echo " šŸ”§ LANGUAGE OVERRIDE: $PROJECT_LANG"
376
376
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
377
377
  echo ""
378
- else
378
+ elif [ -f ".xp-gate-config.json" ]; then
379
+ # ---- Read from project config file (v0.15.2+) ----
380
+ # .xp-gate-config.json is generated by `xp-gate init` or `xp-gate detect-languages`
381
+ # It contains detected languages and their tool status.
382
+ _CONFIG_LANGS=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('.xp-gate-config.json','utf8'));console.log(Object.keys(c.languages||{}).join(' '));}catch(e){console.log('');}" 2>/dev/null || echo "")
383
+ if [ -n "$_CONFIG_LANGS" ]; then
384
+ PROJECT_LANGS="$_CONFIG_LANGS"
385
+ PROJECT_LANG=$(echo "$_CONFIG_LANGS" | awk '{print $1}')
386
+ echo ""
387
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
388
+ echo " šŸ“‹ LANGUAGES FROM .xp-gate-config.json"
389
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
390
+ echo ""
391
+ echo "Detected languages: $PROJECT_LANGS"
392
+ echo ""
393
+ else
394
+ # Config file exists but no languages detected — fall through to extension detection
395
+ _CONFIG_LANGS=""
396
+ fi
397
+ fi
398
+
399
+ # If no languages from override or config, detect from changed files
400
+ if [ -z "$PROJECT_LANGS" ] && [ -z "$_CONFIG_LANGS" ]; then
379
401
  # ---- Extension → language mapping ----
380
402
  # Extracts unique languages from changed file extensions
381
403
  PROJECT_LANGS=""
@@ -676,7 +698,7 @@ CHANGEDFILELIST
676
698
  done
677
699
  echo ""
678
700
  fi
679
- fi
701
+ fi # close language override if/else
680
702
 
681
703
  # Helper: check if a language is in PROJECT_LANGS
682
704
  has_project_lang() {
package/hooks/pre-push CHANGED
@@ -865,16 +865,11 @@ if ! command -v jq &> /dev/null; then
865
865
  fi
866
866
 
867
867
  # Validate result file content
868
- # SHA self-reference paradox fix: if the result file was committed as part of
869
- # HEAD, its content references HEAD^ (the commit being reviewed), not HEAD.
868
+ # SHA self-reference paradox fix (Issue #346):
869
+ # The walkthrough file may reference an earlier commit if it was generated before
870
+ # subsequent walkthrough-only commits. We check if the referenced commit is in
871
+ # the current branch's history and if no functional commits occurred after it.
870
872
  CURRENT_COMMIT=$(git rev-parse HEAD)
871
- if git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | grep -qx "$RESULT_FILE"; then
872
- # Result file is part of the latest commit — compare against the reviewed commit
873
- EXPECTED_COMMIT=$(git rev-parse HEAD^ 2>/dev/null || echo "unknown")
874
- else
875
- # Result file is working-tree only — compare against current HEAD
876
- EXPECTED_COMMIT="$CURRENT_COMMIT"
877
- fi
878
873
 
879
874
  # Check JSON validity
880
875
  if ! jq empty "$RESULT_FILE" 2>/dev/null; then
@@ -906,16 +901,42 @@ if [ "$RESULT_BRANCH" != "$CURRENT_BRANCH" ]; then
906
901
  echo "Proceeding with commit verification..."
907
902
  fi
908
903
 
909
- # Commit match check
910
- if [ "$RESULT_COMMIT" != "$EXPECTED_COMMIT" ]; then
904
+ # Commit match check (Issue #346: flexible matching to avoid circular dependency)
905
+ # Check if RESULT_COMMIT is in the current branch's history
906
+ if ! git merge-base --is-ancestor "$RESULT_COMMIT" HEAD 2>/dev/null; then
907
+ echo ""
908
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
909
+ echo " āŒ RESULT FILE INVALID - PUSH BLOCKED"
910
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
911
+ echo ""
912
+ echo "Result file references a commit not in current branch:"
913
+ echo " Referenced: $RESULT_COMMIT"
914
+ echo " Current HEAD: $CURRENT_COMMIT"
915
+ echo ""
916
+ echo "Re-run: /delphi-review --mode code-walkthrough"
917
+ echo ""
918
+ exit 1
919
+ fi
920
+
921
+ # Check if there are any functional commits after RESULT_COMMIT
922
+ # Functional commits are those that are NOT walkthrough-only updates or Gate MW fixes
923
+ FUNCTIONAL_COMMITS_AFTER=$(git log --oneline "$RESULT_COMMIT"..HEAD -- . \
924
+ ':!.code-walkthrough-result.json' \
925
+ ':!.delphi/code-walkthrough.md' \
926
+ ':!githooks/pre-push' \
927
+ ':!src/npm-package/hooks/pre-push' 2>/dev/null | wc -l)
928
+ if [ "$FUNCTIONAL_COMMITS_AFTER" -gt 0 ]; then
911
929
  echo ""
912
930
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
913
931
  echo " āŒ RESULT FILE OUTDATED - PUSH BLOCKED"
914
932
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
915
933
  echo ""
916
- echo "Result file is for a different commit:"
917
- echo " Expected: $EXPECTED_COMMIT"
918
- echo " Found: $RESULT_COMMIT"
934
+ echo "Functional commits found after walkthrough:"
935
+ git log --oneline "$RESULT_COMMIT"..HEAD -- . \
936
+ ':!.code-walkthrough-result.json' \
937
+ ':!.delphi/code-walkthrough.md' \
938
+ ':!githooks/pre-push' \
939
+ ':!src/npm-package/hooks/pre-push' 2>/dev/null | head -5
919
940
  echo ""
920
941
  echo "Re-run: /delphi-review --mode code-walkthrough"
921
942
  echo ""
@@ -961,9 +982,178 @@ if [[ "$CURRENT_TIME" > "$RESULT_EXPIRES" ]]; then
961
982
  exit 1
962
983
  fi
963
984
 
985
+ # ============================================================================
986
+ # PROVENANCE VALIDATION (Issue #339)
987
+ # Validate that the walkthrough was genuinely produced by Delphi review,
988
+ # not fabricated by an LLM.
989
+ # ============================================================================
990
+
991
+ # Check provenance fields exist
992
+ EXPERTS_COUNT=$(jq -r '.experts // [] | length' "$RESULT_FILE")
993
+ CONSENSUS=$(jq -r '.consensus // 0' "$RESULT_FILE")
994
+ WALKTHROUGH_HASH=$(jq -r '.walkthroughHash // empty' "$RESULT_FILE")
995
+ GENERATED_AT=$(jq -r '.generatedAt // empty' "$RESULT_FILE")
996
+
997
+ # Grace period: configurable via env var (default 30 days)
998
+ GRACE_DAYS="${XP_GATE_MW_GRACE_DAYS:-30}"
999
+
1000
+ # Check if this is a "new" walkthrough (has provenance fields)
1001
+ HAS_PROVENANCE="false"
1002
+ if [ -n "$WALKTHROUGH_HASH" ] && [ -n "$GENERATED_AT" ] && [ "$EXPERTS_COUNT" -ge 3 ]; then
1003
+ HAS_PROVENANCE="true"
1004
+ fi
1005
+
1006
+ if [ "$HAS_PROVENANCE" = "true" ]; then
1007
+ # Full provenance validation
1008
+
1009
+ # 1. Experts count check (minimum 3)
1010
+ if [ "$EXPERTS_COUNT" -lt 3 ]; then
1011
+ echo ""
1012
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1013
+ echo " āŒ INSUFFICIENT EXPERTS - PUSH BLOCKED"
1014
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1015
+ echo ""
1016
+ echo "Walkthrough requires minimum 3 experts:"
1017
+ echo " Found: $EXPERTS_COUNT experts"
1018
+ echo ""
1019
+ echo "Re-run: /delphi-review --mode code-walkthrough"
1020
+ echo ""
1021
+ exit 1
1022
+ fi
1023
+
1024
+ # 2. Consensus check (≄90%)
1025
+ # Use integer math: multiply by 10 to avoid bc dependency
1026
+ CONSENSUS_X10=$(echo "$CONSENSUS * 10" | bc -l | cut -d'.' -f1)
1027
+ if [ "$CONSENSUS_X10" -lt 900 ]; then
1028
+ echo ""
1029
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1030
+ echo " āŒ CONSENSUS TOO LOW - PUSH BLOCKED"
1031
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1032
+ echo ""
1033
+ echo "Walkthrough requires ≄90% consensus:"
1034
+ echo " Found: ${CONSENSUS}%"
1035
+ echo ""
1036
+ echo "Re-run: /delphi-review --mode code-walkthrough"
1037
+ echo ""
1038
+ exit 1
1039
+ fi
1040
+
1041
+ # 3. Walkthrough hash verification
1042
+ WALKTHROUGH_DOC=".delphi/code-walkthrough.md"
1043
+ if [ ! -f "$WALKTHROUGH_DOC" ]; then
1044
+ echo ""
1045
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1046
+ echo " āŒ WALKTHROUGH DOC MISSING - PUSH BLOCKED"
1047
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1048
+ echo ""
1049
+ echo "Walkthrough document not found:"
1050
+ echo " Expected: $WALKTHROUGH_DOC"
1051
+ echo ""
1052
+ echo "Re-run: /delphi-review --mode code-walkthrough"
1053
+ echo ""
1054
+ exit 1
1055
+ fi
1056
+
1057
+ # Cross-platform SHA-256 computation
1058
+ COMPUTED_HASH=""
1059
+ if command -v sha256sum &> /dev/null; then
1060
+ COMPUTED_HASH=$(sha256sum "$WALKTHROUGH_DOC" | cut -d' ' -f1)
1061
+ elif command -v shasum &> /dev/null; then
1062
+ COMPUTED_HASH=$(shasum -a 256 "$WALKTHROUGH_DOC" | cut -d' ' -f1)
1063
+ elif command -v openssl &> /dev/null; then
1064
+ COMPUTED_HASH=$(openssl sha256 -r "$WALKTHROUGH_DOC" | cut -d' ' -f1)
1065
+ fi
1066
+
1067
+ if [ -z "$COMPUTED_HASH" ]; then
1068
+ echo ""
1069
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1070
+ echo " āŒ NO SHA-256 TOOL - PUSH BLOCKED"
1071
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1072
+ echo ""
1073
+ echo "No SHA-256 tool available (install sha256sum, shasum, or openssl)"
1074
+ echo ""
1075
+ exit 1
1076
+ fi
1077
+
1078
+ if [ "$WALKTHROUGH_HASH" != "$COMPUTED_HASH" ]; then
1079
+ echo ""
1080
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1081
+ echo " āŒ WALKTHROUGH HASH MISMATCH - PUSH BLOCKED"
1082
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1083
+ echo ""
1084
+ echo "Walkthrough document has been modified after review:"
1085
+ echo " Expected: $WALKTHROUGH_HASH"
1086
+ echo " Found: $COMPUTED_HASH"
1087
+ echo ""
1088
+ echo "Re-run: /delphi-review --mode code-walkthrough"
1089
+ echo ""
1090
+ exit 1
1091
+ fi
1092
+
1093
+ # 4. generatedAt check (replay protection)
1094
+ if [ -z "$GENERATED_AT" ]; then
1095
+ echo ""
1096
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1097
+ echo " āŒ MISSING generatedAt - PUSH BLOCKED"
1098
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1099
+ echo ""
1100
+ echo "Walkthrough result missing generatedAt timestamp"
1101
+ echo ""
1102
+ echo "Re-run: /delphi-review --mode code-walkthrough"
1103
+ echo ""
1104
+ exit 1
1105
+ fi
1106
+ else
1107
+ # Grace period: old walkthroughs without provenance get WARNING
1108
+ echo ""
1109
+ echo "āš ļø WARNING: Legacy walkthrough format (no provenance fields)"
1110
+ echo " Provenance fields will be required after grace period (${GRACE_DAYS} days)"
1111
+ echo " Consider re-running: /delphi-review --mode code-walkthrough"
1112
+ echo ""
1113
+ fi
1114
+
964
1115
  # ============================================================================
965
1116
  # ALL CHECKS PASSED
966
1117
  # ============================================================================
1118
+ echo ""
1119
+
1120
+ # ============================================================================
1121
+ # SPRINT-STATE BACKUP CHECK (WARNING ONLY — Issue #318)
1122
+ # Scan for worktree sprint-state directories that haven't been backed up
1123
+ # to .sprint-history/ before push. WARN only (doesn't block) — sprint-state
1124
+ # inside worktrees is deleted on `git worktree remove`, causing permanent
1125
+ # data loss if orchestrator forgets to back up during CLOSE phase.
1126
+ # ============================================================================
1127
+ if [ -d ".worktrees/sprint" ]; then
1128
+ for worktree_dir in .worktrees/sprint/sprint-*; do
1129
+ [ -d "$worktree_dir" ] || continue
1130
+ [ -d "$worktree_dir/.sprint-state" ] || continue
1131
+ sprint_id=$(basename "$worktree_dir")
1132
+ if [ -f "$worktree_dir/.sprint-state/sprint-state.json" ]; then
1133
+ if [ ! -d ".sprint-history/$sprint_id" ]; then
1134
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1135
+ echo " āš ļø SPRINT-STATE NOT BACKED UP: $sprint_id"
1136
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1137
+ echo ""
1138
+ echo "Unarchived sprint state found in worktree:"
1139
+ echo " Path: $worktree_dir/.sprint-state/"
1140
+ echo " Backup: .sprint-history/$sprint_id/ (MISSING)"
1141
+ echo ""
1142
+ echo "RISK: If this worktree is removed before CLOSE backup,"
1143
+ echo " design docs, Delphi verdicts, and phase summaries"
1144
+ echo " are permanently lost."
1145
+ echo ""
1146
+ echo "Backup now:"
1147
+ echo " mkdir -p .sprint-history/$sprint_id"
1148
+ echo " cp -r $worktree_dir/.sprint-state/* .sprint-history/$sprint_id/"
1149
+ echo ""
1150
+ echo "See: https://github.com/boyingliu01/xp-gate/issues/318"
1151
+ echo ""
1152
+ fi
1153
+ fi
1154
+ done
1155
+ fi
1156
+
967
1157
  echo ""
968
1158
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
969
1159
  echo " āœ… GATE MW: CODE WALKTHROUGH VERIFIED"
package/lib/doctor.js CHANGED
@@ -1058,6 +1058,53 @@ function ensureTuiRegistration() {
1058
1058
  }
1059
1059
  }
1060
1060
 
1061
+ /**
1062
+ * Diagnose language-specific tool availability.
1063
+ * @returns {number} Number of issues found (missing required tools)
1064
+ */
1065
+ function diagnoseLanguageTools() {
1066
+ try {
1067
+ const { detectProjectLanguages, getToolsForLanguages, LANGUAGE_REGISTRY } = require('./language-tools.js');
1068
+ const projectRoot = process.cwd();
1069
+ const { detected } = detectProjectLanguages(projectRoot);
1070
+
1071
+ if (detected.length === 0) return 0;
1072
+
1073
+ console.log('');
1074
+ console.log('Language-Specific Tools:');
1075
+ console.log('------------------------');
1076
+
1077
+ const toolStatus = getToolsForLanguages(detected);
1078
+ let issues = 0;
1079
+
1080
+ for (const [lang, tools] of Object.entries(toolStatus)) {
1081
+ const langDef = LANGUAGE_REGISTRY[lang];
1082
+ console.log(` ${langDef?.name || lang}:`);
1083
+
1084
+ for (const t of tools) {
1085
+ const statusIcon = t.status === 'available' ? 'āœ“' : 'āœ—';
1086
+ const optMark = t.tool.optional ? ' (optional)' : '';
1087
+ const version = t.version || 'N/A';
1088
+ console.log(` ${statusIcon} ${t.tool.name}${optMark} — ${version}`);
1089
+
1090
+ if (t.status === 'missing' && !t.tool.optional) {
1091
+ issues++;
1092
+ }
1093
+ }
1094
+ }
1095
+
1096
+ if (issues > 0) {
1097
+ console.log('');
1098
+ console.log(` ${issues} required tool(s) missing — gates will SKIP.`);
1099
+ console.log(' Run "xp-gate install-tools --yes" to install.');
1100
+ }
1101
+
1102
+ return issues;
1103
+ } catch {
1104
+ return 0;
1105
+ }
1106
+ }
1107
+
1061
1108
  async function doctor(args) {
1062
1109
  const fixMode = args.includes('--fix');
1063
1110
 
@@ -1101,6 +1148,9 @@ async function doctor(args) {
1101
1148
 
1102
1149
  issues += diagnoseOpenCodePlugin(checks);
1103
1150
 
1151
+ // Language-specific tool status
1152
+ issues += diagnoseLanguageTools();
1153
+
1104
1154
  if (issues === 0) {
1105
1155
  console.log('\nāœ“ All checks passed');
1106
1156
  return 0;
package/lib/init.js CHANGED
@@ -354,6 +354,93 @@ async function init(args) {
354
354
  return code;
355
355
  }
356
356
 
357
+ /**
358
+ * Detect project languages and check/install language-specific tools.
359
+ * @param {string} projectRoot - Project root directory
360
+ * @param {boolean} autoYes - Auto-install missing tools
361
+ */
362
+ async function detectAndReportLanguages(projectRoot, autoYes) {
363
+ const { detectProjectLanguages, getToolsForLanguages, generateProjectConfig, LANGUAGE_REGISTRY } = require('./language-tools.js');
364
+
365
+ console.log('');
366
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
367
+ console.log(' Multi-Language Quality Gates');
368
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
369
+ console.log('');
370
+
371
+ const { detected, configFiles } = detectProjectLanguages(projectRoot);
372
+
373
+ if (detected.length === 0) {
374
+ console.log(' No project languages detected.');
375
+ return;
376
+ }
377
+
378
+ console.log(` Detected ${detected.length} language(s):`);
379
+ for (const lang of detected) {
380
+ const langDef = LANGUAGE_REGISTRY[lang];
381
+ console.log(` • ${langDef?.name || lang}`);
382
+ }
383
+ console.log('');
384
+
385
+ const toolStatus = getToolsForLanguages(detected);
386
+ const missingRequired = [];
387
+ const missingOptional = [];
388
+
389
+ for (const [lang, tools] of Object.entries(toolStatus)) {
390
+ for (const t of tools) {
391
+ if (t.status === 'missing') {
392
+ if (t.tool.optional) {
393
+ missingOptional.push({ lang, tool: t.tool });
394
+ } else {
395
+ missingRequired.push({ lang, tool: t.tool });
396
+ }
397
+ }
398
+ }
399
+ }
400
+
401
+ if (missingRequired.length === 0 && missingOptional.length === 0) {
402
+ console.log(' āœ“ All language-specific tools are available.');
403
+ } else {
404
+ if (missingRequired.length > 0) {
405
+ console.log(` āœ— ${missingRequired.length} required tool(s) missing:`);
406
+ for (const { lang, tool } of missingRequired) {
407
+ console.log(` • ${tool.name} (${lang}) — gates will SKIP`);
408
+ }
409
+ }
410
+ if (missingOptional.length > 0) {
411
+ console.log(` ⚠ ${missingOptional.length} optional tool(s) missing (fallback available):`);
412
+ for (const { lang, tool } of missingOptional) {
413
+ console.log(` • ${tool.name} (${lang})`);
414
+ }
415
+ }
416
+ console.log('');
417
+
418
+ if (autoYes || (process.stdin.isTTY && await askYesNo(' Install missing language-specific tools now?'))) {
419
+ console.log('');
420
+ const { installTool } = require('./language-tools.js');
421
+ let installed = 0;
422
+ for (const { lang, tool } of [...missingRequired, ...missingOptional]) {
423
+ const result = installTool(tool);
424
+ if (result.success) {
425
+ console.log(` āœ“ ${tool.name} installed`);
426
+ installed++;
427
+ } else {
428
+ console.log(` āœ— ${tool.name}: ${result.message}`);
429
+ }
430
+ }
431
+ if (installed > 0) {
432
+ console.log(`\n ${installed} tool(s) installed successfully.`);
433
+ }
434
+ } else {
435
+ console.log(' Skipped. Run "xp-gate install-tools --yes" later.');
436
+ }
437
+ }
438
+
439
+ // Generate project config
440
+ generateProjectConfig(projectRoot, detected, toolStatus);
441
+ console.log(`\n Generated .xp-gate-config.json`);
442
+ }
443
+
357
444
  async function installLocal(args) {
358
445
  const gitDir = getGitDir();
359
446
  if (!gitDir) {
@@ -416,6 +503,10 @@ async function installLocal(args) {
416
503
  // Auto-install missing CLI tools (prompt user)
417
504
  const autoYes = args.includes('--yes') || args.includes('--auto-install');
418
505
  await promptBootstrap(autoYes);
506
+
507
+ // Detect project languages and check language-specific tools
508
+ await detectAndReportLanguages(projectRoot, autoYes);
509
+
419
510
  console.log('━━━ FIRST-CLASS TEST QUALITY ━━━');
420
511
  console.log('XP-Gate treats test code as a first-class citizen:');
421
512
  console.log(' • TypeScript: test files are type-checked, not excluded from tsconfig.json');