Compare commits
114
Commits
f83b0add58
...
main
No files matched your search
+23
-29
@@ -64,23 +64,23 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- name: Check PowerShell runtime
|
- name: Install PowerShell runtime
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if command -v pwsh >/dev/null 2>&1; then
|
set -euo pipefail
|
||||||
echo 'POWERSHELL_AVAILABLE=true' >> "$GITHUB_ENV"
|
if ! command -v pwsh >/dev/null 2>&1; then
|
||||||
exit 0
|
# Microsoft's signed apt repository; the job fails when PowerShell cannot be installed.
|
||||||
fi
|
. /etc/os-release
|
||||||
if sudo apt-get update && sudo apt-get install --yes --no-install-recommends powershell; then
|
wget -q -O "$RUNNER_TEMP/packages-microsoft-prod.deb" "https://packages.microsoft.com/config/$ID/$VERSION_ID/packages-microsoft-prod.deb"
|
||||||
echo 'POWERSHELL_AVAILABLE=true' >> "$GITHUB_ENV"
|
sudo dpkg -i "$RUNNER_TEMP/packages-microsoft-prod.deb"
|
||||||
else
|
sudo apt-get update
|
||||||
echo 'POWERSHELL_AVAILABLE=false' >> "$GITHUB_ENV"
|
sudo apt-get install --yes --no-install-recommends powershell
|
||||||
echo '::warning::PowerShell is unavailable; PowerShell checks skipped for this runner.'
|
|
||||||
fi
|
fi
|
||||||
|
pwsh -NoLogo -NoProfile -NonInteractive -Command '$PSVersionTable.PSVersion.ToString()'
|
||||||
- name: Run PowerShell tests
|
- name: Run PowerShell tests
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ "${POWERSHELL_AVAILABLE:-false}" != true ]; then exit 0; fi
|
set -euo pipefail
|
||||||
pwsh -NoLogo -NoProfile -NonInteractive -File ./scripts/commands/check-capabilities.ps1
|
pwsh -NoLogo -NoProfile -NonInteractive -File ./scripts/commands/check-capabilities.ps1
|
||||||
pwsh -NoLogo -NoProfile -NonInteractive -File ./scripts/commands/build.ps1 -Summary
|
pwsh -NoLogo -NoProfile -NonInteractive -File ./scripts/commands/build.ps1 -Summary
|
||||||
for file in scripts/tests/test-*.ps1; do
|
for file in scripts/tests/test-*.ps1; do
|
||||||
@@ -172,29 +172,23 @@ jobs:
|
|||||||
AI_CONFIG_REQUIRE_CLIENTS: '1'
|
AI_CONFIG_REQUIRE_CLIENTS: '1'
|
||||||
run: timeout 900 python scripts/tests/test-client-smoke.py -v
|
run: timeout 900 python scripts/tests/test-client-smoke.py -v
|
||||||
|
|
||||||
retrieval:
|
qmd:
|
||||||
name: Qwen retrieval integration
|
name: QMD unit tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 10
|
||||||
needs: [python]
|
needs: [python]
|
||||||
|
env:
|
||||||
|
QMD_PACKAGE_DIR: ${{ runner.temp }}/qmd-tests
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v22.16.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
node-version: '24'
|
||||||
cache: pip
|
# CI runs model-free tests only; real-model integration requires a suitable local machine.
|
||||||
cache-dependency-path: shared/retrieval/requirements.txt
|
- name: Install QMD test dependency
|
||||||
- name: Install retrieval dependencies
|
run: npm install --prefix "$QMD_PACKAGE_DIR" --ignore-scripts --no-package-lock --no-audit --no-fund yaml@2.9.0
|
||||||
run: python -m pip install -r shared/retrieval/requirements.txt
|
- name: Run QMD unit tests
|
||||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
run: node --test scripts/tests/test-qmd-lib.mjs scripts/tests/test-qmd-daemon.mjs scripts/tests/test-qmd-search.mjs scripts/tests/test-qmd-warm.mjs scripts/tests/test-qmd-benchmark.mjs
|
||||||
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
|
|
||||||
HF_HUB_DISABLE_XET: '1'
|
|
||||||
run: python scripts/tests/test-retrieval-server.py
|
|
||||||
|
|
||||||
security:
|
security:
|
||||||
name: CI configuration security checks
|
name: CI configuration security checks
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
generated/
|
generated/
|
||||||
|
generated.build-*/
|
||||||
|
generated.old-*/
|
||||||
|
generated.lock
|
||||||
.superpowers/
|
.superpowers/
|
||||||
*.log
|
*.log
|
||||||
*.tmp
|
*.tmp
|
||||||
@@ -12,3 +15,7 @@ credentials.json
|
|||||||
secrets.json
|
secrets.json
|
||||||
!.env.example
|
!.env.example
|
||||||
.idea
|
.idea
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
@@ -30,7 +30,7 @@ Bash:
|
|||||||
## 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, Flashbang, status line, Qwen retrieval, and extension choices.
|
Stop if any command fails. Installation asks for the shell, client, Flashbang, status line, local QMD search models, and extension choices.
|
||||||
|
|
||||||
PowerShell:
|
PowerShell:
|
||||||
|
|
||||||
@@ -96,11 +96,10 @@ Install and update also rebuild internally; the explicit build above lets you ch
|
|||||||
- Separate install, update, and uninstall scripts: install refuses to run against an already-installed destination,
|
- Separate install, update, and uninstall scripts: install refuses to run against an already-installed destination,
|
||||||
update refuses to run against one that is not installed yet, and uninstall refuses to run against one that was
|
update refuses to run against one that is not installed yet, and uninstall refuses to run against one that was
|
||||||
never installed.
|
never installed.
|
||||||
- User-level extension installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, QMD, MCPorter,
|
- User-level extension installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, MCPorter,
|
||||||
Humanizer, Impeccable, and Anthropic Frontend Design, with an interactive extension toggle during both install and update. Only extensions
|
Humanizer, Impeccable, and Anthropic Frontend Design, with an interactive extension toggle during both install and update. Only extensions
|
||||||
installed and recorded by this setup can be removed through deselection.
|
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 search for both clients through QMD with Qwen3-Embedding-0.6B, Qwen3-Reranker-0.6B, and QMD Query Expansion 1.7B (Yes/No/Auto; Auto benchmarks GPU, then CPU).
|
||||||
reranking, disabled by default and removable through the install/update choice.
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -108,8 +107,7 @@ Install and update also rebuild internally; the explicit build above lets you ch
|
|||||||
- Bash 4.3 or newer, with standard utilities such as `awk`, `sed`, and `sha256sum`.
|
- Bash 4.3 or newer, with standard utilities such as `awk`, `sed`, and `sha256sum`.
|
||||||
- Python 3.11 or newer for configuration validation and task-state hooks/helpers (`python` for PowerShell scripts, `python3` for Bash scripts).
|
- Python 3.11 or newer for configuration validation and task-state hooks/helpers (`python` for PowerShell scripts, `python3` for Bash scripts).
|
||||||
- `jq` for Claude's Bash status line; without it, that status line produces no output.
|
- `jq` for Claude's Bash status line; without it, that status line produces no output.
|
||||||
- Python `venv` and `pip` when the optional semantic retrieval layer is enabled.
|
- Node.js 22 or newer and `npm` when local QMD search or MCPorter is selected.
|
||||||
- Node.js 22 or newer and `npm` when QMD or MCPorter is selected.
|
|
||||||
|
|
||||||
Core Bash setup scripts do not require PowerShell. The credential commands use Windows Credential Manager and require
|
Core Bash setup scripts do not require PowerShell. The credential commands use Windows Credential Manager and require
|
||||||
PowerShell, including when invoked through their Bash wrappers. The PowerShell scripts remain compatible with
|
PowerShell, including when invoked through their Bash wrappers. The PowerShell scripts remain compatible with
|
||||||
@@ -129,6 +127,11 @@ shared definitions -> client adapters -> generated packages -> user installation
|
|||||||
| [`scripts/`](docs/scripts.md) | Build, installation, update, diagnosis, and validation scripts. |
|
| [`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. |
|
||||||
|
|
||||||
|
Client settings start in `adapters/codex/config/config.toml` and
|
||||||
|
`adapters/claude/config/settings.json`. Shared Markdown and metadata define behavior;
|
||||||
|
adapters supply client-specific formats and placeholders resolved during build or installation.
|
||||||
|
Generated output is not a source to edit.
|
||||||
|
|
||||||
The build creates:
|
The build creates:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -143,16 +146,13 @@ generated/
|
|||||||
|
|
||||||
| Document | Contents |
|
| Document | Contents |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [Architecture](docs/architecture.md) | Shared sources, adapters, generated packages, and installation. |
|
|
||||||
| [Scripts](docs/scripts.md) | Commands, checks, tests, and internal script layout. |
|
| [Scripts](docs/scripts.md) | Commands, checks, tests, and internal script layout. |
|
||||||
| [Configuration](docs/configuration.md) | Client settings, permission defaults, status displays, and verification. |
|
| [Configuration](docs/configuration.md) | Client settings, permissions, status displays, local QMD search, and verification. |
|
||||||
| [Extending the configuration](docs/extending-configuration.md) | Add shared rules, skills, and agents. |
|
| [Extending the configuration](docs/extending-configuration.md) | Add shared rules, skills, and agents. |
|
||||||
| [Plugins](docs/plugins.md) | Plugin sources, selection, installation, updates, and removal. |
|
| [Plugins](docs/plugins.md) | Plugin sources, selection, installation, updates, and removal. |
|
||||||
| [Hooks](docs/hooks.md) | Registered events, session-state helpers, notifications, and optional utilities. |
|
| [Hooks](docs/hooks.md) | Registered events, session-state helpers, notifications, and optional utilities. |
|
||||||
| [Agents](docs/agents.md) | Agent roles and when delegation is useful. |
|
| [Agents](docs/agents.md) | Agent roles and when delegation is useful. |
|
||||||
| [Skills](docs/skills.md) | Skill categories and conditional rule loading. |
|
| [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. |
|
| [Hook package](shared/hooks/README.md) | Overview shipped with the hook scripts. |
|
||||||
| [Repository instructions](AI-Instructions.md) | Rules for maintaining this repository. |
|
| [Repository instructions](AI-Instructions.md) | Rules for maintaining this repository. |
|
||||||
|
|
||||||
@@ -172,7 +172,9 @@ Bash:
|
|||||||
|
|
||||||
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. Each build writes
|
||||||
|
into its own `generated.build-*` directory and replaces `generated/` only after validation, so a failed or parallel
|
||||||
|
build never leaves it partial.
|
||||||
|
|
||||||
## Installation Script
|
## Installation Script
|
||||||
|
|
||||||
@@ -220,15 +222,21 @@ Existing `.agents/skills/backups/` content is moved there on the next install, u
|
|||||||
Codex `config.toml` synchronization merges setup-owned keys while retaining other root keys, extra fields in managed
|
Codex `config.toml` synchronization merges setup-owned keys while retaining other root keys, extra fields in managed
|
||||||
tables, plugin and marketplace tables, other unmanaged tables, and unowned array tables. The previous
|
tables, plugin and marketplace tables, other unmanaged tables, and unowned array tables. The previous
|
||||||
file is backed up when content changes. Claude `settings.json` synchronization retains unrelated settings and hooks.
|
file is backed up when content changes. Claude `settings.json` synchronization retains unrelated settings and hooks.
|
||||||
|
Every managed file, including `config.toml`, `settings.json`, and the manifest, is written to a temporary sibling
|
||||||
|
file and then renamed over the target, so an interrupted run never leaves a partly written file. Each destination is
|
||||||
|
locked through `.ai-config.lock` while it syncs; a parallel install or update fails instead of interleaving writes.
|
||||||
|
A lock left by an exited process of the same shell on the same host is replaced automatically.
|
||||||
|
Before its first write, each sync records its planned changes in `.ai-config-transaction.json`; if a run is interrupted,
|
||||||
|
the next install or update first rolls that destination back from the backups (`ROLLBACK <destination> <stamp>`).
|
||||||
Dry runs still rebuild `generated/`, but do not modify installation destinations.
|
Dry runs still rebuild `generated/`, but do not modify installation destinations.
|
||||||
|
|
||||||
Selecting Codex also installs shared skills to `.agents/skills`. An interactive extension list includes plugins and
|
Selecting Codex also installs shared skills to `.agents/skills`. An interactive extension list includes plugins and
|
||||||
Codex skills; unchecked extensions are skipped on installation. Normal install and update ask whether to enable
|
Codex skills; unchecked extensions are skipped on installation. Normal install and update ask whether to enable
|
||||||
Flashbang, apply the custom status line, and enable Qwen3 semantic retrieval. Choices are saved for Quickupdate.
|
Flashbang, apply the custom status line, and enable local QMD search. Choices are saved for Quickupdate.
|
||||||
The shared skill catalog includes development skills and focused writing, image, site structure, psychology, and
|
The shared skill catalog includes development skills and focused writing, image, site structure, psychology, and
|
||||||
conversion skills. All retained skills are included in both generated clients.
|
conversion skills. All retained skills are included in both generated clients; installation includes `semantic-search` only when local QMD search is enabled.
|
||||||
The status line defaults to enabled to preserve existing behavior. Disabling it removes the custom client setting and,
|
The status line defaults to enabled to preserve existing behavior. Disabling it removes the custom client setting and,
|
||||||
for Claude, the setup-owned status line script. Disabling retrieval on update removes its setup-owned runtime and index.
|
for Claude, the setup-owned status line script. Disabling local QMD search removes its setup-owned package, models, and repository collections once no client still uses it. See [Local QMD search](docs/configuration.md#local-qmd-search) for storage and removal details.
|
||||||
Dry runs preview changes without updating extensions or retrieval. See [Plugins](docs/plugins.md) for client-specific behavior.
|
Dry runs preview changes without updating extensions or retrieval. See [Plugins](docs/plugins.md) for client-specific behavior.
|
||||||
|
|
||||||
## Update Script
|
## Update Script
|
||||||
@@ -255,7 +263,7 @@ setup; pre-existing or manually installed extensions are retained. Quickupdate r
|
|||||||
## Uninstall Script
|
## 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 local QMD search setup,
|
||||||
and the saved Quickupdate selection (once no destination remains installed for any client).
|
and the saved Quickupdate selection (once no destination remains installed for any client).
|
||||||
|
|
||||||
PowerShell:
|
PowerShell:
|
||||||
@@ -281,8 +289,8 @@ touched. Codex skill backups are kept under `.agents/.ai-config-skill-backups/`.
|
|||||||
deleted once it is fully empty; backups already on disk are always kept.
|
deleted once it is fully empty; backups already on disk are always kept.
|
||||||
Extensions and skills recorded in the ledger are removed the same way `update` removes a deselected extension:
|
Extensions and skills recorded in the ledger are removed the same way `update` removes a deselected extension:
|
||||||
pre-existing or manually installed ones outside the ledger are left alone, and a skill file modified since it was
|
pre-existing or manually installed ones outside the ledger are left alone, and a skill file modified since it was
|
||||||
installed is kept with a warning. An enabled semantic retrieval setup is disabled and its setup-owned runtime and
|
installed is kept with a warning. Local QMD search is disabled for the selected client(s). Its shared runtime,
|
||||||
index removed for the selected client(s). The saved Quickupdate selection (`~/.my-ai-configuration/selection.json`)
|
owned models, and recorded repository collections are removed only when no client still uses it; the shared QMD database and foreign collections are retained. The saved Quickupdate selection (`~/.my-ai-configuration/selection.json`)
|
||||||
is only removed once uninstalling leaves no client installed at all, so uninstalling just one client out of a
|
is only removed once uninstalling leaves no client installed at all, so uninstalling just one client out of a
|
||||||
`Both` installation keeps the saved choices for the client that remains.
|
`Both` installation keeps the saved choices for the client that remains.
|
||||||
|
|
||||||
@@ -293,7 +301,9 @@ keep outside this setup's ledger, or files this setup never installed.
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
Codex defaults to workspace-write sandboxing and automatic review for eligible escalation requests. Claude uses
|
Codex defaults to full access under the account that launches it, with unrestricted command network access.
|
||||||
|
Launch Codex as the repository owner; Codex does not switch to that owner automatically. Full access removes
|
||||||
|
Codex's filesystem sandbox boundaries. Eligible approval requests still use automatic review. Claude uses
|
||||||
automatic permissions and enables its sandbox where the platform supports it; explicit deny rules still apply where it
|
automatic permissions and enables its sandbox where the platform supports it; explicit deny rules still apply where it
|
||||||
does not. Both clients include the same logical agent roles and compact status displays. See
|
does not. Both clients include the same logical agent roles and compact status displays. See
|
||||||
[Configuration](docs/configuration.md) for settings, permissions, and limitations.
|
[Configuration](docs/configuration.md) for settings, permissions, and limitations.
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# Claude Code adapter
|
|
||||||
|
|
||||||
Claude Code uses global `CLAUDE.md`, `settings.json`, skills, and agents under the user profile. This adapter supplies client metadata; semantic content remains in `shared/`.
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
role tools
|
role tools
|
||||||
architect Read,Grep,Glob,WebFetch,WebSearch
|
architect Read,Grep,Glob,WebFetch,WebSearch,__SHELL__
|
||||||
researcher Read,Grep,Glob,WebFetch,WebSearch
|
researcher Read,Grep,Glob,WebFetch,WebSearch,__SHELL__
|
||||||
reviewer Read,Grep,Glob,__SHELL__
|
reviewer Read,Grep,Glob,__SHELL__
|
||||||
verifier Read,Grep,Glob,__SHELL__
|
verifier Read,Grep,Glob,__SHELL__
|
||||||
implementer Read,Grep,Glob,Edit,Write,__SHELL__
|
implementer Read,Grep,Glob,Edit,Write,__SHELL__
|
||||||
|
@@ -16,7 +16,10 @@
|
|||||||
},
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"PreCompact": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__COMPACT_SCRIPT__\"", "timeout": 4}]}],
|
"PreCompact": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__COMPACT_SCRIPT__\"", "timeout": 4}]}],
|
||||||
"SessionStart": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__SESSION_POINTER_SCRIPT__\"", "timeout": 4}]}],
|
"SessionStart": [
|
||||||
|
{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__SESSION_POINTER_SCRIPT__\"", "timeout": 4}]},
|
||||||
|
{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__QMD_WARM_SCRIPT__\"", "timeout": 4}]}
|
||||||
|
],
|
||||||
"Stop": [
|
"Stop": [
|
||||||
{
|
{
|
||||||
"hooks": [
|
"hooks": [
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# Codex adapter
|
|
||||||
|
|
||||||
Codex uses global `AGENTS.md`, `config.toml`, skills, and agent configuration under the user profile. This adapter supplies client metadata; semantic content remains in `shared/`.
|
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
approval_policy = "on-request"
|
approval_policy = "on-request"
|
||||||
approvals_reviewer = "auto_review"
|
approvals_reviewer = "auto_review"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "danger-full-access"
|
||||||
|
|
||||||
[sandbox_workspace_write]
|
|
||||||
network_access = true
|
|
||||||
|
|
||||||
[windows]
|
|
||||||
sandbox = "unelevated"
|
|
||||||
|
|
||||||
[tui]
|
[tui]
|
||||||
status_line = ["model", "reasoning", "approval-mode", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]
|
status_line = ["model", "reasoning", "fast-mode", "approval-mode", "five-hour-limit", "weekly-limit", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]
|
||||||
|
|
||||||
[agents]
|
[agents]
|
||||||
enabled = true
|
enabled = true
|
||||||
@@ -18,6 +12,7 @@ max_depth = 1
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
hooks = true
|
hooks = true
|
||||||
|
api_key_model_discovery = true
|
||||||
|
|
||||||
[[hooks.Stop]]
|
[[hooks.Stop]]
|
||||||
[[hooks.Stop.hooks]]
|
[[hooks.Stop.hooks]]
|
||||||
@@ -25,3 +20,10 @@ type = "command"
|
|||||||
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__"'
|
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__"'
|
||||||
command_windows = '__POWERSHELL_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__POWERSHELL_HOOK_SCRIPT__"'
|
command_windows = '__POWERSHELL_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__POWERSHELL_HOOK_SCRIPT__"'
|
||||||
timeout = 8
|
timeout = 8
|
||||||
|
|
||||||
|
[[hooks.SessionStart]]
|
||||||
|
[[hooks.SessionStart.hooks]]
|
||||||
|
type = "command"
|
||||||
|
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/scripts/__QMD_WARM_SCRIPT__"'
|
||||||
|
command_windows = '__POWERSHELL_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/scripts/__QMD_WARM_SCRIPT__"'
|
||||||
|
timeout = 4
|
||||||
@@ -4,7 +4,6 @@ i-have-adhd ayghri/i-have-adhd i-have-adhd@i-have-adhd ayghri/i-have-adhd i-have
|
|||||||
Superpowers anthropics/claude-plugins-official superpowers@claude-plugins-official openai-curated-remote superpowers@openai-curated-remote plugin - -
|
Superpowers anthropics/claude-plugins-official superpowers@claude-plugins-official openai-curated-remote superpowers@openai-curated-remote plugin - -
|
||||||
Context7 upstash/context7 context7@context7-marketplace upstash/context7 context7@context7-marketplace plugin - - context7 https://mcp.context7.com/mcp
|
Context7 upstash/context7 context7@context7-marketplace upstash/context7 context7@context7-marketplace plugin - - context7 https://mcp.context7.com/mcp
|
||||||
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
|
|
||||||
MCPorter - mcporter - mcporter cli mcporter -
|
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
|
||||||
|
|||||||
|
@@ -24,8 +24,8 @@
|
|||||||
"codex": 8510
|
"codex": 8510
|
||||||
},
|
},
|
||||||
"agent_metadata_tokens": {
|
"agent_metadata_tokens": {
|
||||||
"claude": 126,
|
"claude": 204,
|
||||||
"codex": 52
|
"codex": 124
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,5 +9,5 @@ auto_loaded_rule_tokens claude 678
|
|||||||
auto_loaded_rule_tokens codex 678
|
auto_loaded_rule_tokens codex 678
|
||||||
rule_catalog_tokens claude 28818
|
rule_catalog_tokens claude 28818
|
||||||
rule_catalog_tokens codex 8510
|
rule_catalog_tokens codex 8510
|
||||||
agent_metadata_tokens claude 126
|
agent_metadata_tokens claude 204
|
||||||
agent_metadata_tokens codex 52
|
agent_metadata_tokens codex 124
|
||||||
|
+9
-3
@@ -2,14 +2,20 @@
|
|||||||
|
|
||||||
The five shared agents are logical roles, not mandatory steps.
|
The five shared agents are logical roles, not mandatory steps.
|
||||||
|
|
||||||
- `implementer` executes the smallest complete engineering change and performs self-review.
|
- `implementer` implements scoped features and fixes with proportionate tests and self-review.
|
||||||
- `researcher` investigates code, patterns, evidence, alternatives, and unknowns without modifying implementation code by default.
|
- `researcher` answers questions about repository behavior, external documentation, and unknowns with source evidence, without modifying implementation code by default.
|
||||||
- `architect` evaluates technical boundaries, dependencies, interfaces, constraints, and design options.
|
- `architect` compares design options and recommends technical boundaries and interfaces before implementation; implementation changes require an explicit assignment.
|
||||||
- `verifier` supplies execution evidence from relevant tests, builds, checks, generated output, and behavior reproduction. It inspects implementation only to choose checks and does not review the diff for general correctness.
|
- `verifier` supplies execution evidence from relevant tests, builds, checks, generated output, and behavior reproduction. It inspects implementation only to choose checks and does not review the diff for general correctness.
|
||||||
- `reviewer` starts with the actual diff and changed symbols, then examines necessary callers, dependencies, tests, and contracts for analytical correctness. It does not run tests, builds, or runtime checks; it reports missing execution evidence.
|
- `reviewer` starts with the actual diff and changed symbols, then examines necessary callers, dependencies, tests, and contracts for analytical correctness. It does not run tests, builds, or runtime checks; it reports missing execution evidence.
|
||||||
|
|
||||||
Use only the roles that are likely to improve the outcome. Choose `researcher`, `reviewer`, and `verifier` based on the task's uncertainty, complexity, risk, blast radius, and verification value; none runs automatically. The agent may make evidence-based, reversible technical decisions. Ask the user when technical choices remain materially uncertain or when business behavior, user-visible behavior, UI, UX, APIs, database models, configuration formats, compatibility behavior, or other high-impact decisions are ambiguous or open to interpretation.
|
Use only the roles that are likely to improve the outcome. Choose `researcher`, `reviewer`, and `verifier` based on the task's uncertainty, complexity, risk, blast radius, and verification value; none runs automatically. The agent may make evidence-based, reversible technical decisions. Ask the user when technical choices remain materially uncertain or when business behavior, user-visible behavior, UI, UX, APIs, database models, configuration formats, compatibility behavior, or other high-impact decisions are ambiguous or open to interpretation.
|
||||||
|
|
||||||
|
## Research tools
|
||||||
|
|
||||||
|
Claude's [capability manifest](../adapters/claude/capabilities.tsv) gives `architect` and `researcher` file inspection, web research, and the package's shell tool (`PowerShell` or `Bash`). Shell access supports mandatory semantic search and Context7 research through MCPorter. Their role instructions scope shell use to inspection and research; this is an instruction-level boundary, not a read-only shell sandbox.
|
||||||
|
|
||||||
|
The `reviewer` may inspect Git diffs and run required research commands through its shell tool. Tests, builds, and behavior reproduction belong to the `verifier` or parent. Missing execution evidence remains an explicit review gap. Codex receives the same role instructions; the Claude tool manifest is client-specific.
|
||||||
|
|
||||||
## Coordination
|
## Coordination
|
||||||
|
|
||||||
The main agent owns task state, decisions, and progress. Subagents report to their parent. The parent forwards only relevant conclusions, references, risks, and constraints; direct peer communication requires a concrete dependency and parent coordination. Subagents cannot create further subagents.
|
The main agent owns task state, decisions, and progress. Subagents report to their parent. The parent forwards only relevant conclusions, references, risks, and constraints; direct peer communication requires a concrete dependency and parent coordination. Subagents cannot create further subagents.
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# Architecture
|
|
||||||
|
|
||||||
Shared Markdown and metadata define meaning once. Codex and Claude adapters contain only client-specific paths, filenames, and serialization details. The build script renders those inputs into `generated/`. Installation is a separate, explicit step that copies generated files into global user-level locations with backups.
|
|
||||||
|
|
||||||
Generated output is ignored because it is reproducible build output. The source files are the reviewable record.
|
|
||||||
+108
-3
@@ -1,6 +1,19 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Codex receives a generated global `AGENTS.md`, agent TOML files, skills, rule files, and a `config.toml` adapter. It sets workspace sandboxing with network access for shell commands (so MCPorter calls such as Context7 work without approval), automatic approval review, `agents.enabled = true`, `agents.max_concurrent_threads_per_session = 5`, and `agents.max_depth = 1`. Claude Code receives a generated global `CLAUDE.md`, agent Markdown files, skills, and `settings.json` with `auto` permissions, explicit `Agent` and `mcporter call context7.*` shell permissions, `mcp.context7.com` as the only allowed sandbox domain where the Claude sandbox is enabled, a five-operation concurrency default, at most five concurrent subagents (`CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS`), and a subagent spawn depth of one (`CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH`), so subagents cannot start further subagents. Installation resolves user paths at runtime; the repository contains no machine-specific paths.
|
Source templates are [`adapters/codex/config/config.toml`](../adapters/codex/config/config.toml)
|
||||||
|
and [`adapters/claude/config/settings.json`](../adapters/claude/config/settings.json).
|
||||||
|
Installation targets `~/.codex` and `~/.claude`; Codex skills go to `~/.agents/skills`.
|
||||||
|
Choose the PowerShell or Bash package independently of the client.
|
||||||
|
|
||||||
|
Codex receives a generated global `AGENTS.md`, agent TOML files, skills, rule files, and a `config.toml` adapter.
|
||||||
|
It sets full access for commands, automatic approval review, `agents.enabled = true`,
|
||||||
|
`agents.max_concurrent_threads_per_session = 5`, and `agents.max_depth = 1`.
|
||||||
|
|
||||||
|
Claude Code receives a generated global `CLAUDE.md`, agent Markdown files, skills, and `settings.json` with
|
||||||
|
`auto` permissions and explicit `Agent` and `mcporter call context7.*` shell permissions. Where enabled, its sandbox
|
||||||
|
allows the `mcp.context7.com` domain. The adapter sets a five-operation concurrency default, at most five concurrent
|
||||||
|
subagents (`CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS`), and a spawn depth of one (`CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH`).
|
||||||
|
Installation resolves user paths at runtime. Active session policy can further restrict these defaults.
|
||||||
|
|
||||||
Both clients embed `shared/rules/general.md` and the rule-loading conditions from `shared/global-instructions.md`. Rules for languages and frameworks live in their own directories, with an `index.md` entry point and focused Markdown rules under `references/`. Security has its own directory with a baseline entry point and focused rules for authentication, web, APIs, data, files, network, cryptography, and supply chains. Git, Microsoft, and cross-cutting rules remain directly under `shared/rules/`. Codex receives the full directory tree; Claude generates a skill for each rule and copies supporting references alongside the index skill.
|
Both clients embed `shared/rules/general.md` and the rule-loading conditions from `shared/global-instructions.md`. Rules for languages and frameworks live in their own directories, with an `index.md` entry point and focused Markdown rules under `references/`. Security has its own directory with a baseline entry point and focused rules for authentication, web, APIs, data, files, network, cryptography, and supply chains. Git, Microsoft, and cross-cutting rules remain directly under `shared/rules/`. Codex receives the full directory tree; Claude generates a skill for each rule and copies supporting references alongside the index skill.
|
||||||
|
|
||||||
@@ -12,11 +25,30 @@ Client settings remain thin adapters. Shared semantics remain in `shared/`. User
|
|||||||
|
|
||||||
## Permission defaults
|
## Permission defaults
|
||||||
|
|
||||||
Codex sets `approval_policy = "on-request"`, `approvals_reviewer = "auto_review"`, and `sandbox_mode = "workspace-write"`. Eligible approval requests go through automatic review. Claude sets `permissions.defaultMode = "auto"` and shell-specific deny rules. Its sandbox is enabled on supported macOS, Linux, and WSL2 environments and disabled where unavailable, including native Windows. These are generated defaults, not a guarantee that a running session, account policy, or explicit launch override uses the same mode.
|
Codex sets `sandbox_mode = "danger-full-access"`, `approval_policy = "on-request"`, and
|
||||||
|
`approvals_reviewer = "auto_review"`. All Codex commands run without Codex filesystem or network sandbox
|
||||||
|
restrictions, under the account that launches Codex. Launch Codex as the repository owner to use that identity;
|
||||||
|
this configuration does not impersonate another user or change filesystem ownership. OS permissions, firewalls,
|
||||||
|
and managed organization policies still apply. Eligible approval requests retain automatic review, which does
|
||||||
|
not restore a filesystem boundary. See the [official OpenAI security documentation](https://learn.chatgpt.com/docs/agent-approvals-security).
|
||||||
|
|
||||||
|
The adapter no longer selects the Windows sandbox or workspace writable roots. This avoids using the dedicated
|
||||||
|
`CodexSandboxOnline` account, whose identity differs from the repository owner and can trigger Git's ownership
|
||||||
|
check. Old `[windows]` and `[sandbox_workspace_write]` tables may survive an update as preserved local settings;
|
||||||
|
they do not activate sandboxing while `sandbox_mode` is `danger-full-access`.
|
||||||
|
|
||||||
|
Apply the generated configuration through the normal update process, then start a new Codex session under the
|
||||||
|
repository owner's account. Existing sessions keep their active permissions. Check `whoami` and
|
||||||
|
`git rev-parse --show-toplevel` there; package validation cannot prove the identity or network access of a future
|
||||||
|
session. Launch overrides and managed policies may restrict or reject the configured mode.
|
||||||
|
|
||||||
|
Claude retains `permissions.defaultMode = "auto"` and shell-specific deny rules. Its sandbox is enabled on
|
||||||
|
supported macOS, Linux, and WSL2 environments and disabled where unavailable, including native Windows.
|
||||||
|
These are generated defaults; active session or account policy may differ.
|
||||||
|
|
||||||
## Status displays
|
## Status displays
|
||||||
|
|
||||||
Codex's native `tui.status_line` contains `model`, `reasoning`, `approval-mode`, `project-name`, `git-branch`, `context-window-size`, `context-used`, and `used-tokens`, in that order. `approval-mode` reports the active command review mode. Rendering and colors are controlled by Codex; the adapter does not define custom colors per field.
|
Codex's native `tui.status_line` contains `model`, `reasoning`, `fast-mode`, `approval-mode`, `five-hour-limit`, `weekly-limit`, `project-name`, `git-branch`, `context-window-size`, `context-used`, and `used-tokens`, in that order. `fast-mode` shows `Fast on` or `Fast off` for supported models; it does not change the selected mode. `five-hour-limit` and `weekly-limit` show remaining usage in the primary and secondary quota windows, typically five hours and one week, and are omitted when Codex has no quota data. `approval-mode` reports the active command review mode. Rendering and colors are controlled by Codex; the adapter does not define custom colors per field. These native fields are supported by [Codex 0.161.0](https://github.com/openai/codex/blob/rust-v0.161.0/codex-rs/tui/src/chatwidget/status_surfaces.rs).
|
||||||
|
|
||||||
Claude runs `shared/statusline/statusline.ps1` for PowerShell or `statusline.sh` for Bash. Its two-line display is:
|
Claude runs `shared/statusline/statusline.ps1` for PowerShell or `statusline.sh` for Bash. Its two-line display is:
|
||||||
|
|
||||||
@@ -31,6 +63,79 @@ Model and context-window size are cyan, effort and cumulative tokens magenta, re
|
|||||||
|
|
||||||
Claude's status line displays `permission_mode` or `permissionMode` when Claude provides it, and otherwise shows the configured `auto` review mode.
|
Claude's status line displays `permission_mode` or `permissionMode` when Claude provides it, and otherwise shows the configured `auto` review mode.
|
||||||
|
|
||||||
|
## Local QMD search
|
||||||
|
|
||||||
|
Install and update offer **Yes / No / Auto**, with No as the initial default. This option is separate
|
||||||
|
from the extension selector. The saved `semantic_retrieval` key keeps its name for compatibility.
|
||||||
|
Quickupdate reuses the saved choice and device; it does not rerun the benchmark.
|
||||||
|
|
||||||
|
- **Yes:** install `@tobilu/qmd@2.8.3` if QMD is absent, copy the shared runtime scripts, configure
|
||||||
|
and download the three models, and choose GPU when detected or CPU otherwise. An existing QMD
|
||||||
|
installation is reused, not upgraded by this step.
|
||||||
|
- **Auto:** prepare the same dependencies, then benchmark GPU when available and CPU if needed.
|
||||||
|
Both thresholds must pass: median warm search latency at most 5 seconds and embedding throughput
|
||||||
|
at least 20 chunks/second. Each device has a 10-minute timeout. No passing device disables search
|
||||||
|
and triggers setup-owned resource cleanup. Auto can download models while answering the setup prompt.
|
||||||
|
- **No:** omit the search skill and its instruction line, remove the warm-up hook registration,
|
||||||
|
and disable search for the selected clients. Dry runs do not download, benchmark, or remove QMD resources.
|
||||||
|
|
||||||
|
The models are Qwen3-Embedding-0.6B, Qwen3-Reranker-0.6B, and QMD Query Expansion 1.7B.
|
||||||
|
Exact model URIs live in [`shared/qmd/qmd-lib.mjs`](../shared/qmd/qmd-lib.mjs).
|
||||||
|
Auto thresholds can be overridden with `QMD_BENCHMARK_MAX_QUERY_SECONDS` and
|
||||||
|
`QMD_BENCHMARK_MIN_CHUNKS_PER_SECOND`. The benchmark uses a temporary database and a deterministic
|
||||||
|
100-document fixture; its measurements are not an end-to-end estimate for a user's repository.
|
||||||
|
|
||||||
|
### Search and storage
|
||||||
|
|
||||||
|
The `semantic-search` skill calls the installed wrapper from the current Git repository:
|
||||||
|
|
||||||
|
```text
|
||||||
|
node "$HOME/.my-ai-configuration/qmd/qmd-search.mjs" --query "Where are temporary upload failures retried?" --top-k 5
|
||||||
|
```
|
||||||
|
|
||||||
|
This command works in PowerShell and Bash. `--root` selects a repository explicitly. Results are JSON
|
||||||
|
objects with `path`, `line`, `score`, and `snippet`; repository hits use relative paths and other collections
|
||||||
|
use `qmd://` URIs. Known identifiers still use direct reads or `rg`; unknown identifiers use semantic retrieval first.
|
||||||
|
|
||||||
|
A token-protected daemon listens on `127.0.0.1`, refreshes changed repository collections, skips git-ignored
|
||||||
|
paths, and searches the current repository together with the user's default QMD collections. Repository
|
||||||
|
collections use `repo-` followed by a 12-character path hash. A SessionStart hook warms the daemon and index
|
||||||
|
in the background. Models stay loaded until the daemon stops after 30 idle minutes, configurable with
|
||||||
|
`QMD_IDLE_MINUTES`.
|
||||||
|
|
||||||
|
On daemon failure the wrapper tries the QMD CLI against the current repository collection only. This fallback
|
||||||
|
requires an existing index; it does not create or refresh the collection. If the wrapper fails, the skill
|
||||||
|
reports the limitation and continues with `rg`.
|
||||||
|
|
||||||
|
| Location | Contents |
|
||||||
|
| --- | --- |
|
||||||
|
| `~/.my-ai-configuration/qmd/` | Runtime scripts, daemon state, locks, collection ownership, and `warm.log`. |
|
||||||
|
| `~/.my-ai-configuration/qmd.json` | Selected device, enabled clients, and setup ownership records. |
|
||||||
|
| `$XDG_CACHE_HOME/qmd/` (default `~/.cache/qmd/`) | Shared `index.sqlite` and model cache. |
|
||||||
|
| `$QMD_CONFIG_DIR/index.yml` | Configuration when `QMD_CONFIG_DIR` is set; otherwise `$XDG_CONFIG_HOME/qmd/index.yml`, defaulting to `~/.config/qmd/index.yml`. |
|
||||||
|
|
||||||
|
With the default Codex full-access configuration, QMD uses the launching account's filesystem and network
|
||||||
|
permissions, including custom cache and configuration paths. If a session overrides this with a sandbox,
|
||||||
|
permit writes to the runtime directory, resolved cache, and QMD configuration directory. QMD writes configuration
|
||||||
|
when registering collections and persisting ignore lists.
|
||||||
|
|
||||||
|
Disabling search or uninstalling one client keeps the shared runtime while another client remains enabled.
|
||||||
|
After the last client disables it, cleanup stops the daemon, removes recorded repository collections and
|
||||||
|
setup-owned models/package, restores the previous `models:` block, and removes the runtime scripts.
|
||||||
|
It preserves foreign QMD installations, models, collections, and the shared database. Failed cleanup can leave
|
||||||
|
resources behind; inspect reported warnings rather than deleting the whole cache. Updates also migrate the
|
||||||
|
old setup-owned Python retrieval directory and QMD extension ownership records.
|
||||||
|
|
||||||
|
### Verification and troubleshooting
|
||||||
|
|
||||||
|
Check `~/.my-ai-configuration/qmd/warm.log` for background warm-up errors. First startup or changed repositories
|
||||||
|
can take longer than a warm query. An older session still using the Windows sandbox can fail Git ownership
|
||||||
|
checks or child-process pipe operations. Apply the configuration and start a new session as described in
|
||||||
|
[Permission defaults](#permission-defaults).
|
||||||
|
|
||||||
|
Model-free unit tests do not verify downloads, GPU support, sandbox access, or real search quality.
|
||||||
|
See [QMD tests](scripts.md#qmd-tests) for the separate test commands and dependency setup.
|
||||||
|
|
||||||
## Verification and installation
|
## Verification and installation
|
||||||
|
|
||||||
The agent chooses the smallest sufficient checks based on behavior, affected callers, risk, reversibility, and uncertainty. Inspection can suffice for a small change. Full builds, broad test suites, and independent reviews are not automatic; explicit user and project requirements remain binding.
|
The agent chooses the smallest sufficient checks based on behavior, affected callers, risk, reversibility, and uncertainty. Inspection can suffice for a small change. Full builds, broad test suites, and independent reviews are not automatic; explicit user and project requirements remain binding.
|
||||||
|
|||||||
+5
-2
@@ -9,10 +9,13 @@ The adapters register these events:
|
|||||||
| Codex and Claude | `Stop` | Run the finish notification (`flashbang`). |
|
| Codex and Claude | `Stop` | Run the finish notification (`flashbang`). |
|
||||||
| Claude | `PreCompact` | Record the compaction timestamp under the installed configuration root and point to existing workspace task state. |
|
| Claude | `PreCompact` | Record the compaction timestamp under the installed configuration root and point to existing workspace task state. |
|
||||||
| Claude | `SessionStart` | Print an absolute pointer if the workspace's `.ai-session/state.json` exists. |
|
| Claude | `SessionStart` | Print an absolute pointer if the workspace's `.ai-session/state.json` exists. |
|
||||||
|
| Codex and Claude | `SessionStart` | Warm the QMD index and search daemon in the background (only while local QMD search is enabled). |
|
||||||
|
|
||||||
The main agent alone maintains canonical task state. Hooks never write or summarize it, print its contents, or infer completed verification. They use the hook event's `cwd`, falling back to the process working directory when no workspace is supplied. Malformed event data does not fall back to another project. An absent state file is a successful no-op. Legacy state under the installed configuration root is not automatically adopted; confirm its task before migrating it. The scripts do not grant permissions, bypass approvals, or replace client permission systems.
|
The main agent alone maintains canonical task state. Hooks never write or summarize it, print its contents, or infer completed verification. The task-state hooks use the hook event's `cwd`, falling back to the process working directory when no workspace is supplied. Malformed event data does not fall back to another project. An absent state file is a successful no-op. Legacy state under the installed configuration root is not automatically adopted; confirm its task before migrating it. The scripts do not grant permissions, bypass approvals, or replace client permission systems.
|
||||||
|
|
||||||
When enabled during installation or update, Flashbang runs on `Stop`, after the main agent finishes responding. The default flash lasts about 500 milliseconds. The PowerShell command omits `-WindowStyle Hidden`, which can hide or minimize the shared terminal. The PowerShell hook creates a low-opacity click-through overlay without activating another window when its native GUI APIs are available; unsupported runtimes exit safely. The Bash hook uses Python's `tkinter`, uses `xrandr` for multi-monitor bounds when available, and falls back to `notify-send` when the overlay is unavailable. Registered hooks are best effort: errors do not fail a Claude session.
|
The QMD warm-up hook uses the process working directory and starts only inside a Git repository when Node.js and the installed wrapper are available. It runs detached; wrapper errors are recorded in `~/.my-ai-configuration/qmd/warm.log`. Disabling local search removes its hook registration. See [Local QMD search](configuration.md#local-qmd-search) for daemon behavior and storage.
|
||||||
|
|
||||||
|
When enabled during installation or update, Flashbang runs on `Stop`, after the main agent finishes responding. The default flash lasts about 500 milliseconds. The PowerShell command omits `-WindowStyle Hidden`, which can hide or minimize the shared terminal. The PowerShell hook creates a low-opacity click-through overlay without activating another window when its native GUI APIs are available; unsupported runtimes exit safely. Its native DLL cache is optional: a missing `LOCALAPPDATA` environment variable or an unusable cache falls back to in-process compilation instead of suppressing the notification. The Bash hook uses Python's `tkinter`, uses `xrandr` for multi-monitor bounds when available, and falls back to `notify-send` when the overlay is unavailable. Registered hooks are best effort: errors do not fail a Claude session.
|
||||||
|
|
||||||
`Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are unregistered utilities. The safety utilities check command text for selected dangerous patterns; the PowerShell variant also checks some absolute paths. Neither is a complete security boundary. The verification utilities explicitly run the repository build when invoked; they are not an automatic requirement after each change.
|
`Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are unregistered utilities. The safety utilities check command text for selected dangerous patterns; the PowerShell variant also checks some absolute paths. Neither is a complete security boundary. The verification utilities explicitly run the repository build when invoked; they are not an automatic requirement after each change.
|
||||||
|
|
||||||
|
|||||||
+39
-4
@@ -2,10 +2,39 @@
|
|||||||
|
|
||||||
The user-level plugin manifest is `adapters/plugins.tsv`. It maps each logical extension to the client-specific plugin, skill, or MCP installation method.
|
The user-level plugin manifest is `adapters/plugins.tsv`. It maps each logical extension to the client-specific plugin, skill, or MCP installation method.
|
||||||
|
|
||||||
The current baseline includes Ponytail, i-have-adhd, Superpowers, Context7, Caveman, QMD, MCPorter, Humanizer, Impeccable, and Anthropic Frontend Design. Both scripts operate at user scope and show an extension selector unless running a dry run. Selections are collected before configuration changes. Both rebuild generated packages first.
|
The current baseline includes Ponytail, i-have-adhd, Superpowers, Context7, Caveman, MCPorter, Humanizer, Impeccable, and Anthropic Frontend Design. Install and update operate at user scope and show an extension selector unless running a dry run or Quickupdate. Selections are collected before configuration changes. Both rebuild generated packages before synchronizing client files and extensions. Choosing Auto for local search can prepare and benchmark QMD earlier, during option selection.
|
||||||
|
|
||||||
|
### Automatic workflow integration
|
||||||
|
|
||||||
|
The always-loaded general rules route both clients to the shared `plugin-workflow` skill at session start/resume and task or phase changes. The policy requires Superpowers at development phase boundaries, Ponytail for implementation and review, Impeccable for UI planning through verification, and i-have-adhd response structure with Caveman's concise style when available. Superpowers owns process; Ponytail constrains implementation to the smallest complete, verified change; Impeccable owns UI decisions; Frontend Design supplements uncovered needs without starting another workflow. User authorization, research gates, and proportionate checks retain precedence.
|
||||||
|
|
||||||
|
The installed i-have-adhd 0.4.1 skill declares `disable-model-invocation: true`. The shared policy therefore supplies automatic response guidance without forcing a manual-only skill invocation or relying on its optional hook. It does not diagnose the user. Style stop commands persist through task changes and resume; `normal mode` disables i-have-adhd, Caveman, and Ponytail for the session. Explicit mode levels override defaults. Missing plugins are reported once and do not trigger installation. These are instruction-level requirements, not runtime interception or a guarantee of model compliance. Existing installations receive them through the normal configuration update; builds do not modify local plugin caches or hook trust.
|
||||||
|
|
||||||
|
### Context7 routing
|
||||||
|
|
||||||
Selecting Context7 with MCPorter registers the server in MCPorter. Selecting Context7 without MCPorter installs its native client plugin; that explicit selection permits the native MCP route under the global instructions.
|
Selecting Context7 with MCPorter registers the server in MCPorter. Selecting Context7 without MCPorter installs its native client plugin; that explicit selection permits the native MCP route under the global instructions.
|
||||||
|
|
||||||
|
Both clients receive a mandatory Context7-first research gate through the shared general rules and `technical-research` skill. It triggers whenever answers, planning, coding, reviews, debugging, tests, configuration, or upgrades rely on external technology behavior, without requiring the user to mention Context7. The agent checks project versions, resolves the library, queries the relevant behavior, and verifies the original source. Matching evidence from the same task can be reused across phases and handoffs; repository-only facts need no external call. Missing coverage, version mismatches, and service failures require an explicit fallback to authoritative sources. [Phase checkpoints](skills.md) apply inside plugin workflows as well as the focused shared skills. These are agent instructions, not a runtime enforcement hook.
|
||||||
|
|
||||||
|
### Optional Context7 health check
|
||||||
|
|
||||||
|
For a configured MCPorter route, run these commands in PowerShell or Bash. They make live requests using a public Angular question; they do not install or change configuration. Routine builds and doctor runs do not run this check.
|
||||||
|
|
||||||
|
```text
|
||||||
|
mcporter list context7 --schema
|
||||||
|
mcporter call context7.resolve-library-id 'libraryName=Angular' 'query=How do Angular computed signals track dependencies?' --timeout 30000
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that discovery lists `resolve-library-id` and `query-docs` and resolution returns the official Angular documentation. Then query the returned ID (the example assumes `/websites/angular_dev` was returned):
|
||||||
|
|
||||||
|
```text
|
||||||
|
mcporter call context7.query-docs 'libraryId=/websites/angular_dev' 'query=How do Angular computed signals track dependencies?' --timeout 30000
|
||||||
|
```
|
||||||
|
|
||||||
|
The check passes only if documentation retrieval returns relevant content with an original Angular source link. Empty results, error text, or merely a zero process exit code are insufficient. This checks connectivity and one library, not general coverage or agent compliance. For a native plugin installation, run the equivalent discovery and tool calls inside that client; do not add an MCPorter registration just for this check.
|
||||||
|
|
||||||
|
### Installation and updates
|
||||||
|
|
||||||
`install.ps1`/`install.sh` starts every plugin checked by default; unchecking one skips it. It refuses to run against an already-installed destination (`Already installed, use the update script.`).
|
`install.ps1`/`install.sh` starts every plugin checked by default; unchecking one skips it. It refuses to run against an already-installed destination (`Already installed, use the update script.`).
|
||||||
|
|
||||||
`update.ps1`/`update.sh` lets you revise the saved extension selection. Checking an extension installs it if missing or updates it if owned; existing foreign installations are preserved. Deselection removes only extensions that this setup installed and recorded as owned. Pre-existing or manually installed plugins and skills are retained. Extensions removed from the manifest are also removed when recorded as owned; unrelated installations are retained. Update refuses to run if any selected destination lacks an installation manifest (`Not installed, use the install script.`).
|
`update.ps1`/`update.sh` lets you revise the saved extension selection. Checking an extension installs it if missing or updates it if owned; existing foreign installations are preserved. Deselection removes only extensions that this setup installed and recorded as owned. Pre-existing or manually installed plugins and skills are retained. Extensions removed from the manifest are also removed when recorded as owned; unrelated installations are retained. Update refuses to run if any selected destination lacks an installation manifest (`Not installed, use the install script.`).
|
||||||
@@ -16,11 +45,17 @@ Codex receives Caveman through its native plugin adapter. Humanizer, Impeccable,
|
|||||||
|
|
||||||
On Windows, installation and update repair the known Caveman 1.0.0 hook launcher in setup-owned Codex installations so it resolves the plugin path under PowerShell as well as Command Prompt. Custom commands and foreign installations are preserved. Review the changed hook definitions in Codex `/hooks`; setup does not grant hook trust automatically.
|
On Windows, installation and update repair the known Caveman 1.0.0 hook launcher in setup-owned Codex installations so it resolves the plugin path under PowerShell as well as Command Prompt. Custom commands and foreign installations are preserved. Review the changed hook definitions in Codex `/hooks`; setup does not grant hook trust automatically.
|
||||||
|
|
||||||
[QMD](https://github.com/tobi/qmd) requires Node.js 22 or newer and `npm`. Selecting it installs `@tobilu/qmd` globally. Claude receives QMD's official `qmd@qmd` plugin; Codex can use the `qmd` CLI. Deselecting QMD removes setup-owned integrations and uninstalls the global package when the Codex-owned QMD record is removed.
|
|
||||||
|
|
||||||
Normal installation and update ask whether to enable Flashbang, apply the custom status line, and enable the optional Qwen3-Embedding-0.6B semantic retrieval layer. Retrieval uses a per-user virtual environment and index and is invoked through the installed `server.py search` CLI, disabled by default. Quickupdate reuses the saved shell, client, status line, Flashbang, retrieval, and extension choices without opening menus. Disabling the status line removes its custom setting and setup-owned Claude script; disabling retrieval removes its setup-owned runtime and index. Only successful runs save selections; dry runs do not update extensions or retrieval. Install and update do not modify the Codex or Claude CLI installations.
|
### Setup options outside the extension selector
|
||||||
|
|
||||||
The `server.py search`/`rebuild` CLI transparently reuses a background daemon (spawned automatically on first use per `--data-dir`) that keeps the embedding and reranking models loaded in memory, so only the first query after a cold start or an idle period pays the model-loading cost; every call falls back to loading the models directly in-process if the daemon cannot be reached or started. The daemon exits on its own after 30 minutes of inactivity. Search requires a Git repository and indexes tracked files plus untracked files that Git does not ignore. Indexing skips files larger than 512 KB and, per repository, stops after 2000 indexable files by default; set `SEMANTIC_RETRIEVAL_MAX_FILES` to raise that cap for larger monorepos. When the cap is reached, the CLI prints an explicit warning to stderr rather than silently omitting files.
|
Flashbang, the custom status line, and local QMD search have separate prompts during install and update.
|
||||||
|
Quickupdate reuses their saved choices together with the shell, client, and extension selection. Only successful
|
||||||
|
runs save selections; dry runs do not update extensions or local search. Disabling the status line removes its
|
||||||
|
custom setting and setup-owned Claude script.
|
||||||
|
|
||||||
|
QMD is managed by the local-search option, not as a plugin. See [Local QMD search](configuration.md#local-qmd-search)
|
||||||
|
for models, Auto thresholds, runtime behavior, sandbox paths, and ownership-aware removal.
|
||||||
|
Install and update do not modify the Codex or Claude CLI installations.
|
||||||
|
|
||||||
Plugin installation requires the corresponding client CLI, network access, and any authentication required by the plugin. Context7 may open an OAuth flow. Codex or plugin hooks may require a trust review in `/hooks`. The scripts never install plugins during a build or doctor run and never install them when `--dry-run` or `-DryRun` is used.
|
Plugin installation requires the corresponding client CLI, network access, and any authentication required by the plugin. Context7 may open an OAuth flow. Codex or plugin hooks may require a trust review in `/hooks`. The scripts never install plugins during a build or doctor run and never install them when `--dry-run` or `-DryRun` is used.
|
||||||
|
|
||||||
|
|||||||
+70
-8
@@ -22,6 +22,8 @@ Run scripts from the repository root. PowerShell and Bash commands have equivale
|
|||||||
| `commands/uninstall.ps1`, `commands/uninstall.sh` | Remove setup-owned configuration and extensions. |
|
| `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/doctor.ps1`, `commands/doctor.sh` | Check generated output and installed configuration. |
|
||||||
| `commands/prompt-budget.ps1`, `commands/prompt-budget.sh` | Track permanent context, skill metadata, the rule catalog, and agent metadata; warn above 10,000 permanent-context tokens, fail when any token estimate exceeds its baseline by 10%, or permanent context exceeds 15,000 tokens. |
|
| `commands/prompt-budget.ps1`, `commands/prompt-budget.sh` | Track permanent context, skill metadata, the rule catalog, and agent metadata; warn above 10,000 permanent-context tokens, fail when any token estimate exceeds its baseline by 10%, or permanent context exceeds 15,000 tokens. |
|
||||||
|
| `commands/context-report.ps1`, `commands/context-report.sh` | Report the context installed Claude and Codex actually load, from Claude `/context`, `/skill-doctor`, and its init event, and Codex `debug prompt-input`: categories, active plugins, listed skills, installed skills missing from the listing, and skills listed without description context. `--check` fails on missing or description-less skills. No model request is made. |
|
||||||
|
| `commands/benchmark.ps1`, `commands/benchmark.sh` | Compare real Claude/Codex sessions with and without the setup. See [Session benchmarks](#session-benchmarks) for isolation, costs, and the search comparison. |
|
||||||
| `commands/repository-context.ps1`, `commands/repository-context.sh` | Refresh the local repository-context cache. |
|
| `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/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. |
|
| `commands/telemetry.ps1`, `commands/telemetry.sh` | Append a local telemetry event. |
|
||||||
@@ -30,26 +32,38 @@ Run scripts from the repository root. PowerShell and Bash commands have equivale
|
|||||||
| `commands/add-credentials.ps1`, `commands/add-credentials.sh` | Store a client-scoped tool credential in the operating-system credential store. |
|
| `commands/add-credentials.ps1`, `commands/add-credentials.sh` | Store a client-scoped tool credential in the operating-system credential store. |
|
||||||
| `commands/remove-credentials.ps1`, `commands/remove-credentials.sh` | Delete a selected client-scoped credential from the operating-system credential store. |
|
| `commands/remove-credentials.ps1`, `commands/remove-credentials.sh` | Delete a selected client-scoped credential from the operating-system credential store. |
|
||||||
|
|
||||||
Prompt KPIs estimate tokens as UTF-8 bytes divided by four, using the larger shell package per client. Skill metadata counts `SKILL.md` frontmatter outside rule skills; the rule catalog counts all generated rule entry files but excludes references, so it measures the available catalog size rather than rules loaded in a typical session; agent metadata counts frontmatter for Claude and `name`/`description` for Codex. All metrics fail CI above baseline +10%; permanent context also warns above 10,000 tokens and fails above 15,000 tokens. The baseline covers the retained skill catalog. Project-specific instructions, external plugin prompt bodies, and live MCP/tool schemas are not measured.
|
Prompt KPIs estimate tokens as UTF-8 bytes divided by four, using the larger shell package per client.
|
||||||
|
Skill metadata counts every `SKILL.md` frontmatter, including Claude's generated rule skills. Permanent context
|
||||||
|
combines global instructions, skill and agent metadata, and the security baseline loaded for development tasks.
|
||||||
|
The rule catalog counts Codex rule files outside `references/` and Claude's generated `rules-*/SKILL.md` entries;
|
||||||
|
it measures available guidance, not all rules loaded in a typical session. Agent metadata counts Claude frontmatter
|
||||||
|
and Codex `name`/`description` fields. All metrics fail above baseline +10%; permanent context also warns above
|
||||||
|
10,000 tokens and fails above 15,000. Project instructions, external plugin bodies, and live MCP/tool schemas
|
||||||
|
are excluded. `context-report` complements this estimate with installed-client discovery.
|
||||||
|
|
||||||
Custom instructions are read before generated rules, skills, and plugins. They remain under `~/.my-ai-configuration/instructions/`, so installation and update scripts never manage or reset them.
|
Custom instructions remain under `~/.my-ai-configuration/instructions/`; installation and update scripts never reset their contents. Only clients with a nonempty instruction file receive a startup rule in their installed `AGENTS.md` or `CLAUDE.md`. That rule requires immediate loading before any response or task work and keeps the instructions active throughout the session, including after compaction. Adding or removing instructions refreshes the rule in an existing installation; empty or missing instruction files leave no rule. Generated packages contain no personal instruction rule. Changes apply when the client next loads its global instructions.
|
||||||
To undo an added instruction, pass the same text to `remove-instruction` with `-Client`/`--client` and `-Instruction`/`--instruction`. It removes the last exact matching block per selected client and preserves other text. A missing match leaves the file unchanged.
|
To undo an added instruction, pass the same text to `remove-instruction` with `-Client`/`--client` and `-Instruction`/`--instruction`. It removes the last exact matching block per selected client and preserves other text. A missing match leaves the file unchanged.
|
||||||
|
|
||||||
Credential commands currently require PowerShell and Windows Credential Manager, including their Bash wrappers. Use `~/.my-ai-configuration/bin/with-credential.ps1` or `.sh` to run a tool with a stored key available only in that child process. The key must be a valid environment-variable name; its value is never written to configuration.
|
Credential commands currently require PowerShell and Windows Credential Manager, including their Bash wrappers. Use `~/.my-ai-configuration/bin/with-credential.ps1` or `.sh` to run a tool with a stored key available only in that child process. The key must be a valid environment-variable name; its value is never written to configuration.
|
||||||
Use `remove-credentials` with `-Client`/`--client` and `-Key`/`--key` to delete only that credential. The shared `with-credential` helpers remain installed for other keys.
|
Use `remove-credentials` with `-Client`/`--client` and `-Key`/`--key` to delete only that credential. The shared `with-credential` helpers remain installed for other keys.
|
||||||
|
|
||||||
Qwen Auto selection uses `shared/retrieval/benchmark.py`; run it through `scripts/lib/semantic-retrieval.py benchmark --root . --home <home-directory>`. This warm-model check uses synthetic, repeated short documents. Its result does not measure model startup, first indexing, real code chunks, CLI latency, or memory use; do not use it as an end-to-end performance estimate.
|
The A/B benchmark measures whole sessions. Auto selection of local QMD search uses `shared/qmd/qmd-benchmark.mjs`, run through `scripts/lib/qmd.py`. It measures median search latency and embedding throughput on GPU, then CPU, and is not an end-to-end performance estimate.
|
||||||
|
|
||||||
## Checks and tests
|
## Checks and tests
|
||||||
|
|
||||||
Gitea Actions runs the repository checks from `.gitea/workflows/ci.yml`.
|
Gitea Actions runs the repository checks from `.gitea/workflows/ci.yml`.
|
||||||
|
|
||||||
|
Doctor captures native-command diagnostics and checks exit codes. A warning on stderr does not abort the
|
||||||
|
PowerShell 5.1 script; failed version or configuration checks contribute to the final failure summary.
|
||||||
|
On Windows, an unavailable profile-folder lookup falls back to PowerShell's home path.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `checks/validate-config.py` | Validate generated Codex TOML and Claude JSON. |
|
| `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-build-validation.ps1`, `tests/test-build-validation.sh` | Validate build failures and generated output. |
|
||||||
| `tests/test-capabilities.ps1` | Guard PowerShell capability detection. |
|
| `tests/test-capabilities.ps1` | Guard PowerShell capability detection. |
|
||||||
| `tests/test-install-guards.ps1`, `tests/test-install-guards.sh` | Validate install and update guard conditions. |
|
| `tests/test-install-guards.ps1`, `tests/test-install-guards.sh` | Validate install and update guard conditions. |
|
||||||
|
| `tests/test-config-effects.py` | Check that generated limits and permissions take effect: Codex keeps the V1 multi-agent implementation that honors `agents.max_depth`; with `AI_CONFIG_LIVE_TESTS=1`, short Haiku sessions confirm that Claude withholds the Agent tool from subagents, refuses subagents above the concurrency limit, and blocks `git reset --hard` in every shell tool. |
|
||||||
| `tests/test-client-smoke.py` | Install a package into a temporary home with foreign settings and ask the installed Claude and Codex CLIs which skills, agents, and hooks they discover, without completing a model request. Skips a client whose CLI is unavailable unless `AI_CONFIG_REQUIRE_CLIENTS=1`, which the CI `client-smoke` job sets. |
|
| `tests/test-client-smoke.py` | Install a package into a temporary home with foreign settings and ask the installed Claude and Codex CLIs which skills, agents, and hooks they discover, without completing a model request. Skips a client whose CLI is unavailable unless `AI_CONFIG_REQUIRE_CLIENTS=1`, which the CI `client-smoke` job sets. |
|
||||||
| `tests/test-install-options.py` | Validate interactive option handling and generated manifest changes. |
|
| `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-extensions.py` | Validate plugin, skill, and CLI ownership handling. |
|
||||||
@@ -60,16 +74,64 @@ Gitea Actions runs the repository checks from `.gitea/workflows/ci.yml`.
|
|||||||
| `tests/test-quick-update.py` | Validate saved Quickupdate choices. |
|
| `tests/test-quick-update.py` | Validate saved Quickupdate choices. |
|
||||||
| `tests/test-remove-customizations.py` | Validate targeted instruction and credential removal without accessing the real credential store. |
|
| `tests/test-remove-customizations.py` | Validate targeted instruction and credential removal without accessing the real credential store. |
|
||||||
| `tests/test-quick-update.ps1`, `tests/test-quick-update.sh` | Run the Quickupdate test through the relevant shell. |
|
| `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-qmd-lib.mjs`, `test-qmd-daemon.mjs`, `test-qmd-search.mjs`, `test-qmd-warm.mjs`, `test-qmd-benchmark.mjs` | Model-free QMD unit tests; require the YAML dependency. |
|
||||||
| `tests/test-semantic-retrieval.py` | Validate retrieval installation and removal. |
|
| `tests/test-qmd-integration.mjs` | Manual integration test using real QMD and downloaded models; excluded from CI. |
|
||||||
|
| `tests/test-qmd-setup.py` | Validate QMD installation, migration, and removal. |
|
||||||
| `tests/test-session-state.py` | Validate session-state helpers. |
|
| `tests/test-session-state.py` | Validate session-state helpers. |
|
||||||
| `tests/test-shell-packages.ps1`, `tests/test-shell-packages.sh` | Validate generated package contents and client settings. |
|
| `tests/test-shell-packages.ps1`, `tests/test-shell-packages.sh` | Validate generated package contents and client settings. |
|
||||||
| `tests/test-runtime-status.ps1`, `tests/test-runtime-status.sh` | Validate build summaries and status-line output. |
|
| `tests/test-runtime-status.ps1`, `tests/test-runtime-status.sh` | Validate build summaries and status-line output. |
|
||||||
| `tests/test-install-selections.ps1`, `tests/test-install-selections.sh` | Validate dry-run shell and client selections. |
|
| `tests/test-install-selections.ps1`, `tests/test-install-selections.sh` | Validate dry-run shell and client selections. |
|
||||||
| `tests/test-command-summaries.ps1`, `tests/test-command-summaries.sh` | Validate doctor, install, and update summary output. |
|
| `tests/test-command-summaries.ps1`, `tests/test-command-summaries.sh` | Validate doctor, install, and update summary output. |
|
||||||
| `tests/test-telemetry.py` | Validate telemetry output. |
|
| `tests/test-telemetry.py` | Validate telemetry output. |
|
||||||
|
| `tests/test-personal-instructions.py` | Validate startup instruction pointers and preservation of personal instruction files. |
|
||||||
| `tests/test-user-customizations.py` | Validate user instruction overlays and credential helpers. |
|
| `tests/test-user-customizations.py` | Validate user instruction overlays and credential helpers. |
|
||||||
|
|
||||||
|
## QMD tests
|
||||||
|
|
||||||
|
The CI `qmd` job installs only `yaml@2.9.0` into an isolated directory, sets `QMD_PACKAGE_DIR` to that directory,
|
||||||
|
and runs these five files. It does not install QMD or download models. With a complete global QMD installation,
|
||||||
|
the same tests resolve YAML from QMD automatically:
|
||||||
|
|
||||||
|
```text
|
||||||
|
node --test scripts/tests/test-qmd-lib.mjs scripts/tests/test-qmd-daemon.mjs scripts/tests/test-qmd-search.mjs scripts/tests/test-qmd-warm.mjs scripts/tests/test-qmd-benchmark.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Without QMD installed, use the dependency setup from [the CI workflow](../.gitea/workflows/ci.yml).
|
||||||
|
`QMD_PACKAGE_DIR` must point to the directory containing the test dependency's `node_modules` directory.
|
||||||
|
A missing YAML dependency fails `test-qmd-lib.mjs` before its tests load.
|
||||||
|
|
||||||
|
Run the integration test separately, only with a complete QMD installation and resources for model downloads.
|
||||||
|
Unset `QMD_CONFIG_DIR` first so the test uses its temporary configuration rather than an inherited user path.
|
||||||
|
Clear a unit-test-only `QMD_PACKAGE_DIR` override as well:
|
||||||
|
|
||||||
|
```text
|
||||||
|
node --test scripts/tests/test-qmd-integration.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
The integration test creates a temporary home and repository, downloads models, searches through the daemon,
|
||||||
|
and stops it after successful assertions.
|
||||||
|
Its temporary data is not automatically removed. `QMD_TEST_CACHE` can select an existing QMD cache parent;
|
||||||
|
the test uses its `qmd/` subdirectory, so avoid pointing it at a cache containing unrelated data.
|
||||||
|
|
||||||
|
## Session benchmarks
|
||||||
|
|
||||||
|
The A/B benchmark runs Python, C#/ASP.NET, and TypeScript tasks under `scripts/benchmarks/tasks/`.
|
||||||
|
These are real model sessions and can incur usage charges; `--dry-run` lists planned runs.
|
||||||
|
Both arms use the same model and reasoning effort for each client. Preflight records loaded configuration in
|
||||||
|
`provenance.json` and normally stops when the arms are not comparable. Reports compare pass rate, rework turns,
|
||||||
|
time, tokens, tool calls, cost, and workflow checks per task category.
|
||||||
|
|
||||||
|
For Claude's semantic-search comparison, pass these arguments to the shell's benchmark command:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--clients claude --arms setup setup-no-search --tasks 11-* 12-* 13-* --warm-index
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup-no-search` is Claude-only. Warm-up starts QMD and indexes the fixture before timing setup-arm sessions.
|
||||||
|
The Codex baseline requires a separate login under `--codex-baseline-home`; set `CODEX_HOME` to that path for
|
||||||
|
`codex login`. Supply `--codex-model` if the installed configuration does not select a model.
|
||||||
|
Benchmark fixture READMEs describe the starting task state and are retained as test inputs.
|
||||||
|
|
||||||
## Internal helpers
|
## Internal helpers
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
@@ -85,6 +147,6 @@ Gitea Actions runs the repository checks from `.gitea/workflows/ci.yml`.
|
|||||||
| `lib/remove-instruction.py` | Remove an exact client-specific instruction block. |
|
| `lib/remove-instruction.py` | Remove an exact client-specific instruction block. |
|
||||||
| `lib/selection-state.ps1`, `lib/selection-state.sh` | Persist choices used by Quickupdate. |
|
| `lib/selection-state.ps1`, `lib/selection-state.sh` | Persist choices used by Quickupdate. |
|
||||||
| `lib/selection-state.py` | Read and write Quickupdate state. |
|
| `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/qmd.sh`, `lib/qmd.ps1` | Invoke local QMD search from the installation flow. |
|
||||||
| `lib/semantic-retrieval.py` | Install, index, benchmark, and remove the local retrieval service. |
|
| `lib/qmd.py` | Install, migrate, benchmark, and remove local QMD search. |
|
||||||
| `shared/retrieval/benchmark.py` | Check warm Qwen embedding and reranking speed for Auto selection; not an end-to-end benchmark. |
|
| [`shared/qmd/`](../shared/qmd/) (repository root) | Installed QMD scripts: search CLI, daemon, warm-up, benchmark, and shared library. |
|
||||||
+11
-4
@@ -5,20 +5,23 @@ Skills are reusable workflows. Rules are persistent constraints and agents are s
|
|||||||
The current structure is organized by purpose:
|
The current structure is organized by purpose:
|
||||||
|
|
||||||
- `planning/`: impact analysis and work-item definition
|
- `planning/`: impact analysis and work-item definition
|
||||||
- `research/`: technical and repository research
|
- `research/`: mandatory `research` gate for every search, with technical and repository specialists, plus `semantic-search`, which is installed only while local QMD search is enabled
|
||||||
- `reviews/`: security, performance, WPF, UI/UX, business, API, and EF Core reviews
|
- `reviews/`: security, performance, WPF, UI/UX, business, API, and EF Core reviews
|
||||||
- `implementation/`: SQL, unit-test, and integration-test writing
|
- `implementation/`: SQL, unit-test, and integration-test writing
|
||||||
- `verification/`: facts and work-item verification
|
- `verification/`: facts and work-item verification
|
||||||
- `git/`: pull request reviews
|
- `git/`: pull request reviews
|
||||||
- `content-strategy/`, `copy-editing/`, `copywriting/`: planning, editing, and writing text
|
- `content-strategy/`, `copy-editing/`, `copywriting/`: planning, editing, and writing text
|
||||||
- `image/`, `site-architecture/`: visuals and website structure
|
- `image/`: image assets, screenshots, SEO and performance, social-preview metadata, practical marketing workflows, design tools, and common mistakes
|
||||||
|
- `site-architecture/`: website structure
|
||||||
- `marketing-psychology/`, `offers/`, `product-marketing/`, `cro/`: audience understanding, value framing, and conversion
|
- `marketing-psychology/`, `offers/`, `product-marketing/`, `cro/`: audience understanding, value framing, and conversion
|
||||||
|
|
||||||
General planning, debugging, code review, and completion verification workflows come from Superpowers, so the catalog does not duplicate their triggers. The remaining skills cover focused domains Superpowers does not.
|
General planning, debugging, code review, and completion verification workflows come from Superpowers, so the catalog does not duplicate their triggers. The remaining skills cover focused domains Superpowers does not.
|
||||||
|
|
||||||
|
The shared `plugin-workflow` skill coordinates automatic plugin use: Superpowers for process, Ponytail for implementation and review, Impeccable for UI, and i-have-adhd with Caveman for responses. Global instructions require it at session start/resume and task or phase changes. It routes to installed skills, preserves explicit opt-outs, and handles missing or manual-only plugins without replacing upstream workflows.
|
||||||
|
|
||||||
Each skill defines a focused workflow with the investigation, decision points, and output expected for that task. Keep guidance specific to the skill; shared repository constraints belong in the rules.
|
Each skill defines a focused workflow with the investigation, decision points, and output expected for that task. Keep guidance specific to the skill; shared repository constraints belong in the rules.
|
||||||
|
|
||||||
The single catalog in `shared/skills/` includes development skills and focused skills for writing, images, content and site structure, positioning, psychology, and conversion. Builds include every retained skill for both clients. Updating a managed installation backs up and removes unchanged setup-owned skills deleted from the catalog; local changes are backed up and retained.
|
The single catalog in `shared/skills/` includes development skills and focused skills for writing, image assets, content and site structure, positioning, psychology, and conversion. The image skill has a short core and five focused references loaded only when relevant: optimization, OG metadata, marketing workflows, design tools, and common mistakes. AI-generation guides, model comparisons, prompting recipes, and fixed social-platform size catalogs are omitted. Builds include every retained skill for both clients. Updating a managed installation backs up and removes unchanged setup-owned skills deleted from the catalog; local changes are backed up and retained.
|
||||||
|
|
||||||
Claude discovers only `skills/<name>/SKILL.md`, so its generated package flattens the categorized catalog to one directory per skill name, together with the skill's `references/`; the build fails on duplicate names or a `name` that differs from its directory. Claude's package additionally has a `rules-*` skill for each focused rule file or directory in
|
Claude discovers only `skills/<name>/SKILL.md`, so its generated package flattens the categorized catalog to one directory per skill name, together with the skill's `references/`; the build fails on duplicate names or a `name` that differs from its directory. Claude's package additionally has a `rules-*` skill for each focused rule file or directory in
|
||||||
`shared/rules/`. The rule-to-skill loading conditions live in `shared/global-instructions.md`, so full rule text loads
|
`shared/rules/`. The rule-to-skill loading conditions live in `shared/global-instructions.md`, so full rule text loads
|
||||||
@@ -26,6 +29,10 @@ only when Claude invokes a matching skill instead of sitting permanently in cont
|
|||||||
files directly. Both clients embed the general rules in their global instructions.
|
files directly. Both clients embed the general rules in their global instructions.
|
||||||
Language, framework, library, and security rules use directory indexes plus focused topic files. Their generated Claude skills expose each focused rule separately; the index skills also include the references for contextual lookup. See [Configuration](configuration.md).
|
Language, framework, library, and security rules use directory indexes plus focused topic files. Their generated Claude skills expose each focused rule separately; the index skills also include the references for contextual lookup. See [Configuration](configuration.md).
|
||||||
|
|
||||||
Skills guide a task when needed; their availability does not require a full review, build, or test suite after every
|
Every search or lookup must use `research` and the applicable specialist, including direct reads, exact matches, familiar areas, and follow-up queries during implementation or verification. Loaded instructions can be reused; their checks apply to each search. Semantic retrieval runs first for unknown repository identifiers; known identifiers still use direct reads or `rg` within the research workflow. Disabling QMD does not disable the research requirement. These are agent instructions, not a runtime tool interceptor.
|
||||||
|
|
||||||
|
Context7 phase checkpoints are defined once in `research/technical-research/SKILL.md`. Focused planning, SQL and test-writing, and verification skills call them explicitly, as do the architect, implementer, and verifier roles. The global rule applies the same checkpoints inside Superpowers and other plugin workflows without modifying upstream plugin caches or duplicating general workflow skills. Planning verifies external assumptions before accepting a design; implementation checks them against actual versions before coding; verification requires both applicable source evidence and proportionate execution results. Evidence travels in the existing plan or handoff and is reused across phases when the behavior, version, and environment match. Queries contain only public, sanitized concepts, use bounded timeouts and refinement, and report source fallback or unresolved gaps explicitly. This enforces an instruction contract; it does not guarantee model compliance or add network calls to builds and tests.
|
||||||
|
|
||||||
|
Other skills guide a task when needed; their availability does not require a full review, build, or test suite after every
|
||||||
change. The shared Verification rules govern proportional checking, including when generic skill workflows call
|
change. The shared Verification rules govern proportional checking, including when generic skill workflows call
|
||||||
for more work than the change needs.
|
for more work than the change needs.
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
"""Run the benchmark tasks with and without the installed setup and compare the results.
|
||||||
|
|
||||||
|
Each run copies a task fixture into a fresh Git repository, gives the client the task prompt,
|
||||||
|
then runs hidden checks the agent never saw. A failed check gets one rework turn with the check
|
||||||
|
output. Results land in <out>/results.jsonl and <out>/summary.md.
|
||||||
|
|
||||||
|
Arms:
|
||||||
|
setup the installed configuration (CLAUDE.md, settings, plugins, skills, MCP servers).
|
||||||
|
baseline Claude with --setting-sources "" --strict-mcp-config (no user settings, plugins,
|
||||||
|
user skills, CLAUDE.md, or MCP servers); Codex with CODEX_HOME set to --codex-baseline-home,
|
||||||
|
an empty home where you ran `codex login` once, with every user and plugin skill disabled.
|
||||||
|
|
||||||
|
Both arms of a client use the same pinned model and reasoning effort. Before any run, a preflight
|
||||||
|
records what each arm actually loads (model, effort, skills by source, plugins, MCP servers,
|
||||||
|
instructions) in <out>/provenance.json and stops if the arms are not comparable.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import fnmatch
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import queue
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
TASKS = Path(__file__).resolve().parent / 'tasks'
|
||||||
|
CHECK = [sys.executable, '-m', 'unittest', 'discover', '-s', 'bench_hidden', '-t', '.']
|
||||||
|
CLAUDE_TOOLS = ['Read', 'Edit', 'Write', 'Glob', 'Grep', 'Agent', 'Skill', 'TodoWrite', 'Bash(python *)', 'Bash(git *)',
|
||||||
|
'Bash(ls *)', 'Bash(dotnet *)', 'Bash(node *)', 'PowerShell(python *)', 'PowerShell(git *)', 'PowerShell(Get-ChildItem *)',
|
||||||
|
'PowerShell(dotnet *)', 'PowerShell(node *)']
|
||||||
|
|
||||||
|
|
||||||
|
def clean_env(**values):
|
||||||
|
env = {key: value for key, value in os.environ.items() if not key.startswith(('BUN_INSPECT', 'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'))}
|
||||||
|
env.update(values)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def kill_tree(process):
|
||||||
|
if os.name == 'nt':
|
||||||
|
subprocess.run(['taskkill', '/T', '/F', '/PID', str(process.pid)], capture_output=True, timeout=30)
|
||||||
|
else:
|
||||||
|
process.kill()
|
||||||
|
process.wait(timeout=30)
|
||||||
|
|
||||||
|
|
||||||
|
def run_jsonl(command, cwd, env, timeout, prompt):
|
||||||
|
"""Run a client, return (events, seconds, timed_out); output is read on a thread so the deadline holds.
|
||||||
|
|
||||||
|
The prompt goes through stdin: on Windows the clients start through .cmd shims that cut arguments at newlines.
|
||||||
|
"""
|
||||||
|
start = time.monotonic()
|
||||||
|
process = subprocess.Popen(command, cwd=cwd, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL, text=True, encoding='utf-8', errors='replace')
|
||||||
|
threading.Thread(target=lambda: (process.stdin.write(prompt), process.stdin.close()), daemon=True).start()
|
||||||
|
lines = queue.Queue()
|
||||||
|
threading.Thread(target=lambda: ([lines.put(line) for line in process.stdout], lines.put(None)), daemon=True).start()
|
||||||
|
events, timed_out = [], False
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = lines.get(timeout=max(1, timeout - (time.monotonic() - start)))
|
||||||
|
except queue.Empty:
|
||||||
|
timed_out = True
|
||||||
|
break
|
||||||
|
if line is None:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
events.append(json.loads(line))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
kill_tree(process) if process.poll() is None else process.wait()
|
||||||
|
return events, time.monotonic() - start, timed_out
|
||||||
|
|
||||||
|
|
||||||
|
def claude_flags(arm, args):
|
||||||
|
flags = ['--model', args.claude_model, '--effort', args.claude_effort]
|
||||||
|
if arm == 'baseline':
|
||||||
|
flags += ['--setting-sources', '', '--strict-mcp-config']
|
||||||
|
if arm == 'setup-no-search':
|
||||||
|
flags += ['--disallowedTools', 'Skill(semantic-search)', 'Bash(*qmd-search*)', 'PowerShell(*qmd-search*)',
|
||||||
|
'--append-system-prompt', 'The semantic-search skill and the QMD search wrapper are unavailable in this session.']
|
||||||
|
return flags
|
||||||
|
|
||||||
|
|
||||||
|
def codex_env(arm, args):
|
||||||
|
return clean_env(CODEX_HOME=str(args.codex_baseline_home)) if arm == 'baseline' else clean_env()
|
||||||
|
|
||||||
|
|
||||||
|
def claude_turn(prompt, cwd, arm, args, session):
|
||||||
|
command = [shutil.which('claude'), '-p', '--output-format', 'stream-json', '--verbose',
|
||||||
|
'--permission-mode', 'auto', '--allowedTools', *CLAUDE_TOOLS, *claude_flags(arm, args)]
|
||||||
|
if session:
|
||||||
|
command += ['--resume', session]
|
||||||
|
timeout = args.timeout
|
||||||
|
events, seconds, timed_out = run_jsonl(command, cwd, clean_env(), timeout, prompt)
|
||||||
|
result = next((event for event in reversed(events) if event.get('type') == 'result'), {})
|
||||||
|
usage = result.get('usage', {})
|
||||||
|
calls = [block for event in events if event.get('type') == 'assistant'
|
||||||
|
for block in event.get('message', {}).get('content', []) if block.get('type') == 'tool_use']
|
||||||
|
commands = [str(call.get('input', {}).get('command', '')) for call in calls if call.get('name') in ('Bash', 'PowerShell')]
|
||||||
|
return {
|
||||||
|
'session': result.get('session_id') or session, 'seconds': seconds, 'timed_out': timed_out,
|
||||||
|
'input_tokens': usage.get('input_tokens', 0) + usage.get('cache_read_input_tokens', 0) + usage.get('cache_creation_input_tokens', 0),
|
||||||
|
'output_tokens': usage.get('output_tokens', 0), 'cost_usd': result.get('total_cost_usd') or 0,
|
||||||
|
'tool_calls': len(calls), 'agents': sum(call.get('name') == 'Agent' for call in calls),
|
||||||
|
'skills': [str(call.get('input', {}).get('skill', '')) for call in calls if call.get('name') == 'Skill'],
|
||||||
|
'commands': commands, 'final': str(result.get('result', '')),
|
||||||
|
'models': sorted({str(event['message']['model']) for event in events
|
||||||
|
if event.get('type') == 'assistant' and event.get('message', {}).get('model')}),
|
||||||
|
'error': str(result.get('result', '') or 'no result event')[:300] if result.get('is_error') or not result else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def codex_turn(prompt, cwd, arm, args, session):
|
||||||
|
command = [shutil.which('codex'), 'exec']
|
||||||
|
if session:
|
||||||
|
command += ['resume', session]
|
||||||
|
# `exec resume` rejects --sandbox; the config key works for both forms.
|
||||||
|
command += ['--json', '--skip-git-repo-check', '-c', 'sandbox_mode="workspace-write"',
|
||||||
|
'-m', args.codex_model, '-c', f'model_reasoning_effort="{args.codex_effort}"']
|
||||||
|
command += ['-C', str(cwd)] if not session else []
|
||||||
|
command.append('-')
|
||||||
|
events, seconds, timed_out = run_jsonl(command, cwd, codex_env(arm, args), args.timeout, prompt)
|
||||||
|
items = [event.get('item', {}) for event in events if event.get('type') == 'item.completed']
|
||||||
|
tools = [item for item in items if item.get('type') not in ('agent_message', 'reasoning', 'error')]
|
||||||
|
usage = [event.get('usage', {}) for event in events if event.get('type') == 'turn.completed']
|
||||||
|
thread = next((event.get('thread_id') for event in events if event.get('type') == 'thread.started'), session)
|
||||||
|
return {
|
||||||
|
'session': thread, 'seconds': seconds, 'timed_out': timed_out,
|
||||||
|
'input_tokens': sum(entry.get('input_tokens', 0) for entry in usage),
|
||||||
|
'output_tokens': sum(entry.get('output_tokens', 0) for entry in usage), 'cost_usd': 0,
|
||||||
|
'tool_calls': len(tools), 'agents': sum(item.get('type') == 'collab_tool_call' for item in tools),
|
||||||
|
'skills': [], 'commands': [str(item.get('command', '')) for item in tools if item.get('type') == 'command_execution'],
|
||||||
|
'final': next((str(item.get('text', '')) for item in reversed(items) if item.get('type') == 'agent_message'), ''),
|
||||||
|
'models': [args.codex_model],
|
||||||
|
'error': next((str(event.get('message', event.get('error', '')))[:300] for event in events if event.get('type') in ('error', 'turn.failed')
|
||||||
|
and not str(event.get('message', '')).startswith('Reconnecting')), 'no turn.completed event') if not usage else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AppServer:
|
||||||
|
"""Minimal JSON-RPC client for `codex app-server`; every wait has a deadline."""
|
||||||
|
|
||||||
|
def __init__(self, env, cwd):
|
||||||
|
self.process = subprocess.Popen([shutil.which('codex'), 'app-server'], cwd=cwd, env=env, stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, encoding='utf-8')
|
||||||
|
self.lines, self.next_id = queue.Queue(), 0
|
||||||
|
threading.Thread(target=lambda: [self.lines.put(line) for line in self.process.stdout], daemon=True).start()
|
||||||
|
self.call('initialize', {'clientInfo': {'name': 'ai-config-benchmark', 'version': '1'}})
|
||||||
|
self.process.stdin.write(json.dumps({'jsonrpc': '2.0', 'method': 'initialized'}) + '\n')
|
||||||
|
|
||||||
|
def call(self, method, params):
|
||||||
|
self.next_id += 1
|
||||||
|
self.process.stdin.write(json.dumps({'jsonrpc': '2.0', 'id': self.next_id, 'method': method, 'params': params}) + '\n')
|
||||||
|
self.process.stdin.flush()
|
||||||
|
while True:
|
||||||
|
message = json.loads(self.lines.get(timeout=120))
|
||||||
|
if message.get('id') == self.next_id:
|
||||||
|
if 'error' in message:
|
||||||
|
raise RuntimeError(f'{method}: {message["error"]}')
|
||||||
|
return message['result']
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
kill_tree(self.process)
|
||||||
|
|
||||||
|
|
||||||
|
def codex_configured(env, cwd):
|
||||||
|
server = AppServer(env, cwd)
|
||||||
|
try:
|
||||||
|
return server.call('config/read', {})['config']
|
||||||
|
finally:
|
||||||
|
server.close()
|
||||||
|
|
||||||
|
|
||||||
|
def codex_provenance(arm, args, cwd):
|
||||||
|
server = AppServer(codex_env(arm, args), cwd)
|
||||||
|
try:
|
||||||
|
config = server.call('config/read', {})['config']
|
||||||
|
listing = lambda: server.call('skills/list', {'cwds': [str(cwd)], 'forceReload': True})['data'][0]['skills']
|
||||||
|
skills = listing()
|
||||||
|
if arm == 'baseline':
|
||||||
|
# Codex reads ~/.agents/skills from the real profile even with another CODEX_HOME; disable those skills there.
|
||||||
|
for skill in skills:
|
||||||
|
if skill.get('enabled', True) and (skill.get('scope') == 'user' or skill.get('pluginId')):
|
||||||
|
server.call('skills/config/write', {'path': skill['path'], 'enabled': False})
|
||||||
|
skills = listing()
|
||||||
|
finally:
|
||||||
|
server.close()
|
||||||
|
prompt = subprocess.run([shutil.which('codex'), 'debug', 'prompt-input', '-c', f'model="{args.codex_model}"',
|
||||||
|
'-c', f'model_reasoning_effort="{args.codex_effort}"', 'provenance probe'],
|
||||||
|
cwd=cwd, env=codex_env(arm, args), capture_output=True, text=True, encoding='utf-8', timeout=180).stdout
|
||||||
|
setup_agents = Path.home() / '.codex' / 'AGENTS.md'
|
||||||
|
marker = next((line.strip() for line in setup_agents.read_text(encoding='utf-8-sig').splitlines() if line.strip()), '') if setup_agents.is_file() else ''
|
||||||
|
enabled = [skill for skill in skills if skill.get('enabled', True)]
|
||||||
|
return {
|
||||||
|
'model': args.codex_model, 'effort': args.codex_effort,
|
||||||
|
'configured_model': config.get('model'), 'configured_effort': config.get('model_reasoning_effort'),
|
||||||
|
'sandbox_mode': config.get('sandbox_mode'), 'approval_policy': config.get('approval_policy'),
|
||||||
|
'skills_by_scope': {scope: sum(skill.get('scope') == scope and not skill.get('pluginId') for skill in enabled)
|
||||||
|
for scope in sorted({skill.get('scope') for skill in enabled})},
|
||||||
|
'plugin_skills': sorted(skill['name'] for skill in enabled if skill.get('pluginId')),
|
||||||
|
'user_skills': sorted(skill['name'] for skill in enabled if skill.get('scope') == 'user' and not skill.get('pluginId')),
|
||||||
|
'mcp_servers': sorted(config.get('mcp_servers') or {}),
|
||||||
|
'global_instructions_loaded': bool(marker) and marker in prompt,
|
||||||
|
'prompt_input_tokens_estimate': len(prompt.encode('utf-8')) // 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def claude_provenance(arm, args, cwd):
|
||||||
|
claude = shutil.which('claude')
|
||||||
|
process = subprocess.Popen([claude, '-p', 'provenance probe', '--output-format', 'stream-json', '--verbose', *claude_flags(arm, args)],
|
||||||
|
cwd=cwd, env=clean_env(), stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
||||||
|
text=True, encoding='utf-8')
|
||||||
|
lines = queue.Queue()
|
||||||
|
threading.Thread(target=lambda: [lines.put(line) for line in process.stdout], daemon=True).start()
|
||||||
|
try:
|
||||||
|
init = {}
|
||||||
|
while init.get('subtype') != 'init':
|
||||||
|
init = json.loads(lines.get(timeout=180))
|
||||||
|
finally:
|
||||||
|
kill_tree(process)
|
||||||
|
context = json.loads(subprocess.run([claude, '-p', '/context', '--output-format', 'json', *claude_flags(arm, args)], cwd=cwd, env=clean_env(),
|
||||||
|
capture_output=True, text=True, encoding='utf-8', timeout=180).stdout).get('result', '')
|
||||||
|
|
||||||
|
def rows(heading):
|
||||||
|
section = re.search(rf'(?ms)^### {re.escape(heading)}\s*$(.*?)(?=^### |\Z)', context)
|
||||||
|
cells = [[cell.strip() for cell in line.strip().strip('|').split('|')] for line in (section.group(1) if section else '').splitlines() if line.startswith('|')]
|
||||||
|
return [row for row in cells[2:] if row and row[0]]
|
||||||
|
|
||||||
|
sources = {}
|
||||||
|
for row in rows('Skills'):
|
||||||
|
source = row[1].split(' (')[0] if len(row) > 1 else '?'
|
||||||
|
sources[source] = sources.get(source, 0) + 1
|
||||||
|
model = re.search(r'\*\*Model:\*\*\s*(\S+)', context)
|
||||||
|
return {
|
||||||
|
'model': init.get('model') or (model.group(1) if model else None), 'effort': args.claude_effort,
|
||||||
|
'permission_mode': init.get('permissionMode'),
|
||||||
|
'skills_by_source': sources, 'skills_listed': len(init.get('skills', [])),
|
||||||
|
'plugins': sorted(plugin.get('name', str(plugin)) if isinstance(plugin, dict) else str(plugin) for plugin in init.get('plugins', [])),
|
||||||
|
'mcp_servers': sorted(server.get('name', str(server)) if isinstance(server, dict) else str(server) for server in init.get('mcp_servers', [])),
|
||||||
|
'memory_files': [row[1] for row in rows('Memory Files') if len(row) > 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def preflight(args, clients, arms):
|
||||||
|
"""Record what each arm loads and return the problems that make the comparison invalid."""
|
||||||
|
provenance, problems = {}, []
|
||||||
|
with tempfile.TemporaryDirectory(prefix='ai-bench-preflight-', ignore_cleanup_errors=True) as directory:
|
||||||
|
subprocess.run(['git', 'init', '-q'], cwd=directory, check=True, timeout=60)
|
||||||
|
for client in clients:
|
||||||
|
for arm in arms:
|
||||||
|
if client == 'codex' and arm == 'setup-no-search':
|
||||||
|
continue
|
||||||
|
data = (claude_provenance if client == 'claude' else codex_provenance)(arm, args, Path(directory))
|
||||||
|
provenance[f'{client}/{arm}'] = data
|
||||||
|
if client == 'claude' and arm == 'baseline':
|
||||||
|
if set(data['skills_by_source']) - {'Built-in'}:
|
||||||
|
problems.append(f'claude baseline loads non-built-in skills: {data["skills_by_source"]}')
|
||||||
|
if data['memory_files'] or data['mcp_servers']:
|
||||||
|
problems.append(f'claude baseline loads memory files {data["memory_files"]} or MCP servers {data["mcp_servers"]}')
|
||||||
|
if client == 'codex' and arm == 'baseline':
|
||||||
|
if data['user_skills'] or data['plugin_skills']:
|
||||||
|
problems.append(f'codex baseline still enables user or plugin skills: {data["user_skills"] + data["plugin_skills"]}')
|
||||||
|
if data['global_instructions_loaded'] or data['mcp_servers']:
|
||||||
|
problems.append('codex baseline loads the global AGENTS.md or MCP servers')
|
||||||
|
models = {provenance[key]['model'] for key in provenance if key.startswith(client + '/')}
|
||||||
|
if len(models) > 1:
|
||||||
|
problems.append(f'{client} arms resolve to different models: {sorted(models)}')
|
||||||
|
return provenance, problems
|
||||||
|
|
||||||
|
|
||||||
|
QMD_SCRIPTS = Path.home() / '.my-ai-configuration/qmd'
|
||||||
|
FILLER_TOPICS = [
|
||||||
|
('statement', 'download', 'Render a statement download for the export queue.'),
|
||||||
|
('trial', 'banner', 'Show the trial banner text in the marketing header.'),
|
||||||
|
('upload', 'progress', 'Track upload progress for the dashboard widget.'),
|
||||||
|
('network', 'status', 'Describe the network status shown in the footer.'),
|
||||||
|
('invoice', 'label', 'Format the invoice label printed on envelopes.'),
|
||||||
|
('retry', 'hint', 'Return the retry hint shown next to a failed form field.'),
|
||||||
|
('cent', 'display', 'Format a cent amount for the price badge.'),
|
||||||
|
('customer', 'greeting', 'Pick the greeting for a customer newsletter.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_filler(work, count):
|
||||||
|
"""Write deterministic distractor modules that share vocabulary with the task prompts but not their logic."""
|
||||||
|
for number in range(count):
|
||||||
|
noun, verb, doc = FILLER_TOPICS[number % len(FILLER_TOPICS)]
|
||||||
|
name = f'{noun}_{verb}_{number:03d}'
|
||||||
|
path = work / 'app' / 'modules' / f'{name}.py'
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
body = [f'def {name}(value, locale="en"):', f' """{doc}"""', ' text = str(value).strip()',
|
||||||
|
f' return f"{{locale}}:{noun}:{verb}:{{text}}"', '']
|
||||||
|
path.write_text('\n'.join(body), encoding='utf-8')
|
||||||
|
(work / 'app' / 'modules' / '__init__.py').write_text('', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def warm_index(work):
|
||||||
|
"""Index the fixture and start the QMD daemon before the session, as in a repository that was searched before."""
|
||||||
|
script = QMD_SCRIPTS / 'qmd-warm.mjs'
|
||||||
|
if script.is_file():
|
||||||
|
subprocess.run(['node', str(script), '--root', str(work)], capture_output=True, timeout=1800)
|
||||||
|
|
||||||
|
|
||||||
|
def check(work, task):
|
||||||
|
hidden = work / 'bench_hidden'
|
||||||
|
shutil.rmtree(hidden, ignore_errors=True)
|
||||||
|
shutil.copytree(task / 'hidden', hidden)
|
||||||
|
spec = json.loads((task / 'task.json').read_text(encoding='utf-8'))
|
||||||
|
command = CHECK
|
||||||
|
if spec.get('check'):
|
||||||
|
program, *arguments = spec['check']
|
||||||
|
command = [sys.executable if program == 'python' else shutil.which(program) or program, *arguments]
|
||||||
|
result = subprocess.run(command, cwd=work, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=600)
|
||||||
|
shutil.rmtree(hidden, ignore_errors=True)
|
||||||
|
return result.returncode == 0, (result.stdout + result.stderr)[-3000:]
|
||||||
|
|
||||||
|
|
||||||
|
def workflow_violations(spec, turns):
|
||||||
|
skills = [skill for turn in turns for skill in turn['skills']]
|
||||||
|
agents = sum(turn['agents'] for turn in turns)
|
||||||
|
calls = sum(turn['tool_calls'] for turn in turns)
|
||||||
|
commands = [command for turn in turns for command in turn['commands']]
|
||||||
|
problems = [f'used planning skill {skill}' for skill in skills if skill in spec.get('forbid_skills', [])]
|
||||||
|
if agents > spec.get('max_agents', 99):
|
||||||
|
problems.append(f'{agents} agents > {spec["max_agents"]}')
|
||||||
|
if agents < spec.get('min_agents', 0):
|
||||||
|
problems.append(f'{agents} agents < {spec["min_agents"]}')
|
||||||
|
if calls > spec.get('max_tool_calls', 10 ** 6):
|
||||||
|
problems.append(f'{calls} tool calls > {spec["max_tool_calls"]}')
|
||||||
|
if spec.get('require_command') and not any(re.search(spec['require_command'], command) for command in commands):
|
||||||
|
problems.append(f'no command matching {spec["require_command"]}')
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
def run_one(job, args):
|
||||||
|
client, arm, task, repetition = job
|
||||||
|
spec = json.loads((task / 'task.json').read_text(encoding='utf-8'))
|
||||||
|
with tempfile.TemporaryDirectory(prefix=f'ai-bench-{task.name[:2]}-', ignore_cleanup_errors=True) as directory:
|
||||||
|
work = Path(directory)
|
||||||
|
shutil.copytree(task / 'repo', work, dirs_exist_ok=True)
|
||||||
|
if spec.get('generate', {}).get('filler_modules'):
|
||||||
|
generate_filler(work, spec['generate']['filler_modules'])
|
||||||
|
for git in (['init', '-q'], ['add', '-A'], ['-c', 'user.name=bench', '-c', 'user.email=bench@example.invalid', 'commit', '-qm', 'fixture']):
|
||||||
|
subprocess.run(['git', '-c', 'core.autocrlf=false', *git], cwd=work, check=True, timeout=60)
|
||||||
|
if args.warm_index and arm == 'setup':
|
||||||
|
warm_index(work)
|
||||||
|
turns, prompt, session = [], spec['prompt'], None
|
||||||
|
for round_number in range(1 + args.rework):
|
||||||
|
turn = (claude_turn if client == 'claude' else codex_turn)(prompt, work, arm, args, session)
|
||||||
|
turns.append(turn)
|
||||||
|
session = turn['session']
|
||||||
|
if turn['error']:
|
||||||
|
passed, output = False, ''
|
||||||
|
break
|
||||||
|
passed, output = check(work, task)
|
||||||
|
if passed or turn['timed_out'] or not session:
|
||||||
|
break
|
||||||
|
prompt = f'The acceptance checks failed:\n\n{output}\n\nFix the code so they pass.'
|
||||||
|
if not passed and args.keep_failed:
|
||||||
|
shutil.copytree(work, args.out / 'failed' / f'{client}-{arm}-{task.name}-{repetition}', ignore=shutil.ignore_patterns('.git'))
|
||||||
|
return {
|
||||||
|
'client': client, 'arm': arm, 'task': task.name, 'repetition': repetition, 'passed': passed,
|
||||||
|
'error': next((turn['error'] for turn in turns if turn['error']), ''),
|
||||||
|
'first_try': passed and len(turns) == 1, 'rework_turns': len(turns) - 1,
|
||||||
|
'seconds': round(sum(turn['seconds'] for turn in turns), 1),
|
||||||
|
'input_tokens': sum(turn['input_tokens'] for turn in turns), 'output_tokens': sum(turn['output_tokens'] for turn in turns),
|
||||||
|
'cost_usd': round(sum(turn['cost_usd'] for turn in turns), 4), 'tool_calls': sum(turn['tool_calls'] for turn in turns),
|
||||||
|
'agents': sum(turn['agents'] for turn in turns), 'skills': [skill for turn in turns for skill in turn['skills']],
|
||||||
|
'commands': [command for turn in turns for command in turn['commands']],
|
||||||
|
'models': sorted({model for turn in turns for model in turn['models']}),
|
||||||
|
'searches': sum('qmd-search' in command for turn in turns for command in turn['commands'])
|
||||||
|
+ sum(skill == 'semantic-search' for turn in turns for skill in turn['skills']),
|
||||||
|
'timed_out': any(turn['timed_out'] for turn in turns), 'workflow_kind': spec['workflow'].get('kind'),
|
||||||
|
'workflow_violations': workflow_violations(spec['workflow'], turns), 'check_output': '' if passed else output,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def summarize(results):
|
||||||
|
errors = [result for result in results if result.get('error')]
|
||||||
|
results = [result for result in results if not result.get('error')]
|
||||||
|
lines = [f'Excluded {len(errors)} runs that ended with a client or API error (for example a usage limit).',
|
||||||
|
'Rows are per task category; an overall average would hide a category where one arm is worse.', '',
|
||||||
|
'| category | client | arm | runs | pass | first try | rework turns | median s | median input tok | median output tok | median tool calls | cost USD | workflow ok |',
|
||||||
|
'|---|---|---|---|---|---|---|---|---|---|---|---|---|']
|
||||||
|
groups = {}
|
||||||
|
for result in results:
|
||||||
|
spec = TASKS / result['task'] / 'task.json'
|
||||||
|
category = json.loads(spec.read_text(encoding='utf-8')).get('category', '?') if spec.is_file() else '?'
|
||||||
|
groups.setdefault((category, result['client'], result['arm']), []).append(result)
|
||||||
|
for (category, client, arm), runs in sorted(groups.items()):
|
||||||
|
median = lambda key: statistics.median(run[key] for run in runs)
|
||||||
|
lines.append(f'| {category} | {client} | {arm} | {len(runs)} | {sum(run["passed"] for run in runs) / len(runs):.0%} | '
|
||||||
|
f'{sum(run["first_try"] for run in runs) / len(runs):.0%} | {sum(run["rework_turns"] for run in runs)} | '
|
||||||
|
f'{median("seconds"):.0f} | {median("input_tokens"):.0f} | {median("output_tokens"):.0f} | {median("tool_calls"):.0f} | '
|
||||||
|
f'{sum(run["cost_usd"] for run in runs):.2f} | {sum(not run["workflow_violations"] for run in runs) / len(runs):.0%} |')
|
||||||
|
lines += ['', '## Workflow checks', '', '| client | arm | task | violations |', '|---|---|---|---|']
|
||||||
|
for result in sorted(results, key=lambda run: (run['client'], run['arm'], run['task'], run['repetition'])):
|
||||||
|
if result['workflow_violations']:
|
||||||
|
lines.append(f'| {result["client"]} | {result["arm"]} | {result["task"]} #{result["repetition"]} | {"; ".join(result["workflow_violations"])} |')
|
||||||
|
return '\n'.join(lines) + '\n'
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0], formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
|
||||||
|
parser.add_argument('--clients', nargs='+', choices=('claude', 'codex'), default=['claude', 'codex'])
|
||||||
|
parser.add_argument('--arms', nargs='+', choices=('setup', 'baseline', 'setup-no-search'), default=['setup', 'baseline'],
|
||||||
|
help='setup-no-search is the setup with the semantic-search skill and CLI blocked (Claude only)')
|
||||||
|
parser.add_argument('--warm-index', action='store_true', help='start the QMD daemon and index the fixture before setup-arm sessions, outside the timing')
|
||||||
|
parser.add_argument('--tasks', nargs='+', help='task directory names or patterns such as 11-* (default: all)')
|
||||||
|
parser.add_argument('--repetitions', type=int, default=3)
|
||||||
|
parser.add_argument('--first-repetition', type=int, default=1, help='number of the first repetition, to rerun selected repetitions')
|
||||||
|
parser.add_argument('--rework', type=int, default=1, help='rework turns after a failed check')
|
||||||
|
parser.add_argument('--jobs', type=int, default=2)
|
||||||
|
parser.add_argument('--timeout', type=int, default=900, help='seconds per client turn')
|
||||||
|
parser.add_argument('--claude-model', default='sonnet')
|
||||||
|
parser.add_argument('--claude-effort', default='medium', choices=('low', 'medium', 'high', 'xhigh', 'max'))
|
||||||
|
parser.add_argument('--codex-model', help="default: the setup's configured model, pinned for both arms")
|
||||||
|
parser.add_argument('--codex-effort', help="default: the setup's configured reasoning effort, pinned for both arms")
|
||||||
|
parser.add_argument('--allow-unisolated', action='store_true', help='run even when the preflight finds the arms not comparable')
|
||||||
|
parser.add_argument('--codex-baseline-home', type=Path, default=Path.home() / '.my-ai-configuration/benchmark/codex-home')
|
||||||
|
parser.add_argument('--out', type=Path, default=Path(tempfile.gettempdir()) / f'ai-config-ab-{time.strftime("%Y%m%d-%H%M%S")}')
|
||||||
|
parser.add_argument('--dry-run', action='store_true', help='list the runs without starting clients')
|
||||||
|
parser.add_argument('--keep-failed', action='store_true', help='copy failed work trees to <out>/failed')
|
||||||
|
parser.add_argument('--summarize', nargs='+', type=Path, metavar='RESULTS', help='only summarize results.jsonl files; a later file replaces runs of an earlier one')
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.summarize:
|
||||||
|
merged = {}
|
||||||
|
for path in args.summarize:
|
||||||
|
for line in path.read_text(encoding='utf-8').splitlines():
|
||||||
|
result = json.loads(line)
|
||||||
|
key = (result['client'], result['arm'], result['task'], result['repetition'])
|
||||||
|
if not result.get('error') or key not in merged:
|
||||||
|
merged[key] = result
|
||||||
|
print(summarize(list(merged.values())))
|
||||||
|
return 0
|
||||||
|
tasks = [path for path in sorted(TASKS.iterdir()) if path.is_dir() and (not args.tasks or any(fnmatch.fnmatch(path.name, pattern) for pattern in args.tasks))]
|
||||||
|
for client in args.clients:
|
||||||
|
if not shutil.which(client):
|
||||||
|
parser.error(f'{client} CLI unavailable')
|
||||||
|
if 'codex' in args.clients and 'baseline' in args.arms and not (args.codex_baseline_home / 'auth.json').is_file():
|
||||||
|
parser.error(f'Codex baseline needs a login: run CODEX_HOME="{args.codex_baseline_home}" codex login')
|
||||||
|
jobs = [(client, arm, task, repetition) for repetition in range(args.first_repetition, args.first_repetition + args.repetitions)
|
||||||
|
for task in tasks for client in args.clients for arm in args.arms if not (client == 'codex' and arm == 'setup-no-search')]
|
||||||
|
if 'codex' in args.clients and not (args.codex_model and args.codex_effort):
|
||||||
|
configured = codex_configured(clean_env(), Path.cwd())
|
||||||
|
args.codex_model = args.codex_model or configured.get('model')
|
||||||
|
args.codex_effort = args.codex_effort or configured.get('model_reasoning_effort') or 'medium'
|
||||||
|
if not args.codex_model:
|
||||||
|
parser.error('the setup configures no Codex model; pass --codex-model')
|
||||||
|
provenance, problems = preflight(args, args.clients, args.arms)
|
||||||
|
args.out.mkdir(parents=True, exist_ok=True)
|
||||||
|
(args.out / 'provenance.json').write_text(json.dumps({'provenance': provenance, 'problems': problems}, indent=2), encoding='utf-8')
|
||||||
|
for key, data in provenance.items():
|
||||||
|
print(f'{key}: model={data["model"]} effort={data["effort"]} ' + ' '.join(f'{name}={value}' for name, value in data.items()
|
||||||
|
if name in ('skills_by_source', 'skills_by_scope', 'plugins', 'mcp_servers')))
|
||||||
|
for problem in problems:
|
||||||
|
print(f'NOT COMPARABLE: {problem}')
|
||||||
|
if problems and not args.allow_unisolated:
|
||||||
|
return 2
|
||||||
|
print(f'{len(jobs)} runs -> {args.out}')
|
||||||
|
if args.dry_run:
|
||||||
|
for client, arm, task, repetition in jobs:
|
||||||
|
print(f' {client} {arm} {task.name} #{repetition}')
|
||||||
|
return 0
|
||||||
|
args.out.mkdir(parents=True, exist_ok=True)
|
||||||
|
results, lock = [], threading.Lock()
|
||||||
|
|
||||||
|
def record(job):
|
||||||
|
result = run_one(job, args)
|
||||||
|
with lock:
|
||||||
|
results.append(result)
|
||||||
|
with open(args.out / 'results.jsonl', 'a', encoding='utf-8') as stream:
|
||||||
|
stream.write(json.dumps(result) + '\n')
|
||||||
|
print(f'[{len(results)}/{len(jobs)}] {result["client"]} {result["arm"]} {result["task"]} #{result["repetition"]}: '
|
||||||
|
f'{"PASS" if result["passed"] else "FAIL"} {result["seconds"]}s {result["tool_calls"]} tools', flush=True)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
|
||||||
|
list(pool.map(record, jobs))
|
||||||
|
(args.out / 'summary.md').write_text(summarize(results), encoding='utf-8')
|
||||||
|
print(summarize(results))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""Report the context the installed clients actually load, as the clients report it.
|
||||||
|
|
||||||
|
Complements the byte-based prompt budget gate: Claude's own `/context` and `/skill-doctor`
|
||||||
|
output and its init event, and Codex's `debug prompt-input`. No model request is made.
|
||||||
|
With --check, fail when an installed skill is missing from the client listing or is listed
|
||||||
|
without description context (for example because the client's skill budget dropped it).
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import queue
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
TIMEOUT = 180
|
||||||
|
|
||||||
|
|
||||||
|
def kill_tree(process):
|
||||||
|
if os.name == 'nt':
|
||||||
|
subprocess.run(['taskkill', '/T', '/F', '/PID', str(process.pid)], capture_output=True, timeout=30)
|
||||||
|
else:
|
||||||
|
process.kill()
|
||||||
|
process.wait(timeout=30)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_env():
|
||||||
|
return {key: value for key, value in os.environ.items() if not key.startswith(('BUN_INSPECT', 'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'))}
|
||||||
|
|
||||||
|
|
||||||
|
def skill_names(root):
|
||||||
|
names = set()
|
||||||
|
for path in root.rglob('SKILL.md') if root.is_dir() else []:
|
||||||
|
match = re.search(r'(?m)^name:\s*"?([^"\r\n]+?)"?\s*$', path.read_text(encoding='utf-8-sig', errors='replace'))
|
||||||
|
if match:
|
||||||
|
names.add(match.group(1))
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def tokens(text):
|
||||||
|
# Claude prints "< 20" for small nonzero counts; count it as its upper bound.
|
||||||
|
text = text.strip().lstrip('~<').strip()
|
||||||
|
if text in ('', '-'):
|
||||||
|
return 0
|
||||||
|
return round(float(text[:-1]) * 1000) if text.endswith('k') else round(float(text))
|
||||||
|
|
||||||
|
|
||||||
|
def table_rows(markdown, heading):
|
||||||
|
section = re.search(rf'(?ms)^### {re.escape(heading)}\s*$(.*?)(?=^### |\Z)', markdown)
|
||||||
|
rows = []
|
||||||
|
for line in (section.group(1) if section else '').splitlines():
|
||||||
|
cells = [cell.strip() for cell in line.strip().strip('|').split('|')]
|
||||||
|
if line.startswith('|') and len(cells) >= 2 and not set(cells[0]) <= set('-') and cells[0] not in ('Category', 'Skill', 'Agent Type', 'Type', 'Tool'):
|
||||||
|
rows.append(cells)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def claude_print(claude, work, prompt, extra=()):
|
||||||
|
result = subprocess.run([claude, '-p', prompt, '--output-format', 'json', *extra], cwd=work, env=clean_env(),
|
||||||
|
capture_output=True, text=True, encoding='utf-8', timeout=TIMEOUT)
|
||||||
|
return json.loads(result.stdout).get('result', '')
|
||||||
|
|
||||||
|
|
||||||
|
def claude_init(claude, work, extra=()):
|
||||||
|
process = subprocess.Popen([claude, '-p', 'context probe', '--output-format', 'stream-json', '--verbose', *extra], cwd=work,
|
||||||
|
env=clean_env(), stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
||||||
|
text=True, encoding='utf-8')
|
||||||
|
lines = queue.Queue()
|
||||||
|
threading.Thread(target=lambda: [lines.put(line) for line in process.stdout], daemon=True).start()
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
event = json.loads(lines.get(timeout=TIMEOUT))
|
||||||
|
if event.get('subtype') == 'init':
|
||||||
|
return event
|
||||||
|
finally:
|
||||||
|
kill_tree(process)
|
||||||
|
|
||||||
|
|
||||||
|
def measure_claude(home, work):
|
||||||
|
claude = shutil.which('claude')
|
||||||
|
if not claude:
|
||||||
|
return {'available': False}
|
||||||
|
context = claude_print(claude, work, '/context')
|
||||||
|
doctor = claude_print(claude, work, '/skill-doctor')
|
||||||
|
init = claude_init(claude, work)
|
||||||
|
listed = {row[0]: tokens(row[2]) for row in table_rows(context, 'Skills') if len(row) >= 3}
|
||||||
|
doctor_context = {}
|
||||||
|
for line in doctor.splitlines():
|
||||||
|
cells = line.split()
|
||||||
|
if len(cells) >= 3 and re.fullmatch(r'~?\d+(\.\d+)?k?|-', cells[2]):
|
||||||
|
doctor_context[cells[0]] = tokens(cells[2])
|
||||||
|
installed = skill_names(home / '.claude/skills')
|
||||||
|
return {
|
||||||
|
'available': True,
|
||||||
|
'categories': {row[0]: tokens(row[1]) for row in table_rows(context, 'Estimated usage by category')},
|
||||||
|
'plugins': sorted(plugin.get('name', str(plugin)) if isinstance(plugin, dict) else str(plugin) for plugin in init.get('plugins', [])),
|
||||||
|
'agents': len(init.get('agents', [])),
|
||||||
|
'mcp_servers': len(init.get('mcp_servers', [])),
|
||||||
|
'skills_listed': len(listed),
|
||||||
|
'installed_skills_missing': sorted(installed - set(listed)),
|
||||||
|
'skills_without_context': sorted(name for name, value in {**listed, **doctor_context}.items() if value == 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def measure_codex(home, work):
|
||||||
|
codex = shutil.which('codex')
|
||||||
|
if not codex:
|
||||||
|
return {'available': False}
|
||||||
|
result = subprocess.run([codex, 'debug', 'prompt-input', 'context probe'], cwd=work, env=clean_env(),
|
||||||
|
capture_output=True, text=True, encoding='utf-8', timeout=TIMEOUT)
|
||||||
|
prompt = result.stdout
|
||||||
|
listed = set(re.findall(r'(?m)^- ([^:\n]+?): .+?\(file: ', '\n'.join(
|
||||||
|
part.get('text', '') for item in json.loads(prompt or '[]') for part in item.get('content', []) if isinstance(part, dict))))
|
||||||
|
config_path = home / '.codex/config.toml'
|
||||||
|
config = tomllib.loads(config_path.read_text(encoding='utf-8-sig')) if config_path.is_file() else {}
|
||||||
|
installed = skill_names(home / '.agents/skills')
|
||||||
|
return {
|
||||||
|
'available': True,
|
||||||
|
'prompt_input_tokens_estimate': len(prompt.encode('utf-8')) // 4,
|
||||||
|
'plugins': sorted(name for name, value in config.get('plugins', {}).items() if isinstance(value, dict) and value.get('enabled', True)),
|
||||||
|
'skills_listed': len(listed),
|
||||||
|
'installed_skills_missing': sorted(installed - listed),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
parser.add_argument('--home', type=Path, default=Path.home())
|
||||||
|
parser.add_argument('--json', action='store_true')
|
||||||
|
parser.add_argument('--check', action='store_true')
|
||||||
|
args = parser.parse_args()
|
||||||
|
with tempfile.TemporaryDirectory(prefix='ai-config-context-', ignore_cleanup_errors=True) as directory:
|
||||||
|
subprocess.run(['git', 'init', '-q'], cwd=directory, check=True, timeout=60)
|
||||||
|
report = {'claude': measure_claude(args.home, directory), 'codex': measure_codex(args.home, directory)}
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(report, indent=2))
|
||||||
|
else:
|
||||||
|
for client, data in report.items():
|
||||||
|
if not data['available']:
|
||||||
|
print(f'{client}: CLI unavailable')
|
||||||
|
continue
|
||||||
|
print(f'{client}: {data["skills_listed"]} skills listed, plugins: {", ".join(data["plugins"]) or "none"}')
|
||||||
|
for key, value in data.items():
|
||||||
|
if key not in ('available', 'plugins', 'skills_listed'):
|
||||||
|
print(f' {key}: {value}')
|
||||||
|
problems = [name for data in report.values() if data['available'] for name in data['installed_skills_missing'] + data.get('skills_without_context', [])]
|
||||||
|
return 1 if args.check and problems else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
Whitespace-only changes.
@@ -0,0 +1,16 @@
|
|||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from app import client, config
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_default(self):
|
||||||
|
self.assertEqual(config.DEFAULT_TIMEOUT, 60)
|
||||||
|
self.assertEqual(client.request('x'), 60)
|
||||||
|
self.assertEqual(client.request('x', 5), 5)
|
||||||
|
|
||||||
|
def test_docs(self):
|
||||||
|
for path in ('README.md', 'app/client.py'):
|
||||||
|
text = Path(path).read_text()
|
||||||
|
self.assertIn('60 seconds', text)
|
||||||
|
self.assertNotIn('30 seconds', text)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Client
|
||||||
|
|
||||||
|
Requests time out after 30 seconds unless a timeout is passed.
|
||||||
Whitespace-only changes.
@@ -0,0 +1,6 @@
|
|||||||
|
from app.config import DEFAULT_TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
|
def request(url, timeout=None):
|
||||||
|
"""Return the effective timeout in seconds; defaults to DEFAULT_TIMEOUT (30 seconds)."""
|
||||||
|
return timeout if timeout is not None else DEFAULT_TIMEOUT
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DEFAULT_TIMEOUT = 30
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"category": "small change",
|
||||||
|
"prompt": "Change the default request timeout from 30 to 60 seconds.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "small change without planning ceremony",
|
||||||
|
"forbid_skills": [
|
||||||
|
"superpowers:brainstorming",
|
||||||
|
"superpowers:writing-plans",
|
||||||
|
"superpowers:subagent-driven-development",
|
||||||
|
"superpowers:executing-plans"
|
||||||
|
],
|
||||||
|
"max_agents": 0,
|
||||||
|
"max_tool_calls": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,11 @@
|
|||||||
|
import unittest
|
||||||
|
from app.pagination import paginate
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_last_page(self):
|
||||||
|
self.assertEqual(paginate(list(range(10)), 4, 3), [9])
|
||||||
|
self.assertEqual(paginate(list(range(9)), 3, 3), [6, 7, 8])
|
||||||
|
|
||||||
|
def test_out_of_range(self):
|
||||||
|
self.assertEqual(paginate(list(range(3)), 5, 3), [])
|
||||||
Whitespace-only changes.
@@ -0,0 +1,5 @@
|
|||||||
|
def paginate(items, page, size):
|
||||||
|
"""Return the 1-based page of items with the given page size."""
|
||||||
|
start = (page - 1) * size
|
||||||
|
end = min(start + size, len(items) - 1)
|
||||||
|
return items[start:end]
|
||||||
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
|||||||
|
import unittest
|
||||||
|
from app.pagination import paginate
|
||||||
|
|
||||||
|
|
||||||
|
class PaginationTests(unittest.TestCase):
|
||||||
|
def test_first_page(self):
|
||||||
|
self.assertEqual(paginate(list(range(10)), 1, 3), [0, 1, 2])
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"category": "small change",
|
||||||
|
"prompt": "Users report that the last item is missing on the final page returned by paginate(). Fix the bug.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "bugfix with targeted verification",
|
||||||
|
"forbid_skills": [
|
||||||
|
"superpowers:brainstorming",
|
||||||
|
"superpowers:writing-plans",
|
||||||
|
"superpowers:subagent-driven-development",
|
||||||
|
"superpowers:executing-plans"
|
||||||
|
],
|
||||||
|
"max_agents": 0,
|
||||||
|
"require_command": "test_pagination|pagination"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,17 @@
|
|||||||
|
import unittest
|
||||||
|
from app.parsers.csv_parser import parse_csv
|
||||||
|
from app.parsers.ini_parser import parse_ini
|
||||||
|
from app.parsers.json_parser import parse_json_lines
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_csv(self):
|
||||||
|
self.assertEqual(parse_csv('a, b\n1, "x,y"\n\n2,3\n'), [{'a': '1', 'b': 'x,y'}, {'a': '2', 'b': '3'}])
|
||||||
|
|
||||||
|
def test_json(self):
|
||||||
|
self.assertEqual(parse_json_lines('{"a": 1}\n\n[2]\n'), [{'a': 1}, [2]])
|
||||||
|
with self.assertRaisesRegex(ValueError, '2'):
|
||||||
|
parse_json_lines('{}\n{bad\n')
|
||||||
|
|
||||||
|
def test_ini(self):
|
||||||
|
self.assertEqual(parse_ini('top = 1\n; c\n[s]\n# c\n k = v \n'), {'': {'top': '1'}, 's': {'k': 'v'}})
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,6 @@
|
|||||||
|
def parse_csv(text):
|
||||||
|
"""Parse comma-separated text with a header row into a list of dicts.
|
||||||
|
|
||||||
|
Strip whitespace around cells, skip blank lines, and support double-quoted cells containing commas.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
def parse_ini(text):
|
||||||
|
"""Parse INI text into {section: {key: value}}.
|
||||||
|
|
||||||
|
Ignore lines starting with ; or #, strip whitespace, keep keys before the first section under "".
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
def parse_json_lines(text):
|
||||||
|
"""Parse JSON Lines text into a list of objects.
|
||||||
|
|
||||||
|
Skip blank lines. Raise ValueError naming the 1-based line number for invalid JSON.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"category": "larger implementation",
|
||||||
|
"prompt": "Implement the three independent parsers in app/parsers (csv_parser, json_parser, ini_parser) according to their docstrings, and add unit tests for each. They share no code.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "larger change with justified delegation",
|
||||||
|
"min_agents": 1,
|
||||||
|
"max_agents": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,16 @@
|
|||||||
|
import sqlite3
|
||||||
|
import unittest
|
||||||
|
from app.users import find_user
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.conn = sqlite3.connect(':memory:')
|
||||||
|
self.conn.execute('CREATE TABLE users (id INTEGER, name TEXT)')
|
||||||
|
self.conn.executemany('INSERT INTO users VALUES (?, ?)', [(1, "O'Brien"), (2, 'admin')])
|
||||||
|
|
||||||
|
def test_apostrophe(self):
|
||||||
|
self.assertEqual(find_user(self.conn, "O'Brien"), (1, "O'Brien"))
|
||||||
|
|
||||||
|
def test_injection(self):
|
||||||
|
self.assertIsNone(find_user(self.conn, "x' OR '1'='1"))
|
||||||
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
|||||||
|
def find_user(conn, name):
|
||||||
|
"""Return (id, name) for the user with the given name, or None."""
|
||||||
|
return conn.execute(f"SELECT id, name FROM users WHERE name = '{name}'").fetchone()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"category": "small change",
|
||||||
|
"prompt": "find_user() fails for user names that contain an apostrophe, such as O'Brien. Fix it.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "security-relevant bugfix"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,31 @@
|
|||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
MUTANTS = [
|
||||||
|
('.rstrip("-")', ''),
|
||||||
|
('.lower()', ''),
|
||||||
|
('"ascii", "ignore"', '"ascii", "replace"'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run_tests(root):
|
||||||
|
return subprocess.run([sys.executable, '-m', 'unittest', 'discover', '-s', 'tests', '-t', '.'], cwd=root, capture_output=True, text=True, timeout=120)
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_tests_pass(self):
|
||||||
|
result = run_tests('.')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr[-2000:])
|
||||||
|
self.assertRegex(result.stderr, r'Ran ([3-9]|\d\d+) tests')
|
||||||
|
|
||||||
|
def test_tests_catch_mutants(self):
|
||||||
|
source = Path('app/slug.py').read_text()
|
||||||
|
for old, new in MUTANTS:
|
||||||
|
with self.subTest(old), tempfile.TemporaryDirectory() as directory:
|
||||||
|
shutil.copytree('.', directory, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.git', 'bench_hidden'))
|
||||||
|
Path(directory, 'app/slug.py').write_text(source.replace(old, new, 1))
|
||||||
|
self.assertNotEqual(run_tests(directory).returncode, 0, f'tests miss mutant {old!r}')
|
||||||
Whitespace-only changes.
@@ -0,0 +1,9 @@
|
|||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
|
|
||||||
|
def slugify(text, max_length=50):
|
||||||
|
"""Lowercase ASCII slug with single hyphens, trimmed to max_length without a trailing hyphen."""
|
||||||
|
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode()
|
||||||
|
text = re.sub(r"[^a-zA-Z0-9]+", "-", text).strip("-").lower()
|
||||||
|
return text[:max_length].rstrip("-")
|
||||||
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"category": "larger implementation",
|
||||||
|
"prompt": "Add unittest-based unit tests for app/slug.py under tests/ that cover its edge cases.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "test writing"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,16 @@
|
|||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from app.report import cost_report, sales_report
|
||||||
|
|
||||||
|
ROWS = [(' widgets ', 1234.5), ('gadgets', 99)]
|
||||||
|
EXPECTED = 'Widgets 1,234.50\nGadgets 99.00\nTotal 1,333.50'
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_output(self):
|
||||||
|
self.assertEqual(sales_report(ROWS), EXPECTED)
|
||||||
|
self.assertEqual(cost_report(ROWS), EXPECTED)
|
||||||
|
self.assertEqual(sales_report([]), f"{'Total':<20}{0:>12,.2f}")
|
||||||
|
|
||||||
|
def test_deduplicated(self):
|
||||||
|
self.assertEqual(Path('app/report.py').read_text().count('.title()'), 1)
|
||||||
Whitespace-only changes.
@@ -0,0 +1,20 @@
|
|||||||
|
def sales_report(rows):
|
||||||
|
lines = []
|
||||||
|
for name, amount in rows:
|
||||||
|
label = name.strip().title()
|
||||||
|
value = f"{amount:,.2f}"
|
||||||
|
lines.append(f"{label:<20}{value:>12}")
|
||||||
|
total = sum(amount for _, amount in rows)
|
||||||
|
lines.append(f"{'Total':<20}{total:>12,.2f}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def cost_report(rows):
|
||||||
|
lines = []
|
||||||
|
for name, amount in rows:
|
||||||
|
label = name.strip().title()
|
||||||
|
value = f"{amount:,.2f}"
|
||||||
|
lines.append(f"{label:<20}{value:>12}")
|
||||||
|
total = sum(amount for _, amount in rows)
|
||||||
|
lines.append(f"{'Total':<20}{total:>12,.2f}")
|
||||||
|
return "\n".join(lines)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"category": "larger implementation",
|
||||||
|
"prompt": "Remove the duplicated formatting logic in app/report.py without changing its output.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "behavior-preserving refactor"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,20 @@
|
|||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from app.settings_store import save_settings
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_unknown_keys_survive(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = Path(directory) / 'settings.json'
|
||||||
|
path.write_text(json.dumps({'theme': 'dark', 'plugins': {'a': True}, 'x': 1}))
|
||||||
|
save_settings(path, {'language': 'de'})
|
||||||
|
self.assertEqual(json.loads(path.read_text()), {'theme': 'dark', 'plugins': {'a': True}, 'x': 1, 'language': 'de'})
|
||||||
|
|
||||||
|
def test_new_file(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = Path(directory) / 'settings.json'
|
||||||
|
save_settings(path, {'theme': 'light'})
|
||||||
|
self.assertEqual(json.loads(path.read_text()), {'theme': 'light'})
|
||||||
Whitespace-only changes.
@@ -0,0 +1,15 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
KNOWN = ("theme", "language")
|
||||||
|
|
||||||
|
|
||||||
|
def save_settings(path, updates):
|
||||||
|
"""Apply updates to the JSON settings file at path."""
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as stream:
|
||||||
|
current = json.load(stream)
|
||||||
|
except FileNotFoundError:
|
||||||
|
current = {}
|
||||||
|
merged = {key: updates.get(key, current.get(key)) for key in KNOWN if key in updates or key in current}
|
||||||
|
with open(path, "w", encoding="utf-8") as stream:
|
||||||
|
json.dump(merged, stream, indent=2)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"category": "small change",
|
||||||
|
"prompt": "save_settings() loses keys it does not know about when it updates the settings file. Fix it so unknown keys survive.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "data-preserving bugfix"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,15 @@
|
|||||||
|
import inspect
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from app import api, cli
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_signature(self):
|
||||||
|
self.assertEqual(list(inspect.signature(api.fetch).parameters), ['url', 'max_retries'])
|
||||||
|
self.assertEqual(cli.main('u'), 2)
|
||||||
|
|
||||||
|
def test_docs(self):
|
||||||
|
text = Path('README.md').read_text()
|
||||||
|
self.assertIn('max_retries', text)
|
||||||
|
self.assertNotRegex(text, r'(?<![\w])retries(?=[=`])')
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# API
|
||||||
|
|
||||||
|
`fetch(url, retries=3)` retries a request up to `retries` times.
|
||||||
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
|||||||
|
def fetch(url, retries=3):
|
||||||
|
"""Fetch url, retrying up to retries times. Returns the attempts used."""
|
||||||
|
return min(retries, 3) if url else 0
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from app.api import fetch
|
||||||
|
|
||||||
|
|
||||||
|
def main(url):
|
||||||
|
return fetch(url, retries=2)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"category": "small change",
|
||||||
|
"prompt": "Rename the retries parameter of app.api.fetch to max_retries.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "API change with directly affected documentation"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from app.files import read_upload
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.directory = tempfile.TemporaryDirectory()
|
||||||
|
root = Path(self.directory.name)
|
||||||
|
(root / 'uploads/sub').mkdir(parents=True)
|
||||||
|
(root / 'uploads/sub/a.txt').write_bytes(b'ok')
|
||||||
|
(root / 'secret.txt').write_bytes(b'secret')
|
||||||
|
self.base = str(root / 'uploads')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.directory.cleanup()
|
||||||
|
|
||||||
|
def test_nested(self):
|
||||||
|
self.assertEqual(read_upload(self.base, 'sub/a.txt'), b'ok')
|
||||||
|
|
||||||
|
def test_traversal(self):
|
||||||
|
for name in ('../secret.txt', 'sub/../../secret.txt', os.path.join(os.path.dirname(self.base), 'secret.txt')):
|
||||||
|
with self.subTest(name), self.assertRaises((ValueError, PermissionError, FileNotFoundError)):
|
||||||
|
read_upload(self.base, name)
|
||||||
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def read_upload(base, name):
|
||||||
|
"""Return the bytes of the uploaded file name below the base directory."""
|
||||||
|
with open(os.path.join(base, name), "rb") as stream:
|
||||||
|
return stream.read()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"category": "small change",
|
||||||
|
"prompt": "Make read_upload() safe against path traversal. Legitimate names in nested folders must keep working.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "security hardening"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,16 @@
|
|||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
from app.dedupe import dedupe
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_behavior(self):
|
||||||
|
self.assertEqual(dedupe([3, 1, 3, 2, 1]), [3, 1, 2])
|
||||||
|
self.assertEqual(dedupe([]), [])
|
||||||
|
self.assertEqual(dedupe(['a', 'b', 'a']), ['a', 'b'])
|
||||||
|
|
||||||
|
def test_fast(self):
|
||||||
|
items = list(range(100000)) * 2
|
||||||
|
start = time.perf_counter()
|
||||||
|
self.assertEqual(len(dedupe(items)), 100000)
|
||||||
|
self.assertLess(time.perf_counter() - start, 1.0)
|
||||||
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
|||||||
|
def dedupe(items):
|
||||||
|
"""Return items without duplicates, keeping the first occurrence of each."""
|
||||||
|
result = []
|
||||||
|
for item in items:
|
||||||
|
if item not in result:
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"category": "small change",
|
||||||
|
"prompt": "dedupe() is far too slow for 100,000 items. Make it fast while keeping its behavior and the order of first occurrences.",
|
||||||
|
"workflow": {
|
||||||
|
"kind": "performance fix"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,14 @@
|
|||||||
|
import unittest
|
||||||
|
from datetime import date
|
||||||
|
from app.accounts import Account
|
||||||
|
from app.ledger.archive import render_period_summary
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_expired_evaluation_blocked(self):
|
||||||
|
with self.assertRaises(PermissionError):
|
||||||
|
render_period_summary(Account('evaluation', date(2026, 1, 1)), '2026-01', today=date(2026, 2, 1))
|
||||||
|
|
||||||
|
def test_running_evaluation_and_paid_allowed(self):
|
||||||
|
self.assertIsInstance(render_period_summary(Account('evaluation', date(2026, 3, 1)), '2026-01', today=date(2026, 2, 1)), bytes)
|
||||||
|
self.assertIsInstance(render_period_summary(Account('paid'), '2026-01', today=date(2026, 2, 1)), bytes)
|
||||||
Whitespace-only changes.
@@ -0,0 +1,9 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Account:
|
||||||
|
tier: str # "evaluation" or "paid"
|
||||||
|
evaluation_until: date | None = None
|
||||||
|
entries: list = field(default_factory=list)
|
||||||
Whitespace-only changes.
@@ -0,0 +1,8 @@
|
|||||||
|
from datetime import date
|
||||||
|
|
||||||
|
|
||||||
|
def render_period_summary(account, period, today=None):
|
||||||
|
"""Build the period summary document for an account."""
|
||||||
|
today = today or date.today()
|
||||||
|
lines = [f"{entry.label}: {entry.amount:.2f}" for entry in account.entries if entry.period == period]
|
||||||
|
return "\n".join(lines).encode()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"category": "search",
|
||||||
|
"prompt": "Customers whose free trial has ended can still download their monthly statements. Block that by raising PermissionError, and keep it working for paying customers and for customers whose trial is still running.",
|
||||||
|
"generate": {
|
||||||
|
"filler_modules": 250
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"kind": "concept search in a large repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,32 @@
|
|||||||
|
import unittest
|
||||||
|
from app.transport.courier import dispatch
|
||||||
|
|
||||||
|
|
||||||
|
class Channel:
|
||||||
|
def __init__(self, failures, error=ConnectionResetError):
|
||||||
|
self.failures, self.error, self.calls = failures, error, 0
|
||||||
|
|
||||||
|
def send(self, blob):
|
||||||
|
self.calls += 1
|
||||||
|
if self.calls <= self.failures:
|
||||||
|
raise self.error('flaky')
|
||||||
|
return 'receipt'
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_retries_transient(self):
|
||||||
|
channel = Channel(2)
|
||||||
|
self.assertEqual(dispatch(b'x', channel), 'receipt')
|
||||||
|
self.assertEqual(channel.calls, 3)
|
||||||
|
|
||||||
|
def test_gives_up_after_three(self):
|
||||||
|
channel = Channel(5)
|
||||||
|
with self.assertRaises(ConnectionResetError):
|
||||||
|
dispatch(b'x', channel)
|
||||||
|
self.assertEqual(channel.calls, 3)
|
||||||
|
|
||||||
|
def test_no_retry_for_other_errors(self):
|
||||||
|
channel = Channel(1, ValueError)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
dispatch(b'x', channel)
|
||||||
|
self.assertEqual(channel.calls, 1)
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
|||||||
|
def dispatch(blob, channel):
|
||||||
|
"""Hand a blob to the channel and return the receipt."""
|
||||||
|
return channel.send(blob)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"category": "search",
|
||||||
|
"prompt": "Fix the upload code: uploads that fail with a temporary network error are never retried. They should make at most three attempts in total before giving up with the last error; other errors must not be retried.",
|
||||||
|
"generate": {
|
||||||
|
"filler_modules": 250
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"kind": "concept search in a large repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,10 @@
|
|||||||
|
import unittest
|
||||||
|
from app.fin.tally import consolidate
|
||||||
|
|
||||||
|
|
||||||
|
class Hidden(unittest.TestCase):
|
||||||
|
def test_half_up(self):
|
||||||
|
self.assertEqual(float(consolidate([1.005])), 1.01)
|
||||||
|
self.assertEqual(float(consolidate([2.675])), 2.68)
|
||||||
|
self.assertEqual(float(consolidate([1.0, 0.004])), 1.0)
|
||||||
|
self.assertEqual(float(consolidate([0.1, 0.2])), 0.3)
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
|||||||
|
def consolidate(lines):
|
||||||
|
"""Sum line amounts and return the figure for the document footer."""
|
||||||
|
return round(sum(lines), 2)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"category": "search",
|
||||||
|
"prompt": "Invoice totals are sometimes one cent too low, for example 1.005 shows as 1.00. Round half amounts up to the next cent.",
|
||||||
|
"generate": {
|
||||||
|
"filler_modules": 250
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"kind": "concept search in a large repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
<ProjectReference Include="..\..\src\Shop\Shop.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
Loaded 100 of 246 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user