Compare commits

...
7 Commits
Author SHA1 Message Date
fraujulian 023d1610ba docs: reorganize configuration guides
CI / Capabilities and shell quality (push) Successful in 21s
CI / Python tests and compilation (push) Failing after 35s
CI / Build and inspect generated packages (push) Skipped
CI / Bash integration and installation guards (push) Skipped
CI / Installed client configuration smoke tests (push) Skipped
CI / Qwen retrieval integration (push) Skipped
CI / PowerShell tests on Ubuntu (push) Failing after 14s
CI / CI configuration security checks (push) Successful in 4s
2026-09-18 11:38:30 +02:00
fraujulian 145169dea8 fix(hooks): use relocated build commands 2026-09-18 11:38:09 +02:00
fraujulian 117cdb33ab ci: expand workflow coverage 2026-09-18 11:37:50 +02:00
fraujulian 61ed36560f fix(claude): require sandbox availability 2026-09-18 11:37:29 +02:00
fraujulian ef084f6fef feat(retrieval): offer device benchmark 2026-09-18 11:37:09 +02:00
fraujulian c37ffe4c66 feat(extensions): manage MCPorter CLI 2026-09-18 11:36:39 +02:00
fraujulian 11c2b6dfcb refactor(scripts): organize script entry points 2026-09-18 11:35:38 +02:00
61 changed files with 652 additions and 321 deletions

No files matched your search

