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:
|
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
|
|
||||||
@@ -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 @@
|
|||||||
{"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
|
Claude 7101 8000
|
||||||
Codex 4828 6500
|
Codex 7377 8000
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
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"]
|
[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.' }
|
||||||
|
|||||||
@@ -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
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
|
mcp>=1.9,<2
|
||||||
sentence-transformers>=4.1,<5
|
sentence-transformers>=5.4,<6
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in new issue
Block a user