Compare commits

...
8 Commits
25 changed files with 105 additions and 134 deletions

No files matched your search

-31
View File
@@ -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
View File
@@ -1,4 +1,4 @@
name: CI name: CI
on: on:
push: push:
@@ -12,85 +12,36 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - 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 - name: Bash syntax
run: find scripts shared/hooks -name '*.sh' -print0 | xargs -0 -n1 bash -n shell: bash
- name: Capability checks run: git ls-files -z '*.sh' | xargs -0 -r -n1 bash -n
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
- name: PowerShell syntax - 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 shell: pwsh
run: | run: |
$env:REQUIRE_CODEX_SCHEMA = 'true' $ErrorActionPreference = 'Stop'
./scripts/check-capabilities.ps1 $failed = $false
./scripts/build.ps1 -Summary foreach ($file in (git ls-files '*.ps1')) {
./scripts/doctor.ps1 -Summary $tokens = $null
./scripts/test-shell-packages.ps1 -Summary $parseErrors = $null
./scripts/test-managed-install.ps1 -Summary [System.Management.Automation.Language.Parser]::ParseFile((Join-Path $PWD $file), [ref]$tokens, [ref]$parseErrors) | Out-Null
./scripts/test-build-validation.ps1 -Summary if ($parseErrors.Count) {
./scripts/test-install-guards.ps1 -Summary $parseErrors | ForEach-Object { Write-Host "${file}: $_" }
./scripts/test-plugin-selection.ps1 -Summary $failed = $true
./scripts/test-quick-update.ps1 -Summary }
python ./scripts/test-install-options.py }
python ./scripts/test-quick-update.py if ($failed) { exit 1 }
python ./scripts/test-managed-extensions.py - name: Build
python ./scripts/test-session-state.py shell: bash
python ./scripts/test-telemetry.py run: bash scripts/build.sh --summary
python ./scripts/test-semantic-retrieval.py - name: Bash install dry run
./scripts/install.ps1 -Summary -DryRun -Client Both -Shell PowerShell shell: bash
./scripts/update.ps1 -Summary -DryRun -Client Both -Shell PowerShell run: bash scripts/install.sh --summary --dry-run --client both --shell bash
- name: Shell package tests - name: Bash update dry run
run: ./scripts/test-shell-packages.sh --summary shell: bash
- name: Managed manifest tests run: bash scripts/update.sh --summary --dry-run --client both --shell bash
run: ./scripts/test-managed-install.sh --summary - name: PowerShell install dry run
- name: Build validation tests shell: pwsh
run: ./scripts/test-build-validation.sh --summary run: ./scripts/install.ps1 -Summary -DryRun -Client Both -Shell PowerShell
- name: Install guard tests - name: PowerShell update dry run
run: ./scripts/test-install-guards.sh --summary shell: pwsh
- name: Plugin selection tests run: ./scripts/update.ps1 -Summary -DryRun -Client Both -Shell PowerShell
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
+4 -5
View File
@@ -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 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. 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. 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 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 ### Add a rule
Create a focused Markdown file in `shared/rules/`. For a rule that applies only to a language, framework, tool, or 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 change area, add one row to `adapters/claude/rule-skills.tsv`. The build uses that manifest to generate both Claude's
condition to the Codex rule-loading text in `scripts/build.ps1`/`scripts/build.sh` (Codex loads it as a plain file, by rule skill and Codex's rule-loading condition. Put always-applicable guidance in `general.md`, which the build embeds
path). Put always-applicable guidance in `general.md`, which the build embeds for both clients; a new rule file is for both clients; a new rule file is not automatically embedded.
not automatically embedded.
### Add a skill ### Add a skill
+1 -1
View File
@@ -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}
+2 -2
View File
@@ -1,2 +1,2 @@
Claude 5030 6500 Claude 7101 8000
Codex 4828 6500 Codex 7377 8000
1 Claude 5030 7101 6500 8000
2 Codex 4828 7377 6500 8000
+1 -1
View File
@@ -24,7 +24,7 @@ function Read-InstallOptions {
return @{ return @{
update_agents = Read-InstallBoolean -Prompt "Update selected agent CLIs ($Client)?" -Default $defaults.update_agents 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 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
} }
} }
+1 -1
View File
@@ -33,7 +33,7 @@ read_install_options() {
[ "$dry_run" = false ] || return 0 [ "$dry_run" = false ] || return 0
update_agents=$(read_install_boolean "Update selected agent CLIs ($client)?" "$update_agents") || return update_agents=$(read_install_boolean "Update selected agent CLIs ($client)?" "$update_agents") || return
flashbang=$(read_install_boolean 'Enable the Flashbang notification hook?' "$flashbang") || 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() { is_windows_host() {
+1 -1
View File
@@ -87,7 +87,7 @@ function Sync-ManagedDestination {
$localConfig = [System.IO.File]::ReadAllText($target) $localConfig = [System.IO.File]::ReadAllText($target)
$pluginTables = [regex]::Matches($localConfig, '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])[\s\S]*?(?=^[ \t]*\[|\z)') $pluginTables = [regex]::Matches($localConfig, '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])[\s\S]*?(?=^[ \t]*\[|\z)')
$rootConfig = [regex]::Split($localConfig, '(?m)^[ \t]*\[', 2)[0] $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) { if ($localSettings.Count -gt 0) {
$content = [System.Text.Encoding]::UTF8.GetString($newBytes).TrimEnd([char[]]"`r`n") $content = [System.Text.Encoding]::UTF8.GetString($newBytes).TrimEnd([char[]]"`r`n")
foreach ($setting in $localSettings) { foreach ($setting in $localSettings) {
+1 -1
View File
@@ -87,7 +87,7 @@ sync_managed_destination() {
local plugin_tables local_settings setting_name local plugin_tables local_settings setting_name
local_settings=$(awk ' local_settings=$(awk '
/^[ \t]*\[/ { exit } /^[ \t]*\[/ { exit }
/^[ \t]*(model|reasoning_effort)[ \t]*=/ { print } /^[ \t]*(model|model_reasoning_effort)[ \t]*=/ { print }
' "$target") ' "$target")
if [ -n "$local_settings" ]; then if [ -n "$local_settings" ]; then
while IFS= read -r setting; do while IFS= read -r setting; do
+1 -1
View File
@@ -96,7 +96,7 @@ def sync(root, home, clients, enabled, dry_run, update):
if target.exists() and not state['files']: if target.exists() and not state['files']:
raise ValueError('Semantic retrieval directory exists without setup ownership; refusing to overwrite it.') raise ValueError('Semantic retrieval directory exists without setup ownership; refusing to overwrite it.')
if dry_run: 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 return
target.mkdir(parents=True, exist_ok=True) target.mkdir(parents=True, exist_ok=True)
source = root / 'shared' / 'retrieval' source = root / 'shared' / 'retrieval'
Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.
+3 -3
View File
@@ -54,15 +54,15 @@ try {
[plugins."disabled@market"] [plugins."disabled@market"]
enabled = false enabled = false
'@ '@
[System.IO.File]::WriteAllText((Join-Path $source 'config.toml'), "model = 'new'`nreasoning_effort = 'low'`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'`nreasoning_effort = 'high'`n$pluginState`n[other]`nvalue = true`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) $originalConfig = [System.IO.File]::ReadAllText($configPath)
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-dry' -DryRun $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.' } if ([System.IO.File]::ReadAllText($configPath) -ne $originalConfig) { throw 'Dry run must preserve plugin configuration.' }
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins' $null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins'
$mergedConfig = [System.IO.File]::ReadAllText($configPath) $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 -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.' } 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' $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.' } if ([System.IO.File]::ReadAllText($configPath) -ne $mergedConfig -or (Test-Path (Join-Path $destination 'backups/plugins-repeat'))) { throw 'Preserving plugin configuration must be idempotent.' }
+3 -3
View File
@@ -58,8 +58,8 @@ plugin_state=' [marketplaces.ponytail]
enabled = true enabled = true
[plugins."disabled@market"] [plugins."disabled@market"]
enabled = false' enabled = false'
printf "model = 'new'\nreasoning_effort = 'low'\n" > "$source_dir/config.toml" printf "model = 'new'\nmodel_reasoning_effort = 'low'\napprovals_reviewer = 'auto_review'\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 = '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") original_config=$(cat "$destination/config.toml")
sync_managed_destination "$source_dir" "$destination" plugins-dry "" "" "" true > /dev/null 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; } [ "$(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'/} merged_config_normalized=${merged_config//$'\r'/}
plugin_state_normalized=${plugin_state//$'\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_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; } [ "$(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 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; } [ "$(cat "$destination/config.toml")" = "$merged_config" ] && [ ! -d "$destination/backups/plugins-repeat" ] || { printf 'Preserving plugin configuration must be idempotent.\n' >&2; exit 1; }
Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.
+36
View File
@@ -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
View File
File mode changed.
Regular → Executable
View File
File mode changed.
+1 -1
View File
@@ -1,2 +1,2 @@
mcp>=1.9,<2 mcp>=1.9,<2
sentence-transformers>=4.1,<5 sentence-transformers>=5.4,<6
+19 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 from __future__ import annotations
@@ -11,6 +11,7 @@ import sqlite3
MODEL = "Qwen/Qwen3-Embedding-0.6B" 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"} 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.execute("create table if not exists chunks (path text, text text, vector blob)")
self.db.commit() self.db.commit()
self.model = None self.model = None
self.reranker_model = None
def files(self): def files(self):
ignored = {".git", ".venv", "generated", "node_modules", ".my-ai-configuration"} ignored = {".git", ".venv", "generated", "node_modules", ".my-ai-configuration"}
@@ -49,6 +51,19 @@ class Index:
self.model = SentenceTransformer(MODEL) self.model = SentenceTransformer(MODEL)
return self.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): def rebuild(self):
encoder = self.encoder() encoder = self.encoder()
values = [] values = []
@@ -73,7 +88,8 @@ class Index:
import numpy as np import numpy as np
score = float(np.dot(vector, np.frombuffer(blob, dtype=np.float32))) score = float(np.dot(vector, np.frombuffer(blob, dtype=np.float32)))
rows.append({"path": path, "score": round(score, 6), "text": text}) 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(): def main():
@@ -91,7 +107,7 @@ def main():
@mcp.tool() @mcp.tool()
def semantic_search(query: str, top_k: int = 5) -> list[dict]: 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) return index.search(query, top_k)
@mcp.tool() @mcp.tool()
+1 -1
View File
@@ -21,7 +21,7 @@ Project instructions may specialize global defaults, but must not weaken securit
## Semantic repository search ## 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 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. * 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. * 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.