@boyingliu01/xp-gate 0.19.3 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/gate-4.sh +83 -18
- package/adapters/gate-8.sh +115 -29
- package/adapters/iac.sh +1 -1
- package/adapters/powershell.sh +60 -5
- package/adapters/python.sh +38 -4
- package/adapters/typescript.sh +15 -60
- package/bin/xp-gate.js +15 -16
- package/build-integrity/__tests__/import-resolver.test.ts +59 -0
- package/build-integrity/gate-10.ts +17 -2
- package/gate-10.sh +35 -2
- package/gate-4.sh +110 -21
- package/gate-8.sh +115 -29
- package/hooks/gate-10.sh +35 -2
- package/hooks/gate-4.sh +110 -21
- package/hooks/gate-8.sh +115 -29
- package/hooks/lib/jscpd-run.sh +63 -0
- package/hooks/lib/sprint-gate-report.sh +58 -0
- package/hooks/lib/test-failure.sh +152 -0
- package/hooks/lib/typecheck.sh +141 -0
- package/hooks/lib/validate-code-walkthrough.cjs +43 -5
- package/hooks/post-merge +100 -0
- package/hooks/pre-commit +514 -184
- package/hooks/pre-push +75 -19
- package/lib/__tests__/bootstrap.test.js +23 -41
- package/lib/__tests__/check-version.test.js +8 -1
- package/lib/__tests__/detect-deps.test.js +16 -0
- package/lib/__tests__/doctor-drift-round2.test.js +212 -0
- package/lib/__tests__/doctor-executed-surface.test.js +209 -0
- package/lib/__tests__/doctor-hook-drift.test.js +329 -0
- package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
- package/lib/__tests__/doctor-module-drift.test.js +206 -0
- package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
- package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
- package/lib/__tests__/doctor.test.js +155 -0
- package/lib/__tests__/gate-runner.test.js +19 -3
- package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
- package/lib/__tests__/init.test.js +44 -17
- package/lib/__tests__/install-skill-full-dir.test.js +151 -0
- package/lib/__tests__/install-skill.test.js +51 -132
- package/lib/__tests__/language-tools-detect.test.js +158 -0
- package/lib/__tests__/phase-transition.test.js +15 -1
- package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
- package/lib/__tests__/shared-paths.test.js +20 -0
- package/lib/__tests__/shared-utils.test.js +69 -0
- package/lib/__tests__/skill-flags.test.js +48 -0
- package/lib/__tests__/ui-detector.test.ts +6 -2
- package/lib/__tests__/uninstall.test.js +20 -0
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/update-skill.test.js +7 -4
- package/lib/detect-deps.js +3 -20
- package/lib/doctor-tui.js +22 -5
- package/lib/doctor.js +1178 -31
- package/lib/init.js +184 -33
- package/lib/install-cmd.js +7 -4
- package/lib/install-skill.js +38 -95
- package/lib/language-tools.js +83 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-paths.js +5 -2
- package/lib/shared-utils.js +47 -3
- package/lib/uninstall.js +12 -0
- package/lib/update-hooks.js +4 -4
- package/lib/update-skill.js +6 -7
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/gate-m.test.ts +66 -14
- package/mutation/__tests__/mutmut-runner.test.ts +91 -42
- package/mutation/__tests__/test-filter-parity.test.ts +37 -0
- package/mutation/gate-m.ts +21 -8
- package/package.json +2 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +169 -4
- package/plugins/dsh/dsh-overlay.patch.yml +67 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +24 -19
- package/plugins/dsh/setup-dsh.ps1 +160 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/command.test.ts +66 -2
- package/plugins/dsh/src/command.ts +36 -14
- package/plugins/dsh/src/contract.test.ts +31 -1
- package/plugins/dsh/src/delphi-config.test.ts +190 -0
- package/plugins/dsh/src/delphi-config.ts +223 -0
- package/plugins/dsh/src/delphi-run.test.ts +181 -0
- package/plugins/dsh/src/delphi-run.ts +129 -0
- package/plugins/dsh/src/delphi.test.ts +222 -0
- package/plugins/dsh/src/delphi.ts +247 -0
- package/plugins/dsh/src/dialect.test.ts +59 -0
- package/plugins/dsh/src/dialect.ts +40 -0
- package/plugins/dsh/src/index.ts +70 -4
- package/plugins/dsh/src/skills.test.ts +26 -19
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/agents/delphi-architecture.md +1 -1
- package/plugins/qoder/agents/delphi-feasibility.md +1 -1
- package/plugins/qoder/agents/delphi-technical.md +1 -1
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +9 -8
- package/principles/__tests__/adapter-metrics.test.ts +292 -0
- package/principles/__tests__/analyzer.test.ts +29 -6
- package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
- package/principles/__tests__/baseline-prune.test.ts +338 -0
- package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
- package/principles/__tests__/boy-scout-integration.test.ts +97 -6
- package/principles/__tests__/boy-scout.test.ts +253 -7
- package/principles/__tests__/config-enforcement.test.ts +469 -0
- package/principles/__tests__/config-git-timeout.test.ts +66 -0
- package/principles/__tests__/direct-execution.test.ts +187 -0
- package/principles/__tests__/index.test.ts +16 -3
- package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
- package/principles/adapters/typescript.ts +613 -13
- package/principles/analyzer.ts +22 -0
- package/principles/baseline-prune.ts +217 -0
- package/principles/boy-scout.ts +516 -56
- package/principles/config.ts +362 -54
- package/principles/direct-execution.ts +60 -0
- package/principles/index.ts +40 -17
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
- package/principles/rules/clean-code/code-duplication.ts +8 -8
- package/principles/rules/clean-code/deep-nesting.ts +8 -8
- package/principles/rules/clean-code/god-class.ts +8 -8
- package/principles/rules/clean-code/large-file.ts +15 -11
- package/principles/rules/clean-code/long-function.ts +8 -8
- package/principles/rules/clean-code/magic-numbers.ts +10 -7
- package/principles/rules/clean-code/many-exports.ts +7 -7
- package/principles/rules/clean-code/missing-error-handling.ts +5 -5
- package/principles/rules/clean-code/too-many-params.ts +8 -8
- package/principles/rules/clean-code/unused-imports.ts +5 -5
- package/principles/rules/solid/dip.ts +18 -8
- package/principles/rules/solid/isp.ts +9 -8
- package/principles/rules/solid/lsp.ts +6 -6
- package/principles/rules/solid/ocp.ts +6 -5
- package/principles/rules/solid/srp.ts +7 -8
- package/principles/types.ts +15 -0
- package/scripts/__tests__/sync-package-content.test.js +28 -0
- package/scripts/delphi-external-review.cjs +193 -10
- package/scripts/sync-package-content.js +20 -10
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/delphi-review/SKILL.md +98 -14
- package/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/skills/delphi-review/references/requirements.md +6 -6
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/lib/__tests__/download-skill.test.js +0 -281
- 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
|
-
|
|
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
|
|
642
|
-
*
|
|
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}
|
|
793
|
+
* @param {string} targetRoot - project root (local) or home directory (global)
|
|
646
794
|
*/
|
|
647
|
-
function configureQoderDelphiAgents(srcDir,
|
|
795
|
+
function configureQoderDelphiAgents(srcDir, targetRoot) {
|
|
648
796
|
const platform = detectPlatform();
|
|
649
797
|
if (platform !== 'qoder') {
|
|
650
|
-
|
|
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
|
-
|
|
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
|
|
660
|
-
|
|
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
|
-
|
|
676
|
-
|
|
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
|
-
|
|
817
|
+
if (summary.deployed + summary.skipped > 0) {
|
|
818
|
+
warnQoderBindingBoundary();
|
|
680
819
|
}
|
|
681
|
-
|
|
682
|
-
|
|
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 };
|
package/lib/install-cmd.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
46
|
+
return 0;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
module.exports = { install };
|
package/lib/install-skill.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 {
|
|
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
|
-
|
|
68
|
-
|
|
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(
|
|
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(
|
|
113
|
+
async function performInstall(name, targetDir, verbose) {
|
|
119
114
|
console.log(`Installing ${name}...`);
|
|
120
115
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
}
|
package/lib/language-tools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
package/lib/shared-paths.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
}
|