Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78667b182f | ||
|
|
d01b819b02 | ||
|
|
727c499d2c | ||
|
|
2975619da8 | ||
|
|
6420e54e4e | ||
|
|
84fcb0f583 | ||
|
|
65eee69fb9 | ||
|
|
64fb27f539 |
No files matched your search
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|apply_patch",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
|
||||
"commandWindows": "powershell -NoProfile -NonInteractive -EncodedCommand JABQAHIAbwBnAHIAZQBzAHMAUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwA7ACAAaQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJwBDADoALwBVAHMAZQByAHMALwBqAC4AbABlAGMAaABuAGUAcgAvAC4AYQBnAGUAbgB0AHMALwBzAGsAaQBsAGwAcwAvAGkAbQBwAGUAYwBjAGEAYgBsAGUALwBzAGMAcgBpAHAAdABzAC8AaQBtAHAAZQBjAGMAYQBiAGwAZQAuAGMAbQBkACcAKQAgAHsAIAAmACAAJwBDADoALwBVAHMAZQByAHMALwBqAC4AbABlAGMAaABuAGUAcgAvAC4AYQBnAGUAbgB0AHMALwBzAGsAaQBsAGwAcwAvAGkAbQBwAGUAYwBjAGEAYgBsAGUALwBzAGMAcgBpAHAAdABzAC8AaQBtAHAAZQBjAGMAYQBiAGwAZQAuAGMAbQBkACcAIABoAG8AbwBrADsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0A",
|
||||
"timeout": 5,
|
||||
"statusMessage": "Checking UI changes"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
|
||||
"commandWindows": "powershell -NoProfile -NonInteractive -EncodedCommand JABQAHIAbwBnAHIAZQBzAHMAUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwA7ACAAaQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJwBDADoALwBVAHMAZQByAHMALwBqAC4AbABlAGMAaABuAGUAcgAvAC4AYQBnAGUAbgB0AHMALwBzAGsAaQBsAGwAcwAvAGkAbQBwAGUAYwBjAGEAYgBsAGUALwBzAGMAcgBpAHAAdABzAC8AaQBtAHAAZQBjAGMAYQBiAGwAZQAuAGMAbQBkACcAKQAgAHsAIAAmACAAJwBDADoALwBVAHMAZQByAHMALwBqAC4AbABlAGMAaABuAGUAcgAvAC4AYQBnAGUAbgB0AHMALwBzAGsAaQBsAGwAcwAvAGkAbQBwAGUAYwBjAGEAYgBsAGUALwBzAGMAcgBpAHAAdABzAC8AaQBtAHAAZQBjAGMAYQBiAGwAZQAuAGMAbQBkACcAIABoAG8AbwBrADsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0A",
|
||||
"timeout": 30,
|
||||
"statusMessage": "Design deep pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+30
-79
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -12,85 +12,36 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: Check Linux tooling
|
||||
run: |
|
||||
command -v pwsh >/dev/null || { echo 'pwsh is required on the Linux runner'; exit 1; }
|
||||
- name: Bash syntax
|
||||
run: find scripts shared/hooks -name '*.sh' -print0 | xargs -0 -n1 bash -n
|
||||
- name: Capability checks
|
||||
run: ./scripts/check-capabilities.sh
|
||||
- name: Build
|
||||
run: ./scripts/build.sh --summary
|
||||
- name: Prompt budget
|
||||
run: ./scripts/prompt-budget.sh --summary
|
||||
- name: Repository context cache
|
||||
run: ./scripts/repository-context.sh --summary
|
||||
- name: Doctor
|
||||
run: ./scripts/doctor.sh --summary
|
||||
shell: bash
|
||||
run: git ls-files -z '*.sh' | xargs -0 -r -n1 bash -n
|
||||
- name: PowerShell syntax
|
||||
run: |
|
||||
find scripts shared/hooks -name '*.ps1' -print0 | xargs -0 -n1 pwsh -NoProfile -Command '$tokens=$null; $errors=$null; [System.Management.Automation.Language.Parser]::ParseFile($args[0],[ref]$tokens,[ref]$errors) | Out-Null; if ($errors.Count) { $errors | % { Write-Error $_ }; exit 1 }'
|
||||
- name: PowerShell build and validation
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:REQUIRE_CODEX_SCHEMA = 'true'
|
||||
./scripts/check-capabilities.ps1
|
||||
./scripts/build.ps1 -Summary
|
||||
./scripts/doctor.ps1 -Summary
|
||||
./scripts/test-shell-packages.ps1 -Summary
|
||||
./scripts/test-managed-install.ps1 -Summary
|
||||
./scripts/test-build-validation.ps1 -Summary
|
||||
./scripts/test-install-guards.ps1 -Summary
|
||||
./scripts/test-plugin-selection.ps1 -Summary
|
||||
./scripts/test-quick-update.ps1 -Summary
|
||||
python ./scripts/test-install-options.py
|
||||
python ./scripts/test-quick-update.py
|
||||
python ./scripts/test-managed-extensions.py
|
||||
python ./scripts/test-session-state.py
|
||||
python ./scripts/test-telemetry.py
|
||||
python ./scripts/test-semantic-retrieval.py
|
||||
./scripts/install.ps1 -Summary -DryRun -Client Both -Shell PowerShell
|
||||
./scripts/update.ps1 -Summary -DryRun -Client Both -Shell PowerShell
|
||||
- name: Shell package tests
|
||||
run: ./scripts/test-shell-packages.sh --summary
|
||||
- name: Managed manifest tests
|
||||
run: ./scripts/test-managed-install.sh --summary
|
||||
- name: Build validation tests
|
||||
run: ./scripts/test-build-validation.sh --summary
|
||||
- name: Install guard tests
|
||||
run: ./scripts/test-install-guards.sh --summary
|
||||
- name: Plugin selection tests
|
||||
run: ./scripts/test-plugin-selection.sh --summary
|
||||
- name: Quick-update and install-option tests
|
||||
run: |
|
||||
./scripts/test-quick-update.sh --summary
|
||||
python3 scripts/test-install-options.py
|
||||
python3 scripts/test-telemetry.py
|
||||
python3 scripts/test-semantic-retrieval.py
|
||||
- name: Managed extension and session-state tests
|
||||
run: |
|
||||
python3 scripts/test-managed-extensions.py
|
||||
python3 scripts/test-session-state.py
|
||||
- name: Install dry run
|
||||
run: |
|
||||
export HOME="$RUNNER_TEMP/isolated-home"
|
||||
mkdir -p "$HOME"
|
||||
./scripts/install.sh --summary --dry-run --client both --platform linux
|
||||
- name: Update dry run
|
||||
run: |
|
||||
export HOME="$RUNNER_TEMP/isolated-home"
|
||||
mkdir -p "$HOME"
|
||||
./scripts/update.sh --summary --dry-run --client both --platform linux
|
||||
|
||||
compatibility-canary:
|
||||
if: github.event_name == 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: Latest external clients
|
||||
run: npm install --global @openai/codex@latest
|
||||
- name: Latest-client schema and dry runs
|
||||
run: |
|
||||
REQUIRE_CODEX_SCHEMA=true ./scripts/build.sh --summary
|
||||
HOME="$RUNNER_TEMP/canary-home" ./scripts/install.sh --summary --dry-run --client both --platform linux
|
||||
HOME="$RUNNER_TEMP/canary-home" ./scripts/update.sh --summary --dry-run --client both --platform linux
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$failed = $false
|
||||
foreach ($file in (git ls-files '*.ps1')) {
|
||||
$tokens = $null
|
||||
$parseErrors = $null
|
||||
[System.Management.Automation.Language.Parser]::ParseFile((Join-Path $PWD $file), [ref]$tokens, [ref]$parseErrors) | Out-Null
|
||||
if ($parseErrors.Count) {
|
||||
$parseErrors | ForEach-Object { Write-Host "${file}: $_" }
|
||||
$failed = $true
|
||||
}
|
||||
}
|
||||
if ($failed) { exit 1 }
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: bash scripts/build.sh --summary
|
||||
- name: Bash install dry run
|
||||
shell: bash
|
||||
run: bash scripts/install.sh --summary --dry-run --client both --shell bash
|
||||
- name: Bash update dry run
|
||||
shell: bash
|
||||
run: bash scripts/update.sh --summary --dry-run --client both --shell bash
|
||||
- name: PowerShell install dry run
|
||||
shell: pwsh
|
||||
run: ./scripts/install.ps1 -Summary -DryRun -Client Both -Shell PowerShell
|
||||
- name: PowerShell update dry run
|
||||
shell: pwsh
|
||||
run: ./scripts/update.ps1 -Summary -DryRun -Client Both -Shell PowerShell
|
||||
@@ -211,7 +211,7 @@ and replaced, with a warning. Untracked files outside package target paths are l
|
||||
|
||||
Previously managed files no longer shipped are backed up and removed if unchanged. If locally modified, they are
|
||||
backed up and retained with a warning. Backups are stored under `backups/<timestamp>/` in each destination.
|
||||
Local Codex `model`, `reasoning_effort`, `plugins`, and `marketplaces` settings are preserved when syncing `config.toml`;
|
||||
Local Codex `model`, `model_reasoning_effort`, `plugins`, and `marketplaces` settings are preserved when syncing `config.toml`;
|
||||
other local settings are not generally merged. Dry runs still rebuild `generated/`, but do not modify installation destinations.
|
||||
|
||||
Selecting Codex also installs shared skills to `.agents/skills`. An interactive extension list includes plugins and
|
||||
@@ -296,10 +296,9 @@ turning yellow at 60% and red at 85%; its status-line input does not expose the
|
||||
### Add a rule
|
||||
|
||||
Create a focused Markdown file in `shared/rules/`. For a rule that applies only to a language, framework, tool, or
|
||||
change area, add a row to `adapters/claude/rule-skills.tsv` (Claude generates it as a skill) and add its loading
|
||||
condition to the Codex rule-loading text in `scripts/build.ps1`/`scripts/build.sh` (Codex loads it as a plain file, by
|
||||
path). Put always-applicable guidance in `general.md`, which the build embeds for both clients; a new rule file is
|
||||
not automatically embedded.
|
||||
change area, add one row to `adapters/claude/rule-skills.tsv`. The build uses that manifest to generate both Claude's
|
||||
rule skill and Codex's rule-loading condition. Put always-applicable guidance in `general.md`, which the build embeds
|
||||
for both clients; a new rule file is not automatically embedded.
|
||||
|
||||
### Add a skill
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"Claude permanent context":5030,"Codex permanent context":4828,"Claude permanent context absolute limit":6500,"Codex permanent context absolute limit":6500}
|
||||
{"Claude permanent context":7101,"Codex permanent context":7377,"Claude permanent context absolute limit":8000,"Codex permanent context absolute limit":8000}
|
||||
@@ -1,2 +1,2 @@
|
||||
Claude 5030 6500
|
||||
Codex 4828 6500
|
||||
Claude 7101 8000
|
||||
Codex 7377 8000
|
||||
|
@@ -24,7 +24,7 @@ function Read-InstallOptions {
|
||||
return @{
|
||||
update_agents = Read-InstallBoolean -Prompt "Update selected agent CLIs ($Client)?" -Default $defaults.update_agents
|
||||
flashbang = Read-InstallBoolean -Prompt 'Enable the Flashbang notification hook?' -Default $defaults.flashbang
|
||||
semantic_retrieval = Read-InstallBoolean -Prompt 'Enable Qwen3-Embedding-0.6B semantic retrieval?' -Default $defaults.semantic_retrieval
|
||||
semantic_retrieval = Read-InstallBoolean -Prompt 'Enable Qwen3 embedding and reranking semantic retrieval?' -Default $defaults.semantic_retrieval
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ read_install_options() {
|
||||
[ "$dry_run" = false ] || return 0
|
||||
update_agents=$(read_install_boolean "Update selected agent CLIs ($client)?" "$update_agents") || return
|
||||
flashbang=$(read_install_boolean 'Enable the Flashbang notification hook?' "$flashbang") || return
|
||||
semantic_retrieval=$(read_install_boolean 'Enable Qwen3-Embedding-0.6B semantic retrieval?' "$semantic_retrieval") || return
|
||||
semantic_retrieval=$(read_install_boolean 'Enable Qwen3 embedding and reranking semantic retrieval?' "$semantic_retrieval") || return
|
||||
}
|
||||
|
||||
is_windows_host() {
|
||||
|
||||
@@ -87,7 +87,7 @@ function Sync-ManagedDestination {
|
||||
$localConfig = [System.IO.File]::ReadAllText($target)
|
||||
$pluginTables = [regex]::Matches($localConfig, '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])[\s\S]*?(?=^[ \t]*\[|\z)')
|
||||
$rootConfig = [regex]::Split($localConfig, '(?m)^[ \t]*\[', 2)[0]
|
||||
$localSettings = [regex]::Matches($rootConfig, '(?m)^[ \t]*(?:model|reasoning_effort)[ \t]*=.*$')
|
||||
$localSettings = [regex]::Matches($rootConfig, '(?m)^[ \t]*(?:model|model_reasoning_effort)[ \t]*=.*$')
|
||||
if ($localSettings.Count -gt 0) {
|
||||
$content = [System.Text.Encoding]::UTF8.GetString($newBytes).TrimEnd([char[]]"`r`n")
|
||||
foreach ($setting in $localSettings) {
|
||||
|
||||
@@ -87,7 +87,7 @@ sync_managed_destination() {
|
||||
local plugin_tables local_settings setting_name
|
||||
local_settings=$(awk '
|
||||
/^[ \t]*\[/ { exit }
|
||||
/^[ \t]*(model|reasoning_effort)[ \t]*=/ { print }
|
||||
/^[ \t]*(model|model_reasoning_effort)[ \t]*=/ { print }
|
||||
' "$target")
|
||||
if [ -n "$local_settings" ]; then
|
||||
while IFS= read -r setting; do
|
||||
|
||||
@@ -96,7 +96,7 @@ def sync(root, home, clients, enabled, dry_run, update):
|
||||
if target.exists() and not state['files']:
|
||||
raise ValueError('Semantic retrieval directory exists without setup ownership; refusing to overwrite it.')
|
||||
if dry_run:
|
||||
print('DRYRUN enable semantic retrieval: Qwen3-Embedding-0.6B')
|
||||
print('DRYRUN enable semantic retrieval: Qwen3-Embedding-0.6B + Qwen3-Reranker-0.6B')
|
||||
return
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
source = root / 'shared' / 'retrieval'
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -54,15 +54,15 @@ try {
|
||||
[plugins."disabled@market"]
|
||||
enabled = false
|
||||
'@
|
||||
[System.IO.File]::WriteAllText((Join-Path $source 'config.toml'), "model = 'new'`nreasoning_effort = 'low'`n")
|
||||
[System.IO.File]::WriteAllText($configPath, "model = 'old'`nreasoning_effort = 'high'`n$pluginState`n[other]`nvalue = true`n")
|
||||
[System.IO.File]::WriteAllText((Join-Path $source 'config.toml'), "model = 'new'`nmodel_reasoning_effort = 'low'`napprovals_reviewer = 'auto_review'`n")
|
||||
[System.IO.File]::WriteAllText($configPath, "model = 'old'`nmodel_reasoning_effort = 'high'`napprovals_reviewer = 'user'`n$pluginState`n[other]`nvalue = true`n")
|
||||
$originalConfig = [System.IO.File]::ReadAllText($configPath)
|
||||
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-dry' -DryRun
|
||||
if ([System.IO.File]::ReadAllText($configPath) -ne $originalConfig) { throw 'Dry run must preserve plugin configuration.' }
|
||||
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins'
|
||||
$mergedConfig = [System.IO.File]::ReadAllText($configPath)
|
||||
if (-not ($mergedConfig -replace "`r`n", "`n").Contains(($pluginState -replace "`r`n", "`n"))) { throw 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.' }
|
||||
if (-not $mergedConfig.Contains("model = 'old'") -or -not $mergedConfig.Contains("reasoning_effort = 'high'") -or $mergedConfig.Contains("model = 'new'") -or $mergedConfig.Contains("reasoning_effort = 'low'") -or $mergedConfig.Contains('[other]')) { throw 'Updating config.toml must preserve local model and reasoning effort while updating managed configuration.' }
|
||||
if (-not $mergedConfig.Contains("model = 'old'") -or -not $mergedConfig.Contains("model_reasoning_effort = 'high'") -or -not $mergedConfig.Contains("approvals_reviewer = 'auto_review'") -or $mergedConfig.Contains("model = 'new'") -or $mergedConfig.Contains("model_reasoning_effort = 'low'") -or $mergedConfig.Contains("approvals_reviewer = 'user'") -or $mergedConfig.Contains('[other]')) { throw 'Updating config.toml must preserve local model settings and enforce automatic review.' }
|
||||
if ([System.IO.File]::ReadAllText((Join-Path $destination 'backups/plugins/config.toml')) -ne $originalConfig) { throw 'Original plugin configuration must be backed up.' }
|
||||
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-repeat'
|
||||
if ([System.IO.File]::ReadAllText($configPath) -ne $mergedConfig -or (Test-Path (Join-Path $destination 'backups/plugins-repeat'))) { throw 'Preserving plugin configuration must be idempotent.' }
|
||||
|
||||
@@ -58,8 +58,8 @@ plugin_state=' [marketplaces.ponytail]
|
||||
enabled = true
|
||||
[plugins."disabled@market"]
|
||||
enabled = false'
|
||||
printf "model = 'new'\nreasoning_effort = 'low'\n" > "$source_dir/config.toml"
|
||||
printf "model = 'old'\nreasoning_effort = 'high'\n%s\n[other]\nvalue = true\n" "$plugin_state" > "$destination/config.toml"
|
||||
printf "model = 'new'\nmodel_reasoning_effort = 'low'\napprovals_reviewer = 'auto_review'\n" > "$source_dir/config.toml"
|
||||
printf "model = 'old'\nmodel_reasoning_effort = 'high'\napprovals_reviewer = 'user'\n%s\n[other]\nvalue = true\n" "$plugin_state" > "$destination/config.toml"
|
||||
original_config=$(cat "$destination/config.toml")
|
||||
sync_managed_destination "$source_dir" "$destination" plugins-dry "" "" "" true > /dev/null
|
||||
[ "$(cat "$destination/config.toml")" = "$original_config" ] || { printf 'Dry run changed plugin configuration.\n' >&2; exit 1; }
|
||||
@@ -68,7 +68,7 @@ merged_config=$(cat "$destination/config.toml")
|
||||
merged_config_normalized=${merged_config//$'\r'/}
|
||||
plugin_state_normalized=${plugin_state//$'\r'/}
|
||||
[[ "$merged_config_normalized" == *"$plugin_state_normalized"* ]] || { printf 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.\n' >&2; exit 1; }
|
||||
[[ "$merged_config" == *"model = 'old'"* && "$merged_config" == *"reasoning_effort = 'high'"* && "$merged_config" != *"model = 'new'"* && "$merged_config" != *"reasoning_effort = 'low'"* && "$merged_config" != *'[other]'* ]] || { printf 'Updating config.toml must preserve local model and reasoning effort while updating managed configuration.\n' >&2; exit 1; }
|
||||
[[ "$merged_config" == *"model = 'old'"* && "$merged_config" == *"model_reasoning_effort = 'high'"* && "$merged_config" == *"approvals_reviewer = 'auto_review'"* && "$merged_config" != *"model = 'new'"* && "$merged_config" != *"model_reasoning_effort = 'low'"* && "$merged_config" != *"approvals_reviewer = 'user'"* && "$merged_config" != *'[other]'* ]] || { printf 'Updating config.toml must preserve local model settings and enforce automatic review.\n' >&2; exit 1; }
|
||||
[ "$(cat "$destination/backups/plugins/config.toml")" = "$original_config" ] || { printf 'Original plugin configuration must be backed up.\n' >&2; exit 1; }
|
||||
sync_managed_destination "$source_dir" "$destination" plugins-repeat "" "" "" false > /dev/null
|
||||
[ "$(cat "$destination/config.toml")" = "$merged_config" ] && [ ! -d "$destination/backups/plugins-repeat" ] || { printf 'Preserving plugin configuration must be idempotent.\n' >&2; exit 1; }
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -0,0 +1,36 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SPEC = importlib.util.spec_from_file_location('retrieval_server', ROOT / 'shared/retrieval/server.py')
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class RetrievalServerTests(unittest.TestCase):
|
||||
def test_rerank_uses_cross_encoder_scores(self):
|
||||
index = MODULE.Index.__new__(MODULE.Index)
|
||||
|
||||
class Reranker:
|
||||
def predict(self, pairs):
|
||||
self.pairs = pairs
|
||||
return [0.1, 0.8, 0.5]
|
||||
|
||||
reranker = Reranker()
|
||||
index.reranker = lambda: reranker
|
||||
rows = [
|
||||
{'path': 'a.md', 'score': 0.9, 'text': 'first'},
|
||||
{'path': 'b.md', 'score': 0.8, 'text': 'second'},
|
||||
{'path': 'c.md', 'score': 0.7, 'text': 'third'},
|
||||
]
|
||||
|
||||
result = index.rerank('query', rows, 2)
|
||||
|
||||
self.assertEqual([item['path'] for item in result], ['b.md', 'c.md'])
|
||||
self.assertEqual(reranker.pairs, [('query', 'first'), ('query', 'second'), ('query', 'third')])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -1,2 +1,2 @@
|
||||
mcp>=1.9,<2
|
||||
sentence-transformers>=4.1,<5
|
||||
sentence-transformers>=5.4,<6
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local MCP semantic search backed by Qwen3-Embedding-0.6B."""
|
||||
"""Local MCP semantic search backed by Qwen3 embedding and reranking."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -11,6 +11,7 @@ import sqlite3
|
||||
|
||||
|
||||
MODEL = "Qwen/Qwen3-Embedding-0.6B"
|
||||
RERANKER_MODEL = "Qwen/Qwen3-Reranker-0.6B"
|
||||
TEXT_EXTENSIONS = {".c", ".cpp", ".cs", ".go", ".java", ".js", ".json", ".md", ".py", ".ps1", ".rs", ".sh", ".toml", ".ts", ".tsx", ".txt", ".yaml", ".yml"}
|
||||
|
||||
|
||||
@@ -31,6 +32,7 @@ class Index:
|
||||
self.db.execute("create table if not exists chunks (path text, text text, vector blob)")
|
||||
self.db.commit()
|
||||
self.model = None
|
||||
self.reranker_model = None
|
||||
|
||||
def files(self):
|
||||
ignored = {".git", ".venv", "generated", "node_modules", ".my-ai-configuration"}
|
||||
@@ -49,6 +51,19 @@ class Index:
|
||||
self.model = SentenceTransformer(MODEL)
|
||||
return self.model
|
||||
|
||||
def reranker(self):
|
||||
if self.reranker_model is None:
|
||||
from sentence_transformers import CrossEncoder
|
||||
self.reranker_model = CrossEncoder(RERANKER_MODEL)
|
||||
return self.reranker_model
|
||||
|
||||
def rerank(self, query: str, rows: list[dict], limit: int):
|
||||
candidates = rows[:min(len(rows), max(limit, 20))]
|
||||
scores = self.reranker().predict([(query, item["text"]) for item in candidates])
|
||||
for item, score in zip(candidates, scores):
|
||||
item["score"] = round(float(score), 6)
|
||||
return sorted(candidates, key=lambda item: item["score"], reverse=True)[:limit]
|
||||
|
||||
def rebuild(self):
|
||||
encoder = self.encoder()
|
||||
values = []
|
||||
@@ -73,7 +88,8 @@ class Index:
|
||||
import numpy as np
|
||||
score = float(np.dot(vector, np.frombuffer(blob, dtype=np.float32)))
|
||||
rows.append({"path": path, "score": round(score, 6), "text": text})
|
||||
return sorted(rows, key=lambda item: item["score"], reverse=True)[:max(1, min(top_k, 20))]
|
||||
limit = max(1, min(top_k, 20))
|
||||
return self.rerank(query, sorted(rows, key=lambda item: item["score"], reverse=True), limit)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -91,7 +107,7 @@ def main():
|
||||
|
||||
@mcp.tool()
|
||||
def semantic_search(query: str, top_k: int = 5) -> list[dict]:
|
||||
"""Find repository passages by meaning with Qwen3-Embedding-0.6B."""
|
||||
"""Find repository passages with Qwen3 embedding and reranking."""
|
||||
return index.search(query, top_k)
|
||||
|
||||
@mcp.tool()
|
||||
|
||||
@@ -21,7 +21,7 @@ Project instructions may specialize global defaults, but must not weaken securit
|
||||
|
||||
## Semantic repository search
|
||||
|
||||
* In Codex and Claude Code, proactively use Qwen3-Embedding-0.6B through the `my-ai-qwen3-retrieval` MCP server's `semantic_search` tool whenever available and useful. Discover the tool if it is not already exposed.
|
||||
* In Codex and Claude Code, proactively use the `my-ai-qwen3-retrieval` MCP server's `semantic_search` tool whenever available and useful. It always uses Qwen3-Embedding-0.6B for retrieval and Qwen3-Reranker-0.6B for reranking. Discover the tool if it is not already exposed.
|
||||
* Use semantic search early to find unfamiliar code, understand responsibilities and relationships, locate similar implementations, or investigate behavior described without exact symbols. Reuse relevant results and search again when a new question would benefit.
|
||||
* Use direct file reads for known paths and exact text search for symbols, literals, and exhaustive caller checks. Combine these with semantic search when meaning-based discovery adds value.
|
||||
* Treat retrieved passages as leads: verify them against current files in the intended workspace before relying on them. Empty results do not prove absence; stale or unrelated results require direct inspection.
|
||||
|
||||
Reference in new issue
Block a user