Compare commits

...
25 Commits
Author SHA1 Message Date
Julian lechner 381f99ea54 docs(readme): note preserved Codex settings
CI / linux (push) Failing after 4s
CI / compatibility-canary (push) Skipped
2026-09-17 11:47:24 +02:00
Julian lechner 8ab180c73a docs(configuration): describe status and local settings 2026-09-17 11:47:07 +02:00
Julian lechner 83401a4732 docs(plugins): document QMD integration 2026-09-17 11:46:36 +02:00
Julian lechner 4f6ac68e0c feat(statusline): show Codex permission mode 2026-09-17 11:46:06 +02:00
Julian lechner f07aaf8f63 feat(qmd): add managed extension integration 2026-09-17 11:45:41 +02:00
Julian lechner 1b64c54fda feat(instructions): guide semantic retrieval 2026-09-17 11:45:16 +02:00
Julian lechner 13594fb401 fix(update): preserve local model and effort 2026-09-17 11:45:00 +02:00
Julian lechner 7572ec40d9 fix(hooks): run Impeccable hook on Windows 2026-09-17 11:44:36 +02:00
Julian lechner 79d981e78b fix(retrieval): order Claude MCP arguments 2026-09-17 09:21:45 +02:00
Julian lechner b67eb844bc docs(retrieval): document optional local layer 2026-09-17 09:17:46 +02:00
Julian lechner aaf3a183f4 feat(install): reconcile retrieval runtime 2026-09-17 09:17:20 +02:00
Julian lechner 27f1c1ec79 feat(retrieval): add optional Qwen semantic layer 2026-09-17 09:16:53 +02:00
Julian lechner bfbe001248 feat(selection): persist retrieval preference 2026-09-17 09:16:05 +02:00
Julian lechner 151306a6cc fix(capabilities): check shell and tool requirements 2026-09-17 09:15:37 +02:00
Julian lechner 0079fe2b92 docs(ci): document and run capability checks 2026-09-17 08:43:21 +02:00
Julian lechner 5c23f21586 feat(capabilities): add strict shell checks 2026-09-17 08:43:12 +02:00
Julian lechner bdf98d5b71 feat(telemetry): add local agent efficiency metrics 2026-09-17 08:32:39 +02:00
Julian lechner 4fa8708ba7 feat(session): bind verification to git state 2026-09-17 08:32:28 +02:00
Julian lechner be87628d67 feat(plugins): record resolved extension revisions 2026-09-17 08:32:17 +02:00
Julian lechner 8ae8e7a173 feat(review): standardize finding scope and severity 2026-09-17 08:32:08 +02:00
Julian lechner ea2c80817b feat(agents): generate capabilities and rule loading 2026-09-17 08:31:58 +02:00
Julian lechner 3fad860256 ci: cover bash and powershell on linux 2026-09-17 08:31:47 +02:00
Julian lechner 2c3e63ed29 feat(prompt-budget): enforce absolute context caps 2026-09-17 08:31:38 +02:00
Julian lechner 1d77feeb71 fix(plugins): correct impeccable selector 2026-09-17 08:31:28 +02:00
Julian lechner 5ef3bc5525 docs(readme): use clone in quickstart 2026-09-17 08:31:17 +02:00
53 changed files with 900 additions and 167 deletions

No files matched your search

