@boyingliu01/xp-gate 0.19.3 → 0.20.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.
Files changed (173) hide show
  1. package/adapters/gate-4.sh +83 -18
  2. package/adapters/gate-8.sh +49 -5
  3. package/adapters/iac.sh +1 -1
  4. package/adapters/typescript.sh +15 -60
  5. package/bin/xp-gate.js +15 -16
  6. package/gate-10.sh +35 -2
  7. package/gate-4.sh +110 -21
  8. package/gate-8.sh +49 -5
  9. package/hooks/gate-10.sh +35 -2
  10. package/hooks/gate-4.sh +110 -21
  11. package/hooks/gate-8.sh +49 -5
  12. package/hooks/lib/jscpd-run.sh +63 -0
  13. package/hooks/lib/sprint-gate-report.sh +58 -0
  14. package/hooks/lib/test-failure.sh +152 -0
  15. package/hooks/lib/typecheck.sh +141 -0
  16. package/hooks/lib/validate-code-walkthrough.cjs +43 -5
  17. package/hooks/post-merge +100 -0
  18. package/hooks/pre-commit +389 -162
  19. package/hooks/pre-push +75 -19
  20. package/lib/__tests__/bootstrap.test.js +23 -41
  21. package/lib/__tests__/check-version.test.js +8 -1
  22. package/lib/__tests__/detect-deps.test.js +16 -0
  23. package/lib/__tests__/doctor-drift-round2.test.js +212 -0
  24. package/lib/__tests__/doctor-executed-surface.test.js +209 -0
  25. package/lib/__tests__/doctor-hook-drift.test.js +329 -0
  26. package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
  27. package/lib/__tests__/doctor-module-drift.test.js +206 -0
  28. package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
  29. package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
  30. package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
  31. package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
  32. package/lib/__tests__/doctor.test.js +155 -0
  33. package/lib/__tests__/gate-runner.test.js +19 -3
  34. package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
  35. package/lib/__tests__/init.test.js +44 -17
  36. package/lib/__tests__/install-skill-full-dir.test.js +151 -0
  37. package/lib/__tests__/install-skill.test.js +51 -132
  38. package/lib/__tests__/language-tools-detect.test.js +158 -0
  39. package/lib/__tests__/phase-transition.test.js +15 -1
  40. package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
  41. package/lib/__tests__/shared-paths.test.js +20 -0
  42. package/lib/__tests__/shared-utils.test.js +69 -0
  43. package/lib/__tests__/skill-flags.test.js +48 -0
  44. package/lib/__tests__/ui-detector.test.ts +6 -2
  45. package/lib/__tests__/uninstall.test.js +20 -0
  46. package/lib/__tests__/update-hooks.test.js +12 -0
  47. package/lib/__tests__/update-skill.test.js +7 -4
  48. package/lib/detect-deps.js +3 -20
  49. package/lib/doctor-tui.js +22 -5
  50. package/lib/doctor.js +1178 -31
  51. package/lib/init.js +184 -33
  52. package/lib/install-cmd.js +7 -4
  53. package/lib/install-skill.js +38 -95
  54. package/lib/language-tools.js +83 -4
  55. package/lib/mattpocock-license-verdict.json +1 -1
  56. package/lib/shared-paths.js +5 -2
  57. package/lib/shared-utils.js +47 -3
  58. package/lib/uninstall.js +12 -0
  59. package/lib/update-hooks.js +4 -4
  60. package/lib/update-skill.js +6 -7
  61. package/mock-policy/AGENTS.md +4 -4
  62. package/mutation/AGENTS.md +4 -4
  63. package/mutation/__tests__/gate-m.test.ts +66 -14
  64. package/mutation/__tests__/mutmut-runner.test.ts +91 -42
  65. package/mutation/__tests__/test-filter-parity.test.ts +37 -0
  66. package/mutation/gate-m.ts +21 -8
  67. package/package.json +1 -1
  68. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  69. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  70. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  71. package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
  72. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
  73. package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
  74. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  75. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
  76. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  77. package/plugins/dsh/README.md +169 -4
  78. package/plugins/dsh/dsh-overlay.patch.yml +67 -0
  79. package/plugins/dsh/package.json +1 -1
  80. package/plugins/dsh/scripts/prepack.cjs +24 -19
  81. package/plugins/dsh/setup-dsh.ps1 +160 -0
  82. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  84. package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
  85. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
  86. package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
  87. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  88. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
  89. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  90. package/plugins/dsh/src/command.test.ts +66 -2
  91. package/plugins/dsh/src/command.ts +36 -14
  92. package/plugins/dsh/src/contract.test.ts +31 -1
  93. package/plugins/dsh/src/delphi-config.test.ts +190 -0
  94. package/plugins/dsh/src/delphi-config.ts +223 -0
  95. package/plugins/dsh/src/delphi-run.test.ts +181 -0
  96. package/plugins/dsh/src/delphi-run.ts +129 -0
  97. package/plugins/dsh/src/delphi.test.ts +222 -0
  98. package/plugins/dsh/src/delphi.ts +247 -0
  99. package/plugins/dsh/src/dialect.test.ts +59 -0
  100. package/plugins/dsh/src/dialect.ts +40 -0
  101. package/plugins/dsh/src/index.ts +70 -4
  102. package/plugins/dsh/src/skills.test.ts +26 -19
  103. package/plugins/opencode/package.json +1 -1
  104. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  105. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  106. package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
  107. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
  108. package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
  109. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
  111. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  112. package/plugins/qoder/agents/delphi-architecture.md +1 -1
  113. package/plugins/qoder/agents/delphi-feasibility.md +1 -1
  114. package/plugins/qoder/agents/delphi-technical.md +1 -1
  115. package/plugins/qoder/plugin.json +1 -1
  116. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  117. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  118. package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
  119. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
  120. package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  122. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
  123. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  124. package/principles/AGENTS.md +9 -8
  125. package/principles/__tests__/adapter-metrics.test.ts +292 -0
  126. package/principles/__tests__/analyzer.test.ts +29 -6
  127. package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
  128. package/principles/__tests__/baseline-prune.test.ts +338 -0
  129. package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
  130. package/principles/__tests__/boy-scout-integration.test.ts +97 -6
  131. package/principles/__tests__/boy-scout.test.ts +253 -7
  132. package/principles/__tests__/config-enforcement.test.ts +469 -0
  133. package/principles/__tests__/config-git-timeout.test.ts +66 -0
  134. package/principles/__tests__/direct-execution.test.ts +187 -0
  135. package/principles/__tests__/index.test.ts +16 -3
  136. package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
  137. package/principles/adapters/typescript.ts +613 -13
  138. package/principles/analyzer.ts +22 -0
  139. package/principles/baseline-prune.ts +217 -0
  140. package/principles/boy-scout.ts +481 -51
  141. package/principles/config.ts +362 -54
  142. package/principles/direct-execution.ts +60 -0
  143. package/principles/index.ts +40 -17
  144. package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
  145. package/principles/rules/clean-code/code-duplication.ts +8 -8
  146. package/principles/rules/clean-code/deep-nesting.ts +8 -8
  147. package/principles/rules/clean-code/god-class.ts +8 -8
  148. package/principles/rules/clean-code/large-file.ts +15 -11
  149. package/principles/rules/clean-code/long-function.ts +8 -8
  150. package/principles/rules/clean-code/magic-numbers.ts +10 -7
  151. package/principles/rules/clean-code/many-exports.ts +7 -7
  152. package/principles/rules/clean-code/missing-error-handling.ts +5 -5
  153. package/principles/rules/clean-code/too-many-params.ts +8 -8
  154. package/principles/rules/clean-code/unused-imports.ts +5 -5
  155. package/principles/rules/solid/dip.ts +18 -8
  156. package/principles/rules/solid/isp.ts +9 -8
  157. package/principles/rules/solid/lsp.ts +6 -6
  158. package/principles/rules/solid/ocp.ts +6 -5
  159. package/principles/rules/solid/srp.ts +7 -8
  160. package/principles/types.ts +15 -0
  161. package/scripts/__tests__/sync-package-content.test.js +28 -0
  162. package/scripts/delphi-external-review.cjs +193 -10
  163. package/scripts/sync-package-content.js +7 -4
  164. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  165. package/skills/delphi-review/AGENTS.md +4 -4
  166. package/skills/delphi-review/SKILL.md +98 -14
  167. package/skills/delphi-review/references/code-walkthrough.md +36 -3
  168. package/skills/delphi-review/references/requirements.md +6 -6
  169. package/skills/sprint-flow/AGENTS.md +4 -4
  170. package/skills/sprint-flow/references/phase-2-design.md +3 -3
  171. package/skills/test-specification-alignment/AGENTS.md +4 -4
  172. package/lib/__tests__/download-skill.test.js +0 -281
  173. package/lib/download-skill.js +0 -96
