Compare commits

...
6 Commits
Author SHA1 Message Date
Julian lechner 2192b4613d fix(ci): refresh agent metadata budget baselines
CI / CI configuration security checks (push) Successful in 3s
CI / Capabilities and shell quality (push) Successful in 16s
CI / Python tests and compilation (push) Successful in 23s
CI / QMD unit tests (push) Successful in 12s
CI / Build and inspect generated packages (push) Successful in 17s
CI / PowerShell tests on Ubuntu (push) Successful in 1m6s
CI / Installed client configuration smoke tests (push) Successful in 1m20s
CI / Bash integration and installation guards (push) Successful in 1m21s
2026-10-08 16:09:11 +02:00
Julian lechner 5e80d30936 fix(config): harden doctor checks and enable Codex full access
CI / CI configuration security checks (push) Successful in 3s
CI / Capabilities and shell quality (push) Successful in 18s
CI / Python tests and compilation (push) Successful in 25s
CI / QMD unit tests (push) Successful in 11s
CI / Build and inspect generated packages (push) Successful in 17s
CI / PowerShell tests on Ubuntu (push) Successful in 1m11s
CI / Installed client configuration smoke tests (push) Successful in 1m10s
CI / Bash integration and installation guards (push) Failing after 1m13s
2026-10-08 15:58:47 +02:00
Julian lechner 8aae370bdb fix(agents): align research tools and clarify role boundaries 2026-10-08 15:43:30 +02:00
Julian lechner 8288ea51f0 docs: refresh configuration guides and remove obsolete plans 2026-10-08 15:37:03 +02:00
Julian lechner 17d01d58f5 fix(ci): install YAML dependency for QMD unit tests 2026-10-08 15:30:23 +02:00
Julian lechner 7f441d388d fix(qmd): resolve hoisted dependencies from package context
CI / CI configuration security checks (push) Successful in 3s
CI / Capabilities and shell quality (push) Successful in 15s
CI / Python tests and compilation (push) Successful in 23s
CI / QMD unit tests (push) Failing after 11s
CI / Build and inspect generated packages (push) Successful in 17s
CI / PowerShell tests on Ubuntu (push) Successful in 1m2s
CI / Installed client configuration smoke tests (push) Successful in 1m7s
CI / Bash integration and installation guards (push) Successful in 1m17s
2026-10-08 15:25:33 +02:00
37 changed files with 362 additions and 2218 deletions

No files matched your search