+2 -2
View File
@@ -7,7 +7,7 @@
{
"type": "command",
"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,
"statusMessage": "Checking UI changes"
}
@@ -20,7 +20,7 @@
{
"type": "command",
"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,
"statusMessage": "Design deep pass"
}
+54 -50
View File
@@ -12,20 +12,47 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Install current Codex CLI for schema validation
run: npm install --global @openai/codex@latest
- name: Check Linux tooling
run: |
command -v pwsh >/dev/null || { echo 'pwsh is required on the Linux runner'; exit 1; }
- name: Bash syntax
run: find scripts shared/hooks -name '*.sh' -print0 | xargs -0 -n1 bash -n
- name: Capability checks
run: ./scripts/check-capabilities.sh
- name: Build
run: REQUIRE_CODEX_SCHEMA=true ./scripts/build.sh --summary
run: ./scripts/build.sh --summary
- name: Prompt budget
run: ./scripts/prompt-budget.sh --summary
- name: Repository context cache
run: ./scripts/repository-context.sh --summary
- name: Doctor
run: ./scripts/doctor.sh --summary
- name: Platform package tests
run: ./scripts/test-platform-packages.sh --summary
- name: PowerShell syntax
run: |
find scripts shared/hooks -name '*.ps1' -print0 | xargs -0 -n1 pwsh -NoProfile -Command '$tokens=$null; $errors=$null; [System.Management.Automation.Language.Parser]::ParseFile($args[0],[ref]$tokens,[ref]$errors) | Out-Null; if ($errors.Count) { $errors | % { Write-Error $_ }; exit 1 }'
- name: PowerShell build and validation
shell: pwsh
run: |
$env:REQUIRE_CODEX_SCHEMA = 'true'
./scripts/check-capabilities.ps1
./scripts/build.ps1 -Summary
./scripts/doctor.ps1 -Summary
./scripts/test-shell-packages.ps1 -Summary
./scripts/test-managed-install.ps1 -Summary
./scripts/test-build-validation.ps1 -Summary
./scripts/test-install-guards.ps1 -Summary
./scripts/test-plugin-selection.ps1 -Summary
./scripts/test-quick-update.ps1 -Summary
python ./scripts/test-install-options.py
python ./scripts/test-quick-update.py
python ./scripts/test-managed-extensions.py
python ./scripts/test-session-state.py
python ./scripts/test-telemetry.py
python ./scripts/test-semantic-retrieval.py
./scripts/install.ps1 -Summary -DryRun -Client Both -Shell PowerShell
./scripts/update.ps1 -Summary -DryRun -Client Both -Shell PowerShell
- name: Shell package tests
run: ./scripts/test-shell-packages.sh --summary
- name: Managed manifest tests
run: ./scripts/test-managed-install.sh --summary
- name: Build validation tests
@@ -34,6 +61,16 @@ jobs:
run: ./scripts/test-install-guards.sh --summary
- name: Plugin selection tests
run: ./scripts/test-plugin-selection.sh --summary
- name: Quick-update and install-option tests
run: |
./scripts/test-quick-update.sh --summary
python3 scripts/test-install-options.py
python3 scripts/test-telemetry.py
python3 scripts/test-semantic-retrieval.py
- name: Managed extension and session-state tests
run: |
python3 scripts/test-managed-extensions.py
python3 scripts/test-session-state.py
- name: Install dry run
run: |
export HOME="$RUNNER_TEMP/isolated-home"
@@ -45,48 +82,15 @@ jobs:
mkdir -p "$HOME"
./scripts/update.sh --summary --dry-run --client both --platform linux
# windows:
# runs-on: windows-latest
# steps:
# - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# - name: Install current Codex CLI for schema validation
# shell: powershell
# run: npm install --global @openai/codex@latest
# - name: PowerShell syntax
# shell: powershell
# run: |
# $errors = @()
# Get-ChildItem scripts,shared/hooks -Recurse -Filter *.ps1 | ForEach-Object {
# $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
compatibility-canary:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Latest external clients
run: npm install --global @openai/codex@latest
- name: Latest-client schema and dry runs
run: |
REQUIRE_CODEX_SCHEMA=true ./scripts/build.sh --summary
HOME="$RUNNER_TEMP/canary-home" ./scripts/install.sh --summary --dry-run --client both --platform linux
HOME="$RUNNER_TEMP/canary-home" ./scripts/update.sh --summary --dry-run --client both --platform linux
+35 -14
View File
@@ -11,15 +11,32 @@
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.
## 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
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.
PowerShell:
```powershell
git pull --ff-only
git clone https://git.lechner-systems.at/fraujulian/MyAiConfiguration.git
cd MyAiConfiguration
.\scripts\build.ps1 -Summary
.\scripts\install.ps1 -Summary
```
@@ -27,7 +44,8 @@ git pull --ff-only
Bash:
```bash
git pull --ff-only
git clone https://git.lechner-systems.at/fraujulian/MyAiConfiguration.git
cd MyAiConfiguration
./scripts/build.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.
- 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.
- 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
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
@@ -125,10 +145,9 @@ generated/
- 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`.
- 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.
- The Codex and Claude Code CLIs for the clients that should be installed or diagnosed.
- Network access for plugin installation and updates.
- Python `venv` and `pip` when the optional semantic retrieval layer is enabled.
- 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;
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
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
not generally merged. Dry runs still rebuild `generated/`, but do not modify installation destinations.
Local Codex `model`, `reasoning_effort`, `plugins`, and `marketplaces` settings are preserved when syncing `config.toml`;
other local settings are not generally merged. Dry runs still rebuild `generated/`, but do not modify installation destinations.
Selecting Codex also installs shared skills to `.agents/skills`. An interactive extension list includes plugins and
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
without updating CLIs or extensions. See [Plugins](docs/plugins.md) for client-specific behavior.
the selected client CLIs, enable Flashbang, and enable Qwen3 semantic retrieval. These choices are saved for Quickupdate.
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
@@ -267,8 +287,9 @@ and uncertainty. A small change may need only focused inspection; builds, tests,
automatic. Explicit user and project requirements still apply.
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
items and rendering. See [Configuration](docs/configuration.md) for details and limitations.
Codex also shows its active permission profile and approval mode. Claude uses two colored lines, with context usage
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
+6
View File
@@ -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
1 role tools
2 architect Read,Search,WebFetch
3 researcher Read,Search,WebFetch
4 reviewer Read,Diff,Search
5 verifier Read,Shell
6 implementer Read,Write,Shell,Tests
+1 -1
View File
@@ -3,7 +3,7 @@ approvals_reviewer = "auto_review"
sandbox_mode = "workspace-write"
[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]
enabled = true
+2 -1
View File
@@ -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 - -
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 - -
QMD tobi/qmd qmd@qmd - - qmd @tobilu/qmd qmd
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
1 name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill
4 Superpowers - superpowers@claude-plugins-official openai-curated-remote superpowers@openai-curated-remote plugin - -
5 Context7 upstash/context7 context7@context7-marketplace upstash/context7 context7@context7-marketplace plugin - -
6 Caveman JuliusBrussee/caveman caveman@caveman thinkhome-org/caveman-codex caveman@thinkhome-caveman plugin - -
7 QMD tobi/qmd qmd@qmd - - qmd @tobilu/qmd qmd
8 Humanizer blader/humanizer humanizer@humanizer - - skill blader/humanizer humanizer
9 Impeccable pbakaus/impeccable impeccable@skill impeccable@impeccable pbakaus/impeccable impeccable skill pbakaus/impeccable impeccable
10 Anthropic Frontend Design anthropics/claude-plugins-official frontend-design@claude-plugins-official - - skill anthropics/skills frontend-design
+1 -1
View File
@@ -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}
+2 -2
View File
@@ -1,2 +1,2 @@
Claude 5030
Codex 4828
Claude 5030 6500
Codex 4828 6500
1 Claude 5030 6500
2 Codex 4828 6500
+4 -2
View File
@@ -16,7 +16,7 @@ Codex sets `approval_policy = "on-request"`, `approvals_reviewer = "auto_review"
## 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:
@@ -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 `-`.
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
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).
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
View File
@@ -1,8 +1,8 @@
# 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.`).
@@ -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.
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.
+20 -20
View File
@@ -6,6 +6,7 @@ $shared = Join-Path $root 'shared'
$output = Join-Path $root 'generated'
$pluginManifest = Join-Path $root 'adapters/plugins.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" }
$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 $capabilityManifest)) { throw "Missing capability manifest: $capabilityManifest" }
$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.' }
$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)" }
$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
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:
- 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.
__CODEX_RULE_LOADING__
'@
$codexRuleLoading = $codexRuleLoading.Replace('__CODEX_RULE_LOADING__', $codexRuleLoadingList)
foreach ($shell in @('powershell','bash')) {
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') {
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 {
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
View File
@@ -13,6 +13,7 @@ shared="$root/shared"
output="$root/generated"
plugin_manifest="$root/adapters/plugins.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; }
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; }
[ -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
rule_skill_files=()
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_names+=("$skill_name")
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; }
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() {
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.
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
codex_rule_loading="${codex_rule_loading}"$'\n\n'"${codex_rule_loading_list%$'\n'}"
for shell in powershell bash; do
mkdir -p "$output/codex-$shell" "$output/claude-$shell"
@@ -178,7 +174,8 @@ for client in codex claude; do
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"
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
done
done
+19
View File
@@ -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'
+28
View File
@@ -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
View File
@@ -12,6 +12,7 @@ $root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
. (Join-Path $PSScriptRoot 'lib/install-options.ps1')
. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1')
$Shell = Read-InstallShell -Shell $Shell
$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
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' })) }
exit 0
+2
View File
@@ -21,6 +21,7 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/install-targets.sh"
. "$root/scripts/lib/selection-state.sh"
. "$root/scripts/lib/install-options.sh"
. "$root/scripts/lib/semantic-retrieval.sh"
shell=$(read_install_shell "$shell")
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")
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 [ "$summary" = true ]; then
+2 -1
View File
@@ -15,7 +15,7 @@ function Read-InstallBoolean {
function Read-InstallOptions {
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')) {
try { $defaults = Read-UpdateSelection -HomePath $HomePath -RepositoryRoot $RepositoryRoot -AllowLegacy }
catch { Write-Warning 'Saved options could not be read; confirm new options below.' }
@@ -24,6 +24,7 @@ function Read-InstallOptions {
return @{
update_agents = Read-InstallBoolean -Prompt "Update selected agent CLIs ($Client)?" -Default $defaults.update_agents
flashbang = Read-InstallBoolean -Prompt 'Enable the Flashbang notification hook?' -Default $defaults.flashbang
semantic_retrieval = Read-InstallBoolean -Prompt 'Enable Qwen3-Embedding-0.6B semantic retrieval?' -Default $defaults.semantic_retrieval
}
}
+3 -1
View File
@@ -18,12 +18,13 @@ read_install_boolean() {
read_install_options() {
update_agents=false
flashbang=true
semantic_retrieval=false
local saved key value
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
while IFS=$'\t' read -r key value; do
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"
else
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
update_agents=$(read_install_boolean "Update selected agent CLIs ($client)?" "$update_agents") || return
flashbang=$(read_install_boolean 'Enable the Flashbang notification hook?' "$flashbang") || return
semantic_retrieval=$(read_install_boolean 'Enable Qwen3-Embedding-0.6B semantic retrieval?' "$semantic_retrieval") || return
}
is_windows_host() {
+62 -6
View File
@@ -137,6 +137,17 @@ def fetch_skill(entry):
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:
def __init__(self, home, dry_run=False, update=False, summary=False):
self.home = Path(home).absolute()
@@ -155,7 +166,7 @@ class Manager:
raise ValueError('Invalid extension ownership ledger.')
identities = set()
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.')
if not isinstance(resource.get('name'), str) or not resource['name']:
raise ValueError('Invalid extension ownership name.')
@@ -166,17 +177,23 @@ class Manager:
if resource['kind'] == 'plugin':
if not re.fullmatch(r'[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+', resource.get('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', '')
if not re.fullmatch(r'\.agents/skills/[A-Za-z0-9_-]+', directory) or resource['client'] != 'codex':
raise ValueError('Invalid owned skill directory.')
safe_path(self.home, directory)
if not isinstance(resource.get('files'), dict):
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():
if not relative.startswith(directory + '/') or not re.fullmatch(r'[a-f0-9]{64}', value):
raise ValueError('Invalid owned skill file record.')
safe_path(self.home, relative)
elif resource.get('package') != '@tobilu/qmd' or resource['client'] != 'codex':
raise ValueError('Invalid owned QMD record.')
def save(self):
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]
self.command(arguments)
self.cache[client].pop(selector, None)
else:
elif record['kind'] == 'skill':
empty_candidates = set()
for relative, expected in list(record['files'].items()):
path = safe_path(self.home, relative)
@@ -239,10 +256,15 @@ class Manager:
pass
if record['files']:
return
else:
self.command([record['client'], 'mcp', 'remove', 'qmd'] + (['--scope', 'user'] if record['client'] == 'claude' else []))
self.state['resources'].remove(record)
self.save()
def ensure(self, client, entry):
if entry['codex_method'] == 'qmd':
self.ensure_qmd(client, entry)
return
skill = client == 'codex' and entry['codex_method'] != '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)
@@ -271,11 +293,17 @@ class Manager:
if item is not None and not self.update:
if client == 'claude' and item.get('enabled') is False:
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
if item is not None and client == 'claude':
self.command([client, 'plugin', 'update', selector])
if item.get('enabled') is False:
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
marketplace = entry[client + '_marketplace']
if marketplace not in ('', '-', 'openai-curated-remote'):
@@ -287,11 +315,35 @@ class Manager:
self.command([client, 'plugin', 'marketplace', 'add', marketplace])
arguments = [client, 'plugin', 'install', selector, '--scope', 'user'] if client == 'claude' else [client, 'plugin', 'add', selector]
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:
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.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):
skill = entry['codex_skill']
if not re.fullmatch(r'[A-Za-z0-9_-]+', skill):
@@ -311,12 +363,16 @@ class Manager:
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']:
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:
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.save()
record['complete'] = False
if revision:
record['resolvedRevision'] = revision
self.save()
for name, (content, executable) in files.items():
relative = directory + '/' + name
+11
View File
@@ -86,6 +86,17 @@ function Sync-ManagedDestination {
if ($relative -eq 'config.toml' -and (Test-Path -LiteralPath $target)) {
$localConfig = [System.IO.File]::ReadAllText($target)
$pluginTables = [regex]::Matches($localConfig, '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])[\s\S]*?(?=^[ \t]*\[|\z)')
$rootConfig = [regex]::Split($localConfig, '(?m)^[ \t]*\[', 2)[0]
$localSettings = [regex]::Matches($rootConfig, '(?m)^[ \t]*(?:model|reasoning_effort)[ \t]*=.*$')
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) {
$content = [System.Text.Encoding]::UTF8.GetString($newBytes).TrimEnd([char[]]"`r`n")
if ($content -match '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])') {
+15 -1
View File
@@ -84,7 +84,21 @@ sync_managed_destination() {
new_hash=$(sha256_of_file "$source_file")
fi
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 '
/^[ \t]*\[/ { preserve = ($0 ~ /^[ \t]*\[(plugins|marketplaces)(\.|\])/) }
preserve { print }
+3 -1
View File
@@ -97,7 +97,9 @@ function Select-ConfiguredPlugins {
$checked = @($toggleable | ForEach-Object {
if ($Mode -eq 'Install') { $true; return }
$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)))
} else { [bool](@($installed | Where-Object { $_.$idField -eq $selector }).Count) }
})
+3 -1
View File
@@ -109,7 +109,9 @@ select_configured_plugins() {
continue
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
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
+2 -2
View File
@@ -9,8 +9,8 @@ function Read-UpdateSelection {
function Save-UpdateSelection {
param([string]$HomePath, [string]$RepositoryRoot, [string]$Shell, [string]$Client, [hashtable]$Plugins,
[Parameter(Mandatory=$true)][bool]$UpdateAgents, [Parameter(Mandatory=$true)][bool]$Flashbang)
$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')
[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(), '--semantic-retrieval', $SemanticRetrieval.ToString().ToLowerInvariant(), '--selected')
$arguments += @($Plugins.Selected | ForEach-Object { $_.name })
$arguments += '--deselected'
$arguments += @($Plugins.Deselected | ForEach-Object { $_.name })
+6 -2
View File
@@ -18,7 +18,9 @@ def validate(state, allow_legacy=False):
raise ValueError('Unsupported saved selection.')
if state.get('shell') not in ('powershell', 'bash') or state.get('client') not in ('codex', 'claude', 'both'):
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.')
for key in ('selected', 'deselected'):
values = state.get(key)
@@ -41,6 +43,7 @@ def main():
parser.add_argument('--allow-legacy', action='store_true')
parser.add_argument('--update-agents', 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('--selected', nargs='*', default=[])
parser.add_argument('--deselected', nargs='*', default=[])
@@ -59,7 +62,7 @@ def main():
else:
print('shell\t' + state['shell'])
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())
for key in ('selected', 'deselected'):
for name in state[key]:
@@ -68,6 +71,7 @@ def main():
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',
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))
if (set(state['selected']) | set(state['deselected'])) - names:
raise ValueError('Unknown plugin in selection.')
+2 -1
View File
@@ -12,6 +12,7 @@ read_update_selection() {
client) [ -n "$client" ] || client=$value ;;
update_agents) update_agents=$value ;;
flashbang) flashbang=$value ;;
semantic_retrieval) semantic_retrieval=$value ;;
selected) selected_plugins+=("$value") ;;
deselected) deselected_plugins+=("$value") ;;
esac
@@ -20,6 +21,6 @@ read_update_selection() {
save_update_selection() {
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[@]}"
}
+9
View File
@@ -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.' }
}
+148
View File
@@ -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)
+8
View File
@@ -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[@]}"
}
+12 -3
View File
@@ -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 }
$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
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 } } }
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 ($fail) { Write-Error 'Prompt budget: permanent context increased by more than 15 percent.'; exit 1 }
$baselineValues = @{}
if ($old) {
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
+12 -2
View File
@@ -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}'; }
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
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
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
claude_baseline=0; codex_baseline=0; claude_absolute=0; codex_absolute=0
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; }
exit 0
+1 -1
View File
@@ -6,7 +6,7 @@ $cacheDir = Join-Path $root '.ai-session'
$cachePath = Join-Path $cacheDir 'repository-context.json'
$safeRoot = $root.Replace('\','/')
$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)
$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('=')
+5 -3
View File
@@ -1,8 +1,10 @@
[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 }
try {
$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 'rotate') { if (Test-Path $path) { Move-Item $path ($path + '.1') -Force }; exit 0 }
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 '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 }
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))){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}
$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
View File
@@ -2,9 +2,39 @@
set -euo pipefail
[ "${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"
case "${1:-record}" in
action=${1:-record}; shift || true
case "$action" in
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;;
record) event=${2:?event required};; *) printf 'Unknown action: %s\n' "$1" >&2; exit 1;;
summary) [ -f "$path" ] || { printf 'Telemetry: no events\n'; exit 0; }; python3 - "$path" <<'PY' || exit 0
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
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
+1 -1
View File
@@ -184,7 +184,7 @@ class InstallOptionsTests(unittest.TestCase):
'select_configured_plugins() { :; }\nsync_configured_plugins() { :; }\n'
'run_plugin_command() { printf "CLI-MOCK %s\\n" "$2"; }\n')
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))
command = ([executable, '-NoProfile', '-File', entry, '-Shell', 'Bash', '-Client', 'Both', '-Summary']
if kind == 'powershell' else [executable, entry, '--shell', 'bash', '--client', 'both', '--summary'])
+22 -1
View File
@@ -40,7 +40,7 @@ class ManagedExtensionTests(unittest.TestCase):
def command(self, args, summary, home=None):
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'}
if args[2] in ('remove', 'uninstall'):
self.installed[args[0]].pop(args[3], None)
@@ -110,6 +110,13 @@ class ManagedExtensionTests(unittest.TestCase):
self.assertTrue((directory / 'foreign.txt').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):
original_write = extensions.atomic_write
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.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):
self.manager().sync([skill()], ['codex'], {'Humanizer'})
foreign = self.home / '.agents/skills/humanizer/foreign-empty'
+4 -4
View File
@@ -54,15 +54,15 @@ try {
[plugins."disabled@market"]
enabled = false
'@
[System.IO.File]::WriteAllText((Join-Path $source 'config.toml'), "model = 'new'`n")
[System.IO.File]::WriteAllText($configPath, "model = 'old'`n$pluginState`n[other]`nvalue = true`n")
[System.IO.File]::WriteAllText((Join-Path $source 'config.toml'), "model = 'new'`nreasoning_effort = 'low'`n")
[System.IO.File]::WriteAllText($configPath, "model = 'old'`nreasoning_effort = 'high'`n$pluginState`n[other]`nvalue = true`n")
$originalConfig = [System.IO.File]::ReadAllText($configPath)
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-dry' -DryRun
if ([System.IO.File]::ReadAllText($configPath) -ne $originalConfig) { throw 'Dry run must preserve plugin configuration.' }
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins'
$mergedConfig = [System.IO.File]::ReadAllText($configPath)
if (-not $mergedConfig.Contains($pluginState)) { 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 -replace "`r`n", "`n").Contains(($pluginState -replace "`r`n", "`n"))) { throw 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.' }
if (-not $mergedConfig.Contains("model = 'old'") -or -not $mergedConfig.Contains("reasoning_effort = 'high'") -or $mergedConfig.Contains("model = 'new'") -or $mergedConfig.Contains("reasoning_effort = 'low'") -or $mergedConfig.Contains('[other]')) { throw 'Updating config.toml must preserve local model and reasoning effort while updating managed configuration.' }
if ([System.IO.File]::ReadAllText((Join-Path $destination 'backups/plugins/config.toml')) -ne $originalConfig) { throw 'Original plugin configuration must be backed up.' }
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-repeat'
if ([System.IO.File]::ReadAllText($configPath) -ne $mergedConfig -or (Test-Path (Join-Path $destination 'backups/plugins-repeat'))) { throw 'Preserving plugin configuration must be idempotent.' }
+6 -4
View File
@@ -58,15 +58,17 @@ plugin_state=' [marketplaces.ponytail]
enabled = true
[plugins."disabled@market"]
enabled = false'
printf "model = 'new'\n" > "$source_dir/config.toml"
printf "model = 'old'\n%s\n[other]\nvalue = true\n" "$plugin_state" > "$destination/config.toml"
printf "model = 'new'\nreasoning_effort = 'low'\n" > "$source_dir/config.toml"
printf "model = 'old'\nreasoning_effort = 'high'\n%s\n[other]\nvalue = true\n" "$plugin_state" > "$destination/config.toml"
original_config=$(cat "$destination/config.toml")
sync_managed_destination "$source_dir" "$destination" plugins-dry "" "" "" true > /dev/null
[ "$(cat "$destination/config.toml")" = "$original_config" ] || { printf 'Dry run changed plugin configuration.\n' >&2; exit 1; }
sync_managed_destination "$source_dir" "$destination" plugins "" "" "" false > /dev/null
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" == *"model = 'new'"* && "$merged_config" != *'[other]'* ]] || { printf 'Managed configuration must still be updated.\n' >&2; exit 1; }
merged_config_normalized=${merged_config//$'\r'/}
plugin_state_normalized=${plugin_state//$'\r'/}
[[ "$merged_config_normalized" == *"$plugin_state_normalized"* ]] || { printf 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.\n' >&2; exit 1; }
[[ "$merged_config" == *"model = 'old'"* && "$merged_config" == *"reasoning_effort = 'high'"* && "$merged_config" != *"model = 'new'"* && "$merged_config" != *"reasoning_effort = 'low'"* && "$merged_config" != *'[other]'* ]] || { printf 'Updating config.toml must preserve local model and reasoning effort while updating managed configuration.\n' >&2; exit 1; }
[ "$(cat "$destination/backups/plugins/config.toml")" = "$original_config" ] || { printf 'Original plugin configuration must be backed up.\n' >&2; exit 1; }
sync_managed_destination "$source_dir" "$destination" plugins-repeat "" "" "" false > /dev/null
[ "$(cat "$destination/config.toml")" = "$merged_config" ] && [ ! -d "$destination/backups/plugins-repeat" ] || { printf 'Preserving plugin configuration must be idempotent.\n' >&2; exit 1; }
+8
View File
@@ -71,6 +71,14 @@ class SelectionTests(unittest.TestCase):
self.path.write_text(json.dumps(state))
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):
self.save('--selected', 'Superpowers')
original = self.path.read_bytes()
+56
View File
@@ -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()
+21
View File
@@ -17,6 +17,27 @@ SHELLS = [
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):
with tempfile.TemporaryDirectory() as directory:
workspace = Path(directory)
+9 -1
View File
@@ -22,8 +22,16 @@ foreach ($shell in @('powershell','bash')) {
$package = Join-Path $root "generated/$client-$shell"
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
$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 (@(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 }
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' }
@@ -33,7 +41,7 @@ foreach ($shell in @('powershell','bash')) {
$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 ($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) {
throw "Claude finish hook is incorrect in $package"
} elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') {
+11 -1
View File
@@ -31,7 +31,17 @@ for shell in powershell bash; do
file=config.toml
[ "$client" != claude ] || file=settings.json
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"
if [ "$client" = claude ]; then grep -q '^tools: Read,Diff,Search$' "$package/agents/reviewer.md"; fi
expected_skill_count=$source_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"
@@ -46,7 +56,7 @@ for shell in powershell bash; do
! grep -q 'flashbang-if-input' "$package/$file"
! grep -Eq '^async[[:space:]]*=[[:space:]]*true[[:space:]]*$' "$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
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"
+37
View 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
View File
@@ -13,6 +13,7 @@ $root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
. (Join-Path $PSScriptRoot 'lib/install-options.ps1')
. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1')
$homePath = [Environment]::GetFolderPath('UserProfile')
if ($Quick) {
@@ -65,6 +66,7 @@ try {
$script:PluginNonInteractive = $previousPluginNonInteractive
$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' })) }
exit 0
+2
View File
@@ -23,6 +23,7 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/install-targets.sh"
. "$root/scripts/lib/selection-state.sh"
. "$root/scripts/lib/install-options.sh"
. "$root/scripts/lib/semantic-retrieval.sh"
home_path=${HOME:?HOME is required}
selected_plugins=()
@@ -71,6 +72,7 @@ if [ "$quick" = true ]; then
else
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" true selected_plugins "$summary"
fi
sync_semantic_retrieval "$root" "$client" "$home_path" "$semantic_retrieval" "$dry_run" true
if [ "$dry_run" = false ]; then save_update_selection; fi
if [ "$summary" = true ]; then
+2
View File
@@ -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.
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.
+5 -1
View File
@@ -2,7 +2,11 @@
param()
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$stateDir = Join-Path $root '.ai-session'
if ($env:AI_CONFIG_TELEMETRY -ne '0') {
New-Item $stateDir -ItemType Directory -Force | Out-Null
('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content (Join-Path $stateDir 'telemetry.jsonl')
$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
exit $LASTEXITCODE
+1 -2
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
mkdir -p "$root/.ai-session"
printf '{"event":"compact","timestamp":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$root/.ai-session/telemetry.jsonl"
[ "${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"; }
python3 "$root/hooks/scripts/session-state.py" --hook
+53 -3
View File
@@ -4,6 +4,8 @@ import os
from pathlib import Path
import sys
import tempfile
import subprocess
from urllib.parse import urlsplit, urlunsplit
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.')
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():
try:
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 {}
if not isinstance(state, dict):
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)
if state.get('workspace', workspace) != workspace:
raise ValueError('Task state belongs to another workspace')
for old, new in (('changedFiles', 'changed'), ('pending', 'open'), ('importantFindings', 'decisions')):
if old in state:
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:
continue
value = state[field]
if field == 'goal':
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:
valid = isinstance(value, list) and all(isinstance(item, str) for item in value)
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:
raise ValueError('A different goal requires reset')
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'):
value = getattr(args, field)
if value is not None:
state[field] = value
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()
path.parent.mkdir(exist_ok=True)
temporary = None
@@ -99,7 +146,7 @@ def main():
parser.add_argument('--pointer', action='store_true')
parser.add_argument('--reset', action='store_true')
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='*')
args = parser.parse_args()
if args.hook:
@@ -111,11 +158,14 @@ def main():
if not isinstance(values, dict):
raise ValueError('Invalid task state update')
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')
if field in ('workspace', 'goal'):
if not isinstance(value, str):
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):
raise ValueError('Invalid task state list')
setattr(args, field, value)
+2
View File
@@ -0,0 +1,2 @@
mcp>=1.9,<2
sentence-transformers>=4.1,<5
+106
View File
@@ -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()
+10
View File
@@ -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.
* 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
* Choose checks independently by behavior, callers, impact, reversibility, and uncertainty. Focused inspection can suffice; builds, tests, new tests, and separate reviews are not automatic.