package/lib/init.js CHANGED
@@ -143,7 +143,7 @@ function generateManifest(srcDir, projectRoot) {
143
143
  const githooksDir = path.join(projectRoot, 'githooks');
144
144
 
145
145
  // Hooks
146
- ['pre-commit', 'pre-push'].forEach(hook => {
146
+ ['pre-commit', 'pre-push', 'post-merge'].forEach(hook => {
147
147
  const hookPath = path.join(hooksDir, hook);
148
148
  if (fs.existsSync(hookPath)) {
149
149
  const stat = fs.statSync(hookPath);
@@ -204,7 +204,7 @@ function generateGlobalManifest(srcDir) {
204
204
  };
205
205
 
206
206
  // Global hooks
207
- ['pre-commit', 'pre-push'].forEach(hook => {
207
+ ['pre-commit', 'pre-push', 'post-merge'].forEach(hook => {
208
208
  const hookPath = path.join(GLOBAL_HOOKS_DIR, hook);
209
209
  if (fs.existsSync(hookPath)) {
210
210
  const stat = fs.statSync(hookPath);
@@ -516,7 +516,7 @@ async function installLocal(args) {
516
516
 
517
517
  injectKarpathyPrinciples(projectRoot);
518
518
  configureOpenCodePlugin(srcDir, projectRoot);
519
- configureQoderDelphiAgents(srcDir, projectRoot);
519
+ deployQoderDelphiAgents(srcDir, projectRoot);
520
520
 
521
521
  // Auto-register TUI plugin globally (idempotent)
522
522
  try {
@@ -593,6 +593,10 @@ async function setupGlobal(args) {
593
593
  // missing register/network must not break global setup.
594
594
  installModuleRuntimeDeps(CONFIG_DIR);
595
595
 
596
+ // Qoder loads user-level custom agents from ~/.qoder/agents/, so a global
597
+ // setup must deploy the Delphi expert templates there as well.
598
+ deployQoderDelphiAgents(srcDir, HOME_DIR);
599
+
596
600
  console.log('[setup-global] Configuring git...');
597
601
  const { execSync } = require('child_process');
598
602
  try {
@@ -636,50 +640,197 @@ async function setupGlobal(args) {
636
640
  return 0;
637
641
  }
638
642
 
643
+ /**
644
+ * Qoder Custom Agents must bind their model as "[DisplayName](modelId)"; a bare
645
+ * model name makes Qoder silently fall back to the session model, which would
646
+ * collapse the three Delphi experts onto one model. Paired quotes only, and
647
+ * matched only inside the frontmatter block so example lines in the body cannot
648
+ * satisfy it. Unquoted is rejected: the docs require the paired-quote form, an
649
+ * unquoted link is not a legal YAML scalar, and accepting it would let a
650
+ * hand-edited file pass the audit while Qoder reads something else.
651
+ */
652
+ const QODER_MODEL_BINDING = /^model:[ \t]*(?:"\[[^"\]]+\]\(([A-Za-z0-9_.-]+)\)"|'\[[^'\]]+\]\(([A-Za-z0-9_.-]+)\)')[ \t]*$/m;
653
+
654
+ /**
655
+ * @param {string} file - agent markdown file
656
+ * @returns {string|null} the bound modelId, or null when the binding is missing/invalid
657
+ */
658
+ function readQoderModelId(file) {
659
+ let frontmatter;
660
+ try {
661
+ const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
662
+ if (lines[0].trim() !== '---') return null;
663
+ const end = lines.indexOf('---', 1);
664
+ if (end === -1) return null;
665
+ frontmatter = lines.slice(1, end).join('\n');
666
+ } catch {
667
+ return null;
668
+ }
669
+ const match = QODER_MODEL_BINDING.exec(frontmatter);
670
+ return match ? (match[1] || match[2]) : null;
671
+ }
672
+
673
+ function warnUnusableQoderAgent(file, boundId, bundledId) {
674
+ if (boundId === null) {
675
+ console.warn(` ⚠ ${file} does not bind a model as "[Name](modelId)".`);
676
+ console.warn(' Qoder will silently run this expert on the session model, which breaks');
677
+ console.warn(" Delphi's three-distinct-models contract.");
678
+ } else {
679
+ console.warn(` ⚠ ${file} binds "${boundId}" but this xp-gate version pins "${bundledId}".`);
680
+ console.warn(' A stale or hand-edited binding leaves the expert on a model this review');
681
+ console.warn(' contract was never validated against.');
682
+ }
683
+ console.warn(' Delete the file, re-run `xp-gate init` (or `--global`), then restart the');
684
+ console.warn(' Qoder session so the agent registry reloads.');
685
+ }
686
+
687
+ /**
688
+ * Report already-deployed agents whose binding is missing, stale or hand-diverged.
689
+ * Runs even when the bundled templates are unreadable — a broken install is exactly
690
+ * the case where the user needs the repair hint.
691
+ *
692
+ * @param {string} agentsDestDir - deployed agents directory
693
+ * @param {Object<string, string|null>|null} bundled - modelId per bundled template, null if unbundled
694
+ */
695
+ function auditDeployedQoderAgents(agentsDestDir, bundled) {
696
+ if (!fs.existsSync(agentsDestDir)) return;
697
+ for (const file of fs.readdirSync(agentsDestDir).filter(f => f.endsWith('.md'))) {
698
+ // Unrelated user agents are none of our business; only Delphi experts are.
699
+ if (!file.startsWith('delphi-') && !(bundled && file in bundled)) continue;
700
+ const destFile = path.join(agentsDestDir, file);
701
+ const boundId = readQoderModelId(destFile);
702
+ const bundledId = bundled ? bundled[file] : undefined;
703
+ if (boundId === null || (bundledId !== undefined && bundledId !== boundId)) {
704
+ warnUnusableQoderAgent(destFile, boundId, bundledId);
705
+ }
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Read the bundled agent templates and extract the modelId each one binds.
711
+ *
712
+ * @param {string} agentSrcDir - bundled plugins/qoder/agents directory
713
+ * @returns {Object<string, string|null>|null} file → modelId, null when not bundled
714
+ */
715
+ function readBundledQoderTemplates(agentSrcDir) {
716
+ if (!fs.existsSync(agentSrcDir)) return null;
717
+ const bundled = {};
718
+ for (const file of fs.readdirSync(agentSrcDir).filter(f => f.endsWith('.md'))) {
719
+ bundled[file] = readQoderModelId(path.join(agentSrcDir, file));
720
+ }
721
+ return bundled;
722
+ }
723
+
724
+ /**
725
+ * Copy missing templates into the agents dir, refusing any that lack a valid binding.
726
+ *
727
+ * @param {string} agentSrcDir - bundled plugins/qoder/agents directory
728
+ * @param {string} agentsDestDir - deployed agents directory
729
+ * @param {Object<string, string|null>|null} bundled - file → modelId, null when not bundled
730
+ * @returns {{deployed: number, skipped: number, rejected: number}}
731
+ */
732
+ function deployQoderTemplates(agentSrcDir, agentsDestDir, bundled) {
733
+ const summary = { deployed: 0, skipped: 0, rejected: 0 };
734
+ if (!bundled) return summary;
735
+ fs.mkdirSync(agentsDestDir, { recursive: true });
736
+ for (const file of Object.keys(bundled)) {
737
+ const srcFile = path.join(agentSrcDir, file);
738
+ if (fs.existsSync(path.join(agentsDestDir, file))) {
739
+ summary.skipped++; // Don't overwrite user customizations
740
+ continue;
741
+ }
742
+ if (bundled[file] === null) {
743
+ console.warn(` ⚠ Bundled agent template ${srcFile} has no valid model binding; skipped.`);
744
+ summary.rejected++;
745
+ continue;
746
+ }
747
+ fs.copyFileSync(srcFile, path.join(agentsDestDir, file));
748
+ summary.deployed++;
749
+ }
750
+ return summary;
751
+ }
752
+
753
+ /**
754
+ * @param {{deployed: number, skipped: number, rejected: number}} summary
755
+ * @param {string} agentsDestDir - deployed agents directory
756
+ * @param {number} templateCount - templates found in the bundle
757
+ */
758
+ function reportQoderAgentDeployment(summary, agentsDestDir, templateCount) {
759
+ if (summary.deployed > 0) {
760
+ console.log(` Qoder Delphi agents: deployed ${summary.deployed} agent(s) to ${agentsDestDir}`);
761
+ }
762
+ if (summary.skipped > 0) {
763
+ console.log(` Qoder Delphi agents: ${summary.skipped} existing agent(s) preserved`);
764
+ }
765
+ if (summary.rejected > 0) {
766
+ console.warn(` Qoder Delphi agents: ${summary.rejected} bundled template(s) rejected (invalid model binding)`);
767
+ }
768
+ if (templateCount === 0 && summary.deployed === 0 && summary.skipped === 0 && summary.rejected === 0) {
769
+ console.log(' Qoder Delphi agents: no agent templates found');
770
+ }
771
+ }
772
+
773
+ /**
774
+ * A legal binding is not an executed binding: measured on 2026-09-23, Qoder's
775
+ * subagent dispatch adopts the agent persona but pins the model to the session's.
776
+ * Without this line a user leaves `init` believing the three-model contract holds.
777
+ */
778
+ function warnQoderBindingBoundary() {
779
+ console.warn(' ⚠ Platform boundary: Qoder Custom Agent dispatch does not adopt the model');
780
+ console.warn(' bindings declared in these agent files. Every expert still runs on the');
781
+ console.warn(' current session model, so the native path is a same-model three-persona');
782
+ console.warn(" self-check. Delphi's three-distinct-models contract");
783
+ console.warn(' requires an external provider configured in .delphi-config.json.');
784
+ }
785
+
639
786
  /**
640
787
  * Deploy Qoder-native Delphi review agents when platform is Qoder.
641
- * Copies agent templates from the bundled qoder plugin to the project's
642
- * .qoder/agents/ directory. Idempotent — never overwrites existing files.
788
+ * Copies agent templates from the bundled qoder plugin into <targetRoot>/.qoder/agents/
789
+ * — the project dir for local init, the home dir for global init (Qoder reads
790
+ * user-level agents from ~/.qoder/agents/). Idempotent — never overwrites existing files.
643
791
  *
644
792
  * @param {string} srcDir - npm package source directory
645
- * @param {string} projectRoot - user's project root
793
+ * @param {string} targetRoot - project root (local) or home directory (global)
646
794
  */
647
- function configureQoderDelphiAgents(srcDir, projectRoot) {
795
+ function configureQoderDelphiAgents(srcDir, targetRoot) {
648
796
  const platform = detectPlatform();
649
797
  if (platform !== 'qoder') {
650
- return; // Not Qoder — skip (OpenCode uses .delphi-config.json instead)
798
+ console.log(` Qoder Delphi agents: SKIP (platform detected: ${platform})`);
799
+ return; // Not Qoder — OpenCode uses .delphi-config.json instead
651
800
  }
652
801
 
653
802
  const agentSrcDir = path.join(srcDir, 'plugins', 'qoder', 'agents');
654
- if (!fs.existsSync(agentSrcDir)) {
655
- console.log(' Qoder Delphi agents: SKIP (templates not bundled)');
656
- return;
657
- }
803
+ const agentsDestDir = path.join(targetRoot, '.qoder', 'agents');
658
804
 
659
- const agentsDestDir = path.join(projectRoot, '.qoder', 'agents');
660
- fs.mkdirSync(agentsDestDir, { recursive: true });
661
-
662
- const agentFiles = fs.readdirSync(agentSrcDir).filter(f => f.endsWith('.md'));
663
- let deployed = 0;
664
- let skipped = 0;
665
- for (const file of agentFiles) {
666
- const destFile = path.join(agentsDestDir, file);
667
- if (fs.existsSync(destFile)) {
668
- skipped++;
669
- continue; // Don't overwrite user customizations
670
- }
671
- fs.copyFileSync(path.join(agentSrcDir, file), destFile);
672
- deployed++;
805
+ const bundled = readBundledQoderTemplates(agentSrcDir);
806
+ if (!bundled) {
807
+ console.warn(' Qoder Delphi agents: SKIP (templates not bundled — reinstall xp-gate)');
673
808
  }
674
809
 
675
- if (deployed > 0) {
676
- console.log(` Qoder Delphi agents: deployed ${deployed} agent(s) to .qoder/agents/`);
810
+ const summary = deployQoderTemplates(agentSrcDir, agentsDestDir, bundled);
811
+ auditDeployedQoderAgents(agentsDestDir, bundled);
812
+ // 'templates not bundled' already explains a null bundle; a second,
813
+ // contradictory 'no agent templates found' would muddy the same fact.
814
+ if (bundled) {
815
+ reportQoderAgentDeployment(summary, agentsDestDir, Object.keys(bundled).length);
677
816
  }
678
- if (skipped > 0) {
679
- console.log(` Qoder Delphi agents: ${skipped} existing agent(s) preserved`);
817
+ if (summary.deployed + summary.skipped > 0) {
818
+ warnQoderBindingBoundary();
680
819
  }
681
- if (deployed === 0 && skipped === 0) {
682
- console.log(' Qoder Delphi agents: no agent templates found');
820
+ }
821
+
822
+ /**
823
+ * Best-effort wrapper: a failed or partially readable agents dir must never
824
+ * abort the surrounding setup after hooks/adapters/modules are already copied.
825
+ *
826
+ * @param {string} srcDir - npm package source directory
827
+ * @param {string} targetRoot - project root (local) or home directory (global)
828
+ */
829
+ function deployQoderDelphiAgents(srcDir, targetRoot) {
830
+ try {
831
+ configureQoderDelphiAgents(srcDir, targetRoot);
832
+ } catch (err) {
833
+ console.warn(` Qoder Delphi agents: SKIP (deployment failed: ${err && err.message ? err.message : String(err)})`);
683
834
  }
684
835
  }
685
836
 
@@ -771,4 +922,4 @@ function injectKarpathyPrinciples(projectRoot) {
771
922
  }
772
923
  }
773
924
 
774
- module.exports = { init, promptBootstrap, installModuleRuntimeDeps };
925
+ module.exports = { init, promptBootstrap, installModuleRuntimeDeps, deployQoderDelphiAgents, configureQoderDelphiAgents, readQoderModelId };
@@ -28,7 +28,7 @@ async function install(args = []) {
28
28
  return code;
29
29
  }
30
30
 
31
- // Post-install: run doctor --fix to verify and auto-repair
31
+ // Post-install: run doctor --fix to verify and report
32
32
  console.log('\n━━━ Post-Install Health Check ━━━\n');
33
33
  const { doctor } = require('./doctor.js');
34
34
  const doctorCode = await doctor(['--fix']);
@@ -36,11 +36,14 @@ async function install(args = []) {
36
36
  if (doctorCode === 0) {
37
37
  console.log('\n✓ Installation complete and verified!');
38
38
  } else {
39
- console.log('\n⚠ Installation complete, but some issues remain.');
40
- console.log(' Run "xp-gate doctor" for details.');
39
+ // Doctor findings (hook/module drift needing a human judgement, #495) are
40
+ // diagnostics about the environment, not a failed install. Returning its
41
+ // code made `xp-gate install && next-step` fail on a successful install (#503).
42
+ console.log('\n⚠ Installation complete, but doctor reported issues.');
43
+ console.log(' Review them with "xp-gate doctor" — the install itself succeeded.');
41
44
  }
42
45
 
43
- return doctorCode;
46
+ return 0;
44
47
  }
45
48
 
46
49
  module.exports = { install };
@@ -1,10 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const https = require('https');
4
- const http = require('http');
5
- const { execSync } = require('child_process');
6
3
  const { checkDeps } = require('./detect-deps.js');
7
- const { downloadFromGitHub } = require('./download-skill.js');
8
4
  const { rollback } = require('./rollback.js');
9
5
  const { HOME_DIR, CONFIG_DIR, detectPlatform } = require('./shared-paths.js');
10
6
  const { copyDirRecursive } = require('./shared-utils');
@@ -20,6 +16,9 @@ function getSkillsDir() {
20
16
  return path.join(HOME_DIR, '.config', 'opencode', 'skills');
21
17
  }
22
18
 
19
+ // The npm package ships the canonical skill content; `files` includes skills/.
20
+ const BUNDLED_SKILLS_DIR = path.join(__dirname, '..', 'skills');
21
+
23
22
  function getCliVersion() {
24
23
  try {
25
24
  const versionFile = path.join(__dirname, '..', '..', '..', 'VERSION');
@@ -29,24 +28,20 @@ function getCliVersion() {
29
28
  }
30
29
  }
31
30
 
32
- const SKILLS_REGISTRY = {
33
- 'admin-template-guidelines': { repo: 'boyingliu01/xp-gate', path: 'skills/admin-template-guidelines' },
34
- 'batch-grill-me': { repo: 'boyingliu01/xp-gate', path: 'skills/batch-grill-me' },
35
- 'clipboard-vision': { repo: 'boyingliu01/xp-gate', path: 'skills/clipboard-vision' },
36
- 'delphi-review': { repo: 'boyingliu01/xp-gate', path: 'skills/delphi-review' },
37
- 'domain-modeling': { repo: 'boyingliu01/xp-gate', path: 'skills/domain-modeling' },
38
- 'grill-with-docs': { repo: 'boyingliu01/xp-gate', path: 'skills/grill-with-docs' },
39
- 'grilling': { repo: 'boyingliu01/xp-gate', path: 'skills/grilling' },
40
- 'improve-codebase-architecture': { repo: 'boyingliu01/xp-gate', path: 'skills/improve-codebase-architecture' },
41
- 'ralph-loop': { repo: 'boyingliu01/xp-gate', path: 'skills/ralph-loop' },
42
- 'sprint-flow': { repo: 'boyingliu01/xp-gate', path: 'skills/sprint-flow' },
43
- 'test-driven-development': { repo: 'boyingliu01/xp-gate', path: 'skills/test-driven-development' },
44
- 'test-specification-alignment': { repo: 'boyingliu01/xp-gate', path: 'skills/test-specification-alignment' },
45
- 'to-issues': { repo: 'boyingliu01/xp-gate', path: 'skills/to-issues' }
46
- };
31
+ // The table used to be a second, hand-maintained list of installable skills that
32
+ // could only ever lag behind the bundle. The bundle is the contract now: a skill
33
+ // the installed package does not carry is not installable, and must be answered
34
+ // by updating the package rather than by pulling a mismatched copy off GitHub.
35
+ function bundledSkills() {
36
+ if (!fs.existsSync(BUNDLED_SKILLS_DIR)) return [];
37
+ return fs
38
+ .readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true })
39
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(BUNDLED_SKILLS_DIR, entry.name, 'SKILL.md')))
40
+ .map((entry) => entry.name);
41
+ }
47
42
 
48
43
  async function installSkill(name, options = {}) {
49
- const { offline = false, verbose = false, force = false } = options;
44
+ const { verbose = false, force = false } = options;
50
45
 
51
46
  const platform = detectPlatform();
52
47
  const depCheck = await checkDeps(platform);
@@ -64,8 +59,18 @@ async function installSkill(name, options = {}) {
64
59
  }
65
60
  }
66
61
 
67
- const skillInfo = validateSkillRegistry(name);
68
- if (!skillInfo) return 1;
62
+ // Checked before any destructive step, because backupExisting() rmSyncs the
63
+ // target directory: a skill this package does not carry must never be
64
+ // installed by wiping what the user already has.
65
+ const available = bundledSkills();
66
+ if (!available.includes(name)) {
67
+ console.error(`Error: Unknown skill: ${name}`);
68
+ console.error(
69
+ `xp-gate ${getCliVersion()} bundles: ${available.join(', ')}. ` +
70
+ 'Update the package (npm install -g @boyingliu01/xp-gate@latest) if you need ' + name + '.'
71
+ );
72
+ return 1;
73
+ }
69
74
 
70
75
  const targetDir = path.join(getSkillsDir(), name);
71
76
  const dupError = checkDuplicateInstall(targetDir, force);
@@ -79,7 +84,7 @@ async function installSkill(name, options = {}) {
79
84
  backupExisting(targetDir, installId, backupDir);
80
85
 
81
86
  try {
82
- const result = await performInstall(skillInfo, name, targetDir, offline, verbose);
87
+ const result = await performInstall(name, targetDir, verbose);
83
88
  if (result !== 0) return result;
84
89
  return 0;
85
90
  } catch (err) {
@@ -89,16 +94,6 @@ async function installSkill(name, options = {}) {
89
94
  }
90
95
  }
91
96
 
92
- function validateSkillRegistry(name) {
93
- const skillInfo = SKILLS_REGISTRY[name];
94
- if (!skillInfo) {
95
- console.error(`Error: Unknown skill: ${name}`);
96
- console.error('Available skills: ' + Object.keys(SKILLS_REGISTRY).join(', '));
97
- return null;
98
- }
99
- return skillInfo;
100
- }
101
-
102
97
  function checkDuplicateInstall(targetDir, force) {
103
98
  if (fs.existsSync(targetDir) && !force) {
104
99
  return `Error: ${path.basename(targetDir)} is already installed\nUse --force to overwrite`;
@@ -115,38 +110,22 @@ function backupExisting(targetDir, installId, backupDir) {
115
110
  }
116
111
  }
117
112
 
118
- async function performInstall(skillInfo, name, targetDir, offline, verbose) {
113
+ async function performInstall(name, targetDir, verbose) {
119
114
  console.log(`Installing ${name}...`);
120
115
 
121
- const skillUrl = `https://raw.githubusercontent.com/${skillInfo.repo}/main/${skillInfo.path}/SKILL.md`;
122
- const targetFile = path.join(targetDir, 'SKILL.md');
123
- fs.mkdirSync(path.dirname(targetFile), { recursive: true });
124
-
125
- let downloaded = false;
126
- if (!offline) {
127
- try {
128
- await downloadFile(skillUrl, targetFile, verbose);
129
- downloaded = true;
130
- } catch (err) {
131
- if (verbose) console.warn(`Download failed: ${err.message}`);
132
- }
133
- }
134
-
135
- if (!downloaded) {
136
- if (offline) {
137
- console.error(`Error: --offline specified but ${name} not in cache`);
138
- return 2;
139
- }
140
- console.error(`Error: Failed to download ${name}`);
141
- console.error('Check network connection');
142
- return 1;
143
- }
116
+ // The package ships skills/<name>/ with its references/, templates/ and
117
+ // scripts/, and that copy is what doctor compares against. A single SKILL.md
118
+ // downloaded from `main` used to be the whole install, so 8 of 13 skills
119
+ // landed without the files their own SKILL.md tells the agent to read,
120
+ // update-skill wiped complete directories down to that one file, and the
121
+ // installed content drifted from the CLI version (#416).
122
+ copyDirRecursive(path.join(BUNDLED_SKILLS_DIR, name), targetDir);
144
123
 
145
124
  ensureConfigDir();
146
-
125
+
147
126
  // Read actual CLI version from VERSION file
148
127
  const version = getCliVersion();
149
-
128
+
150
129
  updateConfig({
151
130
  installedSkills: {
152
131
  ...(getConfig().installedSkills || {}),
@@ -159,42 +138,6 @@ async function performInstall(skillInfo, name, targetDir, offline, verbose) {
159
138
  return 0;
160
139
  }
161
140
 
162
- async function downloadFile(url, dest, verbose) {
163
- return new Promise((resolve, reject) => {
164
- const file = fs.createWriteStream(dest);
165
-
166
- const protocol = url.startsWith('https') ? https : http;
167
-
168
- if (verbose) console.log(`Downloading ${url}...`);
169
-
170
- protocol.get(url, { timeout: 30000 }, (response) => {
171
- if (response.statusCode === 301 || response.statusCode === 302) {
172
- const redirectUrl = response.headers.location;
173
- file.close();
174
- fs.unlinkSync(dest);
175
- downloadFile(redirectUrl, dest, verbose).then(resolve).catch(reject);
176
- return;
177
- }
178
-
179
- if (response.statusCode !== 200) {
180
- file.close();
181
- reject(new Error(`HTTP ${response.statusCode}`));
182
- return;
183
- }
184
-
185
- response.pipe(file);
186
- file.on('finish', () => {
187
- file.close();
188
- resolve();
189
- });
190
- }).on('error', (err) => {
191
- file.close();
192
- if (fs.existsSync(dest)) fs.unlinkSync(dest);
193
- reject(err);
194
- });
195
- });
196
- }
197
-
198
141
  function ensureConfigDir() {
199
142
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
200
143
  }
@@ -406,6 +406,72 @@ const LANGUAGE_REGISTRY = {
406
406
  },
407
407
  };
408
408
 
409
+ /**
410
+ * Directories that must never drive language detection: dependency trees ship
411
+ * .sh and .ps1 files of their own, which would label every Node project a Shell
412
+ * project.
413
+ */
414
+ const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'dist', 'build', 'out', 'coverage', '.stryker-tmp', '.next']);
415
+
416
+ // A bounded scan, not a full walk: detection only needs to know whether an
417
+ // extension occurs at all, and huge monorepos must not pay for every file.
418
+ const EXTENSION_SCAN_LIMIT = 2000;
419
+
420
+ function extensionsPresentIn(projectRoot) {
421
+ const extensions = new Set();
422
+ const pending = [projectRoot];
423
+ let inspected = 0;
424
+
425
+ while (pending.length > 0 && inspected < EXTENSION_SCAN_LIMIT) {
426
+ const dir = pending.pop();
427
+ let entries;
428
+ try {
429
+ entries = fs.readdirSync(dir, { withFileTypes: true });
430
+ } catch {
431
+ continue;
432
+ }
433
+ // Sorted: readdir order is filesystem-dependent, and a bounded scan that
434
+ // stops at an entry count must give the same answer on every machine
435
+ // (Round 1: feasibility FC-03, architecture MI-01).
436
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
437
+ for (const entry of entries) {
438
+ if (inspected >= EXTENSION_SCAN_LIMIT) break;
439
+ inspected += 1;
440
+ if (entry.isDirectory()) {
441
+ if (!IGNORED_SCAN_DIRS.has(entry.name)) pending.push(path.join(dir, entry.name));
442
+ continue;
443
+ }
444
+ const ext = path.extname(entry.name).slice(1).toLowerCase();
445
+ if (ext) extensions.add(ext);
446
+ }
447
+ }
448
+
449
+ return extensions;
450
+ }
451
+
452
+ /**
453
+ * Read the --languages selection, in either accepted form.
454
+ * `--languages=<a,b>` and `--languages <a,b>`; a bare `--languages`, and an
455
+ * inline `--languages=` with an empty value, both fall back to auto-detection
456
+ * rather than selecting a language named ''.
457
+ * @param {string[]} args
458
+ * @returns {string[]|null}
459
+ */
460
+ function parseLanguageFlag(args) {
461
+ const inline = args.find((arg) => arg.startsWith('--languages='));
462
+ let value;
463
+ if (inline !== undefined) {
464
+ value = inline.slice('--languages='.length);
465
+ } else {
466
+ const index = args.indexOf('--languages');
467
+ const following = index === -1 ? undefined : args[index + 1];
468
+ value = following && !following.startsWith('-') ? following : undefined;
469
+ }
470
+ if (value === undefined) return null;
471
+ const picked = value.split(',').filter(Boolean);
472
+ return picked.length > 0 ? picked : null;
473
+ }
474
+
409
475
  /**
410
476
  * Detect languages used in the current project.
411
477
  * @param {string} projectRoot - Project root directory
@@ -415,6 +481,14 @@ function detectProjectLanguages(projectRoot) {
415
481
  const detected = new Set();
416
482
  const configFiles = {};
417
483
 
484
+ // Scanned lazily: the extension fallback is only consulted for languages that
485
+ // have no config file to look for, and the scan is bounded, not a full walk.
486
+ let scannedExtensions = null;
487
+ const extensionSeen = (ext) => {
488
+ if (scannedExtensions === null) scannedExtensions = extensionsPresentIn(projectRoot);
489
+ return scannedExtensions.has(ext);
490
+ };
491
+
418
492
  for (const [lang, def] of Object.entries(LANGUAGE_REGISTRY)) {
419
493
  const foundConfigs = [];
420
494
 
@@ -443,6 +517,13 @@ function detectProjectLanguages(projectRoot) {
443
517
  if (foundConfigs.length > 0) {
444
518
  detected.add(lang);
445
519
  configFiles[lang] = foundConfigs;
520
+ } else if (def.configFiles.length === 0 && def.extensions.some(extensionSeen)) {
521
+ // Shell and PowerShell have no package-manager manifest to key off, so file
522
+ // extensions are their only possible signal (#468). Languages that do have
523
+ // config files keep using them: their extension lists are broad (IaC lists
524
+ // yaml/yml), and matching on those would make every repo with a CI YAML an
525
+ // Infrastructure-as-Code project.
526
+ detected.add(lang);
446
527
  }
447
528
  }
448
529
 
@@ -692,8 +773,7 @@ async function handleDetectLanguages(args) {
692
773
  async function handleCheckTools(args) {
693
774
  const projectRoot = process.cwd();
694
775
  const jsonOutput = args.includes('--json');
695
- const langsArg = args.find(a => a.startsWith('--languages='));
696
- let languages = langsArg ? langsArg.split('=')[1].split(',') : null;
776
+ let languages = parseLanguageFlag(args);
697
777
 
698
778
  // Auto-detect if no languages specified
699
779
  if (!languages) {
@@ -761,8 +841,7 @@ async function handleInstallTools(args) {
761
841
  const projectRoot = process.cwd();
762
842
  const dryRun = args.includes('--dry-run');
763
843
  const autoYes = args.includes('--yes') || args.includes('-y');
764
- const langsArg = args.find(a => a.startsWith('--languages='));
765
- let languages = langsArg ? langsArg.split('=')[1].split(',') : null;
844
+ let languages = parseLanguageFlag(args);
766
845
 
767
846
  // Auto-detect if no languages specified
768
847
  if (!languages) {
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-09-24T04:13:16.240Z",
5
+ "checked_at": "2026-10-08T07:48:14.654Z",
6
6
  "source": "network"
7
7
  }
@@ -24,11 +24,14 @@ const GLOBAL_MODULES_DIR = path.join(CONFIG_DIR, 'modules');
24
24
 
25
25
  /**
26
26
  * Detect which AI agent platform is currently in use.
27
- * Mirrors detectPlatform() in detect-deps.js to avoid circular deps.
27
+ * This is the single implementation; detect-deps.js re-exports it (#424).
28
28
  * @returns {'opencode' | 'claude-code' | 'qoder'}
29
29
  */
30
30
  function detectPlatform() {
31
- if (fs.existsSync(path.join(HOME_DIR, '.qoder', 'skills'))) return 'qoder';
31
+ // Any ~/.qoder content counts as Qoder: a brand-new install has no skills/
32
+ // subdirectory yet, so a skills-only marker misdetects it as opencode and
33
+ // configureQoderDelphiAgents deploys zero Delphi agents (#424).
34
+ if (fs.existsSync(path.join(HOME_DIR, '.qoder'))) return 'qoder';
32
35
  if (fs.existsSync(path.join(HOME_DIR, '.claude', 'skills'))) return 'claude-code';
33
36
  return 'opencode';
34
37
  }