Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
381f99ea54 | ||
|
|
8ab180c73a | ||
|
|
83401a4732 | ||
|
|
4f6ac68e0c | ||
|
|
f07aaf8f63 | ||
|
|
1b64c54fda | ||
|
|
13594fb401 | ||
|
|
7572ec40d9 | ||
|
|
79d981e78b | ||
|
|
b67eb844bc | ||
|
|
aaf3a183f4 | ||
|
|
27f1c1ec79 | ||
|
|
bfbe001248 | ||
|
|
151306a6cc | ||
|
|
0079fe2b92 | ||
|
|
5c23f21586 | ||
|
|
bdf98d5b71 | ||
|
|
4fa8708ba7 | ||
|
|
be87628d67 | ||
|
|
8ae8e7a173 | ||
|
|
ea2c80817b | ||
|
|
3fad860256 | ||
|
|
2c3e63ed29 | ||
|
|
1d77feeb71 | ||
|
|
5ef3bc5525 |
No files matched your search
+2
-2
@@ -7,7 +7,7 @@
|
|||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
|
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
|
||||||
"commandWindows": "if exist \"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" (\"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" hook & exit /b)",
|
"commandWindows": "powershell -NoProfile -NonInteractive -EncodedCommand JABQAHIAbwBnAHIAZQBzAHMAUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwA7ACAAaQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJwBDADoALwBVAHMAZQByAHMALwBqAC4AbABlAGMAaABuAGUAcgAvAC4AYQBnAGUAbgB0AHMALwBzAGsAaQBsAGwAcwAvAGkAbQBwAGUAYwBjAGEAYgBsAGUALwBzAGMAcgBpAHAAdABzAC8AaQBtAHAAZQBjAGMAYQBiAGwAZQAuAGMAbQBkACcAKQAgAHsAIAAmACAAJwBDADoALwBVAHMAZQByAHMALwBqAC4AbABlAGMAaABuAGUAcgAvAC4AYQBnAGUAbgB0AHMALwBzAGsAaQBsAGwAcwAvAGkAbQBwAGUAYwBjAGEAYgBsAGUALwBzAGMAcgBpAHAAdABzAC8AaQBtAHAAZQBjAGMAYQBiAGwAZQAuAGMAbQBkACcAIABoAG8AbwBrADsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0A",
|
||||||
"timeout": 5,
|
"timeout": 5,
|
||||||
"statusMessage": "Checking UI changes"
|
"statusMessage": "Checking UI changes"
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
|
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
|
||||||
"commandWindows": "if exist \"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" (\"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" hook & exit /b)",
|
"commandWindows": "powershell -NoProfile -NonInteractive -EncodedCommand JABQAHIAbwBnAHIAZQBzAHMAUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwA7ACAAaQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJwBDADoALwBVAHMAZQByAHMALwBqAC4AbABlAGMAaABuAGUAcgAvAC4AYQBnAGUAbgB0AHMALwBzAGsAaQBsAGwAcwAvAGkAbQBwAGUAYwBjAGEAYgBsAGUALwBzAGMAcgBpAHAAdABzAC8AaQBtAHAAZQBjAGMAYQBiAGwAZQAuAGMAbQBkACcAKQAgAHsAIAAmACAAJwBDADoALwBVAHMAZQByAHMALwBqAC4AbABlAGMAaABuAGUAcgAvAC4AYQBnAGUAbgB0AHMALwBzAGsAaQBsAGwAcwAvAGkAbQBwAGUAYwBjAGEAYgBsAGUALwBzAGMAcgBpAHAAdABzAC8AaQBtAHAAZQBjAGMAYQBiAGwAZQAuAGMAbQBkACcAIABoAG8AbwBrADsAIABlAHgAaQB0ACAAJABMAEEAUwBUAEUAWABJAFQAQwBPAEQARQAgAH0A",
|
||||||
"timeout": 30,
|
"timeout": 30,
|
||||||
"statusMessage": "Design deep pass"
|
"statusMessage": "Design deep pass"
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-50
@@ -12,20 +12,47 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
- name: Install current Codex CLI for schema validation
|
- name: Check Linux tooling
|
||||||
run: npm install --global @openai/codex@latest
|
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
|
run: find scripts shared/hooks -name '*.sh' -print0 | xargs -0 -n1 bash -n
|
||||||
|
- name: Capability checks
|
||||||
|
run: ./scripts/check-capabilities.sh
|
||||||
- name: Build
|
- name: Build
|
||||||
run: REQUIRE_CODEX_SCHEMA=true ./scripts/build.sh --summary
|
run: ./scripts/build.sh --summary
|
||||||
- name: Prompt budget
|
- name: Prompt budget
|
||||||
run: ./scripts/prompt-budget.sh --summary
|
run: ./scripts/prompt-budget.sh --summary
|
||||||
- name: Repository context cache
|
- name: Repository context cache
|
||||||
run: ./scripts/repository-context.sh --summary
|
run: ./scripts/repository-context.sh --summary
|
||||||
- name: Doctor
|
- name: Doctor
|
||||||
run: ./scripts/doctor.sh --summary
|
run: ./scripts/doctor.sh --summary
|
||||||
- name: Platform package tests
|
- name: PowerShell syntax
|
||||||
run: ./scripts/test-platform-packages.sh --summary
|
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
|
- name: Managed manifest tests
|
||||||
run: ./scripts/test-managed-install.sh --summary
|
run: ./scripts/test-managed-install.sh --summary
|
||||||
- name: Build validation tests
|
- name: Build validation tests
|
||||||
@@ -34,6 +61,16 @@ jobs:
|
|||||||
run: ./scripts/test-install-guards.sh --summary
|
run: ./scripts/test-install-guards.sh --summary
|
||||||
- name: Plugin selection tests
|
- name: Plugin selection tests
|
||||||
run: ./scripts/test-plugin-selection.sh --summary
|
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
|
- name: Install dry run
|
||||||
run: |
|
run: |
|
||||||
export HOME="$RUNNER_TEMP/isolated-home"
|
export HOME="$RUNNER_TEMP/isolated-home"
|
||||||
@@ -45,48 +82,15 @@ jobs:
|
|||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
./scripts/update.sh --summary --dry-run --client both --platform linux
|
./scripts/update.sh --summary --dry-run --client both --platform linux
|
||||||
|
|
||||||
# windows:
|
compatibility-canary:
|
||||||
# runs-on: windows-latest
|
if: github.event_name == 'schedule'
|
||||||
# steps:
|
runs-on: ubuntu-latest
|
||||||
# - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
steps:
|
||||||
# - name: Install current Codex CLI for schema validation
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
# shell: powershell
|
- name: Latest external clients
|
||||||
# run: npm install --global @openai/codex@latest
|
run: npm install --global @openai/codex@latest
|
||||||
# - name: PowerShell syntax
|
- name: Latest-client schema and dry runs
|
||||||
# shell: powershell
|
run: |
|
||||||
# run: |
|
REQUIRE_CODEX_SCHEMA=true ./scripts/build.sh --summary
|
||||||
# $errors = @()
|
HOME="$RUNNER_TEMP/canary-home" ./scripts/install.sh --summary --dry-run --client both --platform linux
|
||||||
# Get-ChildItem scripts,shared/hooks -Recurse -Filter *.ps1 | ForEach-Object {
|
HOME="$RUNNER_TEMP/canary-home" ./scripts/update.sh --summary --dry-run --client both --platform linux
|
||||||
# $tokens = $null
|
|
||||||
# $parseErrors = $null
|
|
||||||
# [void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$parseErrors)
|
|
||||||
# $errors += $parseErrors
|
|
||||||
# }
|
|
||||||
# if ($errors.Count) { $errors | ForEach-Object { Write-Error $_ }; exit 1 }
|
|
||||||
# - name: Build
|
|
||||||
# shell: powershell
|
|
||||||
# run: $env:REQUIRE_CODEX_SCHEMA = 'true'; .\scripts\build.ps1
|
|
||||||
# - name: Doctor
|
|
||||||
# shell: powershell
|
|
||||||
# run: .\scripts\doctor.ps1
|
|
||||||
# - name: Platform package tests
|
|
||||||
# shell: powershell
|
|
||||||
# run: .\scripts\test-platform-packages.ps1
|
|
||||||
# - name: Managed manifest tests
|
|
||||||
# shell: powershell
|
|
||||||
# run: .\scripts\test-managed-install.ps1
|
|
||||||
# - name: Build validation tests
|
|
||||||
# shell: powershell
|
|
||||||
# run: .\scripts\test-build-validation.ps1
|
|
||||||
# - name: Install guard tests
|
|
||||||
# shell: powershell
|
|
||||||
# run: .\scripts\test-install-guards.ps1
|
|
||||||
# - name: Plugin selection tests
|
|
||||||
# shell: powershell
|
|
||||||
# run: .\scripts\test-plugin-selection.ps1
|
|
||||||
# - name: Install dry run
|
|
||||||
# shell: powershell
|
|
||||||
# run: .\scripts\install.ps1 -DryRun -Client Both -Platform Windows
|
|
||||||
# - name: Update dry run
|
|
||||||
# shell: powershell
|
|
||||||
# run: .\scripts\update.ps1 -DryRun -Client Both -Platform Windows
|
|
||||||
@@ -11,15 +11,32 @@
|
|||||||
It keeps rules, skills, agents, hooks, and orchestration guidance in shared source files. Client adapters turn those files
|
It keeps rules, skills, agents, hooks, and orchestration guidance in shared source files. Client adapters turn those files
|
||||||
into shell-specific packages that can be inspected before they are installed.
|
into shell-specific packages that can be inspected before they are installed.
|
||||||
|
|
||||||
|
## Capability check
|
||||||
|
|
||||||
|
Check the selected shell's minimum version and required Python and `jq` tools before setup. The command exits with an error when a requirement is missing.
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\check-capabilities.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Bash:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/check-capabilities.sh
|
||||||
|
```
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
From an existing checkout, with the [requirements](#requirements) installed, run pull, build, then install.
|
With the [requirements](#requirements) installed, clone the repository, build it, then install it.
|
||||||
Stop if any command fails. Installation asks for the shell, client, CLI update, Flashbang, and extension choices.
|
Stop if any command fails. Installation asks for the shell, client, CLI update, Flashbang, and extension choices.
|
||||||
|
|
||||||
PowerShell:
|
PowerShell:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git pull --ff-only
|
git clone https://git.lechner-systems.at/fraujulian/MyAiConfiguration.git
|
||||||
|
cd MyAiConfiguration
|
||||||
.\scripts\build.ps1 -Summary
|
.\scripts\build.ps1 -Summary
|
||||||
.\scripts\install.ps1 -Summary
|
.\scripts\install.ps1 -Summary
|
||||||
```
|
```
|
||||||
@@ -27,7 +44,8 @@ git pull --ff-only
|
|||||||
Bash:
|
Bash:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull --ff-only
|
git clone https://git.lechner-systems.at/fraujulian/MyAiConfiguration.git
|
||||||
|
cd MyAiConfiguration
|
||||||
./scripts/build.sh --summary
|
./scripts/build.sh --summary
|
||||||
./scripts/install.sh --summary
|
./scripts/install.sh --summary
|
||||||
```
|
```
|
||||||
@@ -92,9 +110,11 @@ Install and update also rebuild internally; the explicit build above lets you ch
|
|||||||
- Finish notifications, Claude session-state hooks, optional safety utilities, and compact status displays.
|
- Finish notifications, Claude session-state hooks, optional safety utilities, and compact status displays.
|
||||||
- Separate install and update scripts: install refuses to run against an already-installed destination, update refuses
|
- Separate install and update scripts: install refuses to run against an already-installed destination, update refuses
|
||||||
to run against one that is not installed yet.
|
to run against one that is not installed yet.
|
||||||
- User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and
|
- User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, QMD, Humanizer, Impeccable, and
|
||||||
Anthropic Frontend Design, with an interactive extension toggle during both install and update. Only extensions
|
Anthropic Frontend Design, with an interactive extension toggle during both install and update. Only extensions
|
||||||
installed and recorded by this setup can be removed through deselection.
|
integrations installed and recorded by this setup can be removed through deselection.
|
||||||
|
- Optional local semantic retrieval for both clients using Qwen3-Embedding-0.6B, disabled by default and removable
|
||||||
|
through the install/update choice.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -125,10 +145,9 @@ generated/
|
|||||||
- PowerShell 5.1 or newer; PowerShell 7 is supported as well.
|
- PowerShell 5.1 or newer; PowerShell 7 is supported as well.
|
||||||
- Bash 4.3 or newer, with standard utilities such as `awk`, `sed`, and `sha256sum`.
|
- Bash 4.3 or newer, with standard utilities such as `awk`, `sed`, and `sha256sum`.
|
||||||
- Python 3.11 or newer for configuration validation and task-state hooks/helpers (`python` for PowerShell scripts, `python3` for Bash scripts).
|
- Python 3.11 or newer for configuration validation and task-state hooks/helpers (`python` for PowerShell scripts, `python3` for Bash scripts).
|
||||||
- Git for marketplace installation and branch display. External Codex skills are downloaded from their manifest sources.
|
|
||||||
- `jq` for Claude's Bash status line; without it, that status line produces no output.
|
- `jq` for Claude's Bash status line; without it, that status line produces no output.
|
||||||
- The Codex and Claude Code CLIs for the clients that should be installed or diagnosed.
|
- Python `venv` and `pip` when the optional semantic retrieval layer is enabled.
|
||||||
- Network access for plugin installation and updates.
|
- Node.js 22 or newer and `npm` when QMD is selected.
|
||||||
|
|
||||||
The Bash scripts do not require PowerShell. The PowerShell scripts remain compatible with PowerShell 5.1;
|
The Bash scripts do not require PowerShell. The PowerShell scripts remain compatible with PowerShell 5.1;
|
||||||
PowerShell 7 can also run them.
|
PowerShell 7 can also run them.
|
||||||
@@ -192,13 +211,14 @@ 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 `plugins` and `marketplaces` tables are preserved when syncing `config.toml`; other local settings are
|
Local Codex `model`, `reasoning_effort`, `plugins`, and `marketplaces` settings are preserved when syncing `config.toml`;
|
||||||
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
|
||||||
Codex skills; unchecked extensions are skipped on installation. Normal runs also ask whether to update
|
Codex skills; unchecked extensions are skipped on installation. Normal runs also ask whether to update
|
||||||
the selected client CLIs and enable Flashbang. These choices are saved for Quickupdate. Dry runs preview changes
|
the selected client CLIs, enable Flashbang, and enable Qwen3 semantic retrieval. These choices are saved for Quickupdate.
|
||||||
without updating CLIs or extensions. See [Plugins](docs/plugins.md) for client-specific behavior.
|
Disabling retrieval on a later update removes its setup-owned runtime, index, and MCP registrations. Dry runs preview
|
||||||
|
changes without updating CLIs, extensions, or retrieval. See [Plugins](docs/plugins.md) for client-specific behavior.
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
|
|
||||||
@@ -267,8 +287,9 @@ and uncertainty. A small change may need only focused inspection; builds, tests,
|
|||||||
automatic. Explicit user and project requirements still apply.
|
automatic. Explicit user and project requirements still apply.
|
||||||
|
|
||||||
Both status displays include model, effort, repository, branch, context-window size, context used, and tokens used.
|
Both status displays include model, effort, repository, branch, context-window size, context used, and tokens used.
|
||||||
Claude uses two colored lines, with context usage turning yellow at 60% and red at 85%. Codex uses its native status
|
Codex also shows its active permission profile and approval mode. Claude uses two colored lines, with context usage
|
||||||
items and rendering. See [Configuration](docs/configuration.md) for details and limitations.
|
turning yellow at 60% and red at 85%; its status-line input does not expose the active permission mode. See
|
||||||
|
[Configuration](docs/configuration.md) for details and limitations.
|
||||||
|
|
||||||
## Extending the Configuration
|
## Extending the Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
role tools
|
||||||
|
architect Read,Search,WebFetch
|
||||||
|
researcher Read,Search,WebFetch
|
||||||
|
reviewer Read,Diff,Search
|
||||||
|
verifier Read,Shell
|
||||||
|
implementer Read,Write,Shell,Tests
|
||||||
|
@@ -3,7 +3,7 @@ approvals_reviewer = "auto_review"
|
|||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
[tui]
|
[tui]
|
||||||
status_line = ["model", "reasoning", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]
|
status_line = ["model", "reasoning", "permissions", "approval-mode", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]
|
||||||
|
|
||||||
[agents]
|
[agents]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ i-have-adhd ayghri/i-have-adhd i-have-adhd@i-have-adhd ayghri/i-have-adhd i-have
|
|||||||
Superpowers - superpowers@claude-plugins-official openai-curated-remote superpowers@openai-curated-remote plugin - -
|
Superpowers - superpowers@claude-plugins-official openai-curated-remote superpowers@openai-curated-remote plugin - -
|
||||||
Context7 upstash/context7 context7@context7-marketplace upstash/context7 context7@context7-marketplace plugin - -
|
Context7 upstash/context7 context7@context7-marketplace upstash/context7 context7@context7-marketplace plugin - -
|
||||||
Caveman JuliusBrussee/caveman caveman@caveman thinkhome-org/caveman-codex caveman@thinkhome-caveman plugin - -
|
Caveman JuliusBrussee/caveman caveman@caveman thinkhome-org/caveman-codex caveman@thinkhome-caveman plugin - -
|
||||||
|
QMD tobi/qmd qmd@qmd - - qmd @tobilu/qmd qmd
|
||||||
Humanizer blader/humanizer humanizer@humanizer - - skill blader/humanizer humanizer
|
Humanizer blader/humanizer humanizer@humanizer - - skill blader/humanizer humanizer
|
||||||
Impeccable pbakaus/impeccable impeccable@skill pbakaus/impeccable impeccable skill pbakaus/impeccable impeccable
|
Impeccable pbakaus/impeccable impeccable@impeccable pbakaus/impeccable impeccable skill pbakaus/impeccable impeccable
|
||||||
Anthropic Frontend Design anthropics/claude-plugins-official frontend-design@claude-plugins-official - - skill anthropics/skills frontend-design
|
Anthropic Frontend Design anthropics/claude-plugins-official frontend-design@claude-plugins-official - - skill anthropics/skills frontend-design
|
||||||
|
@@ -1 +1 @@
|
|||||||
{"Claude permanent context":5030,"Codex permanent context":4828}
|
{"Claude permanent context":5030,"Codex permanent context":4828,"Claude permanent context absolute limit":6500,"Codex permanent context absolute limit":6500}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
Claude 5030
|
Claude 5030 6500
|
||||||
Codex 4828
|
Codex 4828 6500
|
||||||
|
@@ -16,7 +16,7 @@ Codex sets `approval_policy = "on-request"`, `approvals_reviewer = "auto_review"
|
|||||||
|
|
||||||
## Status displays
|
## Status displays
|
||||||
|
|
||||||
Codex's native `tui.status_line` contains `model`, `reasoning`, `project-name`, `git-branch`, `context-window-size`, `context-used`, and `used-tokens`, in that order. Rendering and colors are controlled by Codex; the adapter does not define custom colors per field.
|
Codex's native `tui.status_line` contains `model`, `reasoning`, `permissions`, `approval-mode`, `project-name`, `git-branch`, `context-window-size`, `context-used`, and `used-tokens`, in that order. `permissions` reports the active permission profile or sandbox summary, and `approval-mode` reports the active command approval mode. Rendering and colors are controlled by Codex; the adapter does not define custom colors per field.
|
||||||
|
|
||||||
Claude runs `shared/statusline/statusline.ps1` for PowerShell or `statusline.sh` for Bash. Its two-line display is:
|
Claude runs `shared/statusline/statusline.ps1` for PowerShell or `statusline.sh` for Bash. Its two-line display is:
|
||||||
|
|
||||||
@@ -29,10 +29,12 @@ Model and context-window size are cyan, effort and cumulative tokens magenta, re
|
|||||||
|
|
||||||
`Ctx` is the context-window capacity; `Used` is the reported percentage occupied. `Tokens` adds the session's reported total input and output tokens, not just the current context. Counts use `k` and `M` suffixes. Missing model, effort, context size, or percentage is shown as `-`; missing token totals start at zero. Repository names fall back to the Git root or current directory; unavailable branches show `-`.
|
`Ctx` is the context-window capacity; `Used` is the reported percentage occupied. `Tokens` adds the session's reported total input and output tokens, not just the current context. Counts use `k` and `M` suffixes. Missing model, effort, context size, or percentage is shown as `-`; missing token totals start at zero. Repository names fall back to the Git root or current directory; unavailable branches show `-`.
|
||||||
|
|
||||||
|
Claude Code reruns its status-line command when the permission mode changes, but does not pass that mode in the status-line JSON. Its line therefore does not show a configured default as though it were the active mode.
|
||||||
|
|
||||||
## Verification and installation
|
## Verification and installation
|
||||||
|
|
||||||
The agent chooses the smallest sufficient checks based on behavior, affected callers, risk, reversibility, and uncertainty. Inspection can suffice for a small change. Full builds, broad test suites, and independent reviews are not automatic; explicit user and project requirements remain binding.
|
The agent chooses the smallest sufficient checks based on behavior, affected callers, risk, reversibility, and uncertainty. Inspection can suffice for a small change. Full builds, broad test suites, and independent reviews are not automatic; explicit user and project requirements remain binding.
|
||||||
|
|
||||||
Delegation and long-running state use the shared orchestration rule, loaded on demand as a Codex rule or Claude skill. The coordinator owns decisions and routes concise evidence-based handoffs. Review and execution verification have separate responsibilities; see [Agents](agents.md).
|
Delegation and long-running state use the shared orchestration rule, loaded on demand as a Codex rule or Claude skill. The coordinator owns decisions and routes concise evidence-based handoffs. Review and execution verification have separate responsibilities; see [Agents](agents.md).
|
||||||
|
|
||||||
Rebuild packages after changes to shared definitions or adapters. Generated defaults reach a user's configuration only through installation or update. Updates back up replaced files and preserve Codex's local `plugins` and `marketplaces` tables; other local settings are not generally merged. See [the README](../README.md) for overwrite and backup behavior.
|
Rebuild packages after changes to shared definitions or adapters. Generated defaults reach a user's configuration only through installation or update. Updates back up replaced files and preserve Codex's local `model`, `reasoning_effort`, `plugins`, and `marketplaces` settings; other local settings are not generally merged. See [the README](../README.md) for overwrite and backup behavior.
|
||||||
+5
-3
@@ -1,8 +1,8 @@
|
|||||||
# Plugins
|
# Plugins
|
||||||
|
|
||||||
The user-level plugin manifest is `adapters/plugins.tsv`. It maps each logical plugin to the marketplace selector understood by Claude Code and Codex.
|
The user-level plugin manifest is `adapters/plugins.tsv`. It maps each logical extension to the client-specific plugin, skill, or MCP installation method.
|
||||||
|
|
||||||
The current baseline includes Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and Anthropic Frontend Design. Both scripts operate at user scope and show a plugin selector unless running a dry run. Selections are collected before configuration changes. Both rebuild generated packages first.
|
The current baseline includes Ponytail, i-have-adhd, Superpowers, Context7, Caveman, QMD, Humanizer, Impeccable, and Anthropic Frontend Design. Both scripts operate at user scope and show a plugin selector unless running a dry run. Selections are collected before configuration changes. Both rebuild generated packages first.
|
||||||
|
|
||||||
`install.ps1`/`install.sh` starts every plugin checked by default; unchecking one skips it. It refuses to run against an already-installed destination (`Already installed, use the update script.`).
|
`install.ps1`/`install.sh` starts every plugin checked by default; unchecking one skips it. It refuses to run against an already-installed destination (`Already installed, use the update script.`).
|
||||||
|
|
||||||
@@ -12,7 +12,9 @@ Codex's local `plugins` and `marketplaces` tables survive configuration synchron
|
|||||||
|
|
||||||
Codex receives Caveman through its native plugin adapter. Humanizer, Impeccable, and Anthropic Frontend Design are downloaded as skill payloads from the GitHub sources in the manifest, without running upstream installers. All three appear in the extension selector for Codex. Ownership is recorded in `~/.my-ai-configuration/extensions.json`. Skill files are tracked by hash; modified or foreign files are retained with a warning. Existing installations from before ownership tracking are not automatically adopted.
|
Codex receives Caveman through its native plugin adapter. Humanizer, Impeccable, and Anthropic Frontend Design are downloaded as skill payloads from the GitHub sources in the manifest, without running upstream installers. All three appear in the extension selector for Codex. Ownership is recorded in `~/.my-ai-configuration/extensions.json`. Skill files are tracked by hash; modified or foreign files are retained with a warning. Existing installations from before ownership tracking are not automatically adopted.
|
||||||
|
|
||||||
Normal installation and update also ask whether to update the selected client CLIs and enable Flashbang. Quickupdate reuses the saved shell, client, CLI update, Flashbang, and extension choices without opening menus. Only successful runs save selections; dry runs do not update CLIs or extensions.
|
[QMD](https://github.com/tobi/qmd) requires Node.js 22 or newer and `npm`. Selecting it installs `@tobilu/qmd` globally. Claude receives QMD's official `qmd@qmd` plugin; Codex registers `qmd mcp` as its MCP server. Deselecting QMD removes the client integration owned by this setup, but leaves the global `qmd` executable installed.
|
||||||
|
|
||||||
|
Normal installation and update also ask whether to update the selected client CLIs, enable Flashbang, and enable the optional Qwen3-Embedding-0.6B semantic retrieval layer. Retrieval is shared by Codex and Claude through a local MCP server, uses a per-user virtual environment and index, and is disabled by default. Quickupdate reuses the saved shell, client, CLI update, Flashbang, retrieval, and extension choices without opening menus. Disabling retrieval removes its setup-owned runtime, index, and MCP registrations. Only successful runs save selections; dry runs do not update CLIs, extensions, or retrieval.
|
||||||
|
|
||||||
Plugin installation requires the corresponding client CLI, network access, and any authentication required by the plugin. Context7 may open an OAuth flow. Codex or plugin hooks may require a trust review in `/hooks`. The scripts never install plugins during a build or doctor run and never install them when `--dry-run` or `-DryRun` is used.
|
Plugin installation requires the corresponding client CLI, network access, and any authentication required by the plugin. Context7 may open an OAuth flow. Codex or plugin hooks may require a trust review in `/hooks`. The scripts never install plugins during a build or doctor run and never install them when `--dry-run` or `-DryRun` is used.
|
||||||
|
|
||||||
|
|||||||
+20
-20
@@ -6,6 +6,7 @@ $shared = Join-Path $root 'shared'
|
|||||||
$output = Join-Path $root 'generated'
|
$output = Join-Path $root 'generated'
|
||||||
$pluginManifest = Join-Path $root 'adapters/plugins.tsv'
|
$pluginManifest = Join-Path $root 'adapters/plugins.tsv'
|
||||||
$ruleSkillManifest = Join-Path $root 'adapters/claude/rule-skills.tsv'
|
$ruleSkillManifest = Join-Path $root 'adapters/claude/rule-skills.tsv'
|
||||||
|
$capabilityManifest = Join-Path $root 'adapters/claude/capabilities.tsv'
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $pluginManifest)) { throw "Missing plugin manifest: $pluginManifest" }
|
if (-not (Test-Path -LiteralPath $pluginManifest)) { throw "Missing plugin manifest: $pluginManifest" }
|
||||||
$pluginEntries = @(Import-Csv -LiteralPath $pluginManifest -Delimiter ([char]9))
|
$pluginEntries = @(Import-Csv -LiteralPath $pluginManifest -Delimiter ([char]9))
|
||||||
@@ -20,6 +21,7 @@ foreach ($pluginEntry in $pluginEntries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $ruleSkillManifest)) { throw "Missing rule-skill manifest: $ruleSkillManifest" }
|
if (-not (Test-Path -LiteralPath $ruleSkillManifest)) { throw "Missing rule-skill manifest: $ruleSkillManifest" }
|
||||||
|
if (-not (Test-Path -LiteralPath $capabilityManifest)) { throw "Missing capability manifest: $capabilityManifest" }
|
||||||
$ruleSkillEntries = @(Import-Csv -LiteralPath $ruleSkillManifest -Delimiter ([char]9) | Sort-Object skill_name)
|
$ruleSkillEntries = @(Import-Csv -LiteralPath $ruleSkillManifest -Delimiter ([char]9) | Sort-Object skill_name)
|
||||||
if ($ruleSkillEntries.Count -eq 0) { throw 'Rule-skill manifest must define at least one entry.' }
|
if ($ruleSkillEntries.Count -eq 0) { throw 'Rule-skill manifest must define at least one entry.' }
|
||||||
$ruleSkillNames = @()
|
$ruleSkillNames = @()
|
||||||
@@ -31,6 +33,18 @@ foreach ($entry in $ruleSkillEntries) {
|
|||||||
if ($ruleSkillNames -contains $entry.skill_name) { throw "Duplicate rule-skill name in manifest: $($entry.skill_name)" }
|
if ($ruleSkillNames -contains $entry.skill_name) { throw "Duplicate rule-skill name in manifest: $($entry.skill_name)" }
|
||||||
$ruleSkillNames += $entry.skill_name
|
$ruleSkillNames += $entry.skill_name
|
||||||
}
|
}
|
||||||
|
$agentTools = @{}
|
||||||
|
foreach ($entry in @(Import-Csv -LiteralPath $capabilityManifest -Delimiter ([char]9))) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($entry.role) -or [string]::IsNullOrWhiteSpace($entry.tools)) { throw 'Invalid capability manifest row.' }
|
||||||
|
$agentTools[$entry.role] = $entry.tools
|
||||||
|
}
|
||||||
|
$codexRuleLoadingLines = @()
|
||||||
|
foreach ($entry in $ruleSkillEntries) {
|
||||||
|
$rulePath = $entry.rule_file
|
||||||
|
if (Test-Path -LiteralPath (Join-Path $shared "rules/$($entry.rule_file)") -PathType Container) { $rulePath = "$($entry.rule_file)/index.md" }
|
||||||
|
$codexRuleLoadingLines += "- rules/$rulePath for $($entry.trigger)."
|
||||||
|
}
|
||||||
|
$codexRuleLoadingList = $codexRuleLoadingLines -join "`r`n"
|
||||||
|
|
||||||
Remove-Item $output -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $output -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
New-Item $output -ItemType Directory -Force | Out-Null
|
New-Item $output -ItemType Directory -Force | Out-Null
|
||||||
@@ -102,26 +116,9 @@ Detect the languages, frameworks, tools, and change areas from the repository an
|
|||||||
|
|
||||||
Load rule files when their subject applies:
|
Load rule files when their subject applies:
|
||||||
|
|
||||||
- rules/security-auth.md for authentication, authorization, sessions, tokens, permissions, or tenant boundaries.
|
__CODEX_RULE_LOADING__
|
||||||
- rules/security-web.md for browser, frontend, cookie, redirect, XSS, or CSRF work.
|
|
||||||
- rules/security-api.md for HTTP APIs, request handling, serialization, or endpoints.
|
|
||||||
- rules/security-data.md for databases, persistence, sensitive data, or multi-tenancy.
|
|
||||||
- rules/security-files.md for files, uploads, archives, paths, processes, IPC, or deserialization.
|
|
||||||
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
|
|
||||||
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
|
|
||||||
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
|
|
||||||
- rules/angular/index.md for Angular work.
|
|
||||||
- rules/typescript.md for TypeScript work.
|
|
||||||
- rules/csharp.md for C# or .NET work.
|
|
||||||
- rules/wpf/index.md for WPF work.
|
|
||||||
- rules/ui-ux/index.md for UI or UX decisions.
|
|
||||||
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
|
|
||||||
- rules/orchestration.md for delegation, subagent work, or long-running task state.
|
|
||||||
- rules/git.md for Git operations.
|
|
||||||
- rules/refactoring.md for refactoring work.
|
|
||||||
- rules/definition-of-done.md when validating completion.
|
|
||||||
- rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains.
|
|
||||||
'@
|
'@
|
||||||
|
$codexRuleLoading = $codexRuleLoading.Replace('__CODEX_RULE_LOADING__', $codexRuleLoadingList)
|
||||||
|
|
||||||
foreach ($shell in @('powershell','bash')) {
|
foreach ($shell in @('powershell','bash')) {
|
||||||
New-Item (Join-Path $output "codex-$shell") -ItemType Directory -Force | Out-Null
|
New-Item (Join-Path $output "codex-$shell") -ItemType Directory -Force | Out-Null
|
||||||
@@ -180,7 +177,10 @@ foreach ($shell in @('powershell','bash')) {
|
|||||||
if ($client -eq 'codex') {
|
if ($client -eq 'codex') {
|
||||||
Set-Content (Join-Path $agentsOutput "$name.toml") "name = $(Quote-Toml $name)`r`ndescription = $(Quote-Toml $description)`r`ndeveloper_instructions = $(Quote-Toml $instructions)" -Encoding UTF8
|
Set-Content (Join-Path $agentsOutput "$name.toml") "name = $(Quote-Toml $name)`r`ndescription = $(Quote-Toml $description)`r`ndeveloper_instructions = $(Quote-Toml $instructions)" -Encoding UTF8
|
||||||
} else {
|
} else {
|
||||||
Set-Content (Join-Path $agentsOutput "$name.md") "---`r`nname: $name`r`ndescription: $description`r`n---`r`n`r`n$instructions" -Encoding UTF8
|
$tools = $agentTools[$name]
|
||||||
|
$frontmatter = "---`r`nname: $name`r`ndescription: $description`r`n"
|
||||||
|
if ($tools) { $frontmatter += "tools: $tools`r`n" }
|
||||||
|
Set-Content (Join-Path $agentsOutput "$name.md") ($frontmatter + "---`r`n`r`n$instructions") -Encoding UTF8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-22
@@ -13,6 +13,7 @@ shared="$root/shared"
|
|||||||
output="$root/generated"
|
output="$root/generated"
|
||||||
plugin_manifest="$root/adapters/plugins.tsv"
|
plugin_manifest="$root/adapters/plugins.tsv"
|
||||||
rule_skill_manifest="$root/adapters/claude/rule-skills.tsv"
|
rule_skill_manifest="$root/adapters/claude/rule-skills.tsv"
|
||||||
|
capability_manifest="$root/adapters/claude/capabilities.tsv"
|
||||||
|
|
||||||
[ -f "$plugin_manifest" ] || { printf 'Missing plugin manifest: %s\n' "$plugin_manifest" >&2; exit 1; }
|
[ -f "$plugin_manifest" ] || { printf 'Missing plugin manifest: %s\n' "$plugin_manifest" >&2; exit 1; }
|
||||||
declare -A plugin_seen
|
declare -A plugin_seen
|
||||||
@@ -29,6 +30,7 @@ done < "$plugin_manifest"
|
|||||||
[ "$plugin_count" -gt 0 ] || { printf 'Plugin manifest must define at least one plugin.\n' >&2; exit 1; }
|
[ "$plugin_count" -gt 0 ] || { printf 'Plugin manifest must define at least one plugin.\n' >&2; exit 1; }
|
||||||
|
|
||||||
[ -f "$rule_skill_manifest" ] || { printf 'Missing rule-skill manifest: %s\n' "$rule_skill_manifest" >&2; exit 1; }
|
[ -f "$rule_skill_manifest" ] || { printf 'Missing rule-skill manifest: %s\n' "$rule_skill_manifest" >&2; exit 1; }
|
||||||
|
[ -f "$capability_manifest" ] || { printf 'Missing capability manifest: %s\n' "$capability_manifest" >&2; exit 1; }
|
||||||
declare -A rule_skill_seen
|
declare -A rule_skill_seen
|
||||||
rule_skill_files=()
|
rule_skill_files=()
|
||||||
rule_skill_names=()
|
rule_skill_names=()
|
||||||
@@ -45,8 +47,21 @@ while IFS=$'\t' read -r rule_file skill_name trigger; do
|
|||||||
rule_skill_files+=("$rule_file")
|
rule_skill_files+=("$rule_file")
|
||||||
rule_skill_names+=("$skill_name")
|
rule_skill_names+=("$skill_name")
|
||||||
rule_skill_triggers+=("$trigger")
|
rule_skill_triggers+=("$trigger")
|
||||||
done < "$rule_skill_manifest"
|
done < <(tail -n +2 "$rule_skill_manifest" | sort -t$'\t' -k2,2)
|
||||||
[ "${#rule_skill_names[@]}" -gt 0 ] || { printf 'Rule-skill manifest must define at least one entry.\n' >&2; exit 1; }
|
[ "${#rule_skill_names[@]}" -gt 0 ] || { printf 'Rule-skill manifest must define at least one entry.\n' >&2; exit 1; }
|
||||||
|
codex_rule_loading_list=""
|
||||||
|
for i in "${!rule_skill_names[@]}"; do
|
||||||
|
rule_file=${rule_skill_files[$i]}
|
||||||
|
rule_path="$rule_file"
|
||||||
|
[ -d "$shared/rules/$rule_file" ] && rule_path="$rule_file/index.md"
|
||||||
|
codex_rule_loading_list="${codex_rule_loading_list}- rules/${rule_path} for ${rule_skill_triggers[$i]}."$'\n'
|
||||||
|
done
|
||||||
|
declare -A agent_tools
|
||||||
|
while IFS=$'\t' read -r role tools; do
|
||||||
|
[ "$role" != role ] || continue
|
||||||
|
[ -n "$role" ] && [ -n "$tools" ] || { printf 'Invalid capability manifest row.\n' >&2; exit 1; }
|
||||||
|
agent_tools[$role]="$tools"
|
||||||
|
done < "$capability_manifest"
|
||||||
|
|
||||||
copy_directory() {
|
copy_directory() {
|
||||||
local source=$1 destination=$2
|
local source=$1 destination=$2
|
||||||
@@ -97,27 +112,8 @@ When programming, always load and apply `rules/security.md`. This includes imple
|
|||||||
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Load every applicable rule file before editing. Load all matching files when multiple technologies apply.
|
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Load every applicable rule file before editing. Load all matching files when multiple technologies apply.
|
||||||
|
|
||||||
Load rule files when their subject applies:
|
Load rule files when their subject applies:
|
||||||
|
|
||||||
- rules/security-auth.md for authentication, authorization, sessions, tokens, permissions, or tenant boundaries.
|
|
||||||
- rules/security-web.md for browser, frontend, cookie, redirect, XSS, or CSRF work.
|
|
||||||
- rules/security-api.md for HTTP APIs, request handling, serialization, or endpoints.
|
|
||||||
- rules/security-data.md for databases, persistence, sensitive data, or multi-tenancy.
|
|
||||||
- rules/security-files.md for files, uploads, archives, paths, processes, IPC, or deserialization.
|
|
||||||
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
|
|
||||||
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
|
|
||||||
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
|
|
||||||
- rules/angular/index.md for Angular work.
|
|
||||||
- rules/typescript.md for TypeScript work.
|
|
||||||
- rules/csharp.md for C# or .NET work.
|
|
||||||
- rules/wpf/index.md for WPF work.
|
|
||||||
- rules/ui-ux/index.md for UI or UX decisions.
|
|
||||||
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
|
|
||||||
- rules/orchestration.md for delegation, subagent work, or long-running task state.
|
|
||||||
- rules/git.md for Git operations.
|
|
||||||
- rules/refactoring.md for refactoring work.
|
|
||||||
- rules/definition-of-done.md when validating completion.
|
|
||||||
- rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains.
|
|
||||||
BLOCK
|
BLOCK
|
||||||
|
codex_rule_loading="${codex_rule_loading}"$'\n\n'"${codex_rule_loading_list%$'\n'}"
|
||||||
|
|
||||||
for shell in powershell bash; do
|
for shell in powershell bash; do
|
||||||
mkdir -p "$output/codex-$shell" "$output/claude-$shell"
|
mkdir -p "$output/codex-$shell" "$output/claude-$shell"
|
||||||
@@ -178,7 +174,8 @@ for client in codex claude; do
|
|||||||
if [ "$client" = codex ]; then
|
if [ "$client" = codex ]; then
|
||||||
printf 'name = %s\ndescription = %s\ndeveloper_instructions = %s\n' "$(quote_toml "$name")" "$(quote_toml "$description")" "$(quote_toml "$instructions")" > "$agents_output/$name.toml"
|
printf 'name = %s\ndescription = %s\ndeveloper_instructions = %s\n' "$(quote_toml "$name")" "$(quote_toml "$description")" "$(quote_toml "$instructions")" > "$agents_output/$name.toml"
|
||||||
else
|
else
|
||||||
printf '%s\nname: %s\ndescription: %s\n%s\n\n%s\n' '---' "$name" "$description" '---' "$instructions" > "$agents_output/$name.md"
|
tools=${agent_tools[$name]:-}
|
||||||
|
if [ -n "$tools" ]; then printf '%s\nname: %s\ndescription: %s\ntools: %s\n%s\n\n%s\n' '---' "$name" "$description" "$tools" '---' "$instructions" > "$agents_output/$name.md"; else printf '%s\nname: %s\ndescription: %s\n%s\n\n%s\n' '---' "$name" "$description" '---' "$instructions" > "$agents_output/$name.md"; fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
$failed = $false
|
||||||
|
function Pass([string]$message) { Write-Output "PASS $message" }
|
||||||
|
function Fail([string]$message) { $script:failed = $true; Write-Error "FAIL $message" -ErrorAction Continue }
|
||||||
|
function Require-Command([string]$name) { if (Get-Command $name -ErrorAction SilentlyContinue) { Pass "$name available" } else { Fail "$name is required" } }
|
||||||
|
function Version-AtLeast([string]$version, [version]$minimum) { try { return ([version]$version -ge $minimum) } catch { return $false } }
|
||||||
|
function Check-Version([string]$name, [string]$version, [version]$minimum) { if (Version-AtLeast $version $minimum) { Pass "$name $version (minimum $minimum)" } else { Fail "$name $version is below minimum $minimum" } }
|
||||||
|
|
||||||
|
Check-Version 'PowerShell' $PSVersionTable.PSVersion.ToString() ([version]'5.1')
|
||||||
|
Require-Command jq; Require-Command python
|
||||||
|
if (Get-Command python -ErrorAction SilentlyContinue) {
|
||||||
|
$pythonVersion = (& python --version 2>$null | Select-Object -First 1) -replace '^Python\s+', ''
|
||||||
|
if ($LASTEXITCODE -eq 0) { Check-Version 'Python' $pythonVersion ([version]'3.11') } else { Fail 'Python 3.11 or newer is required' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failed) { Write-Error 'Capability check: FAIL'; exit 1 }
|
||||||
|
Write-Output 'Capability check: PASS'
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
failed=false
|
||||||
|
pass() { printf 'PASS %s\n' "$1"; }
|
||||||
|
fail() { printf 'FAIL %s\n' "$1" >&2; failed=true; }
|
||||||
|
require_command() {
|
||||||
|
if command -v "$1" >/dev/null 2>&1; then pass "$1 available"; else fail "$1 is required"; fi
|
||||||
|
}
|
||||||
|
version_at_least() {
|
||||||
|
awk -F. -v required="$2" 'BEGIN { split(required,r) } { split($1,v); if (v[1]+0 > r[1]+0 || (v[1]+0 == r[1]+0 && v[2]+0 >= r[2]+0)) exit 0; exit 1 }' <<<"$1"
|
||||||
|
}
|
||||||
|
check_version() {
|
||||||
|
local name=$1 version=$2 required=$3
|
||||||
|
if version_at_least "$version" "$required"; then pass "$name $version (minimum $required)"; else fail "$name $version is below minimum $required"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_version Bash "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}" 4.3
|
||||||
|
require_command awk; require_command sed; require_command sha256sum; require_command jq
|
||||||
|
require_command python3
|
||||||
|
if command -v python3 >/dev/null 2>&1 && python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 11))' >/dev/null 2>&1; then
|
||||||
|
pass "Python $(python3 --version 2>&1 | awk '{print $2}') (minimum 3.11)"
|
||||||
|
else
|
||||||
|
fail 'Python 3.11 or newer is required'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$failed" = true ]; then printf 'Capability check: FAIL\n' >&2; exit 1; fi
|
||||||
|
printf 'Capability check: PASS\n'
|
||||||
+3
-1
@@ -12,6 +12,7 @@ $root = Split-Path $PSScriptRoot -Parent
|
|||||||
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
|
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'lib/install-options.ps1')
|
. (Join-Path $PSScriptRoot 'lib/install-options.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1')
|
||||||
|
|
||||||
$Shell = Read-InstallShell -Shell $Shell
|
$Shell = Read-InstallShell -Shell $Shell
|
||||||
$shell = $Shell.ToLowerInvariant()
|
$shell = $Shell.ToLowerInvariant()
|
||||||
@@ -43,6 +44,7 @@ foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath
|
|||||||
}
|
}
|
||||||
|
|
||||||
Sync-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected
|
Sync-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected
|
||||||
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang }
|
Sync-SemanticRetrieval -RepositoryRoot $root -HomePath $homePath -Client $Client -Enabled $options.semantic_retrieval -DryRun:$DryRun -Summary:$Summary
|
||||||
|
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang -SemanticRetrieval $options.semantic_retrieval }
|
||||||
if ($Summary) { Write-Output "Install: PASS | $Client, $Shell$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) }
|
if ($Summary) { Write-Output "Install: PASS | $Client, $Shell$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) }
|
||||||
exit 0
|
exit 0
|
||||||
@@ -21,6 +21,7 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|||||||
. "$root/scripts/lib/install-targets.sh"
|
. "$root/scripts/lib/install-targets.sh"
|
||||||
. "$root/scripts/lib/selection-state.sh"
|
. "$root/scripts/lib/selection-state.sh"
|
||||||
. "$root/scripts/lib/install-options.sh"
|
. "$root/scripts/lib/install-options.sh"
|
||||||
|
. "$root/scripts/lib/semantic-retrieval.sh"
|
||||||
|
|
||||||
shell=$(read_install_shell "$shell")
|
shell=$(read_install_shell "$shell")
|
||||||
client=$(read_install_client "$client")
|
client=$(read_install_client "$client")
|
||||||
@@ -60,6 +61,7 @@ while IFS='|' read -r source destination; do
|
|||||||
done < <(get_install_targets "$generated" "$home_path" "$shell" "$client")
|
done < <(get_install_targets "$generated" "$home_path" "$shell" "$client")
|
||||||
|
|
||||||
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" false selected_plugins "$summary"
|
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" false selected_plugins "$summary"
|
||||||
|
sync_semantic_retrieval "$root" "$client" "$home_path" "$semantic_retrieval" "$dry_run" false
|
||||||
if [ "$dry_run" = false ]; then save_update_selection; fi
|
if [ "$dry_run" = false ]; then save_update_selection; fi
|
||||||
|
|
||||||
if [ "$summary" = true ]; then
|
if [ "$summary" = true ]; then
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function Read-InstallBoolean {
|
|||||||
|
|
||||||
function Read-InstallOptions {
|
function Read-InstallOptions {
|
||||||
param([string]$HomePath, [string]$RepositoryRoot, [string]$Client, [switch]$DryRun)
|
param([string]$HomePath, [string]$RepositoryRoot, [string]$Client, [switch]$DryRun)
|
||||||
$defaults = @{ update_agents = $false; flashbang = $true }
|
$defaults = @{ update_agents = $false; flashbang = $true; semantic_retrieval = $false }
|
||||||
if (Test-Path -LiteralPath (Join-Path $HomePath '.my-ai-configuration/selection.json')) {
|
if (Test-Path -LiteralPath (Join-Path $HomePath '.my-ai-configuration/selection.json')) {
|
||||||
try { $defaults = Read-UpdateSelection -HomePath $HomePath -RepositoryRoot $RepositoryRoot -AllowLegacy }
|
try { $defaults = Read-UpdateSelection -HomePath $HomePath -RepositoryRoot $RepositoryRoot -AllowLegacy }
|
||||||
catch { Write-Warning 'Saved options could not be read; confirm new options below.' }
|
catch { Write-Warning 'Saved options could not be read; confirm new options below.' }
|
||||||
@@ -24,6 +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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ read_install_boolean() {
|
|||||||
read_install_options() {
|
read_install_options() {
|
||||||
update_agents=false
|
update_agents=false
|
||||||
flashbang=true
|
flashbang=true
|
||||||
|
semantic_retrieval=false
|
||||||
local saved key value
|
local saved key value
|
||||||
if [ -f "$home_path/.my-ai-configuration/selection.json" ]; then
|
if [ -f "$home_path/.my-ai-configuration/selection.json" ]; then
|
||||||
if saved=$(python3 "$root/scripts/lib/selection-state.py" read --home "$home_path" --manifest "$root/adapters/plugins.tsv" --format tsv --allow-legacy); then
|
if saved=$(python3 "$root/scripts/lib/selection-state.py" read --home "$home_path" --manifest "$root/adapters/plugins.tsv" --format tsv --allow-legacy); then
|
||||||
while IFS=$'\t' read -r key value; do
|
while IFS=$'\t' read -r key value; do
|
||||||
value=${value%$'\r'}
|
value=${value%$'\r'}
|
||||||
case "$key" in update_agents) update_agents=$value ;; flashbang) flashbang=$value ;; esac
|
case "$key" in update_agents) update_agents=$value ;; flashbang) flashbang=$value ;; semantic_retrieval) semantic_retrieval=$value ;; esac
|
||||||
done <<< "$saved"
|
done <<< "$saved"
|
||||||
else
|
else
|
||||||
printf 'WARN Saved options could not be read; confirm new options below.\n' >&2
|
printf 'WARN Saved options could not be read; confirm new options below.\n' >&2
|
||||||
@@ -32,6 +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
|
||||||
}
|
}
|
||||||
|
|
||||||
is_windows_host() {
|
is_windows_host() {
|
||||||
|
|||||||
@@ -137,6 +137,17 @@ def fetch_skill(entry):
|
|||||||
return skill, files
|
return skill, files
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_revision(source):
|
||||||
|
try:
|
||||||
|
request = urllib.request.Request(f'https://api.github.com/repos/{source}/commits/HEAD', headers={'Accept': 'application/vnd.github+json', 'User-Agent': 'MyAiConfiguration'})
|
||||||
|
with urllib.request.urlopen(request, timeout=10) as response:
|
||||||
|
payload = json.loads(response.read(1024 * 1024))
|
||||||
|
revision = payload.get('sha') if isinstance(payload, dict) else None
|
||||||
|
return revision if isinstance(revision, str) and re.fullmatch(r'[0-9a-fA-F]{7,64}', revision) else None
|
||||||
|
except (OSError, ValueError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Manager:
|
class Manager:
|
||||||
def __init__(self, home, dry_run=False, update=False, summary=False):
|
def __init__(self, home, dry_run=False, update=False, summary=False):
|
||||||
self.home = Path(home).absolute()
|
self.home = Path(home).absolute()
|
||||||
@@ -155,7 +166,7 @@ class Manager:
|
|||||||
raise ValueError('Invalid extension ownership ledger.')
|
raise ValueError('Invalid extension ownership ledger.')
|
||||||
identities = set()
|
identities = set()
|
||||||
for resource in self.state['resources']:
|
for resource in self.state['resources']:
|
||||||
if not isinstance(resource, dict) or resource.get('client') not in ('codex', 'claude') or resource.get('kind') not in ('plugin', 'skill'):
|
if not isinstance(resource, dict) or resource.get('client') not in ('codex', 'claude') or resource.get('kind') not in ('plugin', 'skill', 'qmd'):
|
||||||
raise ValueError('Invalid extension ownership record.')
|
raise ValueError('Invalid extension ownership record.')
|
||||||
if not isinstance(resource.get('name'), str) or not resource['name']:
|
if not isinstance(resource.get('name'), str) or not resource['name']:
|
||||||
raise ValueError('Invalid extension ownership name.')
|
raise ValueError('Invalid extension ownership name.')
|
||||||
@@ -166,17 +177,23 @@ class Manager:
|
|||||||
if resource['kind'] == 'plugin':
|
if resource['kind'] == 'plugin':
|
||||||
if not re.fullmatch(r'[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+', resource.get('selector', '')):
|
if not re.fullmatch(r'[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+', resource.get('selector', '')):
|
||||||
raise ValueError('Invalid owned plugin selector.')
|
raise ValueError('Invalid owned plugin selector.')
|
||||||
else:
|
if 'source' in resource and not isinstance(resource['source'], str):
|
||||||
|
raise ValueError('Invalid owned plugin source.')
|
||||||
|
elif resource['kind'] == 'skill':
|
||||||
directory = resource.get('directory', '')
|
directory = resource.get('directory', '')
|
||||||
if not re.fullmatch(r'\.agents/skills/[A-Za-z0-9_-]+', directory) or resource['client'] != 'codex':
|
if not re.fullmatch(r'\.agents/skills/[A-Za-z0-9_-]+', directory) or resource['client'] != 'codex':
|
||||||
raise ValueError('Invalid owned skill directory.')
|
raise ValueError('Invalid owned skill directory.')
|
||||||
safe_path(self.home, directory)
|
safe_path(self.home, directory)
|
||||||
if not isinstance(resource.get('files'), dict):
|
if not isinstance(resource.get('files'), dict):
|
||||||
raise ValueError('Invalid owned skill file inventory.')
|
raise ValueError('Invalid owned skill file inventory.')
|
||||||
|
if 'source' in resource and not isinstance(resource['source'], str):
|
||||||
|
raise ValueError('Invalid owned skill source.')
|
||||||
for relative, value in resource['files'].items():
|
for relative, value in resource['files'].items():
|
||||||
if not relative.startswith(directory + '/') or not re.fullmatch(r'[a-f0-9]{64}', value):
|
if not relative.startswith(directory + '/') or not re.fullmatch(r'[a-f0-9]{64}', value):
|
||||||
raise ValueError('Invalid owned skill file record.')
|
raise ValueError('Invalid owned skill file record.')
|
||||||
safe_path(self.home, relative)
|
safe_path(self.home, relative)
|
||||||
|
elif resource.get('package') != '@tobilu/qmd' or resource['client'] != 'codex':
|
||||||
|
raise ValueError('Invalid owned QMD record.')
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
if not self.dry_run:
|
if not self.dry_run:
|
||||||
@@ -209,7 +226,7 @@ class Manager:
|
|||||||
arguments = [client, 'plugin', 'uninstall', selector, '--scope', 'user'] if client == 'claude' else [client, 'plugin', 'remove', selector]
|
arguments = [client, 'plugin', 'uninstall', selector, '--scope', 'user'] if client == 'claude' else [client, 'plugin', 'remove', selector]
|
||||||
self.command(arguments)
|
self.command(arguments)
|
||||||
self.cache[client].pop(selector, None)
|
self.cache[client].pop(selector, None)
|
||||||
else:
|
elif record['kind'] == 'skill':
|
||||||
empty_candidates = set()
|
empty_candidates = set()
|
||||||
for relative, expected in list(record['files'].items()):
|
for relative, expected in list(record['files'].items()):
|
||||||
path = safe_path(self.home, relative)
|
path = safe_path(self.home, relative)
|
||||||
@@ -239,10 +256,15 @@ class Manager:
|
|||||||
pass
|
pass
|
||||||
if record['files']:
|
if record['files']:
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
self.command([record['client'], 'mcp', 'remove', 'qmd'] + (['--scope', 'user'] if record['client'] == 'claude' else []))
|
||||||
self.state['resources'].remove(record)
|
self.state['resources'].remove(record)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def ensure(self, client, entry):
|
def ensure(self, client, entry):
|
||||||
|
if entry['codex_method'] == 'qmd':
|
||||||
|
self.ensure_qmd(client, entry)
|
||||||
|
return
|
||||||
skill = client == 'codex' and entry['codex_method'] != 'plugin'
|
skill = client == 'codex' and entry['codex_method'] != 'plugin'
|
||||||
kind = 'skill' if skill else 'plugin'
|
kind = 'skill' if skill else 'plugin'
|
||||||
record = next((r for r in self.state['resources'] if r['client'] == client and r['name'] == entry['name']), None)
|
record = next((r for r in self.state['resources'] if r['client'] == client and r['name'] == entry['name']), None)
|
||||||
@@ -271,11 +293,17 @@ class Manager:
|
|||||||
if item is not None and not self.update:
|
if item is not None and not self.update:
|
||||||
if client == 'claude' and item.get('enabled') is False:
|
if client == 'claude' and item.get('enabled') is False:
|
||||||
self.command([client, 'plugin', 'enable', selector])
|
self.command([client, 'plugin', 'enable', selector])
|
||||||
|
if record is not None and item.get('version') is not None:
|
||||||
|
record['version'] = item['version']; self.save()
|
||||||
return
|
return
|
||||||
if item is not None and client == 'claude':
|
if item is not None and client == 'claude':
|
||||||
self.command([client, 'plugin', 'update', selector])
|
self.command([client, 'plugin', 'update', selector])
|
||||||
if item.get('enabled') is False:
|
if item.get('enabled') is False:
|
||||||
self.command([client, 'plugin', 'enable', selector])
|
self.command([client, 'plugin', 'enable', selector])
|
||||||
|
if record is not None:
|
||||||
|
self.cache.pop(client, None)
|
||||||
|
version = self.installed(client).get(selector, {}).get('version')
|
||||||
|
if version is not None: record['version'] = version; self.save()
|
||||||
return
|
return
|
||||||
marketplace = entry[client + '_marketplace']
|
marketplace = entry[client + '_marketplace']
|
||||||
if marketplace not in ('', '-', 'openai-curated-remote'):
|
if marketplace not in ('', '-', 'openai-curated-remote'):
|
||||||
@@ -287,11 +315,35 @@ class Manager:
|
|||||||
self.command([client, 'plugin', 'marketplace', 'add', marketplace])
|
self.command([client, 'plugin', 'marketplace', 'add', marketplace])
|
||||||
arguments = [client, 'plugin', 'install', selector, '--scope', 'user'] if client == 'claude' else [client, 'plugin', 'add', selector]
|
arguments = [client, 'plugin', 'install', selector, '--scope', 'user'] if client == 'claude' else [client, 'plugin', 'add', selector]
|
||||||
self.command(arguments)
|
self.command(arguments)
|
||||||
|
resolved_version = None
|
||||||
|
if self.update:
|
||||||
|
self.cache.pop(client, None)
|
||||||
|
resolved_version = self.installed(client).get(selector, {}).get('version')
|
||||||
if record is None:
|
if record is None:
|
||||||
self.state['resources'].append(dict(client=client, name=entry['name'], kind='plugin', selector=selector))
|
record = dict(client=client, name=entry['name'], kind='plugin', selector=selector, source=entry.get(client + '_marketplace', '-'))
|
||||||
|
if resolved_version is not None: record['version'] = resolved_version
|
||||||
|
self.state['resources'].append(record)
|
||||||
self.save()
|
self.save()
|
||||||
self.cache[client][selector] = {'scope': 'user'}
|
self.cache[client][selector] = {'scope': 'user'}
|
||||||
|
|
||||||
|
def ensure_qmd(self, client, entry):
|
||||||
|
package = entry['codex_source']
|
||||||
|
if package != '@tobilu/qmd':
|
||||||
|
raise ValueError('Invalid QMD package.')
|
||||||
|
if not getattr(self, 'qmd_ready', False):
|
||||||
|
self.command(['npm', 'install', '--global', package])
|
||||||
|
self.qmd_ready = True
|
||||||
|
if client == 'claude':
|
||||||
|
entry = dict(entry, codex_method='plugin')
|
||||||
|
self.ensure(client, entry)
|
||||||
|
return
|
||||||
|
record = next((r for r in self.state['resources'] if r['client'] == client and r['name'] == entry['name']), None)
|
||||||
|
if record is not None:
|
||||||
|
return
|
||||||
|
self.command(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'])
|
||||||
|
self.state['resources'].append(dict(client='codex', name=entry['name'], kind='qmd', package=package))
|
||||||
|
self.save()
|
||||||
|
|
||||||
def ensure_skill(self, entry, record):
|
def ensure_skill(self, entry, record):
|
||||||
skill = entry['codex_skill']
|
skill = entry['codex_skill']
|
||||||
if not re.fullmatch(r'[A-Za-z0-9_-]+', skill):
|
if not re.fullmatch(r'[A-Za-z0-9_-]+', skill):
|
||||||
@@ -311,12 +363,16 @@ class Manager:
|
|||||||
record = None
|
record = None
|
||||||
if record and record.get('complete') and not self.update and all(safe_path(self.home, name).is_file() for name in record['files']) and record['files']:
|
if record and record.get('complete') and not self.update and all(safe_path(self.home, name).is_file() for name in record['files']) and record['files']:
|
||||||
return
|
return
|
||||||
_, files = fetch_skill(entry)
|
fetched = fetch_skill(entry)
|
||||||
|
_, files = fetched[:2]
|
||||||
|
revision = fetched[2] if len(fetched) > 2 else resolve_revision(entry['codex_source'])
|
||||||
if record is None:
|
if record is None:
|
||||||
record = dict(client='codex', name=entry['name'], kind='skill', directory=directory, files={}, complete=False)
|
record = dict(client='codex', name=entry['name'], kind='skill', directory=directory, source=entry['codex_source'], files={}, complete=False)
|
||||||
self.state['resources'].append(record)
|
self.state['resources'].append(record)
|
||||||
self.save()
|
self.save()
|
||||||
record['complete'] = False
|
record['complete'] = False
|
||||||
|
if revision:
|
||||||
|
record['resolvedRevision'] = revision
|
||||||
self.save()
|
self.save()
|
||||||
for name, (content, executable) in files.items():
|
for name, (content, executable) in files.items():
|
||||||
relative = directory + '/' + name
|
relative = directory + '/' + name
|
||||||
|
|||||||
@@ -86,6 +86,17 @@ function Sync-ManagedDestination {
|
|||||||
if ($relative -eq 'config.toml' -and (Test-Path -LiteralPath $target)) {
|
if ($relative -eq 'config.toml' -and (Test-Path -LiteralPath $target)) {
|
||||||
$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]
|
||||||
|
$localSettings = [regex]::Matches($rootConfig, '(?m)^[ \t]*(?:model|reasoning_effort)[ \t]*=.*$')
|
||||||
|
if ($localSettings.Count -gt 0) {
|
||||||
|
$content = [System.Text.Encoding]::UTF8.GetString($newBytes).TrimEnd([char[]]"`r`n")
|
||||||
|
foreach ($setting in $localSettings) {
|
||||||
|
$name = ([regex]::Match($setting.Value, '^[ \t]*([^ \t=]+)')).Groups[1].Value
|
||||||
|
$content = [regex]::Replace($content, "(?m)^[ \t]*$([regex]::Escape($name))[ \t]*=.*(?:\r?\n|$)", '')
|
||||||
|
}
|
||||||
|
$preservedSettings = ($localSettings | ForEach-Object { $_.Value.TrimEnd([char[]]"`r`n") }) -join "`n"
|
||||||
|
$newBytes = [System.Text.Encoding]::UTF8.GetBytes("$content`n$preservedSettings`n")
|
||||||
|
}
|
||||||
if ($pluginTables.Count -gt 0) {
|
if ($pluginTables.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")
|
||||||
if ($content -match '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])') {
|
if ($content -match '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])') {
|
||||||
|
|||||||
+15
-1
@@ -84,7 +84,21 @@ sync_managed_destination() {
|
|||||||
new_hash=$(sha256_of_file "$source_file")
|
new_hash=$(sha256_of_file "$source_file")
|
||||||
fi
|
fi
|
||||||
if [ "$relative" = config.toml ] && [ -f "$target" ]; then
|
if [ "$relative" = config.toml ] && [ -f "$target" ]; then
|
||||||
local plugin_tables
|
local plugin_tables local_settings setting_name
|
||||||
|
local_settings=$(awk '
|
||||||
|
/^[ \t]*\[/ { exit }
|
||||||
|
/^[ \t]*(model|reasoning_effort)[ \t]*=/ { print }
|
||||||
|
' "$target")
|
||||||
|
if [ -n "$local_settings" ]; then
|
||||||
|
while IFS= read -r setting; do
|
||||||
|
setting_name=${setting%%=*}
|
||||||
|
setting_name=${setting_name//[[:space:]]/}
|
||||||
|
content=$(printf '%s\n' "$content" | sed "/^[[:space:]]*$setting_name[[:space:]]*=/d")
|
||||||
|
done <<< "$local_settings"
|
||||||
|
content="$content"$'\n'"$local_settings"$'\n'
|
||||||
|
needs_sub=true
|
||||||
|
new_hash=$(sha256_of_string "$content")
|
||||||
|
fi
|
||||||
plugin_tables=$(awk '
|
plugin_tables=$(awk '
|
||||||
/^[ \t]*\[/ { preserve = ($0 ~ /^[ \t]*\[(plugins|marketplaces)(\.|\])/) }
|
/^[ \t]*\[/ { preserve = ($0 ~ /^[ \t]*\[(plugins|marketplaces)(\.|\])/) }
|
||||||
preserve { print }
|
preserve { print }
|
||||||
|
|||||||
@@ -97,7 +97,9 @@ function Select-ConfiguredPlugins {
|
|||||||
$checked = @($toggleable | ForEach-Object {
|
$checked = @($toggleable | ForEach-Object {
|
||||||
if ($Mode -eq 'Install') { $true; return }
|
if ($Mode -eq 'Install') { $true; return }
|
||||||
$selector = if ($referenceClient -eq 'Claude') { $_.claude_plugin } else { $_.codex_plugin }
|
$selector = if ($referenceClient -eq 'Claude') { $_.claude_plugin } else { $_.codex_plugin }
|
||||||
if ($referenceClient -eq 'Codex' -and $_.codex_method -ne 'plugin') {
|
if ($referenceClient -eq 'Codex' -and $_.codex_method -eq 'qmd') {
|
||||||
|
$null -ne (Get-Command qmd -ErrorAction SilentlyContinue)
|
||||||
|
} elseif ($referenceClient -eq 'Codex' -and $_.codex_method -ne 'plugin') {
|
||||||
(Test-Path -LiteralPath (Join-Path $HomePath ('.agents/skills/' + $_.codex_skill))) -or (Test-Path -LiteralPath (Join-Path $HomePath ('.codex/skills/' + $_.codex_skill)))
|
(Test-Path -LiteralPath (Join-Path $HomePath ('.agents/skills/' + $_.codex_skill))) -or (Test-Path -LiteralPath (Join-Path $HomePath ('.codex/skills/' + $_.codex_skill)))
|
||||||
} else { [bool](@($installed | Where-Object { $_.$idField -eq $selector }).Count) }
|
} else { [bool](@($installed | Where-Object { $_.$idField -eq $selector }).Count) }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ select_configured_plugins() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if [ "$reference_client" = claude ]; then selector=$(plugin_field "$root" "$name" claude_plugin); else selector=$(plugin_field "$root" "$name" codex_plugin); fi
|
if [ "$reference_client" = claude ]; then selector=$(plugin_field "$root" "$name" claude_plugin); else selector=$(plugin_field "$root" "$name" codex_plugin); fi
|
||||||
if [ "$reference_client" = codex ] && [ "$(plugin_field "$root" "$name" codex_method)" != plugin ]; then
|
if [ "$reference_client" = codex ] && [ "$(plugin_field "$root" "$name" codex_method)" = qmd ]; then
|
||||||
|
if command -v qmd >/dev/null 2>&1; then checked+=(true); else checked+=(false); fi
|
||||||
|
elif [ "$reference_client" = codex ] && [ "$(plugin_field "$root" "$name" codex_method)" != plugin ]; then
|
||||||
local skill_name
|
local skill_name
|
||||||
skill_name=$(plugin_field "$root" "$name" codex_skill)
|
skill_name=$(plugin_field "$root" "$name" codex_skill)
|
||||||
if [ -d "$HOME/.agents/skills/$skill_name" ] || [ -d "$HOME/.codex/skills/$skill_name" ]; then checked+=(true); else checked+=(false); fi
|
if [ -d "$HOME/.agents/skills/$skill_name" ] || [ -d "$HOME/.codex/skills/$skill_name" ]; then checked+=(true); else checked+=(false); fi
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ function Read-UpdateSelection {
|
|||||||
|
|
||||||
function Save-UpdateSelection {
|
function Save-UpdateSelection {
|
||||||
param([string]$HomePath, [string]$RepositoryRoot, [string]$Shell, [string]$Client, [hashtable]$Plugins,
|
param([string]$HomePath, [string]$RepositoryRoot, [string]$Shell, [string]$Client, [hashtable]$Plugins,
|
||||||
[Parameter(Mandatory=$true)][bool]$UpdateAgents, [Parameter(Mandatory=$true)][bool]$Flashbang)
|
[Parameter(Mandatory=$true)][bool]$UpdateAgents, [Parameter(Mandatory=$true)][bool]$Flashbang, [bool]$SemanticRetrieval = $false)
|
||||||
$arguments = @((Join-Path $PSScriptRoot 'selection-state.py'), 'write', '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'), '--shell', $Shell.ToLowerInvariant(), '--client', $Client.ToLowerInvariant(), '--update-agents', $UpdateAgents.ToString().ToLowerInvariant(), '--flashbang', $Flashbang.ToString().ToLowerInvariant(), '--selected')
|
$arguments = @((Join-Path $PSScriptRoot 'selection-state.py'), 'write', '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'), '--shell', $Shell.ToLowerInvariant(), '--client', $Client.ToLowerInvariant(), '--update-agents', $UpdateAgents.ToString().ToLowerInvariant(), '--flashbang', $Flashbang.ToString().ToLowerInvariant(), '--semantic-retrieval', $SemanticRetrieval.ToString().ToLowerInvariant(), '--selected')
|
||||||
$arguments += @($Plugins.Selected | ForEach-Object { $_.name })
|
$arguments += @($Plugins.Selected | ForEach-Object { $_.name })
|
||||||
$arguments += '--deselected'
|
$arguments += '--deselected'
|
||||||
$arguments += @($Plugins.Deselected | ForEach-Object { $_.name })
|
$arguments += @($Plugins.Deselected | ForEach-Object { $_.name })
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ def validate(state, allow_legacy=False):
|
|||||||
raise ValueError('Unsupported saved selection.')
|
raise ValueError('Unsupported saved selection.')
|
||||||
if state.get('shell') not in ('powershell', 'bash') or state.get('client') not in ('codex', 'claude', 'both'):
|
if state.get('shell') not in ('powershell', 'bash') or state.get('client') not in ('codex', 'claude', 'both'):
|
||||||
raise ValueError('Invalid saved shell or client.')
|
raise ValueError('Invalid saved shell or client.')
|
||||||
if any(type(state.get(key)) is not bool for key in ('update_agents', 'flashbang')):
|
if 'semantic_retrieval' not in state:
|
||||||
|
state['semantic_retrieval'] = False
|
||||||
|
if any(type(state.get(key)) is not bool for key in ('update_agents', 'flashbang', 'semantic_retrieval')):
|
||||||
raise ValueError('Invalid saved install options. Run update once without Quick.')
|
raise ValueError('Invalid saved install options. Run update once without Quick.')
|
||||||
for key in ('selected', 'deselected'):
|
for key in ('selected', 'deselected'):
|
||||||
values = state.get(key)
|
values = state.get(key)
|
||||||
@@ -41,6 +43,7 @@ def main():
|
|||||||
parser.add_argument('--allow-legacy', action='store_true')
|
parser.add_argument('--allow-legacy', action='store_true')
|
||||||
parser.add_argument('--update-agents', choices=('true', 'false'))
|
parser.add_argument('--update-agents', choices=('true', 'false'))
|
||||||
parser.add_argument('--flashbang', choices=('true', 'false'))
|
parser.add_argument('--flashbang', choices=('true', 'false'))
|
||||||
|
parser.add_argument('--semantic-retrieval', choices=('true', 'false'))
|
||||||
parser.add_argument('--client')
|
parser.add_argument('--client')
|
||||||
parser.add_argument('--selected', nargs='*', default=[])
|
parser.add_argument('--selected', nargs='*', default=[])
|
||||||
parser.add_argument('--deselected', nargs='*', default=[])
|
parser.add_argument('--deselected', nargs='*', default=[])
|
||||||
@@ -59,7 +62,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print('shell\t' + state['shell'])
|
print('shell\t' + state['shell'])
|
||||||
print('client\t' + state['client'])
|
print('client\t' + state['client'])
|
||||||
for key in ('update_agents', 'flashbang'):
|
for key in ('update_agents', 'flashbang', 'semantic_retrieval'):
|
||||||
print(key + '\t' + str(state[key]).lower())
|
print(key + '\t' + str(state[key]).lower())
|
||||||
for key in ('selected', 'deselected'):
|
for key in ('selected', 'deselected'):
|
||||||
for name in state[key]:
|
for name in state[key]:
|
||||||
@@ -68,6 +71,7 @@ def main():
|
|||||||
state = validate(dict(version=2, shell=args.shell, client=args.client,
|
state = validate(dict(version=2, shell=args.shell, client=args.client,
|
||||||
update_agents=None if args.update_agents is None else args.update_agents == 'true',
|
update_agents=None if args.update_agents is None else args.update_agents == 'true',
|
||||||
flashbang=None if args.flashbang is None else args.flashbang == 'true',
|
flashbang=None if args.flashbang is None else args.flashbang == 'true',
|
||||||
|
semantic_retrieval=False if args.semantic_retrieval is None else args.semantic_retrieval == 'true',
|
||||||
selected=args.selected, deselected=args.deselected))
|
selected=args.selected, deselected=args.deselected))
|
||||||
if (set(state['selected']) | set(state['deselected'])) - names:
|
if (set(state['selected']) | set(state['deselected'])) - names:
|
||||||
raise ValueError('Unknown plugin in selection.')
|
raise ValueError('Unknown plugin in selection.')
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ read_update_selection() {
|
|||||||
client) [ -n "$client" ] || client=$value ;;
|
client) [ -n "$client" ] || client=$value ;;
|
||||||
update_agents) update_agents=$value ;;
|
update_agents) update_agents=$value ;;
|
||||||
flashbang) flashbang=$value ;;
|
flashbang) flashbang=$value ;;
|
||||||
|
semantic_retrieval) semantic_retrieval=$value ;;
|
||||||
selected) selected_plugins+=("$value") ;;
|
selected) selected_plugins+=("$value") ;;
|
||||||
deselected) deselected_plugins+=("$value") ;;
|
deselected) deselected_plugins+=("$value") ;;
|
||||||
esac
|
esac
|
||||||
@@ -20,6 +21,6 @@ read_update_selection() {
|
|||||||
|
|
||||||
save_update_selection() {
|
save_update_selection() {
|
||||||
python3 "$root/scripts/lib/selection-state.py" write --home "$home_path" --manifest "$root/adapters/plugins.tsv" \
|
python3 "$root/scripts/lib/selection-state.py" write --home "$home_path" --manifest "$root/adapters/plugins.tsv" \
|
||||||
--shell "$shell" --client "$client" --update-agents "$update_agents" --flashbang "$flashbang" \
|
--shell "$shell" --client "$client" --update-agents "$update_agents" --flashbang "$flashbang" --semantic-retrieval "${semantic_retrieval:-false}" \
|
||||||
--selected "${selected_plugins[@]}" --deselected "${deselected_plugins[@]}"
|
--selected "${selected_plugins[@]}" --deselected "${deselected_plugins[@]}"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
function Sync-SemanticRetrieval {
|
||||||
|
param([Parameter(Mandatory=$true)][string]$RepositoryRoot,[Parameter(Mandatory=$true)][string]$HomePath,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,[Parameter(Mandatory=$true)][bool]$Enabled,[switch]$DryRun,[switch]$Update,[switch]$Summary)
|
||||||
|
$statePath = Join-Path $HomePath '.my-ai-configuration/semantic-retrieval.json'
|
||||||
|
if (-not $Enabled -and -not (Test-Path -LiteralPath $statePath)) { return }
|
||||||
|
$arguments = @((Join-Path $PSScriptRoot 'semantic-retrieval.py'), 'sync', '--root', $RepositoryRoot, '--home', $HomePath, '--client', $Client.ToLowerInvariant(), '--enabled', $Enabled.ToString().ToLowerInvariant())
|
||||||
|
if ($DryRun) { $arguments += '--dry-run' }; if ($Update) { $arguments += '--update' }
|
||||||
|
& python @arguments
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Semantic retrieval reconciliation failed.' }
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
NAME = 'my-ai-qwen3-retrieval'
|
||||||
|
FILES = ('server.py', 'requirements.txt')
|
||||||
|
|
||||||
|
|
||||||
|
def digest(path):
|
||||||
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def command(client, arguments, home, dry_run):
|
||||||
|
if dry_run:
|
||||||
|
print('DRYRUN ' + ' '.join(arguments))
|
||||||
|
return True
|
||||||
|
executable = shutil.which(client)
|
||||||
|
if executable is None:
|
||||||
|
raise ValueError(f'{client} CLI is required for semantic retrieval.')
|
||||||
|
result = subprocess.run([executable, *arguments[1:]], stdin=subprocess.DEVNULL, capture_output=True, text=True,
|
||||||
|
timeout=180, env=dict(os.environ, HOME=str(home), USERPROFILE=str(home), CODEX_HOME=str(home / '.codex'), CLAUDE_CONFIG_DIR=str(home / '.claude')))
|
||||||
|
if result.returncode:
|
||||||
|
raise ValueError(f'Semantic retrieval command failed: {client} {" ".join(arguments[1:])}\n{result.stdout}{result.stderr}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def python_path(target):
|
||||||
|
return target / ('Scripts/python.exe' if os.name == 'nt' else 'bin/python')
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_runtime(target, dry_run):
|
||||||
|
if dry_run:
|
||||||
|
return python_path(target / '.venv')
|
||||||
|
venv = target / '.venv'
|
||||||
|
if not venv.exists():
|
||||||
|
subprocess.run([sys.executable, '-m', 'venv', str(venv)], check=True, timeout=180)
|
||||||
|
python = python_path(venv)
|
||||||
|
if not python.is_file():
|
||||||
|
raise ValueError('Semantic retrieval virtual environment is incomplete.')
|
||||||
|
subprocess.run([str(python), '-m', 'pip', 'install', '--disable-pip-version-check', '-r', str(target / 'requirements.txt')],
|
||||||
|
check=True, timeout=900)
|
||||||
|
return python
|
||||||
|
|
||||||
|
|
||||||
|
def load_state(path):
|
||||||
|
if not path.exists():
|
||||||
|
return {'version': 1, 'clients': [], 'files': {}}
|
||||||
|
state = json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
if state.get('version') != 1 or not isinstance(state.get('clients'), list) or not isinstance(state.get('files'), dict):
|
||||||
|
raise ValueError('Invalid semantic retrieval ownership state.')
|
||||||
|
if any(client not in ('codex', 'claude') for client in state['clients']):
|
||||||
|
raise ValueError('Invalid semantic retrieval client state.')
|
||||||
|
if any(name not in FILES or not isinstance(value, str) or len(value) != 64 or any(character not in '0123456789abcdef' for character in value) for name, value in state['files'].items()):
|
||||||
|
raise ValueError('Invalid semantic retrieval file state.')
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(path, state, dry_run):
|
||||||
|
if not dry_run:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(state, indent=2) + '\n', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def sync(root, home, clients, enabled, dry_run, update):
|
||||||
|
base = home / '.my-ai-configuration'
|
||||||
|
target = base / 'semantic-retrieval'
|
||||||
|
state_path = base / 'semantic-retrieval.json'
|
||||||
|
state = load_state(state_path)
|
||||||
|
clients = list(dict.fromkeys(clients))
|
||||||
|
if base.exists() and base.is_symlink():
|
||||||
|
raise ValueError('Semantic retrieval state directory must not be a symbolic link.')
|
||||||
|
if not enabled:
|
||||||
|
owned = [client for client in state['clients'] if client in clients]
|
||||||
|
if dry_run:
|
||||||
|
print('DRYRUN disable semantic retrieval: ' + (', '.join(owned) if owned else 'nothing owned'))
|
||||||
|
return
|
||||||
|
for client in owned:
|
||||||
|
command(client, [client, 'mcp', 'remove', NAME] + (['--scope', 'user'] if client == 'claude' else []), home, False)
|
||||||
|
state['clients'] = [client for client in state['clients'] if client not in clients]
|
||||||
|
if state['clients']:
|
||||||
|
save_state(state_path, state, False)
|
||||||
|
return
|
||||||
|
if target.is_symlink():
|
||||||
|
raise ValueError('Semantic retrieval directory must not be a symbolic link.')
|
||||||
|
if target.exists() and target.is_dir():
|
||||||
|
shutil.rmtree(target)
|
||||||
|
state_path.unlink(missing_ok=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
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')
|
||||||
|
return
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
source = root / 'shared' / 'retrieval'
|
||||||
|
for name in FILES:
|
||||||
|
shutil.copy2(source / name, target / name)
|
||||||
|
state['files'][name] = digest(target / name)
|
||||||
|
python = ensure_runtime(target, False)
|
||||||
|
existing_clients = set(state['clients'])
|
||||||
|
state['clients'] = list(dict.fromkeys(state['clients']))
|
||||||
|
save_state(state_path, state, False)
|
||||||
|
arguments = ['--', str(python), str(target / 'server.py'), '--root', '.', '--data-dir', str(target / 'data'), '--model-cache', str(target / 'model-cache')]
|
||||||
|
for client in clients:
|
||||||
|
command_args = [client, 'mcp', 'add']
|
||||||
|
if client == 'claude':
|
||||||
|
command_args += [NAME, '--scope', 'user', '-e', f'HF_HOME={target / "model-cache"}']
|
||||||
|
else:
|
||||||
|
command_args += ['--env', f'HF_HOME={target / "model-cache"}']
|
||||||
|
command_args += [NAME]
|
||||||
|
command_args += arguments
|
||||||
|
if update and client in existing_clients:
|
||||||
|
command(client, [client, 'mcp', 'remove', NAME] + (['--scope', 'user'] if client == 'claude' else []), home, False)
|
||||||
|
command(client, command_args, home, False)
|
||||||
|
if client not in state['clients']:
|
||||||
|
state['clients'].append(client)
|
||||||
|
save_state(state_path, state, False)
|
||||||
|
save_state(state_path, state, False)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('action', choices=('sync',))
|
||||||
|
parser.add_argument('--root', required=True, type=Path)
|
||||||
|
parser.add_argument('--home', required=True, type=Path)
|
||||||
|
parser.add_argument('--client', choices=('codex', 'claude', 'both'), required=True)
|
||||||
|
parser.add_argument('--enabled', choices=('true', 'false'), required=True)
|
||||||
|
parser.add_argument('--dry-run', action='store_true')
|
||||||
|
parser.add_argument('--update', action='store_true')
|
||||||
|
args = parser.parse_args()
|
||||||
|
clients = ('codex', 'claude') if args.client == 'both' else (args.client,)
|
||||||
|
sync(args.root.resolve(), args.home.resolve(), clients, args.enabled == 'true', args.dry_run, args.update)
|
||||||
|
print('SEMANTIC RETRIEVAL: reconciliation complete' + (' (dry run)' if args.dry_run else ''))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except (OSError, ValueError, json.JSONDecodeError, subprocess.SubprocessError) as error:
|
||||||
|
print(f'Semantic retrieval: {error}', file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
sync_semantic_retrieval() {
|
||||||
|
local root=$1 client=$2 home_path=$3 enabled=$4 dry_run=$5 update=$6
|
||||||
|
[ "$enabled" = true ] || [ -f "$home_path/.my-ai-configuration/semantic-retrieval.json" ] || return 0
|
||||||
|
local arguments=("$root/scripts/lib/semantic-retrieval.py" sync --root "$root" --home "$home_path" --client "$client" --enabled "$enabled")
|
||||||
|
[ "$dry_run" != true ] || arguments+=(--dry-run)
|
||||||
|
[ "$update" != true ] || arguments+=(--update)
|
||||||
|
python3 "${arguments[@]}"
|
||||||
|
}
|
||||||
@@ -19,7 +19,16 @@ $baseline = Join-Path $root 'adapters/prompt-budget-baseline.json'
|
|||||||
$old = if (Test-Path $baseline) { Get-Content $baseline -Raw | ConvertFrom-Json } else { $null }
|
$old = if (Test-Path $baseline) { Get-Content $baseline -Raw | ConvertFrom-Json } else { $null }
|
||||||
$rows = @([pscustomobject]@{Name='Claude permanent context';Bytes=$claude;Tokens=(Tokens $claude)},[pscustomobject]@{Name='Codex permanent context';Bytes=$codex;Tokens=(Tokens $codex)},[pscustomobject]@{Name='Claude skill files (lazy)';Bytes=$skills;Tokens=(Tokens $skills)},[pscustomobject]@{Name='Rule trigger metadata';Bytes=$triggers;Tokens=(Tokens $triggers)},[pscustomobject]@{Name='Largest lazy rule';Bytes=$lazy;Tokens=(Tokens $lazy)})
|
$rows = @([pscustomobject]@{Name='Claude permanent context';Bytes=$claude;Tokens=(Tokens $claude)},[pscustomobject]@{Name='Codex permanent context';Bytes=$codex;Tokens=(Tokens $codex)},[pscustomobject]@{Name='Claude skill files (lazy)';Bytes=$skills;Tokens=(Tokens $skills)},[pscustomobject]@{Name='Rule trigger metadata';Bytes=$triggers;Tokens=(Tokens $triggers)},[pscustomobject]@{Name='Largest lazy rule';Bytes=$lazy;Tokens=(Tokens $lazy)})
|
||||||
$fail = $false
|
$fail = $false
|
||||||
if ($old) { foreach ($row in $rows | Select-Object -First 2) { $previous = [double]$old.($row.Name); if ($previous -gt 0 -and $row.Bytes -gt $previous * 1.15) { $fail = $true } } }
|
$baselineValues = @{}
|
||||||
if ($Summary) { Write-Output 'Prompt Budget'; $rows | ForEach-Object { Write-Output ('{0,-28} {1,8} bytes {2,6} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } else { $rows | ForEach-Object { Write-Output ('{0}: {1} bytes / {2} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } }
|
if ($old) {
|
||||||
if ($fail) { Write-Error 'Prompt budget: permanent context increased by more than 15 percent.'; exit 1 }
|
foreach ($row in $rows | Select-Object -First 2) {
|
||||||
|
$previous = [double]$old.($row.Name)
|
||||||
|
$absolute = [double]$old.(($row.Name + ' absolute limit'))
|
||||||
|
$baselineValues[$row.Name] = @($previous, $absolute)
|
||||||
|
if ($previous -gt 0 -and $row.Bytes -gt $previous * 1.15) { $fail = $true; Write-Error "Prompt budget relative limit failed: $($row.Name) $($row.Bytes) bytes (baseline $previous, limit 15%)." }
|
||||||
|
if ($absolute -gt 0 -and $row.Bytes -gt $absolute) { $fail = $true; Write-Error "Prompt budget absolute limit failed: $($row.Name) $($row.Bytes) bytes (absolute limit $absolute)." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($Summary) { Write-Output 'Prompt Budget'; foreach ($row in $rows | Select-Object -First 2) { $limits = $baselineValues[$row.Name]; $base = if($limits){$limits[0]}else{'-'}; $absolute = if($limits){$limits[1]}else{'-'}; Write-Output ('{0,-28} {1,8} bytes baseline {2} relative 115% absolute {3}' -f $row.Name,$row.Bytes,$base,$absolute) }; $rows | Select-Object -Skip 2 | ForEach-Object { Write-Output ('{0,-28} {1,8} bytes {2,6} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } else { $rows | ForEach-Object { Write-Output ('{0}: {1} bytes / {2} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } }
|
||||||
|
if ($fail) { exit 1 }
|
||||||
exit 0
|
exit 0
|
||||||
@@ -9,7 +9,17 @@ bytes() { wc -c < "$1" | tr -d ' '; }
|
|||||||
tree_bytes() { find "$1" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {print s+0}'; }
|
tree_bytes() { find "$1" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {print s+0}'; }
|
||||||
tokens() { awk -v b="$1" 'BEGIN { printf "%d", (b + 3) / 4 }'; }
|
tokens() { awk -v b="$1" 'BEGIN { printf "%d", (b + 3) / 4 }'; }
|
||||||
claude=$(bytes "$generated/claude-bash/CLAUDE.md"); codex=$(bytes "$generated/codex-bash/AGENTS.md"); skills=$(tree_bytes "$generated/claude-bash/skills"); triggers=$(bytes "$root/adapters/claude/rule-skills.tsv"); lazy=$(find "$root/shared/rules" -type f -printf '%s %p\n' | sort -nr | awk 'NR==1 {print $1+0}'); fail=false
|
claude=$(bytes "$generated/claude-bash/CLAUDE.md"); codex=$(bytes "$generated/codex-bash/AGENTS.md"); skills=$(tree_bytes "$generated/claude-bash/skills"); triggers=$(bytes "$root/adapters/claude/rule-skills.tsv"); lazy=$(find "$root/shared/rules" -type f -printf '%s %p\n' | sort -nr | awk 'NR==1 {print $1+0}'); fail=false
|
||||||
if [ -f "$root/adapters/prompt-budget-baseline.tsv" ]; then while IFS=$'\t' read -r name old; do current=$([ "$name" = Claude ] && printf '%s' "$claude" || printf '%s' "$codex"); awk -v n="$current" -v o="$old" 'BEGIN { exit !(o > 0 && n > o * 1.15) }' && fail=true; done < "$root/adapters/prompt-budget-baseline.tsv"; fi
|
claude_baseline=0; codex_baseline=0; claude_absolute=0; codex_absolute=0
|
||||||
if [ "$summary" = true ]; then printf 'Prompt Budget\nClaude permanent context %8s bytes %6s tokens\nCodex permanent context %8s bytes %6s tokens\nClaude skill files (lazy) %8s bytes %6s tokens\nRule trigger metadata %8s bytes %6s tokens\nLargest lazy rule %8s bytes %6s tokens\n' "$claude" "$(tokens "$claude")" "$codex" "$(tokens "$codex")" "$skills" "$(tokens "$skills")" "$triggers" "$(tokens "$triggers")" "$lazy" "$(tokens "$lazy")"; else printf 'Prompt budget: Claude %s bytes, Codex %s bytes, skills %s bytes, triggers %s bytes, largest lazy rule %s bytes\n' "$claude" "$codex" "$skills" "$triggers" "$lazy"; fi
|
if [ -f "$root/adapters/prompt-budget-baseline.tsv" ]; then
|
||||||
|
while IFS=$'\t' read -r name old absolute; do
|
||||||
|
[ "$name" = Claude ] || [ "$name" = Codex ] || continue
|
||||||
|
current=$([ "$name" = Claude ] && printf '%s' "$claude" || printf '%s' "$codex")
|
||||||
|
[ "$name" = Claude ] && claude_baseline=$old && claude_absolute=$absolute
|
||||||
|
[ "$name" = Codex ] && codex_baseline=$old && codex_absolute=$absolute
|
||||||
|
if awk -v n="$current" -v o="$old" 'BEGIN { exit !(o > 0 && n > o * 1.15) }'; then printf 'Prompt budget relative limit failed: %s %s bytes (baseline %s, limit 15%%).\n' "$name" "$current" "$old" >&2; fail=true; fi
|
||||||
|
if awk -v n="$current" -v a="$absolute" 'BEGIN { exit !(a > 0 && n > a) }'; then printf 'Prompt budget absolute limit failed: %s %s bytes (absolute limit %s).\n' "$name" "$current" "$absolute" >&2; fail=true; fi
|
||||||
|
done < "$root/adapters/prompt-budget-baseline.tsv"
|
||||||
|
fi
|
||||||
|
if [ "$summary" = true ]; then printf 'Prompt Budget\nClaude permanent context %8s bytes baseline %s relative 115%% absolute %s\nCodex permanent context %8s bytes baseline %s relative 115%% absolute %s\nClaude skill files (lazy) %8s bytes %6s tokens\nRule trigger metadata %8s bytes %6s tokens\nLargest lazy rule %8s bytes %6s tokens\n' "$claude" "$claude_baseline" "$claude_absolute" "$codex" "$codex_baseline" "$codex_absolute" "$skills" "$(tokens "$skills")" "$triggers" "$(tokens "$triggers")" "$lazy" "$(tokens "$lazy")"; else printf 'Prompt budget: Claude %s bytes, Codex %s bytes, skills %s bytes, triggers %s bytes, largest lazy rule %s bytes\n' "$claude" "$codex" "$skills" "$triggers" "$lazy"; fi
|
||||||
$fail && { printf 'Prompt budget: permanent context increased by more than 15 percent.\n' >&2; exit 1; }
|
$fail && { printf 'Prompt budget: permanent context increased by more than 15 percent.\n' >&2; exit 1; }
|
||||||
exit 0
|
exit 0
|
||||||
@@ -6,7 +6,7 @@ $cacheDir = Join-Path $root '.ai-session'
|
|||||||
$cachePath = Join-Path $cacheDir 'repository-context.json'
|
$cachePath = Join-Path $cacheDir 'repository-context.json'
|
||||||
$safeRoot = $root.Replace('\','/')
|
$safeRoot = $root.Replace('\','/')
|
||||||
$head = (& git -c "safe.directory=$safeRoot" -C $root rev-parse HEAD 2>$null).Trim()
|
$head = (& git -c "safe.directory=$safeRoot" -C $root rev-parse HEAD 2>$null).Trim()
|
||||||
$inputs = @('AI-Instructions.md','adapters/plugins.tsv','adapters/claude/rule-skills.tsv','adapters/codex/config/config.toml','adapters/claude/config/settings.json')
|
$inputs = @('AI-Instructions.md','adapters/plugins.tsv','adapters/claude/rule-skills.tsv','adapters/claude/capabilities.tsv','adapters/prompt-budget-baseline.json','adapters/prompt-budget-baseline.tsv','adapters/codex/config/config.toml','adapters/claude/config/settings.json')
|
||||||
$inputs += @(Get-ChildItem $root -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj|vendor|\.ai-session)\\' -and $_.Extension -in @('.sln','.csproj','.fsproj','.ts','.tsx','.json','.toml','.yaml','.yml','.cs','.fs','.py','.sh','.ps1') } | ForEach-Object { $_.FullName.Substring($root.Length + 1) } | Sort-Object)
|
$inputs += @(Get-ChildItem $root -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj|vendor|\.ai-session)\\' -and $_.Extension -in @('.sln','.csproj','.fsproj','.ts','.tsx','.json','.toml','.yaml','.yml','.cs','.fs','.py','.sh','.ps1') } | ForEach-Object { $_.FullName.Substring($root.Length + 1) } | Sort-Object)
|
||||||
$keyText = $head + "`n" + (($inputs | Sort-Object -Unique | ForEach-Object { $_ + ':' + (Get-FileHash (Join-Path $root $_) -Algorithm SHA256).Hash }) -join "`n")
|
$keyText = $head + "`n" + (($inputs | Sort-Object -Unique | ForEach-Object { $_ + ':' + (Get-FileHash (Join-Path $root $_) -Algorithm SHA256).Hash }) -join "`n")
|
||||||
$key = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($keyText)).TrimEnd('=')
|
$key = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($keyText)).TrimEnd('=')
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param([ValidateSet('record','summary','rotate')][string]$Action='record',[string]$Event,[string]$Agent,[string]$Role,[int]$DurationMs,[int]$ToolCalls,[string]$CommandClass,[int64]$Bytes,[int]$Count)
|
param([ValidateSet('record','summary','rotate')][string]$Action='record',[string]$Event,[string]$Agent,[string]$Role,[Nullable[int]]$DurationMs,[Nullable[int]]$ToolCalls,[string]$CommandClass,[Nullable[int64]]$Bytes,[Nullable[int]]$Count,[Nullable[int]]$HandoffBytes,[Nullable[int]]$FilesRead,[Nullable[int]]$FilesChanged,[string]$Outcome,[Nullable[int]]$InputTokens,[Nullable[int]]$CachedTokens,[Nullable[int]]$OutputTokens,[Nullable[int]]$ReasoningTokens)
|
||||||
$ErrorActionPreference = 'Stop'; if ($env:AI_CONFIG_TELEMETRY -eq '0') { exit 0 }
|
$ErrorActionPreference = 'Stop'; if ($env:AI_CONFIG_TELEMETRY -eq '0') { exit 0 }
|
||||||
$root = Split-Path $PSScriptRoot -Parent; $dir = Join-Path $root '.ai-session'; $path = Join-Path $dir 'telemetry.jsonl'; New-Item $dir -ItemType Directory -Force | Out-Null
|
try {
|
||||||
if ($Action -eq 'rotate') { if (Test-Path $path) { Move-Item $path ($path + '.1') -Force }; exit 0 }
|
$root = Split-Path $PSScriptRoot -Parent; $dir = Join-Path $root '.ai-session'; $path = Join-Path $dir 'telemetry.jsonl'; New-Item $dir -ItemType Directory -Force | Out-Null
|
||||||
if ($Action -eq 'summary') { if (-not (Test-Path $path)) { Write-Output 'Telemetry: no events'; exit 0 }; $events=@(Get-Content $path | ForEach-Object { try { $_ | ConvertFrom-Json } catch {} }); $agents=@($events | Where-Object event -eq 'subagent-stop'); Write-Output ('Telemetry: {0} events | {1} subagents' -f $events.Count,$agents.Count); exit 0 }
|
if ($Action -eq 'rotate') { if (Test-Path $path) { Move-Item $path ($path + '.1') -Force }; exit 0 }
|
||||||
if ([string]::IsNullOrWhiteSpace($Event)) { throw 'Event is required.' }
|
if ($Action -eq 'summary') { if (-not (Test-Path $path)) { Write-Output 'Telemetry: no events'; exit 0 }; $events=@(Get-Content $path | ForEach-Object { try { $_ | ConvertFrom-Json } catch {} }); Write-Output ('Telemetry: {0} events' -f $events.Count); foreach($group in @($events | Where-Object role | Group-Object role | Sort-Object Name)){ $inputs=@($group.Group|Where-Object tokenUsage|ForEach-Object {$_.tokenUsage.input}|Where-Object {$_ -is [int] -or $_ -is [long]}|Sort-Object); $median=if($inputs.Count){$inputs[[int][Math]::Floor($inputs.Count/2)]}else{'-'}; Write-Output ('{0}: runs {1} findings {2} median input {3}' -f $group.Name,$group.Count,@($group.Group|Where-Object outcome -eq findings).Count,$median)}; exit 0 }
|
||||||
$record=[ordered]@{event=$Event;timestamp=(Get-Date).ToUniversalTime().ToString('o')}; foreach($pair in @(@('agent',$Agent),@('role',$Role),@('durationMs',$DurationMs),@('toolCalls',$ToolCalls),@('commandClass',$CommandClass),@('bytes',$Bytes),@('count',$Count))){if($pair[1]){$record[$pair[0]]=$pair[1]}}; ($record|ConvertTo-Json -Compress)|Add-Content $path; if((Get-Item $path).Length -gt 1048576){Move-Item $path ($path+'.1') -Force}
|
if ([string]::IsNullOrWhiteSpace($Event)) { throw 'Event is required.' }
|
||||||
|
$record=[ordered]@{event=$Event;timestamp=(Get-Date).ToUniversalTime().ToString('o')}; foreach($pair in @(@('agent',$Agent),@('role',$Role),@('durationMs',$DurationMs),@('toolCalls',$ToolCalls),@('commandClass',$CommandClass),@('bytes',$Bytes),@('count',$Count),@('handoffBytes',$HandoffBytes),@('filesRead',$FilesRead),@('filesChanged',$FilesChanged),@('outcome',$Outcome))){if($null -ne $pair[1] -and "$($pair[1])" -ne ''){$record[$pair[0]]=$pair[1]}}; $tokens=[ordered]@{}; foreach($pair in @(@('input',$InputTokens),@('cached',$CachedTokens),@('output',$OutputTokens),@('reasoning',$ReasoningTokens))){if($null -ne $pair[1]){$tokens[$pair[0]]=$pair[1]}}; if($tokens.Count){$record.tokenUsage=$tokens}; ($record|ConvertTo-Json -Compress)|Add-Content $path; if((Get-Item $path).Length -gt 1048576){Move-Item $path ($path+'.1') -Force}
|
||||||
|
} catch { exit 0 }
|
||||||
+34
-4
@@ -2,9 +2,39 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
[ "${AI_CONFIG_TELEMETRY:-1}" = 0 ] && exit 0
|
[ "${AI_CONFIG_TELEMETRY:-1}" = 0 ] && exit 0
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); dir="$root/.ai-session"; path="$dir/telemetry.jsonl"; mkdir -p "$dir"
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); dir="$root/.ai-session"; path="$dir/telemetry.jsonl"; mkdir -p "$dir"
|
||||||
case "${1:-record}" in
|
action=${1:-record}; shift || true
|
||||||
|
case "$action" in
|
||||||
rotate) [ -f "$path" ] && mv -f "$path" "$path.1"; exit 0;;
|
rotate) [ -f "$path" ] && mv -f "$path" "$path.1"; exit 0;;
|
||||||
summary) [ -f "$path" ] || { printf 'Telemetry: no events\n'; exit 0; }; count=$(wc -l < "$path" | tr -d ' '); agents=$(grep -c '"event":"subagent-stop"' "$path" || true); printf 'Telemetry: %s events | %s subagents\n' "$count" "$agents"; exit 0;;
|
summary) [ -f "$path" ] || { printf 'Telemetry: no events\n'; exit 0; }; python3 - "$path" <<'PY' || exit 0
|
||||||
record) event=${2:?event required};; *) printf 'Unknown action: %s\n' "$1" >&2; exit 1;;
|
import json,sys,statistics
|
||||||
|
rows=[]
|
||||||
|
for line in open(sys.argv[1],encoding='utf-8',errors='replace'):
|
||||||
|
try: rows.append(json.loads(line))
|
||||||
|
except Exception: pass
|
||||||
|
print(f'Telemetry: {len(rows)} events')
|
||||||
|
for role in sorted({r.get('role') for r in rows if r.get('role')}):
|
||||||
|
subset=[r for r in rows if r.get('role')==role]
|
||||||
|
inputs=[r['tokenUsage']['input'] for r in subset if isinstance(r.get('tokenUsage'),dict) and isinstance(r['tokenUsage'].get('input'),(int,float))]
|
||||||
|
print(f"{role}: runs {len(subset)} findings {sum(r.get('outcome')=='findings' for r in subset)} median input {int(statistics.median(inputs)) if inputs else '-'}")
|
||||||
|
PY
|
||||||
|
exit 0;;
|
||||||
|
record) event=${1:?event required}; shift;; *) printf 'Unknown action: %s\n' "$action" >&2; exit 1;;
|
||||||
esac
|
esac
|
||||||
printf '{"event":"%s","timestamp":"%s"}\n' "$event" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$path"; [ "$(wc -c < "$path")" -le 1048576 ] || mv -f "$path" "$path.1"
|
python3 - "$path" "$event" "$@" <<'PY' || exit 0
|
||||||
|
import json,sys,datetime
|
||||||
|
path,event=sys.argv[1:3]; args=sys.argv[3:]
|
||||||
|
record={'event':event,'timestamp':datetime.datetime.now(datetime.timezone.utc).isoformat()}
|
||||||
|
keys=['role','durationMs','handoffBytes','filesRead','filesChanged','outcome','input','cached','output','reasoning']
|
||||||
|
if any(a.startswith('--') for a in args):
|
||||||
|
parsed={}; i=0
|
||||||
|
while i < len(args):
|
||||||
|
if args[i].startswith('--') and i+1 < len(args): parsed[args[i][2:]]=args[i+1]; i+=2
|
||||||
|
else: i+=1
|
||||||
|
args=[parsed.get(k,'') for k in keys]
|
||||||
|
for key,value in zip(keys,args):
|
||||||
|
if value: record[key]=int(value) if value.lstrip('-').isdigit() else value
|
||||||
|
tokens={k:record.pop(k) for k in ('input','cached','output','reasoning') if k in record}
|
||||||
|
if tokens: record['tokenUsage']=tokens
|
||||||
|
with open(path,'a',encoding='utf-8') as out: out.write(json.dumps(record,separators=(',',':'))+'\n')
|
||||||
|
PY
|
||||||
|
if [ "$(wc -c < "$path")" -gt 1048576 ]; then mv -f "$path" "$path.1"; fi
|
||||||
@@ -184,7 +184,7 @@ class InstallOptionsTests(unittest.TestCase):
|
|||||||
'select_configured_plugins() { :; }\nsync_configured_plugins() { :; }\n'
|
'select_configured_plugins() { :; }\nsync_configured_plugins() { :; }\n'
|
||||||
'run_plugin_command() { printf "CLI-MOCK %s\\n" "$2"; }\n')
|
'run_plugin_command() { printf "CLI-MOCK %s\\n" "$2"; }\n')
|
||||||
environment = dict(os.environ, HOME=home.as_posix(), TEST_INSTALL_HOME=str(home))
|
environment = dict(os.environ, HOME=home.as_posix(), TEST_INSTALL_HOME=str(home))
|
||||||
for name, quick, answers in [('install', False, 'y\nn\n'), ('update', True, '')]:
|
for name, quick, answers in [('install', False, 'y\nn\nn\n'), ('update', True, '')]:
|
||||||
entry = str(scripts / (name + '.' + suffix))
|
entry = str(scripts / (name + '.' + suffix))
|
||||||
command = ([executable, '-NoProfile', '-File', entry, '-Shell', 'Bash', '-Client', 'Both', '-Summary']
|
command = ([executable, '-NoProfile', '-File', entry, '-Shell', 'Bash', '-Client', 'Both', '-Summary']
|
||||||
if kind == 'powershell' else [executable, entry, '--shell', 'bash', '--client', 'both', '--summary'])
|
if kind == 'powershell' else [executable, entry, '--shell', 'bash', '--client', 'both', '--summary'])
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ManagedExtensionTests(unittest.TestCase):
|
|||||||
|
|
||||||
def command(self, args, summary, home=None):
|
def command(self, args, summary, home=None):
|
||||||
self.commands.append(args)
|
self.commands.append(args)
|
||||||
if args[2] in ('add', 'install') and args[1] == 'plugin':
|
if len(args) > 3 and args[2] in ('add', 'install') and args[1] == 'plugin':
|
||||||
self.installed[args[0]][args[3]] = {'scope': 'user'}
|
self.installed[args[0]][args[3]] = {'scope': 'user'}
|
||||||
if args[2] in ('remove', 'uninstall'):
|
if args[2] in ('remove', 'uninstall'):
|
||||||
self.installed[args[0]].pop(args[3], None)
|
self.installed[args[0]].pop(args[3], None)
|
||||||
@@ -110,6 +110,13 @@ class ManagedExtensionTests(unittest.TestCase):
|
|||||||
self.assertTrue((directory / 'foreign.txt').exists())
|
self.assertTrue((directory / 'foreign.txt').exists())
|
||||||
self.assertFalse((directory / 'references/old.md').exists())
|
self.assertFalse((directory / 'references/old.md').exists())
|
||||||
|
|
||||||
|
def test_skill_records_resolved_revision(self):
|
||||||
|
with patch.object(extensions, 'resolve_revision', return_value='a' * 40):
|
||||||
|
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
||||||
|
record = self.manager().state['resources'][0]
|
||||||
|
self.assertEqual(record['source'], 'blader/humanizer')
|
||||||
|
self.assertEqual(record['resolvedRevision'], 'a' * 40)
|
||||||
|
|
||||||
def test_skill_partial_failure_records_each_successful_file(self):
|
def test_skill_partial_failure_records_each_successful_file(self):
|
||||||
original_write = extensions.atomic_write
|
original_write = extensions.atomic_write
|
||||||
def write(path, content):
|
def write(path, content):
|
||||||
@@ -165,6 +172,20 @@ class ManagedExtensionTests(unittest.TestCase):
|
|||||||
self.manager(dry_run=True, summary=True).sync([plugin(), skill()], ['codex'], {'Example', 'Humanizer'})
|
self.manager(dry_run=True, summary=True).sync([plugin(), skill()], ['codex'], {'Example', 'Humanizer'})
|
||||||
self.assertEqual('EXTENSIONS: reconciliation complete (dry run)\n', output.getvalue())
|
self.assertEqual('EXTENSIONS: reconciliation complete (dry run)\n', output.getvalue())
|
||||||
|
|
||||||
|
def test_qmd_installs_once_and_registers_codex_mcp(self):
|
||||||
|
entry = dict(name='QMD', codex_method='qmd', codex_source='@tobilu/qmd', codex_skill='qmd', codex_plugin='-', codex_marketplace='-', claude_plugin='qmd@qmd', claude_marketplace='tobi/qmd')
|
||||||
|
self.manager().sync([entry], ['codex'], {'QMD'})
|
||||||
|
self.assertEqual([['npm', 'install', '--global', '@tobilu/qmd'], ['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp']], self.commands)
|
||||||
|
self.manager().sync([], ['codex'], set())
|
||||||
|
self.assertEqual(['codex', 'mcp', 'remove', 'qmd'], self.commands[-1])
|
||||||
|
|
||||||
|
def test_qmd_installs_once_for_both_clients(self):
|
||||||
|
entry = dict(name='QMD', codex_method='qmd', codex_source='@tobilu/qmd', codex_skill='qmd', codex_plugin='-', codex_marketplace='-', claude_plugin='qmd@qmd', claude_marketplace='tobi/qmd')
|
||||||
|
self.manager().sync([entry], ['claude', 'codex'], {'QMD'})
|
||||||
|
self.assertEqual(1, self.commands.count(['npm', 'install', '--global', '@tobilu/qmd']))
|
||||||
|
self.assertIn(['claude', 'plugin', 'install', 'qmd@qmd', '--scope', 'user'], self.commands)
|
||||||
|
self.assertIn(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'], self.commands)
|
||||||
|
|
||||||
def test_foreign_empty_directory_survives_skill_removal(self):
|
def test_foreign_empty_directory_survives_skill_removal(self):
|
||||||
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
||||||
foreign = self.home / '.agents/skills/humanizer/foreign-empty'
|
foreign = self.home / '.agents/skills/humanizer/foreign-empty'
|
||||||
|
|||||||
@@ -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'`n")
|
[System.IO.File]::WriteAllText((Join-Path $source 'config.toml'), "model = 'new'`nreasoning_effort = 'low'`n")
|
||||||
[System.IO.File]::WriteAllText($configPath, "model = 'old'`n$pluginState`n[other]`nvalue = true`n")
|
[System.IO.File]::WriteAllText($configPath, "model = 'old'`nreasoning_effort = 'high'`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.Contains($pluginState)) { 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 = 'new'") -or $mergedConfig.Contains('[other]')) { throw 'Managed configuration must still be updated.' }
|
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 ([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,15 +58,17 @@ plugin_state=' [marketplaces.ponytail]
|
|||||||
enabled = true
|
enabled = true
|
||||||
[plugins."disabled@market"]
|
[plugins."disabled@market"]
|
||||||
enabled = false'
|
enabled = false'
|
||||||
printf "model = 'new'\n" > "$source_dir/config.toml"
|
printf "model = 'new'\nreasoning_effort = 'low'\n" > "$source_dir/config.toml"
|
||||||
printf "model = 'old'\n%s\n[other]\nvalue = true\n" "$plugin_state" > "$destination/config.toml"
|
printf "model = 'old'\nreasoning_effort = 'high'\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; }
|
||||||
sync_managed_destination "$source_dir" "$destination" plugins "" "" "" false > /dev/null
|
sync_managed_destination "$source_dir" "$destination" plugins "" "" "" false > /dev/null
|
||||||
merged_config=$(cat "$destination/config.toml")
|
merged_config=$(cat "$destination/config.toml")
|
||||||
[[ "$merged_config" == *"$plugin_state"* ]] || { printf 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.\n' >&2; exit 1; }
|
merged_config_normalized=${merged_config//$'\r'/}
|
||||||
[[ "$merged_config" == *"model = 'new'"* && "$merged_config" != *'[other]'* ]] || { printf 'Managed configuration must still be updated.\n' >&2; exit 1; }
|
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; }
|
||||||
[ "$(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; }
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ class SelectionTests(unittest.TestCase):
|
|||||||
self.path.write_text(json.dumps(state))
|
self.path.write_text(json.dumps(state))
|
||||||
self.assertNotEqual(self.run_state('read').returncode, 0)
|
self.assertNotEqual(self.run_state('read').returncode, 0)
|
||||||
|
|
||||||
|
def test_semantic_retrieval_option_round_trips(self):
|
||||||
|
result = self.run_state('write', '--shell', 'bash', '--client', 'both', '--update-agents', 'false',
|
||||||
|
'--flashbang', 'true', '--semantic-retrieval', 'true')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
state = json.loads(self.run_state('read').stdout)
|
||||||
|
self.assertTrue(state['semantic_retrieval'])
|
||||||
|
self.assertIn('semantic_retrieval\ttrue', self.run_state('read', '--format', 'tsv').stdout)
|
||||||
|
|
||||||
def test_invalid_write_preserves_last_selection(self):
|
def test_invalid_write_preserves_last_selection(self):
|
||||||
self.save('--selected', 'Superpowers')
|
self.save('--selected', 'Superpowers')
|
||||||
original = self.path.read_bytes()
|
original = self.path.read_bytes()
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
SPEC = importlib.util.spec_from_file_location('semantic_retrieval', ROOT / 'scripts/lib/semantic-retrieval.py')
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class SemanticRetrievalTests(unittest.TestCase):
|
||||||
|
def test_claude_registration_places_name_before_environment(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
home = Path(directory) / 'home'
|
||||||
|
calls = []
|
||||||
|
original = MODULE.command
|
||||||
|
original_runtime = MODULE.ensure_runtime
|
||||||
|
MODULE.command = lambda client, arguments, home, dry_run: calls.append((client, arguments))
|
||||||
|
MODULE.ensure_runtime = lambda target, dry_run: target / '.venv' / 'bin' / 'python'
|
||||||
|
try:
|
||||||
|
MODULE.sync(ROOT, home, ['claude'], True, False, False)
|
||||||
|
finally:
|
||||||
|
MODULE.command = original
|
||||||
|
MODULE.ensure_runtime = original_runtime
|
||||||
|
add = calls[0][1]
|
||||||
|
self.assertEqual(add[:5], ['claude', 'mcp', 'add', MODULE.NAME, '--scope'])
|
||||||
|
self.assertIn('-e', add)
|
||||||
|
|
||||||
|
def test_disable_removes_owned_runtime_and_registration(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
home = Path(directory) / 'home'
|
||||||
|
target = home / '.my-ai-configuration/semantic-retrieval'
|
||||||
|
target.mkdir(parents=True)
|
||||||
|
for name in MODULE.FILES:
|
||||||
|
(target / name).write_text(name)
|
||||||
|
state_path = target.parent / 'semantic-retrieval.json'
|
||||||
|
state_path.write_text(json.dumps({'version': 1, 'clients': ['codex'], 'files': {
|
||||||
|
name: hashlib.sha256((target / name).read_bytes()).hexdigest() for name in MODULE.FILES}}))
|
||||||
|
calls = []
|
||||||
|
original = MODULE.command
|
||||||
|
MODULE.command = lambda client, arguments, home, dry_run: calls.append((client, arguments))
|
||||||
|
try:
|
||||||
|
MODULE.sync(ROOT, home, ['codex'], False, False, False)
|
||||||
|
finally:
|
||||||
|
MODULE.command = original
|
||||||
|
self.assertEqual(calls[0][1], ['codex', 'mcp', 'remove', MODULE.NAME])
|
||||||
|
self.assertFalse(target.exists())
|
||||||
|
self.assertFalse(state_path.exists())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -17,6 +17,27 @@ SHELLS = [
|
|||||||
|
|
||||||
|
|
||||||
class SessionStateHookTests(unittest.TestCase):
|
class SessionStateHookTests(unittest.TestCase):
|
||||||
|
def test_git_identity_and_stale_verification(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
workspace = Path(directory)
|
||||||
|
env = dict(os.environ, GIT_AUTHOR_NAME='Test', GIT_AUTHOR_EMAIL='test@example.invalid', GIT_COMMITTER_NAME='Test', GIT_COMMITTER_EMAIL='test@example.invalid')
|
||||||
|
subprocess.run(['git', '-C', str(workspace), 'init', '-q'], check=True)
|
||||||
|
(workspace / 'file').write_text('one')
|
||||||
|
subprocess.run(['git', '-C', str(workspace), 'add', 'file'], check=True)
|
||||||
|
subprocess.run(['git', '-C', str(workspace), 'commit', '-qm', 'one'], check=True, env=env)
|
||||||
|
command = [sys.executable, str(ROOT / 'shared/hooks/scripts/session-state.py'), '--workspace', str(workspace)]
|
||||||
|
self.assertEqual(subprocess.run(command + ['--verified', 'check'], capture_output=True).returncode, 0)
|
||||||
|
state = json.loads((workspace / '.ai-session/state.json').read_text())
|
||||||
|
self.assertTrue(state['head'])
|
||||||
|
self.assertEqual(state['verification'][0]['head'], state['head'])
|
||||||
|
(workspace / 'file').write_text('two')
|
||||||
|
subprocess.run(['git', '-C', str(workspace), 'add', 'file'], check=True)
|
||||||
|
subprocess.run(['git', '-C', str(workspace), 'commit', '-qm', 'two'], check=True, env=env)
|
||||||
|
self.assertEqual(subprocess.run(command, capture_output=True).returncode, 0)
|
||||||
|
state = json.loads((workspace / '.ai-session/state.json').read_text())
|
||||||
|
self.assertTrue(state['verificationStale'])
|
||||||
|
self.assertTrue(state['verification'][0]['stale'])
|
||||||
|
|
||||||
def test_writer_migrates_legacy_state_and_requires_reset_for_new_goal(self):
|
def test_writer_migrates_legacy_state_and_requires_reset_for_new_goal(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
workspace = Path(directory)
|
workspace = Path(directory)
|
||||||
|
|||||||
@@ -22,8 +22,16 @@ foreach ($shell in @('powershell','bash')) {
|
|||||||
$package = Join-Path $root "generated/$client-$shell"
|
$package = Join-Path $root "generated/$client-$shell"
|
||||||
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
|
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
|
||||||
$content = Get-Content -LiteralPath (Join-Path $package $file) -Raw
|
$content = Get-Content -LiteralPath (Join-Path $package $file) -Raw
|
||||||
|
if ($client -eq 'codex') {
|
||||||
|
foreach ($entry in @(Import-Csv (Join-Path $root 'adapters/claude/rule-skills.tsv') -Delimiter ([char]9))) {
|
||||||
|
$rulePath = $entry.rule_file
|
||||||
|
if (Test-Path (Join-Path $root "shared/rules/$($entry.rule_file)") -PathType Container) { $rulePath = "$($entry.rule_file)/index.md" }
|
||||||
|
if ((Get-Content (Join-Path $package 'AGENTS.md') -Raw) -notmatch [regex]::Escape("- rules/$rulePath for $($entry.trigger).")) { throw "Codex rule loading is missing $($entry.rule_file) in $package" }
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($client -eq 'claude') { $settings = $content | ConvertFrom-Json }
|
if ($client -eq 'claude') { $settings = $content | ConvertFrom-Json }
|
||||||
if (@(Get-ChildItem "$package/agents" -File).Count -ne $sourceAgentCount) { throw "Missing agents in $package" }
|
if (@(Get-ChildItem "$package/agents" -File).Count -ne $sourceAgentCount) { throw "Missing agents in $package" }
|
||||||
|
if ($client -eq 'claude' -and (Get-Content (Join-Path $package 'agents/reviewer.md') -Raw) -notmatch '(?m)^tools: Read,Diff,Search\r?$') { throw "Reviewer capability profile is missing in $package" }
|
||||||
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
|
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
|
||||||
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" }
|
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" }
|
||||||
$expected = if ($shell -eq 'powershell') { '__POWERSHELL_COMMAND__ .*flashbang.ps1' } else { 'bash .*flashbang.sh' }
|
$expected = if ($shell -eq 'powershell') { '__POWERSHELL_COMMAND__ .*flashbang.ps1' } else { 'bash .*flashbang.sh' }
|
||||||
@@ -33,7 +41,7 @@ foreach ($shell in @('powershell','bash')) {
|
|||||||
$events = @([regex]::Matches($content, '(?m)^\[\[hooks\.([^.\]]+)\]\]\s*$') | ForEach-Object { $_.Groups[1].Value })
|
$events = @([regex]::Matches($content, '(?m)^\[\[hooks\.([^.\]]+)\]\]\s*$') | ForEach-Object { $_.Groups[1].Value })
|
||||||
if ($events.Count -ne 1 -or $events[0] -ne 'Stop' -or $content -match 'flashbang-if-input' -or $content -match '(?m)^async\s*=\s*true\s*$') { throw "Codex finish hook is incorrect in $package" }
|
if ($events.Count -ne 1 -or $events[0] -ne 'Stop' -or $content -match 'flashbang-if-input' -or $content -match '(?m)^async\s*=\s*true\s*$') { throw "Codex finish hook is incorrect in $package" }
|
||||||
if ($content -notmatch 'approvals_reviewer\s*=\s*"auto_review"') { throw "Codex auto review is missing in $package" }
|
if ($content -notmatch 'approvals_reviewer\s*=\s*"auto_review"') { throw "Codex auto review is missing in $package" }
|
||||||
if ($content -notmatch 'status_line\s*=\s*\["model", "reasoning", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"\]') { throw "Codex status line is incorrect in $package" }
|
if ($content -notmatch 'status_line\s*=\s*\["model", "reasoning", "permissions", "approval-mode", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"\]') { throw "Codex status line is incorrect in $package" }
|
||||||
} elseif ((($settings.hooks.PSObject.Properties.Name | Sort-Object) -join ',') -ne 'PreCompact,SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $settings.hooks.Stop[0].hooks[0].async) {
|
} elseif ((($settings.hooks.PSObject.Properties.Name | Sort-Object) -join ',') -ne 'PreCompact,SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $settings.hooks.Stop[0].hooks[0].async) {
|
||||||
throw "Claude finish hook is incorrect in $package"
|
throw "Claude finish hook is incorrect in $package"
|
||||||
} elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') {
|
} elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') {
|
||||||
|
|||||||
@@ -31,7 +31,17 @@ for shell in powershell bash; do
|
|||||||
file=config.toml
|
file=config.toml
|
||||||
[ "$client" != claude ] || file=settings.json
|
[ "$client" != claude ] || file=settings.json
|
||||||
test -f "$package/$file"
|
test -f "$package/$file"
|
||||||
|
if [ "$client" = codex ]; then
|
||||||
|
while IFS=$'\t' read -r rule_file skill_name trigger; do
|
||||||
|
[ "$rule_file" = rule_file ] && continue
|
||||||
|
[ -n "$rule_file" ] || continue
|
||||||
|
rule_path=$rule_file
|
||||||
|
[ -d "$root/shared/rules/$rule_file" ] && rule_path="$rule_file/index.md"
|
||||||
|
grep -Fq -- "- rules/$rule_path for $trigger." "$package/AGENTS.md"
|
||||||
|
done < "$root/adapters/claude/rule-skills.tsv"
|
||||||
|
fi
|
||||||
test "$(find "$package/agents" -type f | wc -l)" -eq "$source_agent_count"
|
test "$(find "$package/agents" -type f | wc -l)" -eq "$source_agent_count"
|
||||||
|
if [ "$client" = claude ]; then grep -q '^tools: Read,Diff,Search$' "$package/agents/reviewer.md"; fi
|
||||||
expected_skill_count=$source_skill_count
|
expected_skill_count=$source_skill_count
|
||||||
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
|
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
|
||||||
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count"
|
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count"
|
||||||
@@ -46,7 +56,7 @@ for shell in powershell bash; do
|
|||||||
! grep -q 'flashbang-if-input' "$package/$file"
|
! grep -q 'flashbang-if-input' "$package/$file"
|
||||||
! grep -Eq '^async[[:space:]]*=[[:space:]]*true[[:space:]]*$' "$package/$file"
|
! grep -Eq '^async[[:space:]]*=[[:space:]]*true[[:space:]]*$' "$package/$file"
|
||||||
grep -q 'approvals_reviewer[[:space:]]*=[[:space:]]*"auto_review"' "$package/$file"
|
grep -q 'approvals_reviewer[[:space:]]*=[[:space:]]*"auto_review"' "$package/$file"
|
||||||
grep -Fq 'status_line = ["model", "reasoning", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]' "$package/$file"
|
grep -Fq 'status_line = ["model", "reasoning", "permissions", "approval-mode", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]' "$package/$file"
|
||||||
else
|
else
|
||||||
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert set(settings["hooks"]) == {"PreCompact", "SessionStart", "Stop"}' "$package/$file"
|
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert set(settings["hooks"]) == {"PreCompact", "SessionStart", "Stop"}' "$package/$file"
|
||||||
! grep -q 'flashbang-if-input' "$package/$file"
|
! grep -q 'flashbang-if-input' "$package/$file"
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryTests(unittest.TestCase):
|
||||||
|
def test_bash_record_disable_and_summary(self):
|
||||||
|
bash = shutil.which('bash')
|
||||||
|
if not bash:
|
||||||
|
self.skipTest('Bash unavailable')
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
script = root / 'scripts/telemetry.sh'
|
||||||
|
script.parent.mkdir()
|
||||||
|
shutil.copy(ROOT / 'scripts/telemetry.sh', script)
|
||||||
|
env = dict(os.environ, AI_CONFIG_TELEMETRY='1')
|
||||||
|
result = subprocess.run([bash, str(script), 'record', 'subagent-stop', '--role', 'reviewer', '--filesRead', '3', '--outcome', 'findings'], env=env, capture_output=True, text=True)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
event = json.loads((root / '.ai-session/telemetry.jsonl').read_text().strip())
|
||||||
|
self.assertEqual(event['role'], 'reviewer')
|
||||||
|
self.assertEqual(event['filesRead'], 3)
|
||||||
|
self.assertNotIn('prompt', event)
|
||||||
|
disabled = dict(env, AI_CONFIG_TELEMETRY='0')
|
||||||
|
subprocess.run([bash, str(script), 'record', 'ignored'], env=disabled, check=True)
|
||||||
|
summary = subprocess.run([bash, str(script), 'summary'], env=env, capture_output=True, text=True, check=True)
|
||||||
|
self.assertIn('reviewer: runs 1 findings 1', summary.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
+3
-1
@@ -13,6 +13,7 @@ $root = Split-Path $PSScriptRoot -Parent
|
|||||||
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
|
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'lib/install-options.ps1')
|
. (Join-Path $PSScriptRoot 'lib/install-options.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1')
|
||||||
|
|
||||||
$homePath = [Environment]::GetFolderPath('UserProfile')
|
$homePath = [Environment]::GetFolderPath('UserProfile')
|
||||||
if ($Quick) {
|
if ($Quick) {
|
||||||
@@ -65,6 +66,7 @@ try {
|
|||||||
$script:PluginNonInteractive = $previousPluginNonInteractive
|
$script:PluginNonInteractive = $previousPluginNonInteractive
|
||||||
$env:CI = $previousCI
|
$env:CI = $previousCI
|
||||||
}
|
}
|
||||||
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang }
|
Sync-SemanticRetrieval -RepositoryRoot $root -HomePath $homePath -Client $Client -Enabled $options.semantic_retrieval -DryRun:$DryRun -Update -Summary:$Summary
|
||||||
|
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang -SemanticRetrieval $options.semantic_retrieval }
|
||||||
if ($Summary) { Write-Output "Update: PASS | $Client, $Shell$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS update dry-run' } else { 'PASS update' })) }
|
if ($Summary) { Write-Output "Update: PASS | $Client, $Shell$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS update dry-run' } else { 'PASS update' })) }
|
||||||
exit 0
|
exit 0
|
||||||
@@ -23,6 +23,7 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|||||||
. "$root/scripts/lib/install-targets.sh"
|
. "$root/scripts/lib/install-targets.sh"
|
||||||
. "$root/scripts/lib/selection-state.sh"
|
. "$root/scripts/lib/selection-state.sh"
|
||||||
. "$root/scripts/lib/install-options.sh"
|
. "$root/scripts/lib/install-options.sh"
|
||||||
|
. "$root/scripts/lib/semantic-retrieval.sh"
|
||||||
|
|
||||||
home_path=${HOME:?HOME is required}
|
home_path=${HOME:?HOME is required}
|
||||||
selected_plugins=()
|
selected_plugins=()
|
||||||
@@ -71,6 +72,7 @@ if [ "$quick" = true ]; then
|
|||||||
else
|
else
|
||||||
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" true selected_plugins "$summary"
|
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" true selected_plugins "$summary"
|
||||||
fi
|
fi
|
||||||
|
sync_semantic_retrieval "$root" "$client" "$home_path" "$semantic_retrieval" "$dry_run" true
|
||||||
if [ "$dry_run" = false ]; then save_update_selection; fi
|
if [ "$dry_run" = false ]; then save_update_selection; fi
|
||||||
|
|
||||||
if [ "$summary" = true ]; then
|
if [ "$summary" = true ]; then
|
||||||
|
|||||||
@@ -2,4 +2,6 @@
|
|||||||
|
|
||||||
Provide analytical correctness review using the acceptance criteria and actual git diff. Inspect changed symbols first, then direct callers, dependencies, tests, and contracts needed to judge correctness. Expand for shared state, configuration, persistence, security, generated code, or public API effects; avoid broad repository exploration by default.
|
Provide analytical correctness review using the acceptance criteria and actual git diff. Inspect changed symbols first, then direct callers, dependencies, tests, and contracts needed to judge correctness. Expand for shared state, configuration, persistence, security, generated code, or public API effects; avoid broad repository exploration by default.
|
||||||
|
|
||||||
|
For each finding, report SEVERITY (blocking, important, or minor), LOCATION, IMPACT, EVIDENCE, and SCOPE (introduced, pre-existing, or uncertain). Do not report stylistic preferences or pre-existing issues unless they materially affect the requested change. Prioritize actionable correctness defects, keep unrelated cleanup out of scope, and use blocking only for security, data-loss, broken contracts, or deterministic failures.
|
||||||
|
|
||||||
Check logic, regressions, architecture, maintainability, security, and contract consistency. Do not automatically rerun the full test suite or duplicate a verifier's evidence. Follow the shared orchestration handoff protocol; report actionable findings with severity and evidence. Do not modify implementation code unless assigned.
|
Check logic, regressions, architecture, maintainability, security, and contract consistency. Do not automatically rerun the full test suite or duplicate a verifier's evidence. Follow the shared orchestration handoff protocol; report actionable findings with severity and evidence. Do not modify implementation code unless assigned.
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
param()
|
param()
|
||||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||||
$stateDir = Join-Path $root '.ai-session'
|
$stateDir = Join-Path $root '.ai-session'
|
||||||
New-Item $stateDir -ItemType Directory -Force | Out-Null
|
if ($env:AI_CONFIG_TELEMETRY -ne '0') {
|
||||||
('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content (Join-Path $stateDir 'telemetry.jsonl')
|
New-Item $stateDir -ItemType Directory -Force | Out-Null
|
||||||
|
$path = Join-Path $stateDir 'telemetry.jsonl'
|
||||||
|
('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content $path
|
||||||
|
if ((Get-Item $path).Length -gt 1048576) { Move-Item $path ($path + '.1') -Force }
|
||||||
|
}
|
||||||
& python (Join-Path $PSScriptRoot 'session-state.py') --hook
|
& python (Join-Path $PSScriptRoot 'session-state.py') --hook
|
||||||
exit $LASTEXITCODE
|
exit $LASTEXITCODE
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
mkdir -p "$root/.ai-session"
|
[ "${AI_CONFIG_TELEMETRY:-1}" = 0 ] || { mkdir -p "$root/.ai-session" && printf '{"event":"compact","timestamp":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$root/.ai-session/telemetry.jsonl"; [ "$(wc -c < "$root/.ai-session/telemetry.jsonl")" -le 1048576 ] || mv -f "$root/.ai-session/telemetry.jsonl" "$root/.ai-session/telemetry.jsonl.1"; }
|
||||||
printf '{"event":"compact","timestamp":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$root/.ai-session/telemetry.jsonl"
|
|
||||||
python3 "$root/hooks/scripts/session-state.py" --hook
|
python3 "$root/hooks/scripts/session-state.py" --hook
|
||||||
@@ -4,6 +4,8 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +28,28 @@ def show_pointer(path):
|
|||||||
print('Task state is available at ' + str(path) + '; validate its workspace and goal before reuse.')
|
print('Task state is available at ' + str(path) + '; validate its workspace and goal before reuse.')
|
||||||
|
|
||||||
|
|
||||||
|
def git_identity(workspace):
|
||||||
|
try:
|
||||||
|
def git(*args):
|
||||||
|
return subprocess.check_output(['git', '-C', str(workspace), *args], stderr=subprocess.DEVNULL, text=True).strip()
|
||||||
|
root = Path(git('rev-parse', '--show-toplevel')).resolve()
|
||||||
|
try:
|
||||||
|
branch = git('symbolic-ref', '--quiet', '--short', 'HEAD') or 'HEAD'
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
branch = 'HEAD'
|
||||||
|
head = git('rev-parse', 'HEAD')
|
||||||
|
try:
|
||||||
|
repository = git('config', '--get', 'remote.origin.url') or str(root)
|
||||||
|
if '://' in repository:
|
||||||
|
parsed = urlsplit(repository)
|
||||||
|
repository = urlunsplit((parsed.scheme, parsed.hostname or '', parsed.path, '', ''))
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
repository = str(root)
|
||||||
|
return {'repository': repository, 'branch': branch, 'head': head}
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def hook_pointer():
|
def hook_pointer():
|
||||||
try:
|
try:
|
||||||
raw = '' if sys.stdin.isatty() else sys.stdin.buffer.read(1048577).decode('utf-8-sig')
|
raw = '' if sys.stdin.isatty() else sys.stdin.buffer.read(1048577).decode('utf-8-sig')
|
||||||
@@ -47,18 +71,25 @@ def update_state(args):
|
|||||||
state = json.loads(path.read_text(encoding='utf-8-sig')) if path.exists() else {}
|
state = json.loads(path.read_text(encoding='utf-8-sig')) if path.exists() else {}
|
||||||
if not isinstance(state, dict):
|
if not isinstance(state, dict):
|
||||||
raise ValueError('Invalid task state')
|
raise ValueError('Invalid task state')
|
||||||
|
for metadata in ('repository', 'branch', 'head'):
|
||||||
|
if metadata in state and not isinstance(state[metadata], str):
|
||||||
|
raise ValueError('Invalid task state metadata')
|
||||||
|
if 'verificationStale' in state and not isinstance(state['verificationStale'], bool):
|
||||||
|
raise ValueError('Invalid task state metadata')
|
||||||
workspace = str(path.parent.parent)
|
workspace = str(path.parent.parent)
|
||||||
if state.get('workspace', workspace) != workspace:
|
if state.get('workspace', workspace) != workspace:
|
||||||
raise ValueError('Task state belongs to another workspace')
|
raise ValueError('Task state belongs to another workspace')
|
||||||
for old, new in (('changedFiles', 'changed'), ('pending', 'open'), ('importantFindings', 'decisions')):
|
for old, new in (('changedFiles', 'changed'), ('pending', 'open'), ('importantFindings', 'decisions')):
|
||||||
if old in state:
|
if old in state:
|
||||||
state.setdefault(new, state.pop(old))
|
state.setdefault(new, state.pop(old))
|
||||||
for field in ('goal', 'decisions', 'changed', 'verified', 'open', 'risks'):
|
for field in ('goal', 'decisions', 'changed', 'verified', 'open', 'risks', 'verification'):
|
||||||
if field not in state:
|
if field not in state:
|
||||||
continue
|
continue
|
||||||
value = state[field]
|
value = state[field]
|
||||||
if field == 'goal':
|
if field == 'goal':
|
||||||
valid = isinstance(value, str)
|
valid = isinstance(value, str)
|
||||||
|
elif field == 'verification':
|
||||||
|
valid = isinstance(value, list) and all(isinstance(item, dict) and isinstance(item.get('check'), str) and isinstance(item.get('result'), str) for item in value)
|
||||||
else:
|
else:
|
||||||
valid = isinstance(value, list) and all(isinstance(item, str) for item in value)
|
valid = isinstance(value, list) and all(isinstance(item, str) for item in value)
|
||||||
if not valid:
|
if not valid:
|
||||||
@@ -68,12 +99,28 @@ def update_state(args):
|
|||||||
elif args.goal is not None and state.get('goal') and state['goal'] != args.goal:
|
elif args.goal is not None and state.get('goal') and state['goal'] != args.goal:
|
||||||
raise ValueError('A different goal requires reset')
|
raise ValueError('A different goal requires reset')
|
||||||
state['workspace'] = workspace
|
state['workspace'] = workspace
|
||||||
|
previous_identity = {key: state.get(key) for key in ('repository', 'branch', 'head')}
|
||||||
|
identity = git_identity(workspace)
|
||||||
|
if identity:
|
||||||
|
state.update(identity)
|
||||||
|
if path.exists() and any(previous_identity.get(key) not in (None, identity[key]) for key in identity):
|
||||||
|
state['verificationStale'] = True
|
||||||
|
for item in state.get('verification', []):
|
||||||
|
item['stale'] = True
|
||||||
|
else:
|
||||||
|
for key in ('repository', 'branch', 'head'):
|
||||||
|
state.pop(key, None)
|
||||||
for field in ('goal', 'decisions', 'changed', 'verified', 'open', 'risks'):
|
for field in ('goal', 'decisions', 'changed', 'verified', 'open', 'risks'):
|
||||||
value = getattr(args, field)
|
value = getattr(args, field)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
state[field] = value
|
state[field] = value
|
||||||
else:
|
else:
|
||||||
state.setdefault(field, '' if field == 'goal' else [])
|
state.setdefault(field, '' if field == 'goal' else [])
|
||||||
|
state.setdefault('verification', [])
|
||||||
|
if getattr(args, 'verified', None):
|
||||||
|
state['verification'] = [dict(check=check, head=identity['head'], result='pass') for check in args.verified] if identity else state.get('verification', [])
|
||||||
|
if identity:
|
||||||
|
state['verificationStale'] = False
|
||||||
state['updatedAt'] = datetime.now(timezone.utc).isoformat()
|
state['updatedAt'] = datetime.now(timezone.utc).isoformat()
|
||||||
path.parent.mkdir(exist_ok=True)
|
path.parent.mkdir(exist_ok=True)
|
||||||
temporary = None
|
temporary = None
|
||||||
@@ -99,7 +146,7 @@ def main():
|
|||||||
parser.add_argument('--pointer', action='store_true')
|
parser.add_argument('--pointer', action='store_true')
|
||||||
parser.add_argument('--reset', action='store_true')
|
parser.add_argument('--reset', action='store_true')
|
||||||
parser.add_argument('--goal')
|
parser.add_argument('--goal')
|
||||||
for field in ('decisions', 'changed', 'verified', 'open', 'risks'):
|
for field in ('decisions', 'changed', 'verified', 'open', 'risks', 'verification'):
|
||||||
parser.add_argument('--' + field, nargs='*')
|
parser.add_argument('--' + field, nargs='*')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.hook:
|
if args.hook:
|
||||||
@@ -111,11 +158,14 @@ def main():
|
|||||||
if not isinstance(values, dict):
|
if not isinstance(values, dict):
|
||||||
raise ValueError('Invalid task state update')
|
raise ValueError('Invalid task state update')
|
||||||
for field, value in values.items():
|
for field, value in values.items():
|
||||||
if field not in ('workspace', 'goal', 'decisions', 'changed', 'verified', 'open', 'risks'):
|
if field not in ('workspace', 'goal', 'decisions', 'changed', 'verified', 'open', 'risks', 'verification'):
|
||||||
raise ValueError('Invalid task state field')
|
raise ValueError('Invalid task state field')
|
||||||
if field in ('workspace', 'goal'):
|
if field in ('workspace', 'goal'):
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
raise ValueError('Invalid task state value')
|
raise ValueError('Invalid task state value')
|
||||||
|
elif field == 'verification':
|
||||||
|
if not isinstance(value, list) or any(not isinstance(item, dict) or not isinstance(item.get('check'), str) or not isinstance(item.get('result'), str) for item in value):
|
||||||
|
raise ValueError('Invalid verification list')
|
||||||
elif not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
elif not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
||||||
raise ValueError('Invalid task state list')
|
raise ValueError('Invalid task state list')
|
||||||
setattr(args, field, value)
|
setattr(args, field, value)
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
mcp>=1.9,<2
|
||||||
|
sentence-transformers>=4.1,<5
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Local MCP semantic search backed by Qwen3-Embedding-0.6B."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
MODEL = "Qwen/Qwen3-Embedding-0.6B"
|
||||||
|
TEXT_EXTENSIONS = {".c", ".cpp", ".cs", ".go", ".java", ".js", ".json", ".md", ".py", ".ps1", ".rs", ".sh", ".toml", ".ts", ".tsx", ".txt", ".yaml", ".yml"}
|
||||||
|
|
||||||
|
|
||||||
|
def chunks(path: Path):
|
||||||
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
for start in range(0, len(text), 1050):
|
||||||
|
value = text[start:start + 1200].strip()
|
||||||
|
if value:
|
||||||
|
yield value
|
||||||
|
|
||||||
|
|
||||||
|
class Index:
|
||||||
|
def __init__(self, root: Path, data: Path):
|
||||||
|
self.root = root.resolve()
|
||||||
|
self.data = data.resolve()
|
||||||
|
self.data.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.db = sqlite3.connect(self.data / "index.sqlite3")
|
||||||
|
self.db.execute("create table if not exists chunks (path text, text text, vector blob)")
|
||||||
|
self.db.commit()
|
||||||
|
self.model = None
|
||||||
|
|
||||||
|
def files(self):
|
||||||
|
ignored = {".git", ".venv", "generated", "node_modules", ".my-ai-configuration"}
|
||||||
|
result = []
|
||||||
|
for path in self.root.rglob("*"):
|
||||||
|
if len(result) >= 2000 or not path.is_file() or path.suffix.lower() not in TEXT_EXTENSIONS or path.stat().st_size > 524288:
|
||||||
|
continue
|
||||||
|
if ignored.intersection(path.relative_to(self.root).parts):
|
||||||
|
continue
|
||||||
|
result.append(path)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def encoder(self):
|
||||||
|
if self.model is None:
|
||||||
|
from sentence_transformers import SentenceTransformer
|
||||||
|
self.model = SentenceTransformer(MODEL)
|
||||||
|
return self.model
|
||||||
|
|
||||||
|
def rebuild(self):
|
||||||
|
encoder = self.encoder()
|
||||||
|
values = []
|
||||||
|
for path in self.files():
|
||||||
|
for value in chunks(path):
|
||||||
|
values.append((str(path.relative_to(self.root)), value))
|
||||||
|
self.db.execute("delete from chunks")
|
||||||
|
if values:
|
||||||
|
vectors = encoder.encode([value for _, value in values], normalize_embeddings=True)
|
||||||
|
self.db.executemany("insert into chunks values (?, ?, ?)", ((path, value, vector.tobytes()) for (path, value), vector in zip(values, vectors)))
|
||||||
|
self.db.commit()
|
||||||
|
return len(values)
|
||||||
|
|
||||||
|
def search(self, query: str, top_k: int):
|
||||||
|
if not query.strip():
|
||||||
|
raise ValueError("query must not be empty")
|
||||||
|
if self.db.execute("select count(*) from chunks").fetchone()[0] == 0:
|
||||||
|
self.rebuild()
|
||||||
|
vector = self.encoder().encode([query], normalize_embeddings=True)[0]
|
||||||
|
rows = []
|
||||||
|
for path, text, blob in self.db.execute("select path, text, vector from chunks"):
|
||||||
|
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))]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--root", default=os.getcwd(), type=Path)
|
||||||
|
parser.add_argument("--data-dir", required=True, type=Path)
|
||||||
|
parser.add_argument("--model-cache", required=True, type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.model_cache.mkdir(parents=True, exist_ok=True)
|
||||||
|
os.environ.setdefault("HF_HOME", str(args.model_cache.resolve()))
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
index = Index(args.root, args.data_dir)
|
||||||
|
mcp = FastMCP("my-ai-qwen3-retrieval")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def semantic_search(query: str, top_k: int = 5) -> list[dict]:
|
||||||
|
"""Find repository passages by meaning with Qwen3-Embedding-0.6B."""
|
||||||
|
return index.search(query, top_k)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def rebuild_index() -> str:
|
||||||
|
"""Rebuild the local repository embedding index."""
|
||||||
|
return json.dumps({"chunks": index.rebuild()})
|
||||||
|
|
||||||
|
mcp.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -19,6 +19,16 @@ Project instructions may specialize global defaults, but must not weaken securit
|
|||||||
* Ask about business behavior, user-visible behavior, UI, UX, APIs, database models, configuration formats, or compatibility behavior only when it is missing, ambiguous, contradictory, or open to interpretation; implement explicitly specified behavior without asking again.
|
* Ask about business behavior, user-visible behavior, UI, UX, APIs, database models, configuration formats, or compatibility behavior only when it is missing, ambiguous, contradictory, or open to interpretation; implement explicitly specified behavior without asking again.
|
||||||
* For deeper guidance on ambiguous requirements or a meaningful technical choice, consult `decision-rule.md` (Codex: read `rules/decision-rule.md`; Claude: invoke the `rules-decision-rule` skill).
|
* For deeper guidance on ambiguous requirements or a meaningful technical choice, consult `decision-rule.md` (Codex: read `rules/decision-rule.md`; Claude: invoke the `rules-decision-rule` skill).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
* 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.
|
||||||
|
* If retrieval is unavailable, fails, or adds no value, continue with ordinary repository tools. Report failures briefly; avoid repeated failing calls or installing or enabling retrieval without authorization.
|
||||||
|
|
||||||
|
* When QMD is available, use its `qmd` MCP tools for questions about indexed Markdown notes, documents, meeting records, or knowledge bases. Verify retrieved results against the source document when the current content matters.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
* Choose checks independently by behavior, callers, impact, reversibility, and uncertainty. Focused inspection can suffice; builds, tests, new tests, and separate reviews are not automatic.
|
* Choose checks independently by behavior, callers, impact, reversibility, and uncertainty. Focused inspection can suffice; builds, tests, new tests, and separate reviews are not automatic.
|
||||||
|
|||||||
Reference in new issue
Block a user