+4
View File
@@ -177,12 +177,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 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-node@a0853c24544627f65ddf259abe73b1d18a591444 # v22.16.0 - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v22.16.0
with: with:
node-version: '24' node-version: '24'
# CI runs model-free tests only; real-model integration requires a suitable local machine. # CI runs model-free tests only; real-model integration requires a suitable local machine.
- name: Install QMD test dependency
run: npm install --prefix "$QMD_PACKAGE_DIR" --ignore-scripts --no-package-lock --no-audit --no-fund yaml@2.9.0
- name: Run QMD unit tests - name: Run QMD unit tests
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 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
+14 -11
View File
@@ -107,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.
- Node.js 22 or newer and `npm` when local QMD search 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 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
@@ -128,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
@@ -142,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. |
@@ -233,9 +234,9 @@ Selecting Codex also installs shared skills to `.agents/skills`. An interactive
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 local QMD search. 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 local QMD search on update removes only its setup-owned package, models, and repository collections. 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
@@ -288,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 local QMD search 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.
@@ -300,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.
-3
View File
@@ -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/`.
+2 -2
View File
@@ -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__
1 role tools
2 architect Read,Grep,Glob,WebFetch,WebSearch Read,Grep,Glob,WebFetch,WebSearch,__SHELL__
3 researcher Read,Grep,Glob,WebFetch,WebSearch Read,Grep,Glob,WebFetch,WebSearch,__SHELL__
4 reviewer Read,Grep,Glob,__SHELL__
5 verifier Read,Grep,Glob,__SHELL__
6 implementer Read,Grep,Glob,Edit,Write,__SHELL__
-3
View File
@@ -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 -8
View File
@@ -1,13 +1,6 @@
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
writable_roots = ['__AI_CONFIG_ROOT__/../.my-ai-configuration/qmd', '__AI_CONFIG_ROOT__/../.cache/qmd']
[windows]
sandbox = "elevated"
[tui] [tui]
status_line = ["model", "reasoning", "fast-mode", "approval-mode", "five-hour-limit", "weekly-limit", "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"]
+2 -2
View File
@@ -24,8 +24,8 @@
"codex": 8510 "codex": 8510
}, },
"agent_metadata_tokens": { "agent_metadata_tokens": {
"claude": 126, "claude": 204,
"codex": 52 "codex": 124
} }
} }
} }
+2 -2
View File
@@ -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
1 metric client baseline_tokens
9 auto_loaded_rule_tokens codex 678
10 rule_catalog_tokens claude 28818
11 rule_catalog_tokens codex 8510
12 agent_metadata_tokens claude 126 204
13 agent_metadata_tokens codex 52 124
+9 -3
View File
@@ -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.
-5
View File
@@ -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.
+106 -3
View File
@@ -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,9 +25,26 @@ 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).
On native Windows, the Codex adapter selects `windows.sandbox = "elevated"`, the [recommended Windows sandbox](https://learn.chatgpt.com/docs/windows/windows-sandbox). Its setup may require administrator approval; sandboxed commands run as dedicated lower-privilege users. The previous `unelevated` default reproduced `spawnSync git EPERM` when QMD's Node wrapper captured child-process output through pipes. The configuration change preserves workspace boundaries, network settings, and automatic approval review. Apply the generated configuration through the normal update process, complete Codex's sandbox setup if prompted, and start a new session. Verify semantic search there; package validation cannot establish runtime pipe compatibility. Use `unelevated` only as an explicit local fallback when elevated setup is unavailable, with that compatibility limitation in mind. 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
@@ -33,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.
+4 -2
View File
@@ -9,9 +9,11 @@ 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). | | 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.
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. 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.
+11 -2
View File
@@ -2,7 +2,7 @@
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, 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 ### Automatic workflow integration
@@ -46,7 +46,16 @@ 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.
Normal installation and update ask whether to enable Flashbang, apply the custom status line, and enable optional local QMD search (Yes/No/Auto, disabled by default). Local search runs [QMD](https://github.com/tobi/qmd) (`@tobilu/qmd@2.8.3`, Node.js 22 or newer, `npm`) with Qwen3-Embedding-0.6B, Qwen3-Reranker-0.6B, and QMD Query Expansion 1.7B. Auto benchmarks GPU first, then CPU, and enables search only when median search latency is at most 5 s and embedding runs at least 20 chunks/s. While it is enabled, both clients receive the `semantic-search` skill, which the general rules require for questions whose identifiers are unknown; disabling local search removes the skill and its instruction line. The skill calls `qmd-search.mjs`, which reuses a background daemon on loopback 127.0.0.1 protected by a per-start token; the daemon keeps the models loaded and exits after 30 idle minutes (`QMD_IDLE_MINUTES`). A SessionStart hook warms the index and daemon in the background. Models and the index live in `$XDG_CACHE_HOME/qmd` (default `~/.cache/qmd`); the Codex `writable_roots` cover only `~/.cache/qmd`, so a custom `XDG_CACHE_HOME` needs its own writable root. Native Windows packages select the recommended elevated Codex sandbox; its setup can require administrator approval (see [Configuration](configuration.md#permission-defaults)). The previous unelevated mode reproduced `spawnSync git EPERM` during repository-root discovery, before the CLI fallback could run. Runtime search under the elevated mode still needs verification after applying the configuration and starting a new session. The wrapper falls back to the slower QMD CLI path for daemon failures, but that path also needs child-process pipes. Repository collections skip git-ignored paths; the daemon keeps that list as the collection's `ignore` entry in QMD's `index.yml`. The setup saves and restores a user's own QMD `models:` block, and removal touches only the setup-owned package, models, and repository collections; foreign installs and collections survive. Updating migrates from the earlier Python stack by removing `~/.my-ai-configuration/semantic-retrieval/` and old QMD extension ledger records. Quickupdate reuses the saved shell, client, status line, Flashbang, local search, and extension choices without opening menus. Disabling the status line removes its custom setting and setup-owned Claude script. Only successful runs save selections; dry runs do not update extensions or local search. Install and update do not modify the Codex or Claude CLI installations. ### Setup options outside the extension selector
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.
+63 -4
View File
@@ -23,7 +23,7 @@ Run scripts from the repository root. PowerShell and Bash commands have equivale
| `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/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` | Run the Python, C#/ASP.NET, and TypeScript tasks under `scripts/benchmarks/tasks/` with and without the installed setup. Both arms of a client use one pinned model and reasoning effort; a preflight records what each arm actually loads in `provenance.json` and stops when the arms are not comparable; `--arms setup setup-no-search --tasks 11-* 12-* 13-* --warm-index` compares Claude with and without semantic search on concept-search tasks in a large generated repository, and compare pass rate, rework turns, duration, tokens, tool calls, cost, and workflow checks per task category (`category` in `task.json`: small change, larger implementation, search) (no planning ceremony for a small change, targeted verification for a bugfix, bounded delegation for a larger change). Starts real model sessions; use `--dry-run` to list runs. The Codex baseline needs one `codex login` with `CODEX_HOME` set to `--codex-baseline-home`. | | `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. |
@@ -32,7 +32,14 @@ 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 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. 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.
@@ -46,6 +53,10 @@ The A/B benchmark measures whole sessions. Auto selection of local QMD search us
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. |
@@ -63,7 +74,8 @@ 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-qmd-*.mjs` | Validate the QMD library, search, daemon, warm-up, and benchmark scripts. | | `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-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-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. |
@@ -71,8 +83,55 @@ Gitea Actions runs the repository checks from `.gitea/workflows/ci.yml`.
| `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 |
@@ -90,4 +149,4 @@ Gitea Actions runs the repository checks from `.gitea/workflows/ci.yml`.
| `lib/selection-state.py` | Read and write Quickupdate state. | | `lib/selection-state.py` | Read and write Quickupdate state. |
| `lib/qmd.sh`, `lib/qmd.ps1` | Invoke local QMD search from the installation flow. | | `lib/qmd.sh`, `lib/qmd.ps1` | Invoke local QMD search from the installation flow. |
| `lib/qmd.py` | Install, migrate, benchmark, and remove local QMD search. | | `lib/qmd.py` | Install, migrate, benchmark, and remove local QMD search. |
| `shared/qmd/*.mjs` | Installed QMD scripts: search CLI, daemon, warm-up, benchmark, and shared library. | | [`shared/qmd/`](../shared/qmd/) (repository root) | Installed QMD scripts: search CLI, daemon, warm-up, benchmark, and shared library. |
File diff suppressed because it is too large. Load diff
@@ -1,168 +0,0 @@
# QMD local search design
Date: 2026-10-08
Status: approved in conversation, pending written-spec review
## Goal
Replace the Python semantic retrieval stack with QMD as the only local search engine, used by Claude Code and Codex alike.
| Role | Model | QMD URI |
|---|---|---|
| Embedding | Qwen3-Embedding-0.6B | `hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf` |
| Reranking | Qwen3-Reranker-0.6B | `hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf` |
| Query expansion | QMD Query Expansion 1.7B | `hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf` |
Success criteria:
- No Python model code, virtual environment, or Python model dependency remains.
- QMD is no longer an extension in the plugin selector; the local-models question alone controls it.
- When enabled, agents use the search skill for every question whose identifiers are unknown, and the search answers in seconds because the index is warm and the models are loaded.
- When disabled or unsuitable, no skill, hook, instruction line, QMD package, or model installed by the setup remains.
Motivation from the 2026-10-08 Claude A/B benchmark: the Python search was never used in the three concept-search tasks, while the setup's fixed context made every run about 2.5 times more expensive in input tokens. A permanent MCP server would add to that cost, so the integration is a skill over a CLI wrapper.
## Decisions
1. Search scope: primarily the current Git repository; additionally user-registered QMD collections (`includeByDefault: true`), ranked together in one query.
2. Index location: one global QMD index (`~/.cache/qmd`). Each repository is a collection named `repo-<short hash of the absolute repository path>` with `includeByDefault: false`. No files are written into repositories.
3. Agent access: an own `semantic-search` skill for both clients that calls a Node wrapper. QMD's `qmd@qmd` plugin and MCP tools are not used.
4. Warm-up: a SessionStart hook indexes in the background and starts an own search daemon built on the QMD SDK so all three models stay loaded. QMD's HTTP daemon (`qmd mcp --http`) is not used: its `POST /query` accepts only pre-typed `lex`/`vec`/`hyde` searches and never runs query expansion.
5. Daemon lifetime: the daemon exits by itself after 30 idle minutes (`QMD_IDLE_MINUTES`).
6. Auto mode: benchmark GPU first, then CPU; both thresholds must pass; if no device passes, remove what the setup installed and continue as No.
## Components
### Removed
- `shared/retrieval/` (`server.py`, `benchmark.py`, `requirements.txt`).
- `scripts/lib/semantic-retrieval.py`, `.sh`, `.ps1`.
- `scripts/tests/test-retrieval-server.py`, `scripts/tests/test-semantic-retrieval.py`.
- The `retrieval` job in `.gitea/workflows/ci.yml`.
- The `QMD` row in `adapters/plugins.tsv` and QMD-specific ownership in `scripts/lib/managed-extensions.py`, `scripts/lib/plugins.sh`, and `scripts/lib/plugins.ps1`.
- Qwen/Python references in `README.md`, `docs/plugins.md`, `docs/scripts.md`, and other affected documentation.
### Added under `shared/qmd/`
- `qmd-daemon.mjs`: long-lived search process using the QMD SDK (`createStore({ dbPath, configPath })`, `store.search({ query, collections })` with expansion and reranking, `store.update()`, `store.embed()`). Listens on `127.0.0.1` only, on a port chosen at start; writes `{port, token, pid}` to `~/.my-ai-configuration/qmd/daemon.json` readable only by the user; rejects requests without the token header. Exits after `QMD_IDLE_MINUTES` (default 30) without a request. A second start while one is healthy is a no-op.
- `qmd-search.mjs`: ensures the repository collection, refreshes it incrementally, sends the search to the daemon, starts the daemon if it is not running, and falls back to `qmd query --json` when the daemon cannot be used. Collections searched: the repository collection plus every collection with `includeByDefault: true`. Output: JSON list of `{path, line, score, snippet}` with repository-relative paths, best first.
- `qmd-warm.mjs`: started detached by the SessionStart hook. Starts the daemon with the stored device if it is not running and asks it to refresh and embed the repository collection. A per-repository lock file prevents parallel indexing from concurrent sessions.
- `qmd-benchmark.mjs`: the Auto benchmark (see below).
- Thin `.sh` and `.ps1` entry points for every script, as the repository requires.
### Changed
- `shared/skills/research/semantic-search/SKILL.md`: rewritten for QMD; name kept so references and the A/B benchmark stay stable.
- `scripts/lib/qmd.sh`, `scripts/lib/qmd.ps1`: install, update, benchmark, and removal, called from `install` and `update`.
- Hooks: SessionStart entries for Claude and Codex are generated only when local models are enabled.
- `shared/rules/general.md` (and the generated global instructions): the semantic-search line becomes mandatory and is generated only when the skill is installed.
- `scripts/benchmarks/ab.py`: `setup-no-search` blocks the skill and wrapper instead of the Python CLI; `--warm-index` runs `qmd-warm.mjs`.
### Repository collection
- Name: `repo-<first 12 hex characters of SHA-256 of the absolute repository path>`.
- Mask: `**/*.{md,py,cs,ts,tsx,js,jsx,mjs,go,rs,java,ps1,sh,json,yml,yaml,xml,sql}`.
- Chunking: `--chunk-strategy auto` (AST chunking where QMD supports it; regex chunking elsewhere, including C#).
- QMD's built-in exclusions apply (`node_modules`, `.git`, `.cache`, `vendor`, `dist`, `build`).
### Configuration and state
- `~/.config/qmd/index.yml` (or `QMD_CONFIG_DIR`): the setup writes only the `models:` block; other keys and collections are preserved. An unparsable file aborts without writing. The daemon also writes the `ignore` list (the repository's git-ignored paths) of its own `repo-*` collections, because QMD re-syncs SQLite from this file and its `addCollection` write-through drops `ignore`.
- `~/.my-ai-configuration/qmd.json`: `{version, device: "gpu" | "cpu", owned_package: bool, owned_models: [...]}`.
- Device mapping when starting QMD: `gpu` sets `QMD_LLAMA_GPU=auto` (llama.cpp uses all detected GPUs); `cpu` sets `QMD_FORCE_CPU=1`.
## Install and update flow
Question, identical in Bash and PowerShell:
`Enable local QMD search models (Qwen3 embedding, Qwen3 reranker, QMD query expansion)? [y/N/a]`
- The default is the stored selection. The selection key `semantic_retrieval` and the `-SemanticRetrieval` / `--semantic-retrieval` parameters keep their names for compatibility.
- An update that keeps the stored selection does not rerun the benchmark; the benchmark runs only when `a` is chosen explicitly.
Prerequisites: Node.js 22 or newer and `npm`. Missing with Yes: abort with a clear message before any change. Missing with Auto: treat as unsuitable (No) and report the reason.
Yes:
1. `npm install -g @tobilu/qmd@<pinned version>`; record ownership only if QMD was not already installed.
2. Write the `models:` block into `index.yml`.
3. `qmd pull`.
4. Device: `gpu` when QMD's device probe (`qmd doctor`) reports a usable GPU, otherwise `cpu`. No benchmark.
5. Install skill, hook, scripts, and the mandatory instruction line.
Auto:
1. Steps 1 to 3 as for Yes.
2. Benchmark on GPU; if no GPU exists or it fails, benchmark on CPU.
3. The first passing device continues as Yes with that device.
4. If none passes: report the measurements, delete the models the setup downloaded, uninstall QMD only if the setup installed it, restore the configuration, store the selection as `false`, and continue as No.
No:
- Remove skill, hook, scripts, and instruction line; stop a running daemon.
- Remove the setup-owned QMD package and setup-owned models.
- Never remove a QMD installation, model, or collection the setup did not create.
Migration on the first update after this change:
- Remove the Python installation: `~/.my-ai-configuration/semantic-retrieval/` (virtual environment, model cache, data), its daemon, and `semantic-retrieval.json`.
- Remove previous QMD extension records (Claude plugin `qmd@qmd`, Codex QMD record) from the extension state. If the answer is Yes or a passing Auto, the existing npm package is adopted by the new path instead of being uninstalled.
Dry run: print every step as `DRYRUN ...`; no download, benchmark, or removal.
## Auto benchmark
- Fixture: a small fixed document set shipped with the scripts, indexed into a temporary named index (`qmd --index ai-config-benchmark`), removed afterwards.
- Models are warmed before measuring.
- Search: median latency of three `qmd query` runs with expansion, vector search, and reranking of 40 candidates must be at most 5 s (`QMD_BENCHMARK_MAX_QUERY_SECONDS`).
- Indexing: embedding throughput must be at least 20 chunks per second (`QMD_BENCHMARK_MIN_CHUNKS_PER_SECOND`).
- Order: GPU first, then CPU. Timeout per device: 10 minutes; a timeout fails that device.
- Result: JSON `{device, querySeconds, chunksPerSecond, passed}` per measured device, printed in the install summary.
## Runtime
SessionStart hook (Claude and Codex, `.sh` and `.ps1`):
- Runs only inside a Git repository. Starts `qmd-warm.mjs` detached and returns immediately with exit code 0.
- Errors go to `~/.my-ai-configuration/logs/qmd-warm.log`; the hook never blocks or fails a session.
Search (`qmd-search.mjs`):
1. Refresh the repository collection when files changed since the last refresh (`git status` and modification times), then `qmd embed` for it.
2. Query the daemon; start it if it is not running; fall back to `qmd query --json` when the daemon cannot be reached or started.
3. Print the JSON results.
Strict skill:
- Description: "MUST use before reading or grepping code whenever the exact symbol, file, or string is not already known: concept questions, unfamiliar areas, 'where/how is X done', bug reports that describe behavior. Use rg only for known identifiers, paths, or exact strings."
- Body: search first with a full behavioral question; open and verify the top hits; then use `rg` for callers and tests. If no relevant hit appears, run one rephrased search before switching to `rg`. The previous cost check (`status`, repository-size thresholds) is removed.
Global instruction line (only when the skill is installed): "When `semantic-search` is installed, run it first for any question whose identifiers are unknown; fall back to `rg` with a brief note only if it fails."
## Error handling
- Model download or `npm` failure during install or update: roll back what this run created. Yes fails the install; Auto treats it as unsuitable and reports the reason.
- Benchmark hang: per-device timeout of 10 minutes.
- Hook: detached, logged, always exit code 0. `qmd embed` uses QMD's 30-minute session cap and resumes incrementally on the next start.
- Wrapper failure: non-zero exit with a short message on stderr; the skill continues with `rg` and states the limitation.
- Stale `daemon.json` (process gone or token rejected): the wrapper deletes it and starts a new daemon; if that fails it uses the CLI fallback.
- `index.yml` that cannot be parsed: abort without writing.
## Testing
- Node unit tests (`node --test`): collection name and mask, benchmark decision (thresholds, GPU before CPU, unsuitable result), daemon idle exit with an injected clock, token rejection, wrapper fallback to the CLI, `index.yml` merge preserving foreign keys.
- Install guard tests in Bash and PowerShell with a mocked `qmd`: Yes, No, Auto pass and fail, dry run, ownership (a foreign QMD survives No), migration (Python installation removed), skill, hook, and instruction line present only when enabled.
- Integration job replacing the `retrieval` CI job: install QMD, pull models (cached), index a small fixture, assert the wrapper returns the expected hit, start and stop the daemon.
- `ab.py`: the `setup-no-search` arm and `--warm-index` use the QMD path.
- `prompt-budget`: the longer skill description changes the baseline; update it deliberately and report the difference.
## Risks to verify during implementation
- Whether the Codex `workspace-write` sandbox can reach the daemon on `127.0.0.1` and read `~/.cache/qmd`. If not, the Codex skill needs an approval rule for the wrapper or must run it outside the sandbox.
- Whether `qmd doctor`'s device probe is reliable enough to choose `gpu` for Yes without a benchmark.
- Disk and VRAM use: about 2.4 GB of models; the daemon holds them in VRAM until the idle stop.
## Out of scope
- Commands for managing user collections; users register them with `qmd collection add`.
- Any MCP integration of QMD.
+26 -5
View File
@@ -4,6 +4,7 @@ $ErrorActionPreference = 'Stop'
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
. (Join-Path $root 'scripts/lib/manifest.ps1') . (Join-Path $root 'scripts/lib/manifest.ps1')
$homePath = [Environment]::GetFolderPath('UserProfile') $homePath = [Environment]::GetFolderPath('UserProfile')
if ([string]::IsNullOrWhiteSpace($homePath)) { $homePath = $HOME }
$script:fail = $false $script:fail = $false
$script:checkCount = 0 $script:checkCount = 0
function Result($state, $message) { function Result($state, $message) {
@@ -14,7 +15,27 @@ function Result($state, $message) {
} }
} }
foreach ($tool in @('codex','claude')) { if (Get-Command $tool -ErrorAction SilentlyContinue) { $version = & $tool --version 2>&1 | Select-Object -First 1; Result 'PASS' "$tool available ($version)" } else { Result 'WARN' "$tool unavailable" } } function Invoke-DoctorCommand {
param([string]$Command, [string[]]$Arguments)
# PowerShell 5.1 turns native stderr into error records, even when the process succeeds.
$ErrorActionPreference = 'Continue'
$PSNativeCommandUseErrorActionPreference = $false
try {
$executable = Get-Command $Command -ErrorAction Stop
$output = @(& $executable @Arguments 2>&1)
return [pscustomobject]@{ Output = $output; ExitCode = $LASTEXITCODE }
} catch {
return [pscustomobject]@{ Output = @($_); ExitCode = 1 }
}
}
foreach ($tool in @('codex','claude')) {
if (Get-Command $tool -ErrorAction SilentlyContinue) {
$probe = Invoke-DoctorCommand -Command $tool -Arguments @('--version')
if ($probe.ExitCode -eq 0) { Result 'PASS' "$tool available ($($probe.Output -join ' '))" }
else { Result 'FAIL' "$tool --version failed (exit $($probe.ExitCode)): $($probe.Output -join ' ')" }
} else { Result 'WARN' "$tool unavailable" }
}
if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude Bash status line' } else { Result 'WARN' 'jq unavailable; Claude Bash status line is disabled' } if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude Bash status line' } else { Result 'WARN' 'jq unavailable; Claude Bash status line is disabled' }
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory -ErrorAction SilentlyContinue).Count $sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory -ErrorAction SilentlyContinue).Count
@@ -50,14 +71,14 @@ if (Test-Path -LiteralPath $settingsPath) {
} }
$configTomlPath = Join-Path $homePath '.codex/config.toml' $configTomlPath = Join-Path $homePath '.codex/config.toml'
if (Test-Path -LiteralPath $configTomlPath) { if (Test-Path -LiteralPath $configTomlPath) {
$parseResult = & python -c "import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8-sig'))" $configTomlPath 2>&1 $parseResult = Invoke-DoctorCommand -Command 'python' -Arguments @('-c', "import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8-sig'))", $configTomlPath)
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $parseResult" } if ($parseResult.ExitCode -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $($parseResult.Output -join ' ')" }
if (Get-Command codex -ErrorAction SilentlyContinue) { if (Get-Command codex -ErrorAction SilentlyContinue) {
$previousCodexHome = $env:CODEX_HOME $previousCodexHome = $env:CODEX_HOME
try { try {
$env:CODEX_HOME = Split-Path $configTomlPath -Parent $env:CODEX_HOME = Split-Path $configTomlPath -Parent
$schemaResult = & codex --strict-config --help 2>&1 $schemaResult = Invoke-DoctorCommand -Command 'codex' -Arguments @('--strict-config', '--help')
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml matches the installed Codex schema' } else { Result 'FAIL' "installed .codex/config.toml has unsupported Codex settings: $schemaResult" } if ($schemaResult.ExitCode -eq 0) { Result 'PASS' 'installed .codex/config.toml matches the installed Codex schema' } else { Result 'FAIL' "installed .codex/config.toml has unsupported Codex settings: $($schemaResult.Output -join ' ')" }
} finally { $env:CODEX_HOME = $previousCodexHome } } finally { $env:CODEX_HOME = $previousCodexHome }
} else { Result 'WARN' 'Codex CLI unavailable; skipped installed Codex schema validation' } } else { Result 'WARN' 'Codex CLI unavailable; skipped installed Codex schema validation' }
} }
+7 -1
View File
@@ -24,7 +24,13 @@ result() {
} }
for tool in codex claude; do for tool in codex claude; do
if command -v "$tool" >/dev/null; then result PASS "$tool available ($($tool --version 2>&1 | head -n 1))"; else result WARN "$tool unavailable"; fi if command -v "$tool" >/dev/null; then
if version=$("$tool" --version 2>&1); then result PASS "$tool available ($version)"
else
status=$?
result FAIL "$tool --version failed (exit $status): $version"
fi
else result WARN "$tool unavailable"; fi
done done
command -v jq >/dev/null && result PASS 'jq available for Claude Bash status line' || result WARN 'jq unavailable; Claude Bash status line is disabled' command -v jq >/dev/null && result PASS 'jq available for Claude Bash status line' || result WARN 'jq unavailable; Claude Bash status line is disabled'
+3 -5
View File
@@ -1,7 +1,5 @@
// Loads QMD's bundled yaml package for tests. // Loads QMD's yaml dependency for tests, including hoisted installations.
import { qmdPackageDir } from '../../../shared/qmd/qmd-lib.mjs'; import { importQmdDependency } from '../../../shared/qmd/qmd-lib.mjs';
import { pathToFileURL } from 'node:url'; const YAML = await importQmdDependency('yaml');
import { join } from 'node:path';
const YAML = await import(pathToFileURL(join(qmdPackageDir(), 'node_modules', 'yaml', 'dist', 'index.js')).href);
export const parseDocument = YAML.parseDocument; export const parseDocument = YAML.parseDocument;
export default YAML; export default YAML;
+12
View File
@@ -2,6 +2,18 @@
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$doctorPath = Join-Path $root 'scripts/commands/doctor.ps1'
$doctorAst = [System.Management.Automation.Language.Parser]::ParseFile($doctorPath, [ref]$null, [ref]$null)
$probe = $doctorAst.Find({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Invoke-DoctorCommand' }, $true)
if (-not $probe) { throw 'Doctor command probe is missing.' }
Invoke-Expression $probe.Extent.Text
foreach ($code in @(0, 7)) {
$result = Invoke-DoctorCommand -Command 'python' -Arguments @('-c', "import sys; print('fixture 1.0'); print('fixture warning', file=sys.stderr); sys.exit($code)")
if ($result.ExitCode -ne $code -or ($result.Output -join ' ') -notmatch 'fixture warning' -or ($result.Output -join ' ') -notmatch 'fixture 1.0') { throw 'Doctor must capture native warnings and preserve the exit code.' }
if ($ErrorActionPreference -ne 'Stop') { throw 'Doctor command probe changed the caller error preference.' }
}
$missing = Invoke-DoctorCommand -Command 'ai-config-missing-doctor-test-command'
if ($missing.ExitCode -eq 0) { throw 'Doctor must not report a missing command as successful.' }
$doctorSummaryOutput = & (Join-Path $root 'scripts/commands/doctor.ps1') -Summary $doctorSummaryOutput = & (Join-Path $root 'scripts/commands/doctor.ps1') -Summary
if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' } if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' }
if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" } if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" }
+16
View File
@@ -8,6 +8,22 @@ while [ $# -gt 0 ]; do
esac esac
done done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
(
codex() { printf 'fixture warning\n' >&2; printf 'fixture 1.0\n'; return "$DOCTOR_TEST_CODE"; }
export -f codex
for DOCTOR_TEST_CODE in 0 7; do
export DOCTOR_TEST_CODE
if probe_output=$(bash "$root/scripts/commands/doctor.sh" --summary); then probe_status=0; else probe_status=$?; fi
if [ "$DOCTOR_TEST_CODE" -eq 0 ]; then
test "$probe_status" -eq 0
printf '%s\n' "$probe_output" | grep -Eq '^Doctor: PASS \| [0-9]+ checks$'
else
test "$probe_status" -eq 1
printf '%s\n' "$probe_output" | grep -Fq 'FAIL codex --version failed (exit 7): fixture warning'
printf '%s\n' "$probe_output" | grep -Eq '^Doctor: FAIL \| [0-9]+ checks$'
fi
done
)
doctor_summary_output=$(bash "$root/scripts/commands/doctor.sh" --summary) doctor_summary_output=$(bash "$root/scripts/commands/doctor.sh" --summary)
if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then
printf 'Doctor summary mode must not print individual PASS lines.\n' >&2 printf 'Doctor summary mode must not print individual PASS lines.\n' >&2
+15
View File
@@ -18,6 +18,21 @@ SPEC.loader.exec_module(OPTIONS)
class InstallOptionsTests(unittest.TestCase): class InstallOptionsTests(unittest.TestCase):
def test_codex_update_replaces_old_sandbox_mode_and_preserves_user_settings(self):
current = ('sandbox_mode = "workspace-write"\nmodel = "custom-model"\n'
'[windows]\nsandbox = "elevated"\n'
'[sandbox_workspace_write]\nnetwork_access = false\n'
'[mcp_servers.demo]\ncommand = "demo"\n')
managed = (ROOT / 'adapters/codex/config/config.toml').read_text(encoding='utf-8')
result = tomllib.loads(OPTIONS.merge_toml(current, managed))
self.assertEqual(result['sandbox_mode'], 'danger-full-access')
self.assertEqual(result['approval_policy'], 'on-request')
self.assertEqual(result['approvals_reviewer'], 'auto_review')
self.assertEqual(result['model'], 'custom-model')
self.assertEqual(result['mcp_servers']['demo'], {'command': 'demo'})
def test_merge_toml_preserves_local_plugin_table_indentation(self): def test_merge_toml_preserves_local_plugin_table_indentation(self):
plugin_state = (' [marketplaces.ponytail]\n' plugin_state = (' [marketplaces.ponytail]\n'
' source_type = "git"\n' ' source_type = "git"\n'
+25 -3
View File
@@ -1,12 +1,34 @@
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { cpSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as YAML from './fixtures/yaml-shim.mjs'; import * as YAML from './fixtures/yaml-shim.mjs';
import { collectionName, deviceEnv, idleMinutes, qmdConfigPath, writePrivateJson, writeTextAtomic, readJson, runQmdCli, MODELS } from '../../shared/qmd/qmd-lib.mjs'; import { collectionName, deviceEnv, idleMinutes, qmdConfigPath, qmdPackageDir, writePrivateJson, writeTextAtomic, readJson, runQmdCli, MODELS } from '../../shared/qmd/qmd-lib.mjs';
import { setModels, unsetModels, previousModels, restoreModels, setCollectionIgnore } from '../../shared/qmd/qmd-config.mjs'; import { setModels, unsetModels, previousModels, restoreModels, setCollectionIgnore } from '../../shared/qmd/qmd-config.mjs';
for (const layout of ['nested', 'hoisted']) {
test(`config CLI and YAML shim load ${layout} dependencies`, (t) => {
const root = mkdtempSync(join(tmpdir(), 'qmd dependencies '));
t.after(() => rmSync(root, { recursive: true, force: true }));
const pkg = join(root, 'node_modules', '@tobilu', 'qmd');
mkdirSync(pkg, { recursive: true });
const require = createRequire(join(qmdPackageDir(), 'package.json'));
const source = dirname(require.resolve('yaml/package.json'));
cpSync(source, join(layout === 'nested' ? pkg : root, 'node_modules', 'yaml'), { recursive: true });
const env = { ...process.env, QMD_PACKAGE_DIR: pkg, QMD_CONFIG_DIR: join(root, 'config') };
const output = execFileSync(process.execPath, [fileURLToPath(new URL('../../shared/qmd/qmd-config.mjs', import.meta.url)), 'set-models'], { env, encoding: 'utf8' });
assert.deepEqual(JSON.parse(output), { previous: null });
assert.deepEqual(YAML.default.parse(readFileSync(join(env.QMD_CONFIG_DIR, 'index.yml'), 'utf8')).models, MODELS);
const shim = new URL('./fixtures/yaml-shim.mjs', import.meta.url).href;
const parsed = execFileSync(process.execPath, ['--input-type=module', '-e', `import YAML from ${JSON.stringify(shim)}; console.log(JSON.stringify(YAML.parse('answer: 42')));`], { env, encoding: 'utf8' });
assert.deepEqual(JSON.parse(parsed), { answer: 42 });
});
}
test('collection name is stable, prefixed, and path-safe', () => { test('collection name is stable, prefixed, and path-safe', () => {
const a = collectionName('/tmp/My Repo/ĂĽ'); const a = collectionName('/tmp/My Repo/ĂĽ');
assert.match(a, /^repo-[0-9a-f]{12}$/); assert.match(a, /^repo-[0-9a-f]{12}$/);
+6
View File
@@ -46,6 +46,11 @@ foreach ($shell in @('powershell','bash')) {
if (@(Get-ChildItem "$package/agents" -File).Count -ne $sourceAgentCount) { throw "Missing agents in $package" } if (@(Get-ChildItem "$package/agents" -File).Count -ne $sourceAgentCount) { throw "Missing agents in $package" }
$expectedReviewerShellTool = if ($shell -eq 'powershell') { 'PowerShell' } else { 'Bash' } $expectedReviewerShellTool = if ($shell -eq 'powershell') { 'PowerShell' } else { 'Bash' }
if ($client -eq 'claude' -and (Get-Content (Join-Path $package 'agents/reviewer.md') -Raw) -notmatch "(?m)^tools: Read,Grep,Glob,$expectedReviewerShellTool\r?`$") { throw "Reviewer capability profile is missing in $package" } if ($client -eq 'claude' -and (Get-Content (Join-Path $package 'agents/reviewer.md') -Raw) -notmatch "(?m)^tools: Read,Grep,Glob,$expectedReviewerShellTool\r?`$") { throw "Reviewer capability profile is missing in $package" }
if ($client -eq 'claude') {
foreach ($role in @('architect','researcher')) {
if ((Get-Content (Join-Path $package "agents/$role.md") -Raw) -notmatch "(?m)^tools: Read,Grep,Glob,WebFetch,WebSearch,$expectedReviewerShellTool\r?`$") { throw "$role research shell capability is missing in $package" }
}
}
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount } $expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" } if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" }
$securityReview = if ($client -eq 'claude') { 'skills/security-review/SKILL.md' } else { 'skills/reviews/security-review/SKILL.md' } $securityReview = if ($client -eq 'claude') { 'skills/security-review/SKILL.md' } else { 'skills/reviews/security-review/SKILL.md' }
@@ -58,6 +63,7 @@ foreach ($shell in @('powershell','bash')) {
$events = @([regex]::Matches($content, '(?m)^\[\[hooks\.([^.\]]+)\]\]\s*$') | ForEach-Object { $_.Groups[1].Value }) $events = @([regex]::Matches($content, '(?m)^\[\[hooks\.([^.\]]+)\]\]\s*$') | ForEach-Object { $_.Groups[1].Value })
if ((($events | Sort-Object) -join ',') -ne 'SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $content -match '(?m)^async\s*=\s*true\s*$') { throw "Codex finish hook is incorrect in $package" } if ((($events | Sort-Object) -join ',') -ne 'SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $content -match '(?m)^async\s*=\s*true\s*$') { throw "Codex finish hook is incorrect in $package" }
if ($content -notmatch 'approvals_reviewer\s*=\s*"auto_review"') { throw "Codex auto review is missing in $package" } if ($content -notmatch 'approvals_reviewer\s*=\s*"auto_review"') { throw "Codex auto review is missing in $package" }
if ($content -notmatch '(?m)^sandbox_mode\s*=\s*"danger-full-access"\s*$') { throw "Codex full access is missing in $package" }
if ($content -notmatch '(?m)^max_depth\s*=\s*1\s*$') { throw "Codex agent depth limit is missing in $package" } if ($content -notmatch '(?m)^max_depth\s*=\s*1\s*$') { throw "Codex agent depth limit is missing in $package" }
if ($content -notmatch 'status_line\s*=\s*\["model", "reasoning", "fast-mode", "approval-mode", "five-hour-limit", "weekly-limit", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"\]') { throw "Codex status line is incorrect in $package" } if ($content -notmatch 'status_line\s*=\s*\["model", "reasoning", "fast-mode", "approval-mode", "five-hour-limit", "weekly-limit", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"\]') { throw "Codex status line is incorrect in $package" }
} elseif ((($settings.hooks.PSObject.Properties.Name | Sort-Object) -join ',') -ne 'PreCompact,SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $settings.hooks.Stop[0].hooks[0].async) { } elseif ((($settings.hooks.PSObject.Properties.Name | Sort-Object) -join ',') -ne 'PreCompact,SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $settings.hooks.Stop[0].hooks[0].async) {
+6
View File
@@ -52,6 +52,11 @@ for shell in powershell bash; do
expected_reviewer_shell_tool=Bash expected_reviewer_shell_tool=Bash
[ "$shell" != powershell ] || expected_reviewer_shell_tool=PowerShell [ "$shell" != powershell ] || expected_reviewer_shell_tool=PowerShell
if [ "$client" = claude ]; then grep -q "^tools: Read,Grep,Glob,$expected_reviewer_shell_tool\$" "$package/agents/reviewer.md"; fi if [ "$client" = claude ]; then grep -q "^tools: Read,Grep,Glob,$expected_reviewer_shell_tool\$" "$package/agents/reviewer.md"; fi
if [ "$client" = claude ]; then
for role in architect researcher; do
grep -q "^tools: Read,Grep,Glob,WebFetch,WebSearch,$expected_reviewer_shell_tool\$" "$package/agents/$role.md" || { printf '%s research shell capability is missing in %s\n' "$role" "$package"; exit 1; }
done
fi
expected_skill_count=$source_skill_count expected_skill_count=$source_skill_count
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count)) [ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count" test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count"
@@ -78,6 +83,7 @@ for shell in powershell bash; do
! grep -q 'flashbang-if-input' "$package/$file" ! grep -q 'flashbang-if-input' "$package/$file"
! grep -Eq '^async[[:space:]]*=[[:space:]]*true[[:space:]]*$' "$package/$file" ! grep -Eq '^async[[:space:]]*=[[:space:]]*true[[:space:]]*$' "$package/$file"
grep -q 'approvals_reviewer[[:space:]]*=[[:space:]]*"auto_review"' "$package/$file" grep -q 'approvals_reviewer[[:space:]]*=[[:space:]]*"auto_review"' "$package/$file"
grep -Eq '^sandbox_mode[[:space:]]*=[[:space:]]*"danger-full-access"[[:space:]]*$' "$package/$file"
grep -Eq '^max_depth[[:space:]]*=[[:space:]]*1[[:space:]]*$' "$package/$file" grep -Eq '^max_depth[[:space:]]*=[[:space:]]*1[[:space:]]*$' "$package/$file"
grep -Fq 'status_line = ["model", "reasoning", "fast-mode", "approval-mode", "five-hour-limit", "weekly-limit", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]' "$package/$file" grep -Fq 'status_line = ["model", "reasoning", "fast-mode", "approval-mode", "five-hour-limit", "weekly-limit", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]' "$package/$file"
else else
+1 -1
View File
@@ -1,3 +1,3 @@
name: architect name: architect
description: Technical architecture and design description: Compare designs and recommend technical boundaries and interfaces before implementation
role: architect role: architect
+2
View File
@@ -3,3 +3,5 @@
Start from the parent's requirements and references. Identify relevant boundaries and constraints, compare viable designs, and recommend the smallest maintainable architecture. Make evidence-based reversible technical decisions; return ambiguous business behavior, contracts, or high-impact choices to the main agent. Do not invent requirements or broadly explore unrelated code. Use the shared orchestration handoff protocol. Start from the parent's requirements and references. Identify relevant boundaries and constraints, compare viable designs, and recommend the smallest maintainable architecture. Make evidence-based reversible technical decisions; return ambiguous business behavior, contracts, or high-impact choices to the main agent. Do not invent requirements or broadly explore unrelated code. Use the shared orchestration handoff protocol.
Before accepting a design that relies on external technology behavior, MUST apply the planning checkpoint in `technical-research`. Carry supporting evidence or unresolved assumptions into the handoff. Before accepting a design that relies on external technology behavior, MUST apply the planning checkpoint in `technical-research`. Carry supporting evidence or unresolved assumptions into the handoff.
Use shell commands for inspection and required research tools, including semantic search and MCPorter. Do not modify implementation code unless assigned.
+1 -1
View File
@@ -1,3 +1,3 @@
name: implementer name: implementer
description: Execute engineering work description: Implement scoped features and fixes with proportionate tests and self-review
role: implementer role: implementer
+1 -1
View File
@@ -1,3 +1,3 @@
name: researcher name: researcher
description: Repository and technical investigation description: Investigate repository behavior, external documentation, and unknowns with source evidence
role: researcher role: researcher
+2
View File
@@ -1,3 +1,5 @@
# Researcher # Researcher
Start with the parent's question and references. Inspect only code, patterns, documentation, and constraints needed to answer it; expand when evidence requires it. Return conclusions, concrete references, and unknowns using the shared orchestration handoff protocol. Avoid modifying implementation code by default. Start with the parent's question and references. Inspect only code, patterns, documentation, and constraints needed to answer it; expand when evidence requires it. Return conclusions, concrete references, and unknowns using the shared orchestration handoff protocol. Avoid modifying implementation code by default.
Use shell commands for inspection and required research tools, including semantic search and MCPorter.
+1 -1
View File
@@ -1,3 +1,3 @@
name: reviewer name: reviewer
description: Independent quality review description: Review diffs for correctness, security, and contract defects without running tests or builds
role: reviewer role: reviewer
+4 -2
View File
@@ -1,7 +1,9 @@
# Reviewer # Reviewer
Provide static and semantic correctness review using the acceptance criteria and actual git diff. Inspect changed symbols first, then direct callers, dependencies, tests, and contracts needed to judge correctness. Expand for shared state, configuration, persistence, security, generated code, or public API effects; avoid broad repository exploration by default. Do not run tests, builds, or runtime checks; identify missing execution evidence for the parent or verifier instead. Provide static and semantic correctness review using the acceptance criteria and actual git diff. Inspect changed symbols first, then direct callers, dependencies, tests, and contracts needed to judge correctness. Expand for shared state, configuration, persistence, security, generated code, or public API effects; avoid broad repository exploration by default.
Use shell commands to inspect diffs and run required research tools, including semantic search and MCPorter. Do not run tests, builds, or runtime checks; reuse supplied execution evidence and report gaps to the parent or verifier.
For each finding, report SEVERITY (blocking, important, or minor), LOCATION, IMPACT, EVIDENCE, and SCOPE (introduced, pre-existing, or uncertain). Do not report stylistic preferences or pre-existing issues unless they materially affect the requested change. Prioritize actionable correctness defects, keep unrelated cleanup out of scope, and use blocking only for security, data-loss, broken contracts, or deterministic failures. For each finding, report SEVERITY (blocking, important, or minor), LOCATION, IMPACT, EVIDENCE, and SCOPE (introduced, pre-existing, or uncertain). Do not report stylistic preferences or pre-existing issues unless they materially affect the requested change. Prioritize actionable correctness defects, keep unrelated cleanup out of scope, and use blocking only for security, data-loss, broken contracts, or deterministic failures.
Check logic, regressions, architecture, maintainability, security, and contract consistency. Do not execute checks or duplicate a verifier's evidence. Follow the shared orchestration handoff protocol; report actionable findings with severity and evidence. Do not modify implementation code unless assigned. Check logic, regressions, architecture, maintainability, security, and contract consistency. Follow the shared orchestration handoff protocol. Do not modify implementation code unless assigned.
+1 -1
View File
@@ -1,3 +1,3 @@
name: verifier name: verifier
description: Focused verification of completed work description: Run focused tests, builds, and reproductions to verify acceptance criteria and report execution evidence
role: verifier role: verifier
+3 -1
View File
@@ -2,6 +2,8 @@
This directory contains registered hooks and optional utilities. They do not bypass client permission prompts or grant access. This directory contains registered hooks and optional utilities. They do not bypass client permission prompts or grant access.
Both clients register `Stop` (`flashbang`) for finish notifications. Claude also registers `PreCompact` (`Record-Compact`/`record-compact.sh`) and `SessionStart` (`Show-SessionStatePointer`/`show-session-state-pointer.sh`). PreCompact records its timestamp under the installed configuration root. Both session hooks point to an existing workspace `.ai-session/state.json`, using event `cwd` or the process working directory. The main agent owns that state; hooks never write it or print its contents. Missing state is a successful no-op. When Flashbang is enabled, both clients register `Stop` (`flashbang`) for finish notifications. Claude also registers `PreCompact` (`Record-Compact`/`record-compact.sh`) and `SessionStart` (`Show-SessionStatePointer`/`show-session-state-pointer.sh`). PreCompact records its timestamp under the installed configuration root. Both session hooks point to an existing workspace `.ai-session/state.json`, using event `cwd` or the process working directory. The main agent owns that state; hooks never write it or print its contents. Missing state is a successful no-op.
`Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are not registered or invoked automatically. The safety utility checks selected command-text patterns; the verification utility runs the repository build only when explicitly invoked. `Test-SessionConfig`/`test-session-config.sh` checks repository inputs during a build. `Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are not registered or invoked automatically. The safety utility checks selected command-text patterns; the verification utility runs the repository build only when explicitly invoked. `Test-SessionConfig`/`test-session-config.sh` checks repository inputs during a build.
When local QMD search is enabled, both clients also register a `SessionStart` warm-up hook (`Start-QmdWarm.ps1`/`qmd-warm.sh`). It starts the installed Node wrapper in the background from the process working directory, only inside a Git repository. Warm-up errors are recorded in `~/.my-ai-configuration/qmd/warm.log`; they do not fail the session. Disabling local search removes the hook registration.
+2 -3
View File
@@ -1,8 +1,7 @@
// shared/qmd/qmd-config.mjs — edits only the models: block and setup-owned collection ignore lists in QMD's index.yml. // shared/qmd/qmd-config.mjs — edits only the models: block and setup-owned collection ignore lists in QMD's index.yml.
import { existsSync, readFileSync } from 'node:fs'; import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from 'node:url';
import { MODELS, qmdConfigPath, qmdPackageDir, writeTextAtomic } from './qmd-lib.mjs'; import { MODELS, importQmdDependency, qmdConfigPath, writeTextAtomic } from './qmd-lib.mjs';
export function setModels(text, YAML) { export function setModels(text, YAML) {
const document = YAML.parseDocument(text || ''); const document = YAML.parseDocument(text || '');
@@ -45,7 +44,7 @@ export function setCollectionIgnore(text, YAML, name, ignore) {
async function main(action, argument) { async function main(action, argument) {
if (!['set-models', 'unset-models', 'restore-models'].includes(action)) throw new Error('usage: qmd-config.mjs set-models|unset-models|restore-models <json>'); if (!['set-models', 'unset-models', 'restore-models'].includes(action)) throw new Error('usage: qmd-config.mjs set-models|unset-models|restore-models <json>');
const YAML = await import(pathToFileURL(join(qmdPackageDir(), 'node_modules', 'yaml', 'dist', 'index.js')).href); const YAML = await importQmdDependency('yaml');
const path = qmdConfigPath(); const path = qmdConfigPath();
const current = existsSync(path) ? readFileSync(path, 'utf8') : ''; const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
let next; let next;
+3 -3
View File
@@ -6,7 +6,7 @@ import { createServer as createHttpServer } from 'node:http';
import { dirname, join, resolve } from 'node:path'; import { dirname, join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from 'node:url';
import { setCollectionIgnore } from './qmd-config.mjs'; import { setCollectionIgnore } from './qmd-config.mjs';
import { MASK, collectionName, idleMinutes, importQmd, qmdConfigPath, qmdDbPath, readJson, runQmdCli, stateDir, writePrivateJson, writeTextAtomic } from './qmd-lib.mjs'; import { MASK, collectionName, idleMinutes, importQmd, importQmdDependency, qmdConfigPath, qmdDbPath, readJson, runQmdCli, stateDir, writePrivateJson, writeTextAtomic } from './qmd-lib.mjs';
const MAX_BODY = 64 * 1024; const MAX_BODY = 64 * 1024;
@@ -204,8 +204,8 @@ async function main() {
keepModelsLoaded(store); keepModelsLoaded(store);
// QMD keeps index.yml authoritative: createStore and the CLI re-sync it into SQLite, and its addCollection // QMD keeps index.yml authoritative: createStore and the CLI re-sync it into SQLite, and its addCollection
// write-through drops `ignore`, so the list is written into index.yml and re-synced the way QMD does it. // write-through drops `ignore`, so the list is written into index.yml and re-synced the way QMD does it.
const fastGlob = (await importQmd(join('node_modules', 'fast-glob', 'out', 'index.js'))).default; const fastGlob = (await importQmdDependency('fast-glob')).default;
const YAML = await importQmd(join('node_modules', 'yaml', 'dist', 'index.js')); const YAML = await importQmdDependency('yaml');
const { loadConfig } = await importQmd(join('dist', 'collections.js')); const { loadConfig } = await importQmd(join('dist', 'collections.js'));
const { syncConfigToDb } = await importQmd(join('dist', 'store.js')); const { syncConfigToDb } = await importQmd(join('dist', 'store.js'));
const collectionsFile = join(dir, 'collections.json'); const collectionsFile = join(dir, 'collections.json');
+7
View File
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'; import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import { createRequire } from 'node:module';
import { dirname, join, resolve } from 'node:path'; import { dirname, join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from 'node:url';
@@ -82,6 +83,12 @@ export function importQmd(subpath = join('dist', 'index.js')) {
return import(pathToFileURL(join(qmdPackageDir(), subpath)).href); return import(pathToFileURL(join(qmdPackageDir(), subpath)).href);
} }
// Resolve from QMD so both nested and hoisted dependency installations work.
export function importQmdDependency(name) {
const require = createRequire(pathToFileURL(join(qmdPackageDir(), 'package.json')));
return import(pathToFileURL(require.resolve(name)).href);
}
// Runs the QMD CLI through node without a shell, so free-text arguments are never re-parsed. // Runs the QMD CLI through node without a shell, so free-text arguments are never re-parsed.
export function runQmdCli(args, options = {}) { export function runQmdCli(args, options = {}) {
return execFileSync(process.execPath, [join(qmdPackageDir(), 'dist', 'cli', 'qmd.js'), ...args], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, ...options }); return execFileSync(process.execPath, [join(qmdPackageDir(), 'dist', 'cli', 'qmd.js'), ...args], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, ...options });