+181 -27
View File
@@ -1,4 +1,4 @@
name: CI name: CI
on: on:
push: push:
@@ -6,40 +6,194 @@ on:
pull_request: pull_request:
schedule: schedule:
- cron: '23 4 * * 1' - cron: '23 4 * * 1'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
PYTHONUNBUFFERED: '1'
PIP_DISABLE_PIP_VERSION_CHECK: '1'
jobs: jobs:
linux: capabilities:
name: Capabilities and shell quality
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Bash syntax - name: Install shell tools
shell: bash run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends shellcheck jq
- name: Check capabilities
run: bash scripts/commands/check-capabilities.sh
- name: Validate Bash syntax
run: git ls-files -z '*.sh' | xargs -0 -r -n1 bash -n run: git ls-files -z '*.sh' | xargs -0 -r -n1 bash -n
- name: PowerShell checks when runtime exists - name: Lint Bash
shell: bash run: shellcheck --severity=warning $(git ls-files '*.sh')
python:
name: Python tests and compilation
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Compile Python sources
run: python -m compileall -q scripts shared
- name: Run Python tests
run: | run: |
if ! command -v pwsh >/dev/null 2>&1; then set -euo pipefail
echo '::warning::PowerShell unavailable; PowerShell checks skipped.' for file in scripts/tests/test-*.py; do
exit 0 echo "==> $file"
fi python "$file"
for file in $(git ls-files '*.ps1'); do
pwsh -NoLogo -NoProfile -NonInteractive -Command "\$tokens=\$null; \$errors=\$null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path '$file'),[ref]\$tokens,[ref]\$errors)|Out-Null; if(\$errors.Count){\$errors|%{Write-Error \"$file: \$_\"}; exit 1}"
done done
- name: Build - name: Validate generated configuration
shell: bash run: python scripts/checks/validate-config.py generated/codex-powershell/config.toml generated/claude-powershell/settings.json
run: bash scripts/build.sh --summary
- name: Bash install dry run powershell-on-ubuntu:
shell: bash name: PowerShell tests on Ubuntu
run: bash scripts/install.sh --summary --dry-run --client both --shell bash runs-on: ubuntu-latest
- name: Bash update dry run timeout-minutes: 20
shell: bash steps:
run: bash scripts/update.sh --summary --dry-run --client both --shell bash - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: PowerShell install and update dry runs - name: Check PowerShell runtime
shell: bash shell: bash
run: | run: |
if ! command -v pwsh >/dev/null 2>&1; then if command -v pwsh >/dev/null 2>&1; then
echo '::warning::PowerShell unavailable; PowerShell dry runs skipped.' echo 'POWERSHELL_AVAILABLE=true' >> "$GITHUB_ENV"
exit 0 exit 0
fi fi
pwsh -NoLogo -NoProfile -NonInteractive -File ./scripts/install.ps1 -Summary -DryRun -Client Both -Shell PowerShell if sudo apt-get update && sudo apt-get install --yes --no-install-recommends powershell; then
pwsh -NoLogo -NoProfile -NonInteractive -File ./scripts/update.ps1 -Summary -DryRun -Client Both -Shell PowerShell echo 'POWERSHELL_AVAILABLE=true' >> "$GITHUB_ENV"
else
echo 'POWERSHELL_AVAILABLE=false' >> "$GITHUB_ENV"
echo '::warning::PowerShell is unavailable; PowerShell checks skipped for this runner.'
fi
- name: Run PowerShell tests
shell: bash
run: |
if [ "${POWERSHELL_AVAILABLE:-false}" != true ]; then exit 0; fi
pwsh -NoLogo -NoProfile -NonInteractive -File ./scripts/commands/check-capabilities.ps1
for file in scripts/tests/test-*.ps1; do
echo "==> $file"
pwsh -NoLogo -NoProfile -NonInteractive -File "$file" -Summary
done
build:
name: Build and inspect generated packages
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [capabilities, python]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build packages
env:
REQUIRE_CODEX_SCHEMA: 'false'
run: bash scripts/commands/build.sh --summary
- name: Run build validation
run: bash scripts/tests/test-build-validation.sh --summary
- name: Check generated files are reproducible
run: |
before=$(find generated -type f -print0 | sort -z | xargs -0 sha256sum)
bash scripts/commands/build.sh --summary
after=$(find generated -type f -print0 | sort -z | xargs -0 sha256sum)
test "$before" = "$after"
shell-integration:
name: Bash integration and installation guards
runs-on: ubuntu-latest
timeout-minutes: 25
needs: [build]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Run integration tests
run: |
set -euo pipefail
bash scripts/tests/test-install-guards.sh --summary
bash scripts/tests/test-plugin-selection.sh --summary
bash scripts/tests/test-quick-update.sh --summary
bash scripts/tests/test-managed-install.sh --summary
bash scripts/tests/test-shell-packages.sh --summary
- name: Run doctor and prompt budget checks
run: |
bash scripts/commands/doctor.sh --summary
bash scripts/commands/prompt-budget.sh --summary
- name: Verify dry-run installation safety
run: |
bash scripts/commands/install.sh --summary --dry-run --client both --shell bash
bash scripts/commands/update.sh --summary --dry-run --client both --shell bash
client-smoke:
name: Installed client configuration smoke tests
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [build]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v22.16.0
with:
node-version: '24'
- name: Install current client CLIs
run: npm install --global @openai/codex @anthropic-ai/claude-code
- name: Validate current Codex and Claude settings
run: |
set -euo pipefail
schema_home=$(mktemp -d)
cp generated/codex-powershell/config.toml "$schema_home/config.toml"
CODEX_HOME="$schema_home" codex --strict-config --help >/dev/null
claude --settings generated/claude-powershell/settings.json --help >/dev/null
retrieval:
name: Qwen retrieval integration
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [python]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
cache: pip
cache-dependency-path: shared/retrieval/requirements.txt
- name: Install retrieval dependencies
run: python -m pip install -r shared/retrieval/requirements.txt
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .cache/huggingface
key: qwen-retrieval-${{ runner.os }}-${{ hashFiles('shared/retrieval/requirements.txt') }}
- name: Run retrieval tests with real models
env:
QWEN_TEST_MODEL_CACHE: ${{ github.workspace }}/.cache/huggingface
run: python scripts/tests/test-retrieval-server.py
security:
name: CI configuration security checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Reject unsafe workflow settings
run: |
set -euo pipefail
! grep -RInE 'runs-on:[[:space:]]*windows|pull_request_target|permissions:[[:space:]]*write-all|actions/checkout@v|actions/setup-python@v|curl[[:space:]].*\|[[:space:]]*(bash|sh)|wget[[:space:]].*\|[[:space:]]*(bash|sh)' .github/workflows
test "$(grep -Roh 'runs-on: ubuntu-latest' .github/workflows | wc -l)" -ge 1
test "$(grep -Roh 'uses: actions/[^@]*@[0-9a-f]\{40\}' .github/workflows | wc -l)" -ge 1
- name: Check for tracked secrets
run: |
set -euo pipefail
if git ls-files | grep -E '(^|/)(\.env($|\.)|.*\.(pem|key))$'; then
echo 'Potential secret file or private key tracked' >&2
exit 1
fi
if git ls-files -z | xargs -0 -r grep -IlE 'BEGIN (RSA|OPENSSH|EC) PRIVATE KEY'; then
echo 'Potential private key material tracked' >&2
exit 1
fi
+38 -18
View File
@@ -31,7 +31,7 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install --yes --no-install-recommends shellcheck jq sudo apt-get install --yes --no-install-recommends shellcheck jq
- name: Check capabilities - name: Check capabilities
run: bash scripts/check-capabilities.sh run: bash scripts/commands/check-capabilities.sh
- name: Validate Bash syntax - name: Validate Bash syntax
run: git ls-files -z '*.sh' | xargs -0 -r -n1 bash -n run: git ls-files -z '*.sh' | xargs -0 -r -n1 bash -n
- name: Lint Bash - name: Lint Bash
@@ -51,12 +51,12 @@ jobs:
- name: Run Python tests - name: Run Python tests
run: | run: |
set -euo pipefail set -euo pipefail
for file in scripts/test-*.py; do for file in scripts/tests/test-*.py; do
echo "==> $file" echo "==> $file"
python "$file" python "$file"
done done
- name: Validate generated configuration - name: Validate generated configuration
run: python scripts/validate-config.py generated/codex-powershell/config.toml generated/claude-powershell/settings.json run: python scripts/checks/validate-config.py generated/codex-powershell/config.toml generated/claude-powershell/settings.json
powershell-on-ubuntu: powershell-on-ubuntu:
name: PowerShell tests on Ubuntu name: PowerShell tests on Ubuntu
@@ -81,8 +81,8 @@ jobs:
shell: bash shell: bash
run: | run: |
if [ "${POWERSHELL_AVAILABLE:-false}" != true ]; then exit 0; fi if [ "${POWERSHELL_AVAILABLE:-false}" != true ]; then exit 0; fi
pwsh -NoLogo -NoProfile -NonInteractive -File ./scripts/check-capabilities.ps1 pwsh -NoLogo -NoProfile -NonInteractive -File ./scripts/commands/check-capabilities.ps1
for file in scripts/test-*.ps1; do for file in scripts/tests/test-*.ps1; do
echo "==> $file" echo "==> $file"
pwsh -NoLogo -NoProfile -NonInteractive -File "$file" -Summary pwsh -NoLogo -NoProfile -NonInteractive -File "$file" -Summary
done done
@@ -97,13 +97,13 @@ jobs:
- name: Build packages - name: Build packages
env: env:
REQUIRE_CODEX_SCHEMA: 'false' REQUIRE_CODEX_SCHEMA: 'false'
run: bash scripts/build.sh --summary run: bash scripts/commands/build.sh --summary
- name: Run build validation - name: Run build validation
run: bash scripts/test-build-validation.sh --summary run: bash scripts/tests/test-build-validation.sh --summary
- name: Check generated files are reproducible - name: Check generated files are reproducible
run: | run: |
before=$(find generated -type f -print0 | sort -z | xargs -0 sha256sum) before=$(find generated -type f -print0 | sort -z | xargs -0 sha256sum)
bash scripts/build.sh --summary bash scripts/commands/build.sh --summary
after=$(find generated -type f -print0 | sort -z | xargs -0 sha256sum) after=$(find generated -type f -print0 | sort -z | xargs -0 sha256sum)
test "$before" = "$after" test "$before" = "$after"
@@ -117,19 +117,39 @@ jobs:
- name: Run integration tests - name: Run integration tests
run: | run: |
set -euo pipefail set -euo pipefail
bash scripts/test-install-guards.sh --summary bash scripts/tests/test-install-guards.sh --summary
bash scripts/test-plugin-selection.sh --summary bash scripts/tests/test-plugin-selection.sh --summary
bash scripts/test-quick-update.sh --summary bash scripts/tests/test-quick-update.sh --summary
bash scripts/test-managed-install.sh --summary bash scripts/tests/test-managed-install.sh --summary
bash scripts/test-shell-packages.sh --summary bash scripts/tests/test-shell-packages.sh --summary
- name: Run doctor and prompt budget checks - name: Run doctor and prompt budget checks
run: | run: |
bash scripts/doctor.sh --summary bash scripts/commands/doctor.sh --summary
bash scripts/prompt-budget.sh --summary bash scripts/commands/prompt-budget.sh --summary
- name: Verify dry-run installation safety - name: Verify dry-run installation safety
run: | run: |
bash scripts/install.sh --summary --dry-run --client both --shell bash bash scripts/commands/install.sh --summary --dry-run --client both --shell bash
bash scripts/update.sh --summary --dry-run --client both --shell bash bash scripts/commands/update.sh --summary --dry-run --client both --shell bash
client-smoke:
name: Installed client configuration smoke tests
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [build]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v22.16.0
with:
node-version: '24'
- name: Install current client CLIs
run: npm install --global @openai/codex @anthropic-ai/claude-code
- name: Validate current Codex and Claude settings
run: |
set -euo pipefail
schema_home=$(mktemp -d)
cp generated/codex-powershell/config.toml "$schema_home/config.toml"
CODEX_HOME="$schema_home" codex --strict-config --help >/dev/null
claude --settings generated/claude-powershell/settings.json --help >/dev/null
retrieval: retrieval:
name: Qwen retrieval integration name: Qwen retrieval integration
@@ -152,7 +172,7 @@ jobs:
- name: Run retrieval tests with real models - name: Run retrieval tests with real models
env: env:
QWEN_TEST_MODEL_CACHE: ${{ github.workspace }}/.cache/huggingface QWEN_TEST_MODEL_CACHE: ${{ github.workspace }}/.cache/huggingface
run: python scripts/test-retrieval-server.py run: python scripts/tests/test-retrieval-server.py
security: security:
name: CI configuration security checks name: CI configuration security checks
+3 -3
View File
@@ -36,8 +36,8 @@ Write all repository content, generated instructions, scripts, prompts, document
## Workflow ## Workflow
- Choose the smallest sufficient verification based on the change's behavior, affected consumers, risk, and remaining uncertainty. Inspection may suffice for low-risk changes; full builds, test suites, and independent reviews are not automatic. - Choose the smallest sufficient verification based on the change's behavior, affected consumers, risk, and remaining uncertainty. Inspection may suffice for low-risk changes; full builds, test suites, and independent reviews are not automatic.
- Run `scripts/build.ps1 -Summary` to regenerate packages when shared definitions, adapters, or generation logic change. This generates configuration packages; it does not require building applications that use them. - Run `scripts/commands/build.ps1 -Summary` to regenerate packages when shared definitions, adapters, or generation logic change. This generates configuration packages; it does not require building applications that use them.
- Run `scripts/doctor.ps1 -Summary` when installation, client configuration, hooks, or environment changes need its checks. Changes to instruction wording alone do not require it. - Run `scripts/commands/doctor.ps1 -Summary` when installation, client configuration, hooks, or environment changes need its checks. Changes to instruction wording alone do not require it.
- Run focused script tests when script behavior changes. Broaden or repeat checks only when failures, affected dependencies, or unresolved risks justify it. - Run focused script tests when script behavior changes. Broaden or repeat checks only when failures, affected dependencies, or unresolved risks justify it.
- Preserve backward compatibility where practical. - Preserve backward compatibility where practical.
- Keep changes focused on the requested configuration behavior. - Keep changes focused on the requested configuration behavior.
@@ -52,7 +52,7 @@ Write all repository content, generated instructions, scripts, prompts, document
## Installation and security ## Installation and security
- Do not install global configuration automatically. - Do not install global configuration automatically.
- Perform global installation only after explicit user instruction, using `scripts/install.ps1` so replaced managed files are backed up. - Perform global installation only after explicit user instruction, using `scripts/commands/install.ps1` so replaced managed files are backed up.
- Do not add project-specific rules, secrets, credentials, authentication state, or generated runtime state. - Do not add project-specific rules, secrets, credentials, authentication state, or generated runtime state.
## Version control ## Version control
+60 -131
View File
@@ -18,27 +18,27 @@ Check the selected shell's minimum version and required Python and `jq` tools be
PowerShell: PowerShell:
```powershell ```powershell
.\scripts\check-capabilities.ps1 .\scripts\commands\check-capabilities.ps1
``` ```
Bash: Bash:
```bash ```bash
./scripts/check-capabilities.sh ./scripts/commands/check-capabilities.sh
``` ```
## Quickstart ## Quickstart
With the [requirements](#requirements) installed, clone the repository, build it, then install it. With the [requirements](#requirements) installed, clone the repository, build it, then install it.
Stop if any command fails. Installation asks for the shell, client, CLI update, Flashbang, and extension choices. Stop if any command fails. Installation asks for the shell, client, CLI update, Flashbang, Qwen retrieval, and extension choices.
PowerShell: PowerShell:
```powershell ```powershell
git clone https://git.lechner-systems.at/fraujulian/MyAiConfiguration.git git clone https://git.lechner-systems.at/fraujulian/MyAiConfiguration.git
cd MyAiConfiguration cd MyAiConfiguration
.\scripts\build.ps1 -Summary .\scripts\commands\build.ps1 -Summary
.\scripts\install.ps1 -Summary .\scripts\commands\install.ps1 -Summary
``` ```
Bash: Bash:
@@ -46,55 +46,40 @@ Bash:
```bash ```bash
git clone https://git.lechner-systems.at/fraujulian/MyAiConfiguration.git git clone https://git.lechner-systems.at/fraujulian/MyAiConfiguration.git
cd MyAiConfiguration cd MyAiConfiguration
./scripts/build.sh --summary ./scripts/commands/build.sh --summary
./scripts/install.sh --summary ./scripts/commands/install.sh --summary
``` ```
For an already-installed configuration, use Quickupdate instead. For an already-installed configuration, use Quickupdate instead.
## Quickupdate ## Quickupdate
Run these commands from the repository root. Quick reuses the shell, client, CLI update, Flashbang, and extension choices from the last Run these commands from the repository root. Quick reuses the shell, client, CLI update, Flashbang, retrieval, and extension choices from the last
successful installation or update without opening selection menus. Stop if any command fails. successful installation or update without opening selection menus. Stop if any command fails.
PowerShell: PowerShell:
```powershell ```powershell
git pull --ff-only git pull --ff-only
.\scripts\build.ps1 -Summary .\scripts\commands\build.ps1 -Summary
.\scripts\update.ps1 -Quick -Summary .\scripts\commands\update.ps1 -Quick -Summary
``` ```
Bash: Bash:
```bash ```bash
git pull --ff-only git pull --ff-only
./scripts/build.sh --summary ./scripts/commands/build.sh --summary
./scripts/update.sh --quick --summary ./scripts/commands/update.sh --quick --summary
``` ```
If no selection has been saved yet, or it predates the CLI update and Flashbang options, Quick stops with a hint. Run `scripts/update.ps1` or `./scripts/update.sh` once If no selection has been saved yet, or it predates a required option, Quick stops with a hint. Run `scripts/commands/update.ps1` or `./scripts/commands/update.sh` once
without Quick to save your choices. They are stored in `~/.my-ai-configuration/selection.json`; dry runs and failed without Quick to save your choices. They are stored in `~/.my-ai-configuration/selection.json`; dry runs and failed
runs do not overwrite them. To change the selection later, run a normal update again. Quick does not automatically runs do not overwrite them. To change the selection later, run a normal update again. Quick does not automatically
select newly added plugins. select newly added plugins.
Install and update also rebuild internally; the explicit build above lets you check the packages before installation. Install and update also rebuild internally; the explicit build above lets you check the packages before installation.
## Documentation
| Document | Contents |
| --- | --- |
| [Architecture](docs/architecture.md) | Shared sources, adapters, generated packages, and installation. |
| [Configuration](docs/configuration.md) | Client settings, permission defaults, status displays, and verification. |
| [Plugins](docs/plugins.md) | Plugin sources, selection, installation, updates, and removal. |
| [Hooks](docs/hooks.md) | Registered events, session-state helpers, notifications, and optional utilities. |
| [Agents](docs/agents.md) | Agent roles and when delegation is useful. |
| [Skills](docs/skills.md) | Skill categories and conditional rule loading. |
| [Codex adapter](adapters/codex/README.md) | Codex-specific configuration files. |
| [Claude Code adapter](adapters/claude/README.md) | Claude-specific configuration files. |
| [Hook package](shared/hooks/README.md) | Overview shipped with the hook scripts. |
| [Repository instructions](AI-Instructions.md) | Rules for maintaining this repository. |
## Features ## Features
- One shared source of truth for Codex and Claude Code. - One shared source of truth for Codex and Claude Code.
@@ -113,10 +98,22 @@ Install and update also rebuild internally; the explicit build above lets you ch
never installed. never installed.
- User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, QMD, Humanizer, Impeccable, and - User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, QMD, Humanizer, Impeccable, and
Anthropic Frontend Design, with an interactive extension toggle during both install and update. Only extensions Anthropic Frontend Design, with an interactive extension toggle during both install and update. Only extensions
integrations installed and recorded by this setup can be removed through deselection. installed and recorded by this setup can be removed through deselection.
- Optional local semantic retrieval for both clients using Qwen3-Embedding-0.6B embeddings and Qwen3-Reranker-0.6B - Optional local semantic retrieval for both clients using Qwen3-Embedding-0.6B embeddings and Qwen3-Reranker-0.6B
reranking, disabled by default and removable through the install/update choice. reranking, disabled by default and removable through the install/update choice.
## Requirements
- 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).
- `jq` for Claude's Bash status line; without it, that status line produces no output.
- Python `venv` and `pip` when the optional semantic retrieval layer is enabled.
- Node.js 22 or newer and `npm` when QMD or MCPorter is selected.
The Bash scripts do not require PowerShell. The PowerShell scripts remain compatible with PowerShell 5.1;
PowerShell 7 can also run them.
## Architecture ## Architecture
```text ```text
@@ -128,7 +125,7 @@ shared definitions -> client adapters -> generated packages -> user installation
| `shared/` | Client-independent rules, agents, skills, hooks, status lines, and global instructions. | | `shared/` | Client-independent rules, agents, skills, hooks, status lines, and global instructions. |
| `adapters/` | Codex and Claude Code formats, settings, templates, and the plugin manifest. | | `adapters/` | Codex and Claude Code formats, settings, templates, and the plugin manifest. |
| `generated/` | Reproducible PowerShell and Bash packages. This directory is ignored by Git. | | `generated/` | Reproducible PowerShell and Bash packages. This directory is ignored by Git. |
| `scripts/` | Build, installation, update, diagnosis, and shell validation entry points. | | [`scripts/`](docs/scripts.md) | Build, installation, update, diagnosis, and validation scripts. |
| `docs/` | Focused documentation about the configuration design. | | `docs/` | Focused documentation about the configuration design. |
The build creates: The build creates:
@@ -141,37 +138,42 @@ generated/
`-- claude-bash/ `-- claude-bash/
``` ```
## Requirements ## Documentation
- PowerShell 5.1 or newer; PowerShell 7 is supported as well. | Document | Contents |
- 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). | [Architecture](docs/architecture.md) | Shared sources, adapters, generated packages, and installation. |
- `jq` for Claude's Bash status line; without it, that status line produces no output. | [Scripts](docs/scripts.md) | Commands, checks, tests, and internal script layout. |
- Python `venv` and `pip` when the optional semantic retrieval layer is enabled. | [Configuration](docs/configuration.md) | Client settings, permission defaults, status displays, and verification. |
- Node.js 22 or newer and `npm` when QMD is selected. | [Extending the configuration](docs/extending-configuration.md) | Add shared rules, skills, and agents. |
| [Plugins](docs/plugins.md) | Plugin sources, selection, installation, updates, and removal. |
| [Hooks](docs/hooks.md) | Registered events, session-state helpers, notifications, and optional utilities. |
| [Agents](docs/agents.md) | Agent roles and when delegation is useful. |
| [Skills](docs/skills.md) | Skill categories and conditional rule loading. |
| [Codex adapter](adapters/codex/README.md) | Codex-specific configuration files. |
| [Claude Code adapter](adapters/claude/README.md) | Claude-specific configuration files. |
| [Hook package](shared/hooks/README.md) | Overview shipped with the hook scripts. |
| [Repository instructions](AI-Instructions.md) | Rules for maintaining this repository. |
The Bash scripts do not require PowerShell. The PowerShell scripts remain compatible with PowerShell 5.1; ## Build Script
PowerShell 7 can also run them.
## Build
Run the matching command from the repository root: Run the matching command from the repository root:
PowerShell: PowerShell:
```powershell ```powershell
.\scripts\build.ps1 .\scripts\commands\build.ps1
``` ```
Bash: Bash:
```bash ```bash
./scripts/build.sh ./scripts/commands/build.sh
``` ```
Rebuild when shared definitions, adapters, or generation logic change. Choose other verification according to the Rebuild when shared definitions, adapters, or generation logic change. Choose other verification according to the
change's risk; documentation-only edits do not require a build. Building writes reproducible packages below change's risk; documentation-only edits do not require a build. Building writes reproducible packages below
`generated/` and may use temporary validation files; it does not install global user configuration. `generated/` and may use temporary validation files; it does not install global user configuration.
## Installation ## Installation Script
The installer asks for the target shell and client, unless they are passed as parameters, then refuses to continue The installer asks for the target shell and client, unless they are passed as parameters, then refuses to continue
if any selected destination is already installed (`Already installed, use the update script.`). Selecting PowerShell or if any selected destination is already installed (`Already installed, use the update script.`). Selecting PowerShell or
@@ -182,24 +184,24 @@ Preview an installation without changing user files, backups, or plugins:
PowerShell: PowerShell:
```powershell ```powershell
.\scripts\install.ps1 -DryRun .\scripts\commands\install.ps1 -DryRun
``` ```
Bash: Bash:
```bash ```bash
./scripts/install.sh --dry-run ./scripts/commands/install.sh --dry-run
``` ```
Install the selected configuration interactively: Install the selected configuration interactively:
PowerShell: PowerShell:
```powershell ```powershell
.\scripts\install.ps1 .\scripts\commands\install.ps1
``` ```
Bash: Bash:
```bash ```bash
./scripts/install.sh ./scripts/commands/install.sh
``` ```
Omitting either target parameter opens its selection prompt. There is no automatic shell default. Omitting either target parameter opens its selection prompt. There is no automatic shell default.
@@ -230,7 +232,7 @@ not (for example, installed through a native installer or another package manage
in place with a warning, and only a fresh `npm install -g` is run alongside it. Codex is skipped with a warning while in place with a warning, and only a fresh `npm install -g` is run alongside it. Codex is skipped with a warning while
`codex.exe` is running. A dry run never asks this question and never previews it, since the choice is not persisted. `codex.exe` is running. A dry run never asks this question and never previews it, since the choice is not persisted.
## Update ## Update Script
Once a destination is installed, use the update script (not the installer) to refresh its files and plugins. It refuses Once a destination is installed, use the update script (not the installer) to refresh its files and plugins. It refuses
to continue if any selected destination is not installed yet (`Not installed, use the install script.`), also skipped to continue if any selected destination is not installed yet (`Not installed, use the install script.`), also skipped
@@ -238,20 +240,20 @@ for a dry run.
PowerShell: PowerShell:
```powershell ```powershell
.\scripts\update.ps1 .\scripts\commands\update.ps1
``` ```
Bash: Bash:
```bash ```bash
./scripts/update.sh ./scripts/commands/update.sh
``` ```
It accepts the same `-DryRun`/`--dry-run`, `-Client`/`--client`, and `-Shell`/`--shell` parameters as the It accepts the same `-DryRun`/`--dry-run`, `-Client`/`--client`, and `-Shell`/`--shell` parameters as the
installer, re-syncs every managed file, and lets you revise the CLI update, Flashbang, and extension choices. installer, re-syncs every managed file, and lets you revise the CLI update, Flashbang, retrieval, and extension choices.
Selected extensions are installed or updated. Deselection removes only extensions recorded as installed by this Selected extensions are installed or updated. Deselection removes only extensions recorded as installed by this
setup; pre-existing or manually installed extensions are retained. Quickupdate reuses the saved choices. setup; pre-existing or manually installed extensions are retained. Quickupdate reuses the saved choices.
## Uninstall ## Uninstall Script
Remove everything this setup previously installed for the selected client(s): managed files tracked in each Remove everything this setup previously installed for the selected client(s): managed files tracked in each
destination's manifest, extensions and skills recorded in the extension ledger, an enabled semantic retrieval setup, destination's manifest, extensions and skills recorded in the extension ledger, an enabled semantic retrieval setup,
@@ -259,12 +261,12 @@ and the saved Quickupdate selection (once no destination remains installed for a
PowerShell: PowerShell:
```powershell ```powershell
.\scripts\uninstall.ps1 .\scripts\commands\uninstall.ps1
``` ```
Bash: Bash:
```bash ```bash
./scripts/uninstall.sh ./scripts/commands/uninstall.sh
``` ```
Omitting `-Client`/`--client` opens the same selection prompt as install and update. Uninstall refuses to continue Omitting `-Client`/`--client` opens the same selection prompt as install and update. Uninstall refuses to continue
@@ -287,84 +289,11 @@ is only removed once uninstalling leaves no client installed at all, so uninstal
Uninstall does not remove the CLI tools themselves (`codex`, `claude`), any plugin marketplace registrations they Uninstall does not remove the CLI tools themselves (`codex`, `claude`), any plugin marketplace registrations they
keep outside this setup's ledger, or files this setup never installed. keep outside this setup's ledger, or files this setup never installed.
## Doctor ## Configuration overview
Check client availability, generated packages, source directories, hooks, and installed managed files: Codex and Claude Code receive the same shared rules, skills, agents, hooks, and status data. Codex loads applicable rules from generated files, while Claude Code exposes technology- and situation-specific rules as generated skills.
PowerShell: The default configuration keeps writes workspace-scoped and enables automatic review for eligible escalation requests. Both clients include the same logical agent roles and compact status displays. See [Configuration](docs/configuration.md) for settings, permissions, and limitations.
```powershell
.\scripts\doctor.ps1
```
Bash:
```bash
./scripts/doctor.sh
```
Use `-Summary` (PowerShell) or `--summary` (Bash) with build, doctor, install, update, and test scripts for compact output.
Omit the flag for detailed output. Summary mode retains warnings and failure status, but can suppress captured
plugin-command output on failure. Set `AI_CONFIG_VERBOSE=1` when full plugin failure output is needed.
## Configuration
Codex receives global instructions, rules, skills, agent TOML files, hooks, and `config.toml`. Its defaults keep writes
workspace-scoped and use automatic review for eligible escalation requests. Every rule ships as a plain file under
`rules/`, and `AGENTS.md` tells Codex to load the matching one by path.
Claude Code receives the equivalent global instructions, agents, hooks, and `settings.json`. General rules are embedded
once in `CLAUDE.md` (and in Codex's `AGENTS.md`); every technology- or
situation-specific rule (`adapters/rule-skills.tsv`) is generated as a skill under `skills/rules/` instead, so
only its name and description are permanently visible and the full rule text loads only when Claude invokes it. Its
generated settings use the supported automatic permission mode where available.
Both clients receive the same logical agent roles:
- Architect
- Implementer
- Researcher
- Reviewer
- Verifier
Agent use is proportional to the task. Small changes can stay with the implementer, while additional roles are available
when research, design, independent review, or focused verification adds value.
Verification is also proportional: the agent selects checks based on changed behavior, affected callers, risk,
and uncertainty. A small change may need only focused inspection; builds, tests, and separate reviews are not
automatic. Explicit user and project requirements still apply.
Both status displays include model, effort, repository, branch, context-window size, context used, and tokens used.
Codex shows its review mode. Claude also shows its review mode in two colored lines, with context usage
turning yellow at 60% and red at 85%. See
[Configuration](docs/configuration.md) for details and limitations.
## Extending the Configuration
### Add a rule
Create a focused Markdown file in `shared/rules/`. For a rule that applies only to a language, framework, tool, or
change area, add one row to `adapters/rule-skills.tsv`. The build uses that manifest to generate both Claude's
rule skill and Codex's rule-loading condition. Put always-applicable guidance in `general.md`, which the build embeds
for both clients; a new rule file is not automatically embedded.
### Add a skill
Create `shared/skills/<category>/<skill>/SKILL.md` with valid YAML frontmatter and a concise workflow. Reference shared
rules instead of repeating persistent standards.
### Add an agent
Create `shared/agents/<agent>/agent.yml` and `instructions.md`. Keep semantic behavior in these shared files; the build
renders the Codex and Claude Code formats.
Rebuild after every semantic change.
## Security
- Never store API keys, OAuth tokens, credentials, authentication state, or sensitive logs in this repository.
- Review generated output and use the installer's dry-run before installation.
- Global installation happens only through an explicit installer invocation.
- Plugins execute with permissions granted by their client; review their upstream sources and trust prompts.
- Codex keeps workspace sandboxing enabled and requires escalation outside expected development boundaries.
## Support ## Support
+2 -1
View File
@@ -13,7 +13,8 @@
}, },
"sandbox": { "sandbox": {
"enabled": true, "enabled": true,
"allowUnsandboxedCommands": false "allowUnsandboxedCommands": false,
"failIfUnavailable": true
}, },
"env": { "env": {
"CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY": "__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__" "CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY": "__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__"
+1
View File
@@ -5,6 +5,7 @@ Superpowers - superpowers@claude-plugins-official openai-curated-remote superpow
Context7 upstash/context7 context7@context7-marketplace upstash/context7 context7@context7-marketplace plugin - - Context7 upstash/context7 context7@context7-marketplace upstash/context7 context7@context7-marketplace plugin - -
Caveman JuliusBrussee/caveman caveman@caveman thinkhome-org/caveman-codex caveman@thinkhome-caveman plugin - - Caveman JuliusBrussee/caveman caveman@caveman thinkhome-org/caveman-codex caveman@thinkhome-caveman plugin - -
QMD tobi/qmd qmd@qmd - - qmd @tobilu/qmd qmd QMD tobi/qmd qmd@qmd - - qmd @tobilu/qmd qmd
MCPorter - mcporter - mcporter cli mcporter -
Humanizer blader/humanizer humanizer@humanizer - - skill blader/humanizer humanizer Humanizer blader/humanizer humanizer@humanizer - - skill blader/humanizer humanizer
Impeccable pbakaus/impeccable impeccable@impeccable pbakaus/impeccable impeccable skill pbakaus/impeccable impeccable Impeccable pbakaus/impeccable impeccable@impeccable pbakaus/impeccable impeccable skill pbakaus/impeccable impeccable
Anthropic Frontend Design anthropics/claude-plugins-official frontend-design@claude-plugins-official - - skill anthropics/skills frontend-design Anthropic Frontend Design anthropics/claude-plugins-official frontend-design@claude-plugins-official - - skill anthropics/skills frontend-design
1 name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill
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 MCPorter - mcporter - mcporter cli mcporter -
9 Humanizer blader/humanizer humanizer@humanizer - - skill blader/humanizer humanizer
10 Impeccable pbakaus/impeccable impeccable@impeccable pbakaus/impeccable impeccable skill pbakaus/impeccable impeccable
11 Anthropic Frontend Design anthropics/claude-plugins-official frontend-design@claude-plugins-official - - skill anthropics/skills frontend-design
+1 -1
View File
@@ -22,7 +22,7 @@ The shared [orchestration rule](../shared/rules/orchestration.md) defines the `R
For long-running work, the main agent maintains one compact state containing `goal`, `decisions`, `changed`, `verified`, `open`, and `risks`. An existing task-state facility is preferred; the file fallback is `.ai-session/state.json` in the current workspace. Subagents may read it when needed, but send updates to the parent. Keep unrelated concurrent tasks in separate checkouts, and keep runtime state out of commits. For long-running work, the main agent maintains one compact state containing `goal`, `decisions`, `changed`, `verified`, `open`, and `risks`. An existing task-state facility is preferred; the file fallback is `.ai-session/state.json` in the current workspace. Subagents may read it when needed, but send updates to the parent. Keep unrelated concurrent tasks in separate checkouts, and keep runtime state out of commits.
The optional `scripts/session-state.ps1` and `scripts/session-state.sh` helpers update this workspace file. Omitted fields are preserved. Their `-Workspace`/`--workspace` option selects the workspace explicitly; the default is the current working directory. Both accept goal, decisions, changed files, completed verification, open items, and risks. `-Pointer`/`--pointer` only prints the existing file's location. The optional `scripts/commands/session-state.ps1` and `scripts/commands/session-state.sh` helpers update this workspace file. Omitted fields are preserved. Their `-Workspace`/`--workspace` option selects the workspace explicitly; the default is the current working directory. Both accept goal, decisions, changed files, completed verification, open items, and risks. `-Pointer`/`--pointer` only prints the existing file's location.
Starting a different goal requires `-Reset -Goal ...` or `--reset --goal ...`, which clears the previous task's decisions and verification. Updates are atomic; legacy field names are migrated when reading existing state. Starting a different goal requires `-Reset -Goal ...` or `--reset --goal ...`, which clears the previous task's decisions and verification. Updates are atomic; legacy field names are migrated when reading existing state.
+15
View File
@@ -0,0 +1,15 @@
# Extending the configuration
Shared definitions are the source of truth. Rebuild after every semantic change.
## Add a rule
Create a focused Markdown file in `shared/rules/`. For a rule that applies only to a language, framework, tool, or change area, add one row to `adapters/rule-skills.tsv`. The build uses that manifest to generate both Claude's rule skill and Codex's rule-loading condition. Put always-applicable guidance in `general.md`, which the build embeds for both clients; a new rule file is not automatically embedded.
## Add a skill
Create `shared/skills/<category>/<skill>/SKILL.md` with valid YAML frontmatter and a concise workflow. Reference shared rules instead of repeating persistent standards.
## Add an agent
Create `shared/agents/<agent>/agent.yml` and `instructions.md`. Keep semantic behavior in these shared files; the build renders the Codex and Claude Code formats.
+66
View File
@@ -0,0 +1,66 @@
# Scripts
Run scripts from the repository root. PowerShell and Bash commands have equivalent behavior unless noted otherwise.
## Layout
| Directory | Purpose |
| --- | --- |
| `scripts/commands/` | User-facing build, installation, maintenance, and local utility commands. |
| `scripts/checks/` | Standalone configuration validation. |
| `scripts/tests/` | Repository verification scripts. |
| `scripts/lib/` | Internal helpers used by commands; do not invoke them directly. |
## Commands
| Script | Purpose |
| --- | --- |
| `commands/build.ps1`, `commands/build.sh` | Generate the four client packages in `generated/`. |
| `commands/check-capabilities.ps1`, `commands/check-capabilities.sh` | Check required tools and versions. |
| `commands/install.ps1`, `commands/install.sh` | Install a selected client configuration. |
| `commands/update.ps1`, `commands/update.sh` | Update an existing installation; Quick reuses saved choices. |
| `commands/uninstall.ps1`, `commands/uninstall.sh` | Remove setup-owned configuration and extensions. |
| `commands/doctor.ps1`, `commands/doctor.sh` | Check generated output and installed configuration. |
| `commands/prompt-budget.ps1`, `commands/prompt-budget.sh` | Check generated instruction sizes against their budget. |
| `commands/repository-context.ps1`, `commands/repository-context.sh` | Refresh the local repository-context cache. |
| `commands/session-state.ps1`, `commands/session-state.sh` | Read or update the local agent session state. |
| `commands/telemetry.ps1`, `commands/telemetry.sh` | Append a local telemetry event. |
Qwen retrieval uses `shared/retrieval/benchmark.py`; run it through `scripts/lib/semantic-retrieval.py benchmark --root . --home <home-directory>`.
## Checks and tests
| Script | Purpose |
| --- | --- |
| `checks/validate-config.py` | Validate generated Codex TOML and Claude JSON. |
| `tests/test-build-validation.ps1`, `tests/test-build-validation.sh` | Validate build failures and generated output. |
| `tests/test-install-guards.ps1`, `tests/test-install-guards.sh` | Validate install and update guard conditions. |
| `tests/test-install-options.py` | Validate interactive option handling and generated manifest changes. |
| `tests/test-managed-extensions.py` | Validate plugin, skill, and CLI ownership handling. |
| `tests/test-managed-install.ps1`, `tests/test-managed-install.sh` | Validate managed-file synchronization and backups. |
| `tests/test-plugin-selection.ps1`, `tests/test-plugin-selection.sh` | Validate plugin selection. |
| `tests/test-prompt-inventory.py` | Validate prompt inventory collection. |
| `tests/test-quick-update.py` | Validate saved Quickupdate choices. |
| `tests/test-quick-update.ps1`, `tests/test-quick-update.sh` | Run the Quickupdate test through the relevant shell. |
| `tests/test-retrieval-server.py` | Validate retrieval indexing, ranking, and optional real-model execution. |
| `tests/test-semantic-retrieval.py` | Validate retrieval installation and removal. |
| `tests/test-session-state.py` | Validate session-state helpers. |
| `tests/test-shell-packages.ps1`, `tests/test-shell-packages.sh` | Validate generated packages and shell entry points. |
| `tests/test-telemetry.py` | Validate telemetry output. |
## Internal helpers
| Script | Purpose |
| --- | --- |
| `lib/install-options.ps1`, `lib/install-options.sh` | Read installation options and calculate client-specific choices. |
| `lib/install-options.py` | Filter configuration templates according to installation options. |
| `lib/install-targets.ps1`, `lib/install-targets.sh` | Resolve configuration targets for the selected client and shell. |
| `lib/manifest.ps1`, `lib/manifest.sh` | Synchronize managed files and their ownership manifest. |
| `lib/plugins.ps1`, `lib/plugins.sh` | Select and install configured plugins. |
| `lib/managed-extensions.py` | Reconcile CLI extensions and MCPorter-managed plugins. |
| `lib/prompt-inventory.py` | Collect prompt files for the build. |
| `lib/selection-state.ps1`, `lib/selection-state.sh` | Persist choices used by Quickupdate. |
| `lib/selection-state.py` | Read and write Quickupdate state. |
| `lib/semantic-retrieval.ps1`, `lib/semantic-retrieval.sh` | Invoke semantic retrieval from the installation flow. |
| `lib/semantic-retrieval.py` | Install, index, benchmark, and remove the local retrieval service. |
| `shared/retrieval/benchmark.py` | Measure Qwen embedding and reranking speed for the Auto recommendation. |
@@ -11,7 +11,7 @@ from pathlib import Path
CLAUDE_TOP_LEVEL = {"permissions", "sandbox", "env", "statusLine", "hooks"} CLAUDE_TOP_LEVEL = {"permissions", "sandbox", "env", "statusLine", "hooks"}
CLAUDE_PERMISSION_KEYS = {"defaultMode", "allow", "deny"} CLAUDE_PERMISSION_KEYS = {"defaultMode", "allow", "deny"}
CLAUDE_SANDBOX_KEYS = {"enabled", "allowUnsandboxedCommands"} CLAUDE_SANDBOX_KEYS = {"enabled", "allowUnsandboxedCommands", "failIfUnavailable"}
CLAUDE_STATUS_LINE_KEYS = {"type", "command"} CLAUDE_STATUS_LINE_KEYS = {"type", "command"}
CLAUDE_HOOK_KEYS = {"type", "command", "timeout"} CLAUDE_HOOK_KEYS = {"type", "command", "timeout"}
CLAUDE_PERMISSION_MODES = {"acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"} CLAUDE_PERMISSION_MODES = {"acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"}
@@ -40,8 +40,8 @@ def validate_claude(path: Path) -> None:
if not isinstance(permissions.get(key), list) or not all(isinstance(item, str) for item in permissions[key]): if not isinstance(permissions.get(key), list) or not all(isinstance(item, str) for item in permissions[key]):
fail(f"permissions.{key} must be an array of strings") fail(f"permissions.{key} must be an array of strings")
sandbox = require_keys(settings.get("sandbox"), CLAUDE_SANDBOX_KEYS, "sandbox") sandbox = require_keys(settings.get("sandbox"), CLAUDE_SANDBOX_KEYS, "sandbox")
if sandbox.get("enabled") is not True or sandbox.get("allowUnsandboxedCommands") is not False: if sandbox.get("enabled") is not True or sandbox.get("allowUnsandboxedCommands") is not False or sandbox.get("failIfUnavailable") is not True:
fail("sandbox must be enabled without unsandboxed command retries") fail("sandbox must be enabled, fail closed, and disallow unsandboxed command retries")
env = settings.get("env") env = settings.get("env")
if not isinstance(env, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in env.items()): if not isinstance(env, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in env.items()):
fail("env must be an object of string values") fail("env must be an object of string values")
@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$shared = Join-Path $root 'shared' $shared = Join-Path $root 'shared'
$output = Join-Path $root 'generated' $output = Join-Path $root 'generated'
$pluginManifest = Join-Path $root 'adapters/plugins.tsv' $pluginManifest = Join-Path $root 'adapters/plugins.tsv'
@@ -202,7 +202,7 @@ foreach ($shell in @('powershell','bash')) {
$tomlPath = Join-Path $output "codex-$shell/config.toml" $tomlPath = Join-Path $output "codex-$shell/config.toml"
$settingsPath = Join-Path $output "claude-$shell/settings.json" $settingsPath = Join-Path $output "claude-$shell/settings.json"
& python (Join-Path $root 'scripts/validate-config.py') $tomlPath $settingsPath & python (Join-Path $root 'scripts/checks/validate-config.py') $tomlPath $settingsPath
if ($LASTEXITCODE -ne 0) { throw "Generated configuration validation failed for $shell." } if ($LASTEXITCODE -ne 0) { throw "Generated configuration validation failed for $shell." }
Test-CodexSchema $tomlPath Test-CodexSchema $tomlPath
} }
+3 -3
View File
@@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
shared="$root/shared" shared="$root/shared"
output="$root/generated" output="$root/generated"
plugin_manifest="$root/adapters/plugins.tsv" plugin_manifest="$root/adapters/plugins.tsv"
@@ -85,7 +85,7 @@ quote_toml() {
printf '"%s"' "$value" printf '"%s"' "$value"
} }
session_output=$(bash "$shared/hooks/scripts/test-session-config.sh" "$root") || { printf '%s\n' "$session_output" >&2; exit 1; } session_output=$(bash "$shared/hooks/scripts/tests/test-session-config.sh" "$root") || { printf '%s\n' "$session_output" >&2; exit 1; }
if [ "$summary" = true ]; then if [ "$summary" = true ]; then
printf '%s\n' "$session_output" | sed '/^PASS session configuration$/d' printf '%s\n' "$session_output" | sed '/^PASS session configuration$/d'
else else
@@ -213,7 +213,7 @@ done
toml_path="$output/codex-$shell/config.toml" toml_path="$output/codex-$shell/config.toml"
settings_path="$output/claude-$shell/settings.json" settings_path="$output/claude-$shell/settings.json"
python3 "$root/scripts/validate-config.py" "$toml_path" "$settings_path" python3 "$root/scripts/checks/validate-config.py" "$toml_path" "$settings_path"
if command -v codex >/dev/null; then if command -v codex >/dev/null; then
schema_home=$(mktemp -d) schema_home=$(mktemp -d)
cp "$toml_path" "$schema_home/config.toml" cp "$toml_path" "$schema_home/config.toml"
File renamed without changes.
View File
File renamed without changes.
@@ -1,8 +1,8 @@
[CmdletBinding()] [CmdletBinding()]
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $root 'scripts/lib/manifest.ps1')
$homePath = [Environment]::GetFolderPath('UserProfile') $homePath = [Environment]::GetFolderPath('UserProfile')
$script:fail = $false $script:fail = $false
$script:checkCount = 0 $script:checkCount = 0
+1 -1
View File
@@ -9,7 +9,7 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
home_path=${HOME:?HOME is required} home_path=${HOME:?HOME is required}
failed=false failed=false
@@ -6,13 +6,13 @@ param(
[switch]$Summary [switch]$Summary
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
. (Join-Path $PSScriptRoot 'lib/plugins.ps1') . (Join-Path $root 'scripts/lib/plugins.ps1')
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $root 'scripts/lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') . (Join-Path $root 'scripts/lib/install-targets.ps1')
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1') . (Join-Path $root 'scripts/lib/selection-state.ps1')
. (Join-Path $PSScriptRoot 'lib/install-options.ps1') . (Join-Path $root 'scripts/lib/install-options.ps1')
. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1') . (Join-Path $root 'scripts/lib/semantic-retrieval.ps1')
$Shell = Read-InstallShell -Shell $Shell $Shell = Read-InstallShell -Shell $Shell
$shell = $Shell.ToLowerInvariant() $shell = $Shell.ToLowerInvariant()
@@ -26,7 +26,7 @@ if (-not $DryRun -and (Test-AnyManifestPresent (Get-InstallDestinations -HomePat
$options = Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun $options = Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun
$reinstallViaNpm = Read-CliReinstallOption -Client $Client -DryRun:$DryRun $reinstallViaNpm = Read-CliReinstallOption -Client $Client -DryRun:$DryRun
$buildScript = Join-Path $PSScriptRoot 'build.ps1' $buildScript = Join-Path $root 'scripts/commands/build.ps1'
& $buildScript -Summary:$Summary & $buildScript -Summary:$Summary
if (-not $?) { throw 'Build failed. Installation was not started.' } if (-not $?) { throw 'Build failed. Installation was not started.' }
$generated = Join-Path $root 'generated' $generated = Join-Path $root 'generated'
+2 -2
View File
@@ -15,7 +15,7 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh" . "$root/scripts/lib/install-targets.sh"
@@ -40,7 +40,7 @@ reinstall_via_npm=$(read_cli_reinstall_option "$client" "$dry_run")
build_args=() build_args=()
[ "$summary" = false ] || build_args+=(--summary) [ "$summary" = false ] || build_args+=(--summary)
bash "$root/scripts/build.sh" "${build_args[@]}" bash "$root/scripts/commands/build.sh" "${build_args[@]}"
generated="$root/generated" generated="$root/generated"
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; } [ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$generated = Join-Path $root 'generated' $generated = Join-Path $root 'generated'
if (-not (Test-Path (Join-Path $generated 'claude-bash/CLAUDE.md'))) { throw 'Generated output is missing; run build first.' } if (-not (Test-Path (Join-Path $generated 'claude-bash/CLAUDE.md'))) { throw 'Generated output is missing; run build first.' }
function Measure-Tree([string]$path) { function Measure-Tree([string]$path) {
@@ -30,7 +30,7 @@ if ($old) {
} }
} }
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 ($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) } }
& python (Join-Path $PSScriptRoot 'lib/prompt-inventory.py') --home ([Environment]::GetFolderPath('UserProfile')) & python (Join-Path $root 'scripts/lib/prompt-inventory.py') --home ([Environment]::GetFolderPath('UserProfile'))
if ($LASTEXITCODE -ne 0) { throw 'Prompt inventory failed.' } if ($LASTEXITCODE -ne 0) { throw 'Prompt inventory failed.' }
if ($fail) { exit 1 } if ($fail) { exit 1 }
exit 0 exit 0
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
summary=false summary=false
for arg in "$@"; do case "$arg" in --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done for arg in "$@"; do case "$arg" in --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
generated="$root/generated" generated="$root/generated"
[ -f "$generated/claude-bash/CLAUDE.md" ] || { printf 'Generated output is missing; run build first.\n' >&2; exit 1; } [ -f "$generated/claude-bash/CLAUDE.md" ] || { printf 'Generated output is missing; run build first.\n' >&2; exit 1; }
bytes() { wc -c < "$1" | tr -d ' '; } bytes() { wc -c < "$1" | tr -d ' '; }
@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param([switch]$Force,[switch]$Summary) param([switch]$Force,[switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$cacheDir = Join-Path $root '.ai-session' $cacheDir = Join-Path $root '.ai-session'
$cachePath = Join-Path $cacheDir 'repository-context.json' $cachePath = Join-Path $cacheDir 'repository-context.json'
$safeRoot = $root.Replace('\','/') $safeRoot = $root.Replace('\','/')
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
force=false; summary=false force=false; summary=false
for arg in "$@"; do case "$arg" in --force) force=true;; --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done for arg in "$@"; do case "$arg" in --force) force=true;; --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); cache_dir="$root/.ai-session"; cache_path="$cache_dir/repository-context.json" root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd); cache_dir="$root/.ai-session"; cache_path="$cache_dir/repository-context.json"
head=$(git -c "safe.directory=$root" -C "$root" rev-parse HEAD 2>/dev/null || true); key_input=$head head=$(git -c "safe.directory=$root" -C "$root" rev-parse HEAD 2>/dev/null || true); key_input=$head
while IFS= read -r file; do key_input+="\n$file:$(sha256sum "$file" | awk '{print $1}')"; done < <(find "$root" -type f \( -name AI-Instructions.md -o -path '*/adapters/*' \) ! -path '*/generated/*' ! -path '*/.git/*' ! -path '*/.ai-session/*' | sort) while IFS= read -r file; do key_input+="\n$file:$(sha256sum "$file" | awk '{print $1}')"; done < <(find "$root" -type f \( -name AI-Instructions.md -o -path '*/adapters/*' \) ! -path '*/generated/*' ! -path '*/.git/*' ! -path '*/.ai-session/*' | sort)
key=$(printf '%s' "$key_input" | sha256sum | awk '{print $1}') key=$(printf '%s' "$key_input" | sha256sum | awk '{print $1}')
@@ -12,7 +12,7 @@ param(
[switch]$Reset [switch]$Reset
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$helper = Join-Path (Split-Path $PSScriptRoot -Parent) 'shared/hooks/scripts/session-state.py' $helper = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'shared/hooks/scripts/session-state.py'
$values = @{ workspace = $Workspace } $values = @{ workspace = $Workspace }
$fields = @{ Goal = 'goal'; Decisions = 'decisions'; ChangedFiles = 'changed'; Verified = 'verified'; Pending = 'open'; Risks = 'risks' } $fields = @{ Goal = 'goal'; Decisions = 'decisions'; ChangedFiles = 'changed'; Verified = 'verified'; Pending = 'open'; Risks = 'risks' }
foreach ($parameter in $fields.Keys) { foreach ($parameter in $fields.Keys) {
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
arguments=() arguments=()
if [ "${AI_TASK_GOAL+x}" = x ]; then arguments+=(--goal "$AI_TASK_GOAL"); fi if [ "${AI_TASK_GOAL+x}" = x ]; then arguments+=(--goal "$AI_TASK_GOAL"); fi
python3 "$root/shared/hooks/scripts/session-state.py" "${arguments[@]}" "$@" python3 "$root/shared/hooks/scripts/session-state.py" "${arguments[@]}" "$@"
@@ -2,7 +2,7 @@
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) param([ValidateSet('record','summary','rotate')][string]$Action='record',[string]$Event,[string]$Agent,[string]$Role,[Nullable[int]]$DurationMs,[Nullable[int]]$ToolCalls,[string]$CommandClass,[Nullable[int64]]$Bytes,[Nullable[int]]$Count,[Nullable[int]]$HandoffBytes,[Nullable[int]]$FilesRead,[Nullable[int]]$FilesChanged,[string]$Outcome,[Nullable[int]]$InputTokens,[Nullable[int]]$CachedTokens,[Nullable[int]]$OutputTokens,[Nullable[int]]$ReasoningTokens)
$ErrorActionPreference = 'Stop'; if ($env:AI_CONFIG_TELEMETRY -eq '0') { exit 0 } $ErrorActionPreference = 'Stop'; if ($env:AI_CONFIG_TELEMETRY -eq '0') { exit 0 }
try { 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 $root = Split-Path (Split-Path $PSScriptRoot -Parent) -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 '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 {} }); 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 ($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.' } if ([string]::IsNullOrWhiteSpace($Event)) { throw 'Event is required.' }
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
[ "${AI_CONFIG_TELEMETRY:-1}" = 0 ] && exit 0 [ "${AI_CONFIG_TELEMETRY:-1}" = 0 ] && exit 0
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); dir="$root/.ai-session"; path="$dir/telemetry.jsonl"; mkdir -p "$dir" root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd); dir="$root/.ai-session"; path="$dir/telemetry.jsonl"; mkdir -p "$dir"
action=${1:-record}; shift || true action=${1:-record}; shift || true
case "$action" in case "$action" in
rotate) [ -f "$path" ] && mv -f "$path" "$path.1"; exit 0;; rotate) [ -f "$path" ] && mv -f "$path" "$path.1"; exit 0;;
@@ -6,11 +6,11 @@ param(
[switch]$Summary [switch]$Summary
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
. (Join-Path $PSScriptRoot 'lib/plugins.ps1') . (Join-Path $root 'scripts/lib/plugins.ps1')
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $root 'scripts/lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') . (Join-Path $root 'scripts/lib/install-targets.ps1')
. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1') . (Join-Path $root 'scripts/lib/semantic-retrieval.ps1')
$Client = Read-InstallClient -Client $Client $Client = Read-InstallClient -Client $Client
$homePath = [Environment]::GetFolderPath('UserProfile') $homePath = [Environment]::GetFolderPath('UserProfile')
@@ -15,7 +15,7 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh" . "$root/scripts/lib/install-targets.sh"
@@ -7,13 +7,13 @@ param(
[switch]$Summary [switch]$Summary
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
. (Join-Path $PSScriptRoot 'lib/plugins.ps1') . (Join-Path $root 'scripts/lib/plugins.ps1')
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $root 'scripts/lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') . (Join-Path $root 'scripts/lib/install-targets.ps1')
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1') . (Join-Path $root 'scripts/lib/selection-state.ps1')
. (Join-Path $PSScriptRoot 'lib/install-options.ps1') . (Join-Path $root 'scripts/lib/install-options.ps1')
. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1') . (Join-Path $root 'scripts/lib/semantic-retrieval.ps1')
$homePath = [Environment]::GetFolderPath('UserProfile') $homePath = [Environment]::GetFolderPath('UserProfile')
if ($Quick) { if ($Quick) {
@@ -32,7 +32,7 @@ if (-not $DryRun -and -not (Test-AllManifestsPresent (Get-InstallDestinations -H
$options = if ($Quick) { $saved } else { Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun } $options = if ($Quick) { $saved } else { Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun }
$buildScript = Join-Path $PSScriptRoot 'build.ps1' $buildScript = Join-Path $root 'scripts/commands/build.ps1'
& $buildScript -Summary:$Summary & $buildScript -Summary:$Summary
if (-not $?) { throw 'Build failed. Update was not started.' } if (-not $?) { throw 'Build failed. Update was not started.' }
$generated = Join-Path $root 'generated' $generated = Join-Path $root 'generated'
+2 -2
View File
@@ -17,7 +17,7 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh" . "$root/scripts/lib/install-targets.sh"
@@ -45,7 +45,7 @@ if [ "$quick" = false ]; then read_install_options; fi
build_args=() build_args=()
[ "$summary" = false ] || build_args+=(--summary) [ "$summary" = false ] || build_args+=(--summary)
bash "$root/scripts/build.sh" "${build_args[@]}" bash "$root/scripts/commands/build.sh" "${build_args[@]}"
generated="$root/generated" generated="$root/generated"
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; } [ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
+17 -1
View File
@@ -24,7 +24,23 @@ function Read-InstallOptions {
return @{ return @{
update_agents = Read-InstallBoolean -Prompt "Update selected agent CLIs ($Client)?" -Default $defaults.update_agents update_agents = Read-InstallBoolean -Prompt "Update selected agent CLIs ($Client)?" -Default $defaults.update_agents
flashbang = Read-InstallBoolean -Prompt 'Enable the Flashbang notification hook?' -Default $defaults.flashbang flashbang = Read-InstallBoolean -Prompt 'Enable the Flashbang notification hook?' -Default $defaults.flashbang
semantic_retrieval = Read-InstallBoolean -Prompt 'Enable Qwen3 embedding and reranking semantic retrieval?' -Default $defaults.semantic_retrieval semantic_retrieval = Read-SemanticRetrievalOption -Default $defaults.semantic_retrieval -RepositoryRoot $RepositoryRoot -HomePath $HomePath
}
}
function Read-SemanticRetrievalOption {
param([bool]$Default,[string]$RepositoryRoot,[string]$HomePath)
$hint = if ($Default) { 'Y/n/a' } else { 'y/N/a' }
while ($true) {
Write-Host "Enable Qwen3 embedding and reranking semantic retrieval? [$hint] " -NoNewline
$answer = [Console]::ReadLine()
if ($null -eq $answer) { throw 'Input ended before semantic retrieval was confirmed.' }
switch ($answer.Trim().ToLowerInvariant()) {
'' { return $Default }
{ $_ -in @('y', 'yes') } { return $true }
{ $_ -in @('n', 'no') } { return $false }
{ $_ -in @('a', 'auto') } { return Test-SemanticRetrievalDevice -RepositoryRoot $RepositoryRoot -HomePath $HomePath }
}
} }
} }
+22 -1
View File
@@ -34,7 +34,28 @@ read_install_options() {
[ "$dry_run" = false ] || return 0 [ "$dry_run" = false ] || return 0
update_agents=$(read_install_boolean "Update selected agent CLIs ($client)?" "$update_agents") || return update_agents=$(read_install_boolean "Update selected agent CLIs ($client)?" "$update_agents") || return
flashbang=$(read_install_boolean 'Enable the Flashbang notification hook?' "$flashbang") || return flashbang=$(read_install_boolean 'Enable the Flashbang notification hook?' "$flashbang") || return
semantic_retrieval=$(read_install_boolean 'Enable Qwen3 embedding and reranking semantic retrieval?' "$semantic_retrieval") || return semantic_retrieval=$(read_semantic_retrieval_option "$semantic_retrieval") || return
}
read_semantic_retrieval_option() {
local default=$1 hint=y/N/a answer
[ "$default" = false ] || hint=Y/n/a
while :; do
printf 'Enable Qwen3 embedding and reranking semantic retrieval? [%s] ' "$hint" >&2
IFS= read -r answer || { printf 'Input ended before semantic retrieval was confirmed.\n' >&2; return 1; }
answer=${answer%$'\r'}
case "$answer" in
'') printf '%s\n' "$default"; return ;;
y|Y|yes|Yes|YES) printf 'true\n'; return ;;
n|N|no|No|NO) printf 'false\n'; return ;;
a|A|auto|Auto|AUTO)
if test_semantic_retrieval_device "$root" "$home_path"; then printf 'true\n'; return; fi
status=$?
[ "$status" -eq 1 ] && { printf 'false\n'; return; }
return "$status"
;;
esac
done
} }
global_npm_package() { global_npm_package() {
+22 -31
View File
@@ -166,7 +166,7 @@ class Manager:
raise ValueError('Invalid extension ownership ledger.') raise ValueError('Invalid extension ownership ledger.')
identities = set() identities = set()
for resource in self.state['resources']: for resource in self.state['resources']:
if not isinstance(resource, dict) or resource.get('client') not in ('codex', 'claude') or resource.get('kind') not in ('plugin', 'skill', 'qmd'): if not isinstance(resource, dict) or resource.get('client') not in ('codex', 'claude') or resource.get('kind') not in ('plugin', 'skill', 'qmd', 'cli'):
raise ValueError('Invalid extension ownership record.') raise ValueError('Invalid extension ownership record.')
if not isinstance(resource.get('name'), str) or not resource['name']: if not isinstance(resource.get('name'), str) or not resource['name']:
raise ValueError('Invalid extension ownership name.') raise ValueError('Invalid extension ownership name.')
@@ -192,8 +192,10 @@ class Manager:
if not relative.startswith(directory + '/') or not re.fullmatch(r'[a-f0-9]{64}', value): if not relative.startswith(directory + '/') or not re.fullmatch(r'[a-f0-9]{64}', value):
raise ValueError('Invalid owned skill file record.') raise ValueError('Invalid owned skill file record.')
safe_path(self.home, relative) safe_path(self.home, relative)
elif resource.get('package') != '@tobilu/qmd' or resource['client'] != 'codex': elif resource['kind'] == 'qmd' and (resource.get('package') != '@tobilu/qmd' or resource['client'] != 'codex'):
raise ValueError('Invalid owned QMD record.') raise ValueError('Invalid owned QMD record.')
elif resource['kind'] == 'cli' and not re.fullmatch(r'[a-z0-9@/_.-]+', resource.get('package', '')):
raise ValueError('Invalid owned CLI record.')
def save(self): def save(self):
if not self.dry_run: if not self.dry_run:
@@ -227,41 +229,19 @@ class Manager:
self.command(arguments) self.command(arguments)
self.cache[client].pop(selector, None) self.cache[client].pop(selector, None)
elif record['kind'] == 'skill': elif record['kind'] == 'skill':
empty_candidates = set()
for relative, expected in list(record['files'].items()):
path = safe_path(self.home, relative)
if path.exists():
if not path.is_file() or digest(path) != expected:
print(f'WARN Preserving modified skill file: {relative}')
continue
path.unlink()
parent = path.parent
directory = safe_path(self.home, record['directory'])
while parent != directory.parent:
empty_candidates.add(parent)
parent = parent.parent
del record['files'][relative]
self.save()
directory = safe_path(self.home, record['directory']) directory = safe_path(self.home, record['directory'])
if directory.exists(): if directory.exists():
for path in sorted(empty_candidates, key=lambda p: len(p.parts), reverse=True): shutil.rmtree(directory)
if path.is_dir() and not path.is_symlink():
try:
path.rmdir()
except OSError:
pass
try:
directory.rmdir()
except OSError:
pass
if record['files']:
return
else: else:
self.command([record['client'], 'mcp', 'remove', 'qmd'] + (['--scope', 'user'] if record['client'] == 'claude' else [])) package = record['package']
self.command(['npm', 'uninstall', '--global', package])
self.state['resources'].remove(record) self.state['resources'].remove(record)
self.save() self.save()
def ensure(self, client, entry): def ensure(self, client, entry):
if entry['codex_method'] == 'cli':
self.ensure_cli(client, entry)
return
if entry['codex_method'] == 'qmd': if entry['codex_method'] == 'qmd':
self.ensure_qmd(client, entry) self.ensure_qmd(client, entry)
return return
@@ -326,6 +306,18 @@ class Manager:
self.save() self.save()
self.cache[client][selector] = {'scope': 'user'} self.cache[client][selector] = {'scope': 'user'}
def ensure_cli(self, client, entry):
package = entry['codex_source']
if not re.fullmatch(r'[a-z0-9@/_.-]+', package):
raise ValueError('Invalid CLI package.')
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(['npm', 'install', '--global', package])
if not self.dry_run:
self.state['resources'].append(dict(client=client, name=entry['name'], kind='cli', package=package))
self.save()
def ensure_qmd(self, client, entry): def ensure_qmd(self, client, entry):
package = entry['codex_source'] package = entry['codex_source']
if package != '@tobilu/qmd': if package != '@tobilu/qmd':
@@ -340,7 +332,6 @@ class Manager:
record = next((r for r in self.state['resources'] if r['client'] == client and r['name'] == entry['name']), None) record = next((r for r in self.state['resources'] if r['client'] == client and r['name'] == entry['name']), None)
if record is not None: if record is not None:
return return
self.command(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'])
self.state['resources'].append(dict(client='codex', name=entry['name'], kind='qmd', package=package)) self.state['resources'].append(dict(client='codex', name=entry['name'], kind='qmd', package=package))
self.save() self.save()
+1
View File
@@ -96,6 +96,7 @@ function Select-ConfiguredPlugins {
$idField = if ($referenceClient -eq 'Claude') { 'id' } else { 'pluginId' } $idField = if ($referenceClient -eq 'Claude') { 'id' } else { 'pluginId' }
$checked = @($toggleable | ForEach-Object { $checked = @($toggleable | ForEach-Object {
if ($Mode -eq 'Install') { $true; return } if ($Mode -eq 'Install') { $true; return }
if ($_.codex_method -eq 'cli') { return $null -ne (Get-Command $_.codex_source -ErrorAction SilentlyContinue) }
$selector = if ($referenceClient -eq 'Claude') { $_.claude_plugin } else { $_.codex_plugin } $selector = if ($referenceClient -eq 'Claude') { $_.claude_plugin } else { $_.codex_plugin }
if ($referenceClient -eq 'Codex' -and $_.codex_method -eq 'qmd') { if ($referenceClient -eq 'Codex' -and $_.codex_method -eq 'qmd') {
$null -ne (Get-Command qmd -ErrorAction SilentlyContinue) $null -ne (Get-Command qmd -ErrorAction SilentlyContinue)
+4
View File
@@ -110,6 +110,10 @@ select_configured_plugins() {
checked+=(true) checked+=(true)
continue continue
fi fi
if [ "$(plugin_field "$root" "$name" codex_method)" = cli ]; then
if command -v "$(plugin_field "$root" "$name" codex_source)" >/dev/null 2>&1; then checked+=(true); else checked+=(false); fi
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" = 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)" = qmd ]; 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 if command -v qmd >/dev/null 2>&1; then checked+=(true); else checked+=(false); fi
+8
View File
@@ -8,3 +8,11 @@ function Sync-SemanticRetrieval {
& python @arguments & python @arguments
if ($LASTEXITCODE -ne 0) { throw 'Semantic retrieval reconciliation failed.' } if ($LASTEXITCODE -ne 0) { throw 'Semantic retrieval reconciliation failed.' }
} }
function Test-SemanticRetrievalDevice {
param([Parameter(Mandatory=$true)][string]$RepositoryRoot,[Parameter(Mandatory=$true)][string]$HomePath,[switch]$Summary)
& python (Join-Path $PSScriptRoot 'semantic-retrieval.py') benchmark --root $RepositoryRoot --home $HomePath $(if ($Summary) { '--summary' }) | Write-Host
if ($LASTEXITCODE -eq 0) { return $true }
if ($LASTEXITCODE -eq 2) { return $false }
throw 'Semantic retrieval benchmark failed.'
}
+42 -4
View File
@@ -8,7 +8,7 @@ import subprocess
import sys import sys
FILES = ('server.py', 'requirements.txt') FILES = ('server.py', 'benchmark.py', 'requirements.txt')
CUDA_INDEX = 'https://download.pytorch.org/whl/cu126' CUDA_INDEX = 'https://download.pytorch.org/whl/cu126'
@@ -106,17 +106,55 @@ def sync(root, home, clients, enabled, dry_run, update, summary=False):
save_state(state_path, state, False) save_state(state_path, state, False)
def benchmark(root, home, dry_run, summary=False):
base = home / '.my-ai-configuration'
target = base / 'semantic-retrieval'
state_path = base / 'semantic-retrieval.json'
if dry_run:
if not summary:
print('DRYRUN benchmark semantic retrieval: Qwen3-Embedding-0.6B + Qwen3-Reranker-0.6B')
return True
state = load_state(state_path)
source = root / 'shared' / 'retrieval'
if target.exists() and not state['files']:
if not all((target / name).is_file() and digest(target / name) == digest(source / name) for name in FILES):
raise ValueError('Semantic retrieval directory exists without setup ownership; refusing to overwrite it.')
target.mkdir(parents=True, exist_ok=True)
for name in FILES:
shutil.copy2(source / name, target / name)
state['files'][name] = digest(target / name)
python = ensure_runtime(target, False)
result = subprocess.run([str(python), str(target / 'benchmark.py'), '--model-cache', str(target / 'model-cache')], check=True,
stdout=subprocess.PIPE, text=True, timeout=300)
outcome = json.loads(result.stdout)
if not isinstance(outcome, dict) or not isinstance(outcome.get('recommended'), bool):
raise ValueError('Semantic retrieval benchmark returned an invalid result.')
if outcome['recommended']:
save_state(state_path, state, False)
else:
shutil.rmtree(target)
state_path.unlink(missing_ok=True)
if not summary:
print('Semantic retrieval benchmark: ' + ('recommended' if outcome['recommended'] else 'not recommended') + f" (embedding {outcome['embeddingSeconds']}s, reranking {outcome['rerankingSeconds']}s)")
return outcome['recommended']
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('action', choices=('sync',)) parser.add_argument('action', choices=('sync', 'benchmark'))
parser.add_argument('--root', required=True, type=Path) parser.add_argument('--root', required=True, type=Path)
parser.add_argument('--home', 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('--client', choices=('codex', 'claude', 'both'))
parser.add_argument('--enabled', choices=('true', 'false'), required=True) parser.add_argument('--enabled', choices=('true', 'false'))
parser.add_argument('--dry-run', action='store_true') parser.add_argument('--dry-run', action='store_true')
parser.add_argument('--update', action='store_true') parser.add_argument('--update', action='store_true')
parser.add_argument('--summary', action='store_true') parser.add_argument('--summary', action='store_true')
args = parser.parse_args() args = parser.parse_args()
if args.action == 'benchmark':
recommended = benchmark(args.root.resolve(), args.home.resolve(), args.dry_run, args.summary)
raise SystemExit(0 if recommended else 2)
if args.client is None or args.enabled is None:
parser.error('sync requires --client and --enabled')
clients = ('codex', 'claude') if args.client == 'both' else (args.client,) 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, args.summary) sync(args.root.resolve(), args.home.resolve(), clients, args.enabled == 'true', args.dry_run, args.update, args.summary)
print('SEMANTIC RETRIEVAL: reconciliation complete' + (' (dry run)' if args.dry_run else '')) print('SEMANTIC RETRIEVAL: reconciliation complete' + (' (dry run)' if args.dry_run else ''))
+9
View File
@@ -9,3 +9,12 @@ sync_semantic_retrieval() {
[ "$summary" != true ] || arguments+=(--summary) [ "$summary" != true ] || arguments+=(--summary)
python3 "${arguments[@]}" python3 "${arguments[@]}"
} }
test_semantic_retrieval_device() {
local root=$1 home_path=$2 summary=${3:-false}
local arguments=("$root/scripts/lib/semantic-retrieval.py" benchmark --root "$root" --home "$home_path")
[ "$summary" != true ] || arguments+=(--summary)
if python3 "${arguments[@]}" >&2; then return 0; fi
[ "$?" -eq 2 ] && return 1
return 2
}
@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-build-validation-" + [Guid]::NewGuid()) $work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-build-validation-" + [Guid]::NewGuid())
New-Item $work -ItemType Directory -Force | Out-Null New-Item $work -ItemType Directory -Force | Out-Null
@@ -18,14 +18,14 @@ function Test-BuildFails([string]$Label, [scriptblock]$Mutate) {
$copy = New-RepoCopy $copy = New-RepoCopy
& $Mutate $copy & $Mutate $copy
$failed = $false $failed = $false
try { & (Join-Path $copy 'scripts/build.ps1') 2>&1 | Out-Null } try { & (Join-Path $copy 'scripts/commands/build.ps1') 2>&1 | Out-Null }
catch { $failed = $true } catch { $failed = $true }
Remove-Item $copy -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $copy -Recurse -Force -ErrorAction SilentlyContinue
if (-not $failed) { throw "Expected build to fail for case '$Label' but it succeeded." } if (-not $failed) { throw "Expected build to fail for case '$Label' but it succeeded." }
} }
$baseline = New-RepoCopy $baseline = New-RepoCopy
& (Join-Path $baseline 'scripts/build.ps1') -Summary | Out-Null & (Join-Path $baseline 'scripts/commands/build.ps1') -Summary | Out-Null
Test-BuildFails 'invalid Claude settings.json' { Test-BuildFails 'invalid Claude settings.json' {
param($copy) param($copy)
+3 -3
View File
@@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
work=$(mktemp -d) work=$(mktemp -d)
cleanup() { rm -rf -- "$work"; } cleanup() { rm -rf -- "$work"; }
trap cleanup EXIT trap cleanup EXIT
@@ -24,7 +24,7 @@ test_build_fails() {
local label=$1 mutate_fn=$2 copy local label=$1 mutate_fn=$2 copy
copy=$(new_repo_copy) copy=$(new_repo_copy)
"$mutate_fn" "$copy" "$mutate_fn" "$copy"
if bash "$copy/scripts/build.sh" >/dev/null 2>&1; then if bash "$copy/scripts/commands/build.sh" >/dev/null 2>&1; then
printf 'Expected build to fail for case "%s" but it succeeded.\n' "$label" >&2 printf 'Expected build to fail for case "%s" but it succeeded.\n' "$label" >&2
exit 1 exit 1
fi fi
@@ -32,7 +32,7 @@ test_build_fails() {
} }
baseline=$(new_repo_copy) baseline=$(new_repo_copy)
bash "$baseline/scripts/build.sh" --summary >/dev/null bash "$baseline/scripts/commands/build.sh" --summary >/dev/null
mutate_invalid_json() { mutate_invalid_json() {
printf '} this is not json {' >> "$1/adapters/claude/config/settings.json" printf '} this is not json {' >> "$1/adapters/claude/config/settings.json"
@@ -1,9 +1,9 @@
[CmdletBinding()] [CmdletBinding()]
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $root 'scripts/lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') . (Join-Path $root 'scripts/lib/install-targets.ps1')
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-install-guard-test-" + [Guid]::NewGuid()) $work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-install-guard-test-" + [Guid]::NewGuid())
try { try {
+1 -1
View File
@@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh" . "$root/scripts/lib/install-targets.sh"
@@ -9,7 +9,7 @@ import tomllib
import unittest import unittest
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
SPEC = importlib.util.spec_from_file_location('install_options', ROOT / 'scripts/lib/install-options.py') SPEC = importlib.util.spec_from_file_location('install_options', ROOT / 'scripts/lib/install-options.py')
OPTIONS = importlib.util.module_from_spec(SPEC) OPTIONS = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(OPTIONS) SPEC.loader.exec_module(OPTIONS)
@@ -172,19 +172,19 @@ class ManagedExtensionTests(unittest.TestCase):
self.manager(dry_run=True, summary=True).sync([plugin(), skill()], ['codex'], {'Example', 'Humanizer'}) self.manager(dry_run=True, summary=True).sync([plugin(), skill()], ['codex'], {'Example', 'Humanizer'})
self.assertEqual('EXTENSIONS: reconciliation complete (dry run)\n', output.getvalue()) self.assertEqual('EXTENSIONS: reconciliation complete (dry run)\n', output.getvalue())
def test_qmd_installs_once_and_registers_codex_mcp(self): def test_qmd_installs_once_as_a_cli_without_registering_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') 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.manager().sync([entry], ['codex'], {'QMD'})
self.assertEqual([['npm', 'install', '--global', '@tobilu/qmd'], ['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp']], self.commands) self.assertEqual([['npm', 'install', '--global', '@tobilu/qmd']], self.commands)
self.manager().sync([], ['codex'], set()) self.manager().sync([], ['codex'], set())
self.assertEqual(['codex', 'mcp', 'remove', 'qmd'], self.commands[-1]) self.assertEqual(['npm', 'uninstall', '--global', '@tobilu/qmd'], self.commands[-1])
def test_qmd_installs_once_for_both_clients(self): 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') 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.manager().sync([entry], ['claude', 'codex'], {'QMD'})
self.assertEqual(1, self.commands.count(['npm', 'install', '--global', '@tobilu/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(['claude', 'plugin', 'install', 'qmd@qmd', '--scope', 'user'], self.commands)
self.assertIn(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'], self.commands) self.assertNotIn(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'], self.commands)
def test_foreign_empty_directory_survives_skill_removal(self): def test_foreign_empty_directory_survives_skill_removal(self):
self.manager().sync([skill()], ['codex'], {'Humanizer'}) self.manager().sync([skill()], ['codex'], {'Humanizer'})
@@ -1,9 +1,9 @@
[CmdletBinding()] [CmdletBinding()]
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $root 'scripts/lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') . (Join-Path $root 'scripts/lib/install-targets.ps1')
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-manifest-test-" + [Guid]::NewGuid()) $work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-manifest-test-" + [Guid]::NewGuid())
New-Item $work -ItemType Directory -Force | Out-Null New-Item $work -ItemType Directory -Force | Out-Null
+1 -1
View File
@@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh" . "$root/scripts/lib/install-targets.sh"
@@ -1,8 +1,9 @@
[CmdletBinding()] [CmdletBinding()]
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
function Write-Host { param($Object) if (-not $Summary) { Microsoft.PowerShell.Utility\Write-Host $Object } } function Write-Host { param($Object) if (-not $Summary) { Microsoft.PowerShell.Utility\Write-Host $Object } }
. (Join-Path $PSScriptRoot 'lib/plugins.ps1') . (Join-Path $root 'scripts/lib/plugins.ps1')
$script:answers = [System.Collections.Generic.Queue[string]]::new() $script:answers = [System.Collections.Generic.Queue[string]]::new()
$script:answers.Enqueue('1') $script:answers.Enqueue('1')
@@ -43,22 +44,22 @@ $null = Get-InstalledPlugins -Client Codex -HomePath 'C:/temporary profile'
if ($observedCodexHome -ne (Join-Path 'C:/temporary profile' '.codex') -or $observedProfile -ne 'C:/temporary profile') { throw 'Plugin inspection must use the requested user home.' } if ($observedCodexHome -ne (Join-Path 'C:/temporary profile' '.codex') -or $observedProfile -ne 'C:/temporary profile') { throw 'Plugin inspection must use the requested user home.' }
if ($env:CODEX_HOME -ne $previousCodexHome -or $env:USERPROFILE -ne $previousProfile) { throw 'Plugin inspection must restore the process environment.' } if ($env:CODEX_HOME -ne $previousCodexHome -or $env:USERPROFILE -ne $previousProfile) { throw 'Plugin inspection must restore the process environment.' }
$script:answers.Enqueue('done') $script:answers.Enqueue('done')
$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Mode Update $selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Mode Update
if (@($selection.Selected | Where-Object { $_.codex_method -eq 'plugin' }).Count -ne 5) { throw 'All installed Codex plugins must start checked when updating.' } if (@($selection.Selected | Where-Object { $_.codex_method -eq 'plugin' }).Count -ne 5) { throw 'All installed Codex plugins must start checked when updating.' }
$script:answers.Enqueue('4') $script:answers.Enqueue('4')
$script:answers.Enqueue('done') $script:answers.Enqueue('done')
$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Mode Update $selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Mode Update
if (@($selection.Deselected | Where-Object { $_.name -eq 'Context7' }).Count -ne 1) { throw 'Only the explicitly unchecked installed plugin must be deselected.' } if (@($selection.Deselected | Where-Object { $_.name -eq 'Context7' }).Count -ne 1) { throw 'Only the explicitly unchecked installed plugin must be deselected.' }
$script:managedArguments = @() $script:managedArguments = @()
function python { $script:managedArguments = @($args); $global:LASTEXITCODE = 0 } function python { $script:managedArguments = @($args); $global:LASTEXITCODE = 0 }
$entry = [pscustomobject]@{ name = 'Test'; codex_method = 'plugin'; codex_marketplace = '-'; codex_plugin = 'test@market' } $entry = [pscustomobject]@{ name = 'Test'; codex_method = 'plugin'; codex_marketplace = '-'; codex_plugin = 'test@market' }
Sync-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -HomePath 'C:/temporary home' -Update -Summary -Entries @($entry) Sync-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -HomePath 'C:/temporary home' -Update -Summary -Entries @($entry)
if ($managedArguments -notcontains 'sync' -or $managedArguments -notcontains '--update' -or $managedArguments -notcontains 'Test' -or $managedArguments -notcontains 'C:/temporary home') { throw 'Managed reconciliation must receive selection, home, and update mode.' } if ($managedArguments -notcontains 'sync' -or $managedArguments -notcontains '--update' -or $managedArguments -notcontains 'Test' -or $managedArguments -notcontains 'C:/temporary home') { throw 'Managed reconciliation must receive selection, home, and update mode.' }
Sync-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Entries @() Sync-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Entries @()
if ($managedArguments[-1] -ne '--selected') { throw 'Empty selection must reach reconciliation for managed cleanup.' } if ($managedArguments[-1] -ne '--selected') { throw 'Empty selection must reach reconciliation for managed cleanup.' }
function Get-InstalledPlugins { throw 'Empty selection must not query installed plugins.' } function Get-InstalledPlugins { throw 'Empty selection must not query installed plugins.' }
function Invoke-PluginCommand { throw 'Empty selection must not run plugin commands.' } function Invoke-PluginCommand { throw 'Empty selection must not run plugin commands.' }
Install-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Both -Entries @() Install-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Both -Entries @()
Uninstall-DeselectedPlugins -Client Both -Entries @() Uninstall-DeselectedPlugins -Client Both -Entries @()
if ($Summary) { Write-Output 'Tests: PASS | plugin selection' } else { Write-Output 'PASS plugin toggle selection: toggle and confirm' } if ($Summary) { Write-Output 'Tests: PASS | plugin selection' } else { Write-Output 'PASS plugin toggle selection: toggle and confirm' }
+1 -1
View File
@@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/plugins.sh"
# shellcheck disable=SC2034 # shellcheck disable=SC2034
File renamed without changes.
File renamed without changes.
@@ -8,7 +8,7 @@ import tempfile
import unittest import unittest
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
HELPER = ROOT / 'scripts/lib/selection-state.py' HELPER = ROOT / 'scripts/lib/selection-state.py'
@@ -112,7 +112,7 @@ class SelectionTests(unittest.TestCase):
env=environment, capture_output=True, text=True, timeout=10) env=environment, capture_output=True, text=True, timeout=10)
self.assertEqual(save.returncode, 0, save.stdout + save.stderr) self.assertEqual(save.returncode, 0, save.stdout + save.stderr)
original = self.path.read_bytes() original = self.path.read_bytes()
source = (ROOT / 'scripts/update.ps1').read_text(encoding='utf-8-sig') source = (ROOT / 'scripts/commands/update.ps1').read_text(encoding='utf-8-sig')
source = source.replace("[Environment]::GetFolderPath('UserProfile')", '$env:TEST_SELECTION_HOME') source = source.replace("[Environment]::GetFolderPath('UserProfile')", '$env:TEST_SELECTION_HOME')
with tempfile.NamedTemporaryFile(mode='w', suffix='.tmp.ps1', dir=ROOT / 'scripts', with tempfile.NamedTemporaryFile(mode='w', suffix='.tmp.ps1', dir=ROOT / 'scripts',
encoding='utf-8', delete=False) as stream: encoding='utf-8', delete=False) as stream:
@@ -148,7 +148,7 @@ class SelectionTests(unittest.TestCase):
self.assertEqual(save.returncode, 0, save.stdout + save.stderr) self.assertEqual(save.returncode, 0, save.stdout + save.stderr)
original = self.path.read_bytes() original = self.path.read_bytes()
environment = dict(os.environ, HOME=self.home.as_posix()) environment = dict(os.environ, HOME=self.home.as_posix())
result = subprocess.run([shell, str(ROOT / 'scripts/update.sh'), '--quick', '--dry-run'], result = subprocess.run([shell, str(ROOT / 'scripts/commands/update.sh'), '--quick', '--dry-run'],
env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90) env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertNotIn('Select plugins', result.stdout) self.assertNotIn('Select plugins', result.stdout)
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
case "${1:-}" in ''|--summary) ;; *) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;; esac case "${1:-}" in ''|--summary) ;; *) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;; esac
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
python3 "$root/scripts/test-quick-update.py" python3 "$root/scripts/tests/test-quick-update.py"
printf 'Tests: PASS | quick update selection\n' printf 'Tests: PASS | quick update selection\n'
@@ -11,7 +11,7 @@ import unittest
from unittest.mock import patch from unittest.mock import patch
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
SPEC = importlib.util.spec_from_file_location('retrieval_server', ROOT / 'shared/retrieval/server.py') SPEC = importlib.util.spec_from_file_location('retrieval_server', ROOT / 'shared/retrieval/server.py')
MODULE = importlib.util.module_from_spec(SPEC) MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE) SPEC.loader.exec_module(MODULE)
@@ -9,7 +9,7 @@ import unittest
from unittest.mock import patch from unittest.mock import patch
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
SPEC = importlib.util.spec_from_file_location('semantic_retrieval', ROOT / 'scripts/lib/semantic-retrieval.py') SPEC = importlib.util.spec_from_file_location('semantic_retrieval', ROOT / 'scripts/lib/semantic-retrieval.py')
MODULE = importlib.util.module_from_spec(SPEC) MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE) SPEC.loader.exec_module(MODULE)
@@ -8,7 +8,7 @@ import tempfile
import unittest import unittest
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
SHELLS = [ SHELLS = [
('powershell', shutil.which('powershell') or shutil.which('pwsh')), ('powershell', shutil.which('powershell') or shutil.which('pwsh')),
('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe') ('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
@@ -88,16 +88,16 @@ class SessionStateHookTests(unittest.TestCase):
if shell == 'powershell': if shell == 'powershell':
prefix = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command'] prefix = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command']
commands = [ commands = [
'& ./scripts/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Goal $env:TASK_GOAL -Decisions @("keep", "line two") -Pending @("check")', '& ./scripts/commands/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Goal $env:TASK_GOAL -Decisions @("keep", "line two") -Pending @("check")',
'& ./scripts/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Verified @("passed")', '& ./scripts/commands/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Verified @("passed")',
'& ./scripts/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Pointer', '& ./scripts/commands/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Pointer',
] ]
else: else:
prefix = [executable, '-c'] prefix = [executable, '-c']
commands = [ commands = [
'bash ./scripts/session-state.sh --workspace "$TASK_WORKSPACE" --goal "$TASK_GOAL" --decisions keep "line two" --open check', 'bash ./scripts/commands/session-state.sh --workspace "$TASK_WORKSPACE" --goal "$TASK_GOAL" --decisions keep "line two" --open check',
'bash ./scripts/session-state.sh --workspace "$TASK_WORKSPACE" --verified passed', 'bash ./scripts/commands/session-state.sh --workspace "$TASK_WORKSPACE" --verified passed',
'bash ./scripts/session-state.sh --workspace "$TASK_WORKSPACE" --pointer', 'bash ./scripts/commands/session-state.sh --workspace "$TASK_WORKSPACE" --pointer',
] ]
for command in commands: for command in commands:
result = subprocess.run(prefix + [command], cwd=repository, capture_output=True, result = subprocess.run(prefix + [command], cwd=repository, capture_output=True,
@@ -1,13 +1,13 @@
[CmdletBinding()] [CmdletBinding()]
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw $powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw
$bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw $bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw
if ($powerShellHook -notmatch '\$HoldMs\s*=\s*250' -or $powerShellHook -notmatch '\$FadeMs\s*=\s*250' -or ([regex]::Matches($bashHook, 'default=250').Count -lt 2)) { throw 'Flashbang defaults must total 500 milliseconds on PowerShell and Bash.' } if ($powerShellHook -notmatch '\$HoldMs\s*=\s*250' -or $powerShellHook -notmatch '\$FadeMs\s*=\s*250' -or ([regex]::Matches($bashHook, 'default=250').Count -lt 2)) { throw 'Flashbang defaults must total 500 milliseconds on PowerShell and Bash.' }
$statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"' + $root.Replace('\','\\') + '","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' $statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"' + $root.Replace('\','\\') + '","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}'
$branch = & git -C $root branch --show-current $branch = & git -C $root branch --show-current
$buildSummaryOutput = & (Join-Path $root 'scripts/build.ps1') -Summary $buildSummaryOutput = & (Join-Path $root 'scripts/commands/build.ps1') -Summary
if (@($buildSummaryOutput | Where-Object { $_ -eq 'Build: PASS | 4 packages' }).Count -ne 1) { throw "build.ps1 -Summary must still print the PASS line: $($buildSummaryOutput -join '; ')" } if (@($buildSummaryOutput | Where-Object { $_ -eq 'Build: PASS | 4 packages' }).Count -ne 1) { throw "build.ps1 -Summary must still print the PASS line: $($buildSummaryOutput -join '; ')" }
$statusOutput = ($statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')) -join "`n" $statusOutput = ($statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')) -join "`n"
$plainStatusOutput = [regex]::Replace($statusOutput, [char]27 + '\[[0-9;]*m', '') $plainStatusOutput = [regex]::Replace($statusOutput, [char]27 + '\[[0-9;]*m', '')
@@ -48,7 +48,7 @@ foreach ($shell in @('powershell','bash')) {
throw "Claude finish hook is incorrect in $package" throw "Claude finish hook is incorrect in $package"
} elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') { } elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') {
throw "Claude auto permission mode is missing in $package" throw "Claude auto permission mode is missing in $package"
} elseif (-not $settings.sandbox.enabled -or $settings.sandbox.allowUnsandboxedCommands -ne $false) { } elseif (-not $settings.sandbox.enabled -or $settings.sandbox.allowUnsandboxedCommands -ne $false -or $settings.sandbox.failIfUnavailable -ne $true) {
throw "Claude strict sandbox is missing in $package" throw "Claude strict sandbox is missing in $package"
} elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })") { } elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })") {
throw "Claude status line is incorrect in $package" throw "Claude status line is incorrect in $package"
@@ -70,17 +70,17 @@ foreach ($shellSelection in @('1','2')) {
$script:answers.Enqueue($shellSelection) $script:answers.Enqueue($shellSelection)
$script:answers.Enqueue($clientSelection) $script:answers.Enqueue($clientSelection)
function Read-Host { param($Prompt) $answers.Dequeue() } function Read-Host { param($Prompt) $answers.Dequeue() }
$output = & "$PSScriptRoot/install.ps1" -DryRun 6>$null $output = & (Join-Path $root 'scripts/commands/install.ps1') -DryRun 6>$null
if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' } if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' }
$shell = if ($shellSelection -eq '1') { 'powershell' } else { 'bash' } $shell = if ($shellSelection -eq '1') { 'powershell' } else { 'bash' }
if (($output -join "`n") -notmatch "-$shell") { throw 'Incorrect selected package.' } if (($output -join "`n") -notmatch "-$shell") { throw 'Incorrect selected package.' }
} }
} }
$doctorSummaryOutput = & (Join-Path $root 'scripts/doctor.ps1') -Summary $doctorSummaryOutput = & (Join-Path $root 'scripts/commands/doctor.ps1') -Summary
if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' } if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' }
if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" } if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" }
foreach ($entryPoint in @('install','update')) { foreach ($entryPoint in @('install','update')) {
$preview = @(& "$PSScriptRoot/$entryPoint.ps1" -Summary -DryRun -Client Both -Shell PowerShell) $preview = @(& (Join-Path $root "scripts/commands/$entryPoint.ps1") -Summary -DryRun -Client Both -Shell PowerShell)
if ($preview -match '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) ') { throw "$entryPoint summary leaked per-item output." } if ($preview -match '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) ') { throw "$entryPoint summary leaked per-item output." }
if (@($preview | Where-Object { $_ -match ('^' + $entryPoint + ': PASS') }).Count -ne 1) { throw "$entryPoint summary is missing." } if (@($preview | Where-Object { $_ -match ('^' + $entryPoint + ': PASS') }).Count -ne 1) { throw "$entryPoint summary is missing." }
} }
+6 -6
View File
@@ -7,12 +7,12 @@ while [ $# -gt 0 ]; do
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;; *) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
grep -Eq '\$HoldMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1" grep -Eq '\$HoldMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1" grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
[ "$(grep -c 'default=250' "$root/shared/hooks/flashbang.sh")" -ge 2 ] [ "$(grep -c 'default=250' "$root/shared/hooks/flashbang.sh")" -ge 2 ]
branch=$(git -C "$root" branch --show-current) branch=$(git -C "$root" branch --show-current)
build_summary_output=$(bash "$root/scripts/build.sh" --summary) build_summary_output=$(bash "$root/scripts/commands/build.sh" --summary)
if ! printf '%s\n' "$build_summary_output" | grep -qx 'Build: PASS | 4 packages'; then if ! printf '%s\n' "$build_summary_output" | grep -qx 'Build: PASS | 4 packages'; then
printf 'build.sh --summary must still print the PASS line: %s\n' "$build_summary_output" >&2 printf 'build.sh --summary must still print the PASS line: %s\n' "$build_summary_output" >&2
exit 1 exit 1
@@ -65,7 +65,7 @@ for shell in powershell bash; do
! grep -q 'flashbang-if-input' "$package/$file" ! grep -q 'flashbang-if-input' "$package/$file"
! grep -q '"async"[[:space:]]*:[[:space:]]*true' "$package/$file" ! grep -q '"async"[[:space:]]*:[[:space:]]*true' "$package/$file"
grep -q '"defaultMode"[[:space:]]*:[[:space:]]*"auto"' "$package/$file" grep -q '"defaultMode"[[:space:]]*:[[:space:]]*"auto"' "$package/$file"
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert settings["sandbox"] == {"enabled": True, "allowUnsandboxedCommands": False}' "$package/$file" python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert settings["sandbox"] == {"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True}' "$package/$file"
statusline_extension='sh' statusline_extension='sh'
[ "$shell" != powershell ] || statusline_extension=ps1 [ "$shell" != powershell ] || statusline_extension=ps1
test -f "$package/statusline/statusline.$statusline_extension" test -f "$package/statusline/statusline.$statusline_extension"
@@ -100,7 +100,7 @@ for shell in powershell bash; do
done done
for shell_selection in 1 2; do for shell_selection in 1 2; do
for client_selection in 1 2 3; do for client_selection in 1 2 3; do
output=$(printf '%s\n%s\n' "$shell_selection" "$client_selection" | bash "$root/scripts/install.sh" --dry-run 2>&1) || { printf '%s\n' "$output" >&2; exit 1; } output=$(printf '%s\n%s\n' "$shell_selection" "$client_selection" | bash "$root/scripts/commands/install.sh" --dry-run 2>&1) || { printf '%s\n' "$output" >&2; exit 1; }
printf '%s\n' "$output" | grep -E 'WARN|FAIL|WARNING|ERROR' || true printf '%s\n' "$output" | grep -E 'WARN|FAIL|WARNING|ERROR' || true
[[ "$output" == *'PASS install dry-run'* ]] [[ "$output" == *'PASS install dry-run'* ]]
shell=powershell shell=powershell
@@ -108,7 +108,7 @@ for shell_selection in 1 2; do
[[ "$output" == *"-$shell"* ]] [[ "$output" == *"-$shell"* ]]
done done
done done
doctor_summary_output=$(bash "$root/scripts/doctor.sh" --summary) doctor_summary_output=$(bash "$root/scripts/commands/doctor.sh" --summary)
if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then
printf 'Doctor summary mode must not print individual PASS lines.\n' >&2 printf 'Doctor summary mode must not print individual PASS lines.\n' >&2
exit 1 exit 1
@@ -118,7 +118,7 @@ if ! printf '%s\n' "$doctor_summary_output" | grep -Eq '^Doctor: PASS \| [0-9]+
exit 1 exit 1
fi fi
for entry_point in install update; do for entry_point in install update; do
preview=$(bash "$root/scripts/$entry_point.sh" --summary --dry-run --client both --shell bash) preview=$(bash "$root/scripts/commands/$entry_point.sh" --summary --dry-run --client both --shell bash)
if printf '%s\n' "$preview" | grep -Eq '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) '; then if printf '%s\n' "$preview" | grep -Eq '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) '; then
printf '%s summary leaked per-item output.\n' "$entry_point" >&2 printf '%s summary leaked per-item output.\n' "$entry_point" >&2
exit 1 exit 1
@@ -7,7 +7,7 @@ import tempfile
import unittest import unittest
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
class TelemetryTests(unittest.TestCase): class TelemetryTests(unittest.TestCase):
@@ -17,9 +17,9 @@ class TelemetryTests(unittest.TestCase):
self.skipTest('Bash unavailable') self.skipTest('Bash unavailable')
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
root = Path(directory) root = Path(directory)
script = root / 'scripts/telemetry.sh' script = root / 'scripts/commands/telemetry.sh'
script.parent.mkdir() script.parent.mkdir()
shutil.copy(ROOT / 'scripts/telemetry.sh', script) shutil.copy(ROOT / 'scripts/commands/telemetry.sh', script)
env = dict(os.environ, AI_CONFIG_TELEMETRY='1') 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) 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) self.assertEqual(result.returncode, 0, result.stderr)
@@ -1,5 +1,5 @@
param([string]$RepositoryRoot = (Split-Path $PSScriptRoot -Parent | Split-Path -Parent | Split-Path -Parent)) param([string]$RepositoryRoot = (Split-Path $PSScriptRoot -Parent | Split-Path -Parent | Split-Path -Parent))
$build = Join-Path $RepositoryRoot 'scripts/build.ps1' $build = Join-Path $RepositoryRoot 'scripts/commands/build.ps1'
if (-not (Test-Path $build)) { Write-Error 'Build script is missing.'; exit 1 } if (-not (Test-Path $build)) { Write-Error 'Build script is missing.'; exit 1 }
$shellCommand = if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { 'powershell' } else { 'pwsh' } $shellCommand = if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { 'powershell' } else { 'pwsh' }
& $shellCommand -NoProfile -ExecutionPolicy Bypass -File $build & $shellCommand -NoProfile -ExecutionPolicy Bypass -File $build
@@ -2,5 +2,5 @@
set -euo pipefail set -euo pipefail
root=${1:?Repository root is required} root=${1:?Repository root is required}
bash "$root/scripts/build.sh" bash "$root/scripts/commands/build.sh"
printf 'PASS post-change verification\n' printf 'PASS post-change verification\n'
+56
View File
@@ -0,0 +1,56 @@
import argparse
import json
import os
from pathlib import Path
import statistics
import sys
import time
def elapsed(call):
started = time.monotonic()
call()
return time.monotonic() - started
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--model-cache', required=True, type=Path)
parser.add_argument('--max-seconds', type=float, default=float(os.environ.get('QWEN_BENCHMARK_MAX_SECONDS', '8')))
args = parser.parse_args()
if args.max_seconds <= 0:
raise ValueError('--max-seconds must be greater than zero.')
os.environ['HF_HOME'] = str(args.model_cache)
from server import shared_encoder, shared_reranker
encoder = shared_encoder()
reranker = shared_reranker()
query = 'Find the implementation that updates the selected client configuration.'
documents = ['Path: scripts/commands/install.py\n\n' + query] * 50
print('Benchmark: warming up embedding model...', file=sys.stderr, flush=True)
encoder.encode([query], prompt_name='query')
print('Benchmark: probing reranker speed...', file=sys.stderr, flush=True)
probe = elapsed(lambda: reranker.predict([(query, documents[0])], show_progress_bar=False))
if probe * len(documents) > args.max_seconds:
print(json.dumps({
'embeddingSeconds': None,
'rerankingSeconds': round(probe, 3),
'maxSeconds': args.max_seconds,
'recommended': False,
}))
return
print('Benchmark: measuring full retrieval batch...', file=sys.stderr, flush=True)
reranker.predict([(query, document) for document in documents], show_progress_bar=False)
embedding = statistics.median(elapsed(lambda: encoder.encode([query], prompt_name='query')) for _ in range(3))
reranking = statistics.median(elapsed(lambda: reranker.predict([(query, document) for document in documents], show_progress_bar=False)) for _ in range(3))
result = {
'embeddingSeconds': round(embedding, 3),
'rerankingSeconds': round(reranking, 3),
'maxSeconds': args.max_seconds,
'recommended': embedding <= args.max_seconds and reranking <= args.max_seconds,
}
print(json.dumps(result))
if __name__ == '__main__':
main()