Compare commits

..
22 Commits
Author SHA1 Message Date
fraujulian fb65c79f80 refactor(ide): delete unwanted .idea
CI / linux (push) Failing after 9s
2026-09-16 19:30:31 +02:00
fraujulian 74dc0dda12 chore(ide): add shared webstorm project configuration 2026-09-16 19:18:28 +02:00
fraujulian 89e8f7ac4e docs(readme): add quickstart quickupdate and documentation index 2026-09-16 19:18:27 +02:00
fraujulian 389442a395 docs(readme): correct requirements and installation behavior 2026-09-16 19:18:27 +02:00
fraujulian 3de4a100ed docs(specs): mark token efficiency design as historical 2026-09-16 19:18:27 +02:00
fraujulian e882eea2ee docs(plans): mark token efficiency plan as historical 2026-09-16 19:18:27 +02:00
fraujulian a3f62f0992 docs(skills): clarify conditional loading and proportional checks 2026-09-16 19:18:26 +02:00
fraujulian 81d215a02e docs(plugins): clarify selection and registration preservation 2026-09-16 19:18:26 +02:00
fraujulian 07ba85de6a docs(hooks): distinguish registered hooks from optional utilities 2026-09-16 19:18:26 +02:00
fraujulian 801ef0e7eb docs(config): explain permission defaults and status displays 2026-09-16 19:18:26 +02:00
fraujulian 18d884bbe7 test(update): cover saved choices and unattended quick previews 2026-09-16 19:18:25 +02:00
fraujulian e62609d433 feat(update): reuse saved choices with quick mode 2026-09-16 19:18:25 +02:00
fraujulian f0fd4349c2 feat(install): remember successful installation choices 2026-09-16 19:18:25 +02:00
fraujulian 3a5790e67c feat(selection): persist validated update preferences atomically 2026-09-16 19:18:24 +02:00
fraujulian 50cb82074e feat(statusline): show compact colored context and token metrics 2026-09-16 19:18:24 +02:00
fraujulian d5f7a61c0f chore(workflow): replace blanket validation with targeted checks 2026-09-16 19:18:24 +02:00
fraujulian 35c39e7bfe refactor(rules): scope refactoring checks to affected behavior 2026-09-16 19:18:23 +02:00
fraujulian 317776f405 refactor(rules): align completion criteria with targeted verification 2026-09-16 19:18:23 +02:00
fraujulian 516a87cbb3 refactor(rules): make verification proportional to change risk 2026-09-16 19:18:23 +02:00
fraujulian c92ae50e30 test(plugins): cover installed plugin preselection 2026-09-16 19:18:23 +02:00
fraujulian 122cbba9b7 fix(plugins): correct navigation keys and menu encoding 2026-09-16 19:18:23 +02:00
fraujulian c749fcd792 fix(install): preserve codex plugin registrations during sync 2026-09-16 19:18:22 +02:00
34 changed files with 641 additions and 93 deletions

No files matched your search

+1
View File
@@ -11,3 +11,4 @@ auth.json
credentials.json credentials.json
secrets.json secrets.json
!.env.example !.env.example
.idea
+4 -3
View File
@@ -33,9 +33,10 @@ Write all repository content, generated instructions, scripts, prompts, document
## Workflow ## Workflow
- Run `scripts/build.ps1` to validate every project change before completion. - Choose the smallest sufficient verification based on the change's behavior, affected consumers, risk, and remaining uncertainty. Inspection may suffice for low-risk changes; full builds, test suites, and independent reviews are not automatic.
- Rebuild generated output after semantic changes. - Run `scripts/build.ps1 -Summary` to regenerate packages when shared definitions, adapters, or generation logic change. This generates configuration packages; it does not require building applications that use them.
- Run `scripts/doctor.ps1` when changes affect generated output, installation, client configuration, hooks, or environment validation. - Run `scripts/doctor.ps1 -Summary` when installation, client configuration, hooks, or environment changes need its checks. Changes to instruction wording alone do not require it.
- Run focused script tests when script behavior changes. Broaden or repeat checks only when failures, affected dependencies, or unresolved risks justify it.
- Preserve backward compatibility where practical. - Preserve backward compatibility where practical.
- Keep changes focused on the requested configuration behavior. - Keep changes focused on the requested configuration behavior.
+109 -31
View File
@@ -11,20 +11,90 @@
It keeps rules, skills, agents, hooks, and orchestration guidance in shared source files. Client adapters turn those files It keeps rules, skills, agents, hooks, and orchestration guidance in shared source files. Client adapters turn those files
into platform-specific packages that can be inspected before they are installed. into platform-specific packages that can be inspected before they are installed.
## Quickstart
From an existing checkout, with the [requirements](#requirements) installed, run pull, build, then install.
Stop if any command fails. Installation asks for the platform, client, and plugin selection.
PowerShell (Windows):
```powershell
git pull --ff-only
.\scripts\build.ps1 -Summary
.\scripts\install.ps1 -Summary
```
Bash (Linux):
```bash
git pull --ff-only
./scripts/build.sh --summary
./scripts/install.sh --summary
```
For an already-installed configuration, use Quickupdate instead.
## Quickupdate
Run these commands from the repository root. Quick reuses the platform, client, and plugin selection from the last
successful installation or update without opening selection menus. Stop if any command fails.
PowerShell (Windows):
```powershell
git pull --ff-only
.\scripts\build.ps1 -Summary
.\scripts\update.ps1 -Quick -Summary
```
Bash (Linux):
```bash
git pull --ff-only
./scripts/build.sh --summary
./scripts/update.sh --quick --summary
```
If no selection has been saved yet, Quick stops with a hint. Run `scripts/update.ps1` or `./scripts/update.sh` once
without Quick to save your choices. They are stored in `~/.my-ai-configuration/selection.json`; dry runs and failed
runs do not overwrite them. To change the selection later, run a normal update again. Quick does not automatically
select newly added plugins.
Install and update also rebuild internally; the explicit build above lets you check the packages before installation.
## Documentation
| Document | Contents |
| --- | --- |
| [Architecture](docs/architecture.md) | Shared sources, adapters, generated packages, and installation. |
| [Configuration](docs/configuration.md) | Client settings, permission defaults, status displays, and verification. |
| [Plugins](docs/plugins.md) | Plugin sources, selection, installation, updates, and removal. |
| [Hooks](docs/hooks.md) | Registered events, session-state helpers, notifications, and optional utilities. |
| [Agents](docs/agents.md) | Agent roles and when delegation is useful. |
| [Skills](docs/skills.md) | Skill categories and conditional rule loading. |
| [Codex adapter](adapters/codex/README.md) | Codex-specific configuration files. |
| [Claude Code adapter](adapters/claude/README.md) | Claude-specific configuration files. |
| [Hook package](shared/hooks/README.md) | Overview shipped with the hook scripts. |
| [Repository instructions](AI-Instructions.md) | Rules for maintaining this repository. |
Historical records, not current runbooks:
- [Token-efficiency design](docs/superpowers/specs/2026-09-16-token-efficiency-design.md)
- [Token-efficiency implementation plan](docs/superpowers/plans/2026-09-16-token-efficiency.md)
## Features ## Features
- One shared source of truth for Codex and Claude Code. - One shared source of truth for Codex and Claude Code.
- Separate generated packages for Windows and Linux. - Separate generated packages for Windows and Linux.
- Native PowerShell 5.1 and Bash scripts. - Native PowerShell 5.1 and Bash scripts.
- Interactive or non-interactive (`-Client`/`-Platform`) installation for Codex, Claude Code, or both. - Installation for Codex, Claude Code, or both, with explicit target parameters and an interactive plugin selection.
- Claude Code loads technology- and situation-specific rules as skills, so only their name and description sit - Claude Code loads technology- and situation-specific rules as skills, so only their name and description sit
permanently in context; the full rule text loads only when the skill is invoked. Codex keeps loading rules as plain permanently in context; the full rule text loads only when the skill is invoked. Codex keeps loading rules as plain
files, unchanged. files, unchanged.
- A managed-file manifest (path and SHA-256 per destination) makes installation idempotent, detects files this setup - A managed-file manifest tracks unchanged and stale files. Replaced files are backed up; locally modified stale
previously installed but no longer ships, and protects files a user changed locally instead of silently overwriting files are retained with a warning.
or deleting them. - Dry-run support (no destination writes, backups, or plugin changes) and timestamped backup directories.
- Dry-run support (no writes, no backups, no plugin changes) and timestamped, per-run backup directories. - Finish notifications, Claude session-state hooks, optional safety utilities, and compact status displays.
- Shared safety, notification, validation, and status-line hooks.
- Separate install and update scripts: install refuses to run against an already-installed destination, update refuses - Separate install and update scripts: install refuses to run against an already-installed destination, update refuses
to run against one that is not installed yet. to run against one that is not installed yet.
- User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and - User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and
@@ -58,7 +128,10 @@ generated/
## Requirements ## Requirements
- Windows PowerShell 5.1 or newer on Windows. - Windows PowerShell 5.1 or newer on Windows.
- Bash on Linux. - Bash 4.3 or newer on Linux, with standard utilities such as `awk`, `sed`, and `sha256sum`.
- Python 3.11 or newer for configuration validation (`python` for PowerShell scripts, `python3` for Bash scripts).
- Git for marketplace installation and branch display; Node.js/npm (`npx`) for Codex's external skill installers.
- `jq` for Claude's Bash status line; without it, that status line produces no output.
- The Codex and Claude Code CLIs for the clients that should be installed or diagnosed. - The Codex and Claude Code CLIs for the clients that should be installed or diagnosed.
- Network access for plugin installation and updates. - Network access for plugin installation and updates.
@@ -79,8 +152,9 @@ Bash: (Linux)
./scripts/build.sh ./scripts/build.sh
``` ```
Every repository change must pass the build before completion. Building only writes reproducible files below Rebuild when shared definitions, adapters, or generation logic change. Choose other verification according to the
`generated/`; it does not modify global user configuration. change's risk; documentation-only edits do not require a build. Building writes reproducible packages below
`generated/` and may use temporary validation files; it does not install global user configuration.
## Installation ## Installation
@@ -113,30 +187,23 @@ Bash: (Linux)
./scripts/install.sh ./scripts/install.sh
``` ```
Install non-interactively, for scripting or CI: Omitting either target parameter opens its selection prompt. There is no automatic platform default.
For a noninteractive preview, supply both target parameters together with `-DryRun` or `--dry-run`.
Powershell: (Windows) Every installed file is tracked in a per-destination manifest (`.ai-config-manifest.tsv`). On update, unchanged files
```powershell are not written or backed up. Files still shipped by the package are replaced when their content differs, including
.\scripts\install.ps1 -Client Both -Platform Windows local edits; the previous content is backed up first. An untracked file at a package target path is also backed up
``` and replaced, with a warning. Untracked files outside package target paths are left alone.
Bash: (Linux) Previously managed files no longer shipped are backed up and removed if unchanged. If locally modified, they are
```bash backed up and retained with a warning. Backups are stored under `backups/<timestamp>/` in each destination.
./scripts/install.sh --client both --platform linux Local Codex `plugins` and `marketplaces` tables are preserved when syncing `config.toml`; other local settings are
``` not generally merged. Dry runs still rebuild `generated/`, but do not modify installation destinations.
`-Platform`/`--platform` defaults to the current platform when omitted together with `-Client`/`--client`.
Every installed file is tracked in a per-destination manifest (`.ai-config-manifest.tsv`). A second run of the same
installation is a no-op for unchanged files (no write, no backup). A file this setup installed before but no longer
ships is backed up and removed, unless it was changed locally since the last install, in which case it is backed up
and left in place with a warning instead. A file the installer never tracked is never touched, even if a file of the
same name is now part of the package. Backups land in a single timestamped directory per run under
`backups/<timestamp>/` inside each destination.
Selecting Codex also installs shared skills to `.agents/skills`. Before plugins are installed, an interactive list lets Selecting Codex also installs shared skills to `.agents/skills`. Before plugins are installed, an interactive list lets
you toggle each configured plugin on or off (all checked by default); unchecked plugins are skipped. This step is also you toggle each configured plugin on or off (all checked by default); unchecked plugins are skipped. This step is also
skipped for a dry run. skipped for a dry run. Codex-only selection omits Humanizer, Impeccable, and Anthropic Frontend Design because they
use separate skill installers and are always ensured. See [Plugins](docs/plugins.md) for client-specific behavior.
## Update ## Update
@@ -157,7 +224,8 @@ Bash: (Linux)
It accepts the same `-DryRun`/`--dry-run`, `-Client`/`--client`, and `-Platform`/`--platform` parameters as the It accepts the same `-DryRun`/`--dry-run`, `-Client`/`--client`, and `-Platform`/`--platform` parameters as the
installer, re-syncs every managed file the same way, and shows the same plugin toggle list — but pre-checks plugins installer, re-syncs every managed file the same way, and shows the same plugin toggle list — but pre-checks plugins
that are already installed. Checking a plugin that is not installed installs it; checking one that is installed that are already installed. Checking a plugin that is not installed installs it; checking one that is installed
updates it; unchecking an installed plugin uninstalls it. updates it; unchecking an installed plugin uninstalls it. Selection happens before managed files are synchronized.
For `Both`, the initial selection follows Claude's installed plugins, not the union of both clients.
## Doctor ## Doctor
@@ -174,7 +242,8 @@ Bash: (Linux)
``` ```
Use `-Summary` (PowerShell) or `--summary` (Bash) with build, doctor, install, update, and test scripts for compact output. Use `-Summary` (PowerShell) or `--summary` (Bash) with build, doctor, install, update, and test scripts for compact output.
Omit the flag for detailed output. Warnings and failure diagnostics remain visible in both modes. Omit the flag for detailed output. Summary mode retains warnings and failure status, but can suppress captured
plugin-command output on failure. Set `AI_CONFIG_VERBOSE=1` when full plugin failure output is needed.
## Configuration ## Configuration
@@ -199,6 +268,14 @@ Both clients receive the same logical agent roles:
Agent use is proportional to the task. Small changes can stay with the implementer, while additional roles are available Agent use is proportional to the task. Small changes can stay with the implementer, while additional roles are available
when research, design, independent review, or focused verification adds value. when research, design, independent review, or focused verification adds value.
Verification is also proportional: the agent selects checks based on changed behavior, affected callers, risk,
and uncertainty. A small change may need only focused inspection; builds, tests, and separate reviews are not
automatic. Explicit user and project requirements still apply.
Both status displays include model, effort, repository, branch, context-window size, context used, and tokens used.
Claude uses two colored lines, with context usage turning yellow at 60% and red at 85%. Codex uses its native status
items and rendering. See [Configuration](docs/configuration.md) for details and limitations.
## Extending the Configuration ## Extending the Configuration
### Add a rule ### Add a rule
@@ -206,7 +283,8 @@ when research, design, independent review, or focused verification adds value.
Create a focused Markdown file in `shared/rules/`. For a rule that applies only to a language, framework, tool, or Create a focused Markdown file in `shared/rules/`. For a rule that applies only to a language, framework, tool, or
change area, add a row to `adapters/claude/rule-skills.tsv` (Claude generates it as a skill) and add its loading change area, add a row to `adapters/claude/rule-skills.tsv` (Claude generates it as a skill) and add its loading
condition to the Codex rule-loading text in `scripts/build.ps1`/`scripts/build.sh` (Codex loads it as a plain file, by condition to the Codex rule-loading text in `scripts/build.ps1`/`scripts/build.sh` (Codex loads it as a plain file, by
path — unchanged from before). A rule that should always apply, like `general.md`, needs neither. path). Put always-applicable guidance in `general.md`, which the build embeds for both clients; a new rule file is
not automatically embedded.
### Add a skill ### Add a skill
+27 -2
View File
@@ -2,10 +2,35 @@
Codex receives a generated global `AGENTS.md`, agent TOML files, skills, rule files, and a `config.toml` adapter. It keeps workspace sandboxing, 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` permission, and five-operation concurrency. Installation resolves user paths at runtime; the repository contains no machine-specific paths. Codex receives a generated global `AGENTS.md`, agent TOML files, skills, rule files, and a `config.toml` adapter. It keeps workspace sandboxing, 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` permission, and five-operation concurrency. Installation resolves user paths at runtime; the repository contains no machine-specific paths.
Codex keeps rules as files and loads only applicable focused rules. Claude embeds `general.md` and generates the remaining rules as skills from `adapters/claude/rule-skills.tsv`. `security.md` is a short universal baseline; focused security rules load only for authentication, web, API, data, files, network, cryptography, and supply-chain work. Both clients embed `shared/rules/general.md` in their global instructions. Codex loads applicable focused rules from `rules/`; Claude generates them as skills from `adapters/claude/rule-skills.tsv`. Angular, WPF, and UI/UX rules have small entry points with references loaded by topic. `security.md` is the baseline for programming tasks; focused security rules cover authentication, web, API, data, files, network, cryptography, and supply-chain work.
`agents.max_depth = 1` limits V1 Codex subagent recursion. `features.multi_agent_v2` is not enabled. If enabled later, V2 takes precedence over `agents.enabled` and ignores `agents.max_depth`. The Codex adapter sets `agents.max_depth = 1` and does not enable `features.multi_agent_v2`. Shared instructions also prohibit subagents from creating further subagents. Agent roles are optional, not stages that every task must run.
The build parses TOML with Python's standard-library `tomllib`, validates the emitted Claude settings shape, and uses `codex --strict-config` against an isolated generated configuration when Codex is available. This rejects malformed and unknown Codex configuration keys for the installed CLI version. The build parses TOML with Python's standard-library `tomllib`, validates the emitted Claude settings shape, and uses `codex --strict-config` against an isolated generated configuration when Codex is available. This rejects malformed and unknown Codex configuration keys for the installed CLI version.
Client settings remain thin adapters. Shared semantics remain in `shared/`. User plugins are managed separately through `adapters/plugins.tsv` and client-native installation commands. Client settings remain thin adapters. Shared semantics remain in `shared/`. User plugins are managed separately through `adapters/plugins.tsv` and client-native installation commands.
## Permission defaults
Codex sets `approval_policy = "on-request"`, `approvals_reviewer = "auto_review"`, and `sandbox_mode = "workspace-write"`. Eligible approval requests go through automatic review; sandboxing remains enabled. Claude sets `permissions.defaultMode = "auto"`. These are generated defaults, not a guarantee that a running session, account policy, or explicit launch override uses the same mode.
## Status displays
Codex's native `tui.status_line` contains `model`, `reasoning`, `project-name`, `git-branch`, `context-window-size`, `context-used`, and `used-tokens`, in that order. Rendering and colors are controlled by Codex; the adapter does not define custom colors per field.
Claude runs `shared/statusline/statusline.ps1` on Windows or `statusline.sh` on Linux. Its two-line display is:
```text
Opus · Effort high · MyRepo @ main
Ctx 200k · Used 42% · Tokens 16.7k
```
Model and context-window size are cyan, effort and cumulative tokens magenta, repository blue, and branch green. Context usage is green below 60%, yellow from 60%, and red from 85%, using the rounded displayed percentage. A nonempty `NO_COLOR` disables ANSI colors. The Bash implementation requires `jq`.
`Ctx` is the context-window capacity; `Used` is the reported percentage occupied. `Tokens` adds the session's reported total input and output tokens, not just the current context. Counts use `k` and `M` suffixes. Missing model, effort, context size, or percentage is shown as `-`; missing token totals start at zero. Repository names fall back to the Git root or current directory; unavailable branches show `-`.
## 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.
Rebuild packages after changes to shared definitions or adapters. Generated defaults reach a user's configuration only through installation or update. Updates back up replaced files and preserve Codex's local `plugins` and `marketplaces` tables; other local settings are not generally merged. See [the README](../README.md) for overwrite and backup behavior.
+15 -3
View File
@@ -1,7 +1,19 @@
# Hooks # Hooks
Deterministic safety and validation hooks live in `shared/hooks/scripts/`. The flashbang entry points live directly in `shared/hooks/`. The command safety hook rejects obvious destructive commands outside the current development workspace. The session validation hook checks repository-local generated inputs. The post-change hook is an optional verification entry point. Hook scripts and optional utilities live in `shared/hooks/scripts/`. The flashbang entry points live directly in `shared/hooks/`. Shipping a script does not automatically register it as a hook.
The build includes the hooks and their client-specific registration in each payload. Installation places them beside the generated client files. The scripts do not grant permissions, bypass approvals, or replace client permission systems. The adapters register these events:
The PowerShell and Bash hook variants are registered only for the Codex and Claude `Stop` events. They run after the main agent finishes responding, not during tool calls, thinking, permission requests, or notifications. The default flash lasts about 500 milliseconds. The Windows command deliberately 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. The Bash hook creates the equivalent temporary overlay through Python's standard `tkinter` module, uses `xrandr` for multi-monitor bounds when available, and does not reactivate the terminal. It falls back to `notify-send` in headless or minimal environments. Hooks are intentionally not executed during build or doctor. | Client | Event | Action |
| --- | --- | --- |
| Codex and Claude | `Stop` | Run the finish notification (`flashbang`). |
| Claude | `PreCompact` | Append an event and UTC timestamp to `.ai-session/telemetry.jsonl` under the installed configuration root. |
| Claude | `SessionStart` | Print a short pointer if `.ai-session/state.json` exists under that root. |
The session-state hooks do not create a task summary or restore conversation state. Their paths are relative to the installed configuration root, not the current project. The scripts do not grant permissions, bypass approvals, or replace client permission systems.
The flashbang runs on `Stop`, after the main agent finishes responding. The default flash lasts about 500 milliseconds. The Windows 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. 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.
`Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are unregistered utilities. The former checks command text for selected dangerous patterns and outside-workspace paths; it is not a complete security boundary. The latter explicitly runs the repository build when invoked; it is not an automatic requirement after each change.
`Test-SessionConfig`/`test-session-config.sh` checks required repository inputs and is called by the build. Build and doctor do not execute the registered notification or session-state hooks. Claude's status line is a separate `statusLine` command, not a hook event.
+5 -3
View File
@@ -2,13 +2,15 @@
The user-level plugin manifest is `adapters/plugins.tsv`. It maps each logical plugin to the marketplace selector understood by Claude Code and Codex. The user-level plugin manifest is `adapters/plugins.tsv`. It maps each logical plugin to the marketplace selector understood by Claude Code and Codex.
The current baseline includes Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and Anthropic Frontend Design. Both the install and update scripts operate at user scope and show an interactive list to toggle each configured plugin on or off before applying anything (skipped for a dry run). The current baseline includes Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and Anthropic Frontend Design. Both scripts operate at user scope and show a plugin selector unless running a dry run. Installation copies configuration before selection; update selects plugins before copying configuration. Both rebuild generated packages first.
`install.ps1`/`install.sh` starts every plugin checked by default; unchecking one skips it. It refuses to run against an already-installed destination (`Already installed, use the update script.`). `install.ps1`/`install.sh` starts every plugin checked by default; unchecking one skips it. It refuses to run against an already-installed destination (`Already installed, use the update script.`).
`update.ps1`/`update.sh` starts each plugin checked to match what is already installed (a disabled but installed Claude plugin still counts as installed and is re-enabled), so a plain confirm re-syncs and updates the existing set without silently touching anything else. Checking a plugin that is not installed installs it; unchecking an installed plugin uninstalls it (`claude plugin uninstall`/`codex plugin remove`). It refuses to run against a destination that is not installed yet (`Not installed, use the install script.`). `update.ps1`/`update.sh` pre-checks plugins installed in the selected client. For `Both`, Claude is the reference client; the selection is then applied to both clients. A disabled but installed Claude plugin is checked and re-enabled when updated. Checking a missing plugin installs it; unchecking an installed plugin uninstalls it (`claude plugin uninstall --scope user`/`codex plugin remove`). Unlisted plugins are not explicitly installed or removed. Update refuses to run if any selected destination lacks an installation manifest (`Not installed, use the install script.`).
Codex receives Caveman through its native plugin adapter. Humanizer and Anthropic Frontend Design are installed through the cross-agent Skills CLI; Impeccable uses its provider-aware Codex installer. All four are therefore available to both clients, although Codex does not expose all four as marketplace plugins. Because those three have no install/uninstall state on Codex, they are always ensured on every Codex run and are left out of the toggle list when only Codex is selected (they still appear, toggleable, for Claude or Both). Codex's local `plugins` and `marketplaces` tables survive configuration synchronization. Cached plugin files alone are not treated as proof of installation: selection uses the client CLI's installed list. If an older update removed the registrations, inspect the backed-up `config.toml` under `~/.codex/backups/` and reselect the affected plugins on the next update.
Codex receives Caveman through its native plugin adapter. Humanizer and Anthropic Frontend Design use the cross-agent Skills CLI; Impeccable uses its provider-aware Codex installer. The scripts do not track or uninstall these three Codex skill installations. They are always ensured and omitted from the selector on Codex-only runs. For Claude or Both they are toggleable; deselection can remove the Claude plugin but does not remove an existing Codex skill installation.
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.
+6 -1
View File
@@ -17,4 +17,9 @@ The files intentionally define only a small baseline. Detailed personal workflow
Claude's generated package additionally has `skills/rules/`: one skill per technology- or situation-specific rule in Claude's generated package additionally has `skills/rules/`: one skill per technology- or situation-specific rule in
`shared/rules/` (driven by `adapters/claude/rule-skills.tsv`), so the full rule text loads only when Claude invokes the `shared/rules/` (driven by `adapters/claude/rule-skills.tsv`), so the full rule text loads only when Claude invokes the
matching skill instead of sitting permanently in context. This category does not exist for Codex, which keeps loading matching skill instead of sitting permanently in context. This category does not exist for Codex, which keeps loading
every rule as a plain file instead. See `docs/configuration.md`. applicable focused rules as plain files instead. Both clients embed the general rules in their global instructions.
Split Angular, WPF, and UI/UX rules load supporting references only for the topic being worked on. See [Configuration](configuration.md).
Skills guide a task when needed; their availability does not require a full review, build, or test suite after every
change. The shared Verification rules govern proportional checking, including when generic skill workflows call
for more work than the change needs.
@@ -1,6 +1,6 @@
# Token-Efficiency Improvements Implementation Plan # Token-Efficiency Improvements Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > Historical implementation plan, retained for context. It is not a current runbook or authorization to execute its commands. Checkboxes do not represent current completion status. Later changes supersede its validation requirements and examples; use [AI-Instructions.md](../../../AI-Instructions.md) and the [README](../../../README.md) for current guidance. Commit steps below require explicit user authorization.
**Goal:** Cut low-value tokens agents pay for in both Claude Code and Codex — chatty script output, always-loaded rule content that's rarely needed in full, duplicated instructions, and low-value delegation — without weakening correctness, security, or verification quality. **Goal:** Cut low-value tokens agents pay for in both Claude Code and Codex — chatty script output, always-loaded rule content that's rarely needed in full, duplicated instructions, and low-value delegation — without weakening correctness, security, or verification quality.
@@ -1,6 +1,6 @@
# Token-Efficiency Improvements — Design # Token-Efficiency Improvements — Design
Status: approved by user (chat), 2026-09-16. Implementation via writing-plans. Historical design approved on 2026-09-16, retained to explain the original decisions. It is not a description of every current behavior. Later changes supersede its blanket validation steps; see [AI-Instructions.md](../../../AI-Instructions.md), [Configuration](../../configuration.md), and the [README](../../../README.md) for current guidance.
## Goal ## Goal
+2
View File
@@ -10,6 +10,7 @@ $root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/plugins.ps1') . (Join-Path $PSScriptRoot 'lib/plugins.ps1')
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $PSScriptRoot 'lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') . (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
$Platform = Read-InstallPlatform -Platform $Platform $Platform = Read-InstallPlatform -Platform $Platform
$platform = $Platform.ToLowerInvariant() $platform = $Platform.ToLowerInvariant()
@@ -38,5 +39,6 @@ foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath
$plugins = Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Install' -DryRun:$DryRun $plugins = Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Install' -DryRun:$DryRun
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Platform $Platform -Client $Client -Plugins $plugins }
if ($Summary) { Write-Output "Install: PASS | $Client, $Platform$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) } if ($Summary) { Write-Output "Install: PASS | $Client, $Platform$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) }
exit 0 exit 0
+2
View File
@@ -19,6 +19,7 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh" . "$root/scripts/lib/install-targets.sh"
. "$root/scripts/lib/selection-state.sh"
platform=$(read_install_platform "$platform") platform=$(read_install_platform "$platform")
client=$(read_install_client "$client") client=$(read_install_client "$client")
@@ -52,6 +53,7 @@ selected_plugins=()
deselected_plugins=() deselected_plugins=()
select_configured_plugins "$root" "$client" Install "$dry_run" selected_plugins deselected_plugins select_configured_plugins "$root" "$client" Install "$dry_run" selected_plugins deselected_plugins
install_configured_plugins "$root" "$client" "$dry_run" false selected_plugins "$summary" install_configured_plugins "$root" "$client" "$dry_run" false selected_plugins "$summary"
if [ "$dry_run" = false ]; then save_update_selection; fi
if [ "$summary" = true ]; then if [ "$summary" = true ]; then
suffix= suffix=
+12
View File
@@ -77,6 +77,18 @@ function Sync-ManagedDestination {
} else { } else {
$newBytes = [System.IO.File]::ReadAllBytes($_.FullName) $newBytes = [System.IO.File]::ReadAllBytes($_.FullName)
} }
if ($relative -eq 'config.toml' -and (Test-Path -LiteralPath $target)) {
$localConfig = [System.IO.File]::ReadAllText($target)
$pluginTables = [regex]::Matches($localConfig, '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])[\s\S]*?(?=^[ \t]*\[|\z)')
if ($pluginTables.Count -gt 0) {
$content = [System.Text.Encoding]::UTF8.GetString($newBytes).TrimEnd([char[]]"`r`n")
if ($content -match '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])') {
throw 'Cannot overwrite local plugin configuration with generated plugin tables.'
}
$preserved = ($pluginTables | ForEach-Object { $_.Value.TrimEnd([char[]]"`r`n") }) -join "`n"
$newBytes = [System.Text.Encoding]::UTF8.GetBytes("$content`n`n$preserved`n")
}
}
$newHash = Get-Sha256HashOfBytes $newBytes $newHash = Get-Sha256HashOfBytes $newBytes
$newManifest[$relative] = $newHash $newManifest[$relative] = $newHash
+16
View File
@@ -78,6 +78,22 @@ sync_managed_destination() {
else else
new_hash=$(sha256_of_file "$source_file") new_hash=$(sha256_of_file "$source_file")
fi fi
if [ "$relative" = config.toml ] && [ -f "$target" ]; then
local plugin_tables
plugin_tables=$(awk '
/^[ \t]*\[/ { preserve = ($0 ~ /^[ \t]*\[(plugins|marketplaces)(\.|\])/) }
preserve { print }
' "$target")
if [ -n "$plugin_tables" ]; then
if printf '%s\n' "$content" | grep -Eq '^[[:blank:]]*\[(plugins|marketplaces)(\.|\])'; then
printf 'Cannot overwrite local plugin configuration with generated plugin tables.\n' >&2
return 1
fi
content="$content"$'\n\n'"$plugin_tables"$'\n'
needs_sub=true
new_hash=$(sha256_of_string "$content")
fi
fi
new_manifest[$relative]=$new_hash new_manifest[$relative]=$new_hash
exists=false exists=false
+4 -4
View File
@@ -9,14 +9,14 @@ function Invoke-PluginCommand {
if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN $display" }; return } if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN $display" }; return }
$resolvedCommand = Get-Command $Command -ErrorAction Stop $resolvedCommand = Get-Command $Command -ErrorAction Stop
if (-not $Summary) { if (-not $Summary) {
& $resolvedCommand @Arguments if ($script:PluginNonInteractive) { $null | & $resolvedCommand @Arguments } else { & $resolvedCommand @Arguments }
if ($LASTEXITCODE -ne 0) { throw "Plugin command failed: $display" } if ($LASTEXITCODE -ne 0) { throw "Plugin command failed: $display" }
return return
} }
$previousPreference = $ErrorActionPreference $previousPreference = $ErrorActionPreference
try { try {
$ErrorActionPreference = 'Continue' $ErrorActionPreference = 'Continue'
$output = @(& $resolvedCommand @Arguments 2>&1) $output = if ($script:PluginNonInteractive) { @($null | & $resolvedCommand @Arguments 2>&1) } else { @(& $resolvedCommand @Arguments 2>&1) }
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
} finally { $ErrorActionPreference = $previousPreference } } finally { $ErrorActionPreference = $previousPreference }
if (-not $Summary -or $env:AI_CONFIG_VERBOSE -eq '1') { $output | Write-Output } elseif ($exitCode -eq 0) { $output | Where-Object { "$_" -match '(?i)warn|error|fail|deprecat' } } if (-not $Summary -or $env:AI_CONFIG_VERBOSE -eq '1') { $output | Write-Output } elseif ($exitCode -eq 0) { $output | Where-Object { "$_" -match '(?i)warn|error|fail|deprecat' } }
@@ -60,7 +60,7 @@ function Ensure-CodexMarketplace {
$previousErrorActionPreference = $ErrorActionPreference $previousErrorActionPreference = $ErrorActionPreference
try { try {
$ErrorActionPreference = 'Continue' $ErrorActionPreference = 'Continue'
$output = @(& codex plugin marketplace add $Source 2>&1) $output = if ($script:PluginNonInteractive) { @($null | & codex plugin marketplace add $Source 2>&1) } else { @(& codex plugin marketplace add $Source 2>&1) }
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
} catch { } catch {
$output = @($_) $output = @($_)
@@ -88,7 +88,7 @@ function Read-PluginToggleSelection {
) )
$state = @($Checked) $state = @($Checked)
if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') { while($true){ Write-Host 'Select plugins (enter a number to toggle, "done" to confirm):'; for($i=0;$i -lt $Names.Count;$i++){Write-Host (" {0}) [{1}] {2}" -f ($i+1),$(if($state[$i]){'x'}else{' '}),$Names[$i])}; $answer=Read-Host 'Toggle number or "done"'; if([string]::IsNullOrWhiteSpace($answer)-or $answer -eq 'done'){return $state}; $index=0;if([int]::TryParse($answer,[ref]$index)-and $index -ge 1 -and $index -le $Names.Count){$state[$index-1]= -not $state[$index-1]} } } if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') { while($true){ Write-Host 'Select plugins (enter a number to toggle, "done" to confirm):'; for($i=0;$i -lt $Names.Count;$i++){Write-Host (" {0}) [{1}] {2}" -f ($i+1),$(if($state[$i]){'x'}else{' '}),$Names[$i])}; $answer=Read-Host 'Toggle number or "done"'; if([string]::IsNullOrWhiteSpace($answer)-or $answer -eq 'done'){return $state}; $index=0;if([int]::TryParse($answer,[ref]$index)-and $index -ge 1 -and $index -le $Names.Count){$state[$index-1]= -not $state[$index-1]} } }
$index=0; while($true){ Clear-Host; Write-Host 'Select plugins'; for($i=0;$i -lt $Names.Count;$i++){ $focus=if($i -eq $index){'>'}else{' '}; $mark=if($state[$i]){'x'}else{' '}; Write-Host "$focus [$mark] $($Names[$i])" }; Write-Host '↑/↓ Navigate Space Toggle Enter Confirm'; $key=[Console]::ReadKey($true); if($key.Key -eq 'Up'){$index=($index+$Names.Count-1)%$Names.Count}elseif($key.Key -eq 'Down'){$index=($index+1)%$Names.Count}elseif($key.Key -eq 'Spacebar'){$state[$index]= -not $state[$index]}elseif($key.Key -eq 'Enter'){Clear-Host;return $state} } $index=0; while($true){ Clear-Host; Write-Host 'Select plugins'; for($i=0;$i -lt $Names.Count;$i++){ $focus=if($i -eq $index){'>'}else{' '}; $mark=if($state[$i]){'x'}else{' '}; Write-Host "$focus [$mark] $($Names[$i])" }; Write-Host 'Up/Down Navigate Space Toggle Enter Confirm'; $key=[Console]::ReadKey($true); if($key.Key -eq 'UpArrow'){$index=($index+$Names.Count-1)%$Names.Count}elseif($key.Key -eq 'DownArrow'){$index=($index+1)%$Names.Count}elseif($key.Key -eq 'Spacebar'){$state[$index]= -not $state[$index]}elseif($key.Key -eq 'Enter'){Clear-Host;return $state} }
} }
function Update-CodexMarketplace { function Update-CodexMarketplace {
+1 -1
View File
@@ -91,7 +91,7 @@ read_plugin_toggle_selection() {
local -n checked_ref=$2 local -n checked_ref=$2
local answer i mark index=0 key local answer i mark index=0 key
if [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ]; then if [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ]; then
while :; do clear_interactive; printf 'Select plugins\n'; for ((i=0;i<${#names_ref[@]};i++)); do mark=' '; [ "${checked_ref[$i]}" = true ] && mark=x; [ "$i" -eq "$index" ] && printf '> [%s] %s\n' "$mark" "${names_ref[$i]}" || printf ' [%s] %s\n' "$mark" "${names_ref[$i]}"; done; printf '↑/↓ Navigate Space Toggle Enter Confirm\n'; IFS= read -rsn1 key || return 1; case "$key" in $'\x1b') read -rsn2 key; case "$key" in '[A') index=$(( (index+${#names_ref[@]}-1)%${#names_ref[@]} ));; '[B') index=$(( (index+1)%${#names_ref[@]} ));; esac;; ' ') [ "${checked_ref[$index]}" = true ] && checked_ref[$index]=false || checked_ref[$index]=true;; '') clear_interactive; return 0;; esac; done while :; do clear_interactive; printf 'Select plugins\n'; for ((i=0;i<${#names_ref[@]};i++)); do mark=' '; [ "${checked_ref[$i]}" = true ] && mark=x; [ "$i" -eq "$index" ] && printf '> [%s] %s\n' "$mark" "${names_ref[$i]}" || printf ' [%s] %s\n' "$mark" "${names_ref[$i]}"; done; printf 'Up/Down Navigate Space Toggle Enter Confirm\n'; IFS= read -rsn1 key || return 1; case "$key" in $'\x1b') read -rsn2 key; case "$key" in '[A') index=$(( (index+${#names_ref[@]}-1)%${#names_ref[@]} ));; '[B') index=$(( (index+1)%${#names_ref[@]} ));; esac;; ' ') [ "${checked_ref[$index]}" = true ] && checked_ref[$index]=false || checked_ref[$index]=true;; '') clear_interactive; return 0;; esac; done
fi fi
while :; do while :; do
printf 'Select plugins (enter a number to toggle, "done" to confirm):\n' printf 'Select plugins (enter a number to toggle, "done" to confirm):\n'
+16
View File
@@ -0,0 +1,16 @@
function Read-UpdateSelection {
param([string]$HomePath, [string]$RepositoryRoot)
$output = & python (Join-Path $PSScriptRoot 'selection-state.py') read --home $HomePath --manifest (Join-Path $RepositoryRoot 'adapters/plugins.tsv')
if ($LASTEXITCODE -ne 0) { throw 'Could not load the saved selection.' }
return ($output | Out-String | ConvertFrom-Json)
}
function Save-UpdateSelection {
param([string]$HomePath, [string]$RepositoryRoot, [string]$Platform, [string]$Client, [hashtable]$Plugins)
$arguments = @((Join-Path $PSScriptRoot 'selection-state.py'), 'write', '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'), '--platform', $Platform.ToLowerInvariant(), '--client', $Client.ToLowerInvariant(), '--selected')
$arguments += @($Plugins.Selected | ForEach-Object { $_.name })
$arguments += '--deselected'
$arguments += @($Plugins.Deselected | ForEach-Object { $_.name })
& python @arguments
if ($LASTEXITCODE -ne 0) { throw 'Could not save the selection.' }
}
+77
View File
@@ -0,0 +1,77 @@
import argparse
import csv
import json
import os
from pathlib import Path
import sys
import tempfile
def validate(state):
if not isinstance(state, dict) or state.get('version') != 1:
raise ValueError('Unsupported saved selection.')
if state.get('platform') not in ('windows', 'linux') or state.get('client') not in ('codex', 'claude', 'both'):
raise ValueError('Invalid saved platform or client.')
for key in ('selected', 'deselected'):
values = state.get(key)
if not isinstance(values, list) or any(not isinstance(value, str) or not value or any(ord(c) < 32 for c in value) for value in values):
raise ValueError('Invalid saved plugin selection.')
if len(values) != len(set(values)):
raise ValueError('Duplicate saved plugin selection.')
if set(state['selected']) & set(state['deselected']):
raise ValueError('Conflicting saved plugin selection.')
return state
def main():
parser = argparse.ArgumentParser()
parser.add_argument('action', choices=('read', 'write'))
parser.add_argument('--home', required=True)
parser.add_argument('--manifest', required=True)
parser.add_argument('--format', choices=('json', 'tsv'), default='json')
parser.add_argument('--platform')
parser.add_argument('--client')
parser.add_argument('--selected', nargs='*', default=[])
parser.add_argument('--deselected', nargs='*', default=[])
args = parser.parse_args()
path = Path(args.home) / '.my-ai-configuration' / 'selection.json'
with open(args.manifest, encoding='utf-8-sig', newline='') as stream:
names = {row['name'] for row in csv.DictReader(stream, delimiter='\t')}
if args.action == 'read':
if not path.is_file():
raise ValueError('No saved selection. Run install or update once without Quick.')
state = validate(json.loads(path.read_text(encoding='utf-8-sig')))
for key in ('selected', 'deselected'):
state[key] = [name for name in state[key] if name in names]
if args.format == 'json':
print(json.dumps(state))
else:
print('platform\t' + state['platform'])
print('client\t' + state['client'])
for key in ('selected', 'deselected'):
for name in state[key]:
print(key + '\t' + name)
return
state = validate(dict(version=1, platform=args.platform, client=args.client,
selected=args.selected, deselected=args.deselected))
if (set(state['selected']) | set(state['deselected'])) - names:
raise ValueError('Unknown plugin in selection.')
path.parent.mkdir(parents=True, exist_ok=True)
temporary = None
try:
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=path.parent, delete=False) as stream:
temporary = Path(stream.name)
json.dump(state, stream, indent=2)
stream.write('\n')
os.replace(temporary, path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
if __name__ == '__main__':
try:
main()
except (OSError, ValueError, KeyError) as error:
print(f'Selection state: {error}', file=sys.stderr)
sys.exit(1)
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
read_update_selection() {
local saved key value
saved=$(python3 "$root/scripts/lib/selection-state.py" read --home "$home_path" --manifest "$root/adapters/plugins.tsv" --format tsv) || return
selected_plugins=()
deselected_plugins=()
while IFS=$'\t' read -r key value; do
value=${value%$'\r'}
case "$key" in
platform) [ -n "$platform" ] || platform=$value ;;
client) [ -n "$client" ] || client=$value ;;
selected) selected_plugins+=("$value") ;;
deselected) deselected_plugins+=("$value") ;;
esac
done <<< "$saved"
}
save_update_selection() {
python3 "$root/scripts/lib/selection-state.py" write --home "$home_path" --manifest "$root/adapters/plugins.tsv" \
--platform "$platform" --client "$client" --selected "${selected_plugins[@]}" --deselected "${deselected_plugins[@]}"
}
+23
View File
@@ -44,6 +44,29 @@ try {
if (Compare-Object $beforeDry $afterDry) { throw 'A dry run must not change the destination at all.' } if (Compare-Object $beforeDry $afterDry) { throw 'A dry run must not change the destination at all.' }
if (@($outDry | Where-Object { $_ -match 'DRYRUN REMOVE.*a\.md' }).Count -ne 1) { throw "Dry run should report the planned removal without performing it: $($outDry -join '; ')" } if (@($outDry | Where-Object { $_ -match 'DRYRUN REMOVE.*a\.md' }).Count -ne 1) { throw "Dry run should report the planned removal without performing it: $($outDry -join '; ')" }
$configPath = Join-Path $destination 'config.toml'
$pluginState = @'
[marketplaces.ponytail]
source_type = "git"
source = "https://github.com/DietrichGebert/ponytail.git"
[plugins."ponytail@ponytail"]
enabled = true
[plugins."disabled@market"]
enabled = false
'@
[System.IO.File]::WriteAllText((Join-Path $source 'config.toml'), "model = 'new'`n")
[System.IO.File]::WriteAllText($configPath, "model = 'old'`n$pluginState`n[other]`nvalue = true`n")
$originalConfig = [System.IO.File]::ReadAllText($configPath)
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-dry' -DryRun
if ([System.IO.File]::ReadAllText($configPath) -ne $originalConfig) { throw 'Dry run must preserve plugin configuration.' }
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins'
$mergedConfig = [System.IO.File]::ReadAllText($configPath)
if (-not $mergedConfig.Contains($pluginState)) { throw 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.' }
if (-not $mergedConfig.Contains("model = 'new'") -or $mergedConfig.Contains('[other]')) { throw 'Managed configuration must still be updated.' }
if ([System.IO.File]::ReadAllText((Join-Path $destination 'backups/plugins/config.toml')) -ne $originalConfig) { throw 'Original plugin configuration must be backed up.' }
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-repeat'
if ([System.IO.File]::ReadAllText($configPath) -ne $mergedConfig -or (Test-Path (Join-Path $destination 'backups/plugins-repeat'))) { throw 'Preserving plugin configuration must be idempotent.' }
$outSummary = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'summary' -Summary) $outSummary = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'summary' -Summary)
if (@($outSummary | Where-Object { $_ -like 'CREATE*' -or $_ -like 'UPDATE*' -or $_ -like 'UNCHANGED*' -or $_ -like 'BACKUP*' }).Count -ne 0) { throw "Summary mode must not print per-file lines: $($outSummary -join '; ')" } if (@($outSummary | Where-Object { $_ -like 'CREATE*' -or $_ -like 'UPDATE*' -or $_ -like 'UNCHANGED*' -or $_ -like 'BACKUP*' }).Count -ne 0) { throw "Summary mode must not print per-file lines: $($outSummary -join '; ')" }
if (@($outSummary | Where-Object { $_ -match '^SYNC .* unchanged' }).Count -ne 1) { throw "Summary mode must print exactly one SYNC tally line: $($outSummary -join '; ')" } if (@($outSummary | Where-Object { $_ -match '^SYNC .* unchanged' }).Count -ne 1) { throw "Summary mode must print exactly one SYNC tally line: $($outSummary -join '; ')" }
+20
View File
@@ -51,6 +51,26 @@ after_dry=$(find "$destination" -type f | sort)
[ "$before_dry" = "$after_dry" ] || { printf 'A dry run must not change the destination at all.\n' >&2; exit 1; } [ "$before_dry" = "$after_dry" ] || { printf 'A dry run must not change the destination at all.\n' >&2; exit 1; }
printf '%s\n' "$out_dry" | grep -q '^DRYRUN REMOVE .*a\.md$' || { printf 'Dry run should report the planned removal without performing it:\n%s\n' "$out_dry" >&2; exit 1; } printf '%s\n' "$out_dry" | grep -q '^DRYRUN REMOVE .*a\.md$' || { printf 'Dry run should report the planned removal without performing it:\n%s\n' "$out_dry" >&2; exit 1; }
plugin_state=' [marketplaces.ponytail]
source_type = "git"
source = "https://github.com/DietrichGebert/ponytail.git"
[plugins."ponytail@ponytail"]
enabled = true
[plugins."disabled@market"]
enabled = false'
printf "model = 'new'\n" > "$source_dir/config.toml"
printf "model = 'old'\n%s\n[other]\nvalue = true\n" "$plugin_state" > "$destination/config.toml"
original_config=$(cat "$destination/config.toml")
sync_managed_destination "$source_dir" "$destination" plugins-dry "" "" "" true > /dev/null
[ "$(cat "$destination/config.toml")" = "$original_config" ] || { printf 'Dry run changed plugin configuration.\n' >&2; exit 1; }
sync_managed_destination "$source_dir" "$destination" plugins "" "" "" false > /dev/null
merged_config=$(cat "$destination/config.toml")
[[ "$merged_config" == *"$plugin_state"* ]] || { printf 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.\n' >&2; exit 1; }
[[ "$merged_config" == *"model = 'new'"* && "$merged_config" != *'[other]'* ]] || { printf 'Managed configuration must still be updated.\n' >&2; exit 1; }
[ "$(cat "$destination/backups/plugins/config.toml")" = "$original_config" ] || { printf 'Original plugin configuration must be backed up.\n' >&2; exit 1; }
sync_managed_destination "$source_dir" "$destination" plugins-repeat "" "" "" false > /dev/null
[ "$(cat "$destination/config.toml")" = "$merged_config" ] && [ ! -d "$destination/backups/plugins-repeat" ] || { printf 'Preserving plugin configuration must be idempotent.\n' >&2; exit 1; }
out_summary=$(sync_managed_destination "$source_dir" "$destination" "summary" "" "" "" false true) out_summary=$(sync_managed_destination "$source_dir" "$destination" "summary" "" "" "" false true)
if printf '%s\n' "$out_summary" | grep -Eq '^(CREATE|UPDATE|UNCHANGED|BACKUP) '; then if printf '%s\n' "$out_summary" | grep -Eq '^(CREATE|UPDATE|UNCHANGED|BACKUP) '; then
printf 'Summary mode must not print per-file lines: %s\n' "$out_summary" >&2 printf 'Summary mode must not print per-file lines: %s\n' "$out_summary" >&2
+1 -1
View File
@@ -11,7 +11,7 @@ $buildSummaryOutput = & (Join-Path $root 'scripts/build.ps1') -Summary
if (@($buildSummaryOutput | Where-Object { $_ -eq 'Build: PASS | 4 packages' }).Count -ne 1) { throw "build.ps1 -Summary must still print the PASS line: $($buildSummaryOutput -join '; ')" } if (@($buildSummaryOutput | Where-Object { $_ -eq 'Build: PASS | 4 packages' }).Count -ne 1) { throw "build.ps1 -Summary must still print the PASS line: $($buildSummaryOutput -join '; ')" }
$statusOutput = ($statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')) -join "`n" $statusOutput = ($statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')) -join "`n"
$plainStatusOutput = [regex]::Replace($statusOutput, [char]27 + '\[[0-9;]*m', '') $plainStatusOutput = [regex]::Replace($statusOutput, [char]27 + '\[[0-9;]*m', '')
$expectedStatusOutput = "Test Model $([char]0x00B7) high`nTestRepo on $branch $([char]0x00B7) 9% ctx (16.7k tok)" $expectedStatusOutput = "Test Model $([char]0x00B7) Effort high $([char]0x00B7) TestRepo @ $branch`nCtx 200k $([char]0x00B7) Used 9% $([char]0x00B7) Tokens 16.7k"
if ($buildSummaryOutput.Count -ne 1) { throw 'Build summary must contain only one success line.' } if ($buildSummaryOutput.Count -ne 1) { throw 'Build summary must contain only one success line.' }
if ($plainStatusOutput -ne $expectedStatusOutput) { throw 'PowerShell status line output is incorrect.' } if ($plainStatusOutput -ne $expectedStatusOutput) { throw 'PowerShell status line output is incorrect.' }
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count $sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count
+1 -2
View File
@@ -20,7 +20,7 @@ fi
[ "$build_summary_output" = 'Build: PASS | 4 packages' ] [ "$build_summary_output" = 'Build: PASS | 4 packages' ]
status_output=$(printf '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"%s","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' "$root" | bash "$root/shared/statusline/statusline.sh") status_output=$(printf '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"%s","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' "$root" | bash "$root/shared/statusline/statusline.sh")
plain_status_output=$(printf '%s' "$status_output" | sed -E $'s/\x1b\\[[0-9;]*m//g') plain_status_output=$(printf '%s' "$status_output" | sed -E $'s/\x1b\\[[0-9;]*m//g')
expected_status_output=$'Test Model \xc2\xb7 high\nTestRepo on '"$branch"$' \xc2\xb7 9% ctx (16.7k tok)' expected_status_output=$'Test Model \xc2\xb7 Effort high \xc2\xb7 TestRepo @ '"$branch"$'\nCtx 200k \xc2\xb7 Used 9% \xc2\xb7 Tokens 16.7k'
[ "$plain_status_output" = "$expected_status_output" ] [ "$plain_status_output" = "$expected_status_output" ]
source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d | wc -l) source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d | wc -l)
source_skill_count=$(find "$root/shared/skills" -name SKILL.md | wc -l) source_skill_count=$(find "$root/shared/skills" -name SKILL.md | wc -l)
@@ -113,4 +113,3 @@ for entry_point in install update; do
done done
printf '%s\n' "$doctor_summary_output" | grep '^WARN ' || true printf '%s\n' "$doctor_summary_output" | grep '^WARN ' || true
if [ "$summary" = true ]; then printf 'Tests: PASS | platform packages\n'; else printf 'PASS four platform packages and six Bash selections\n'; fi if [ "$summary" = true ]; then printf 'Tests: PASS | platform packages\n'; else printf 'PASS four platform packages and six Bash selections\n'; fi
+8
View File
@@ -36,6 +36,14 @@ catch { $failed = $true }
$env:AI_CONFIG_VERBOSE = $null $env:AI_CONFIG_VERBOSE = $null
if (-not $failed -or $diagnostics -notcontains 'download progress' -or $diagnostics -notcontains 'WARN test warning') { throw 'Plugin failure must retain full diagnostics and fail.' } if (-not $failed -or $diagnostics -notcontains 'download progress' -or $diagnostics -notcontains 'WARN test warning') { throw 'Plugin failure must retain full diagnostics and fail.' }
$global:LASTEXITCODE = 0 $global:LASTEXITCODE = 0
function codex { '{"installed":[{"pluginId":"ponytail@ponytail"},{"pluginId":"i-have-adhd@i-have-adhd"},{"pluginId":"superpowers@openai-curated-remote"},{"pluginId":"context7@context7-marketplace"},{"pluginId":"caveman@thinkhome-caveman"}],"available":[]}' }
$script:answers.Enqueue('done')
$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Mode Update
if ($selection.Deselected.Count -ne 0 -or $selection.Selected.Count -ne 8) { throw 'All installed Codex plugins must start checked when updating.' }
$script:answers.Enqueue('4')
$script:answers.Enqueue('done')
$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Mode Update
if ($selection.Deselected.Count -ne 1 -or $selection.Deselected[0].name -ne 'Context7') { throw 'Only the explicitly unchecked installed plugin must be deselected.' }
function Get-InstalledPlugins { [pscustomobject]@{ pluginId = 'test@market' } } function Get-InstalledPlugins { [pscustomobject]@{ pluginId = 'test@market' } }
$script:commands = @() $script:commands = @()
function Invoke-PluginCommand { param($Command, $Arguments, [switch]$DryRun, [switch]$Summary) $script:commands += "$Command $($Arguments -join ' ')" } function Invoke-PluginCommand { param($Command, $Arguments, [switch]$DryRun, [switch]$Summary) $script:commands += "$Command $($Arguments -join ' ')" }
+6
View File
@@ -0,0 +1,6 @@
[CmdletBinding()]
param([switch]$Summary)
$ErrorActionPreference = 'Stop'
& python (Join-Path $PSScriptRoot 'test-quick-update.py')
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Output 'Tests: PASS | quick update selection'
+134
View File
@@ -0,0 +1,134 @@
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parent.parent
HELPER = ROOT / 'scripts/lib/selection-state.py'
class SelectionTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.home = Path(self.temporary.name)
self.path = self.home / '.my-ai-configuration/selection.json'
def run_state(self, action, *arguments):
return subprocess.run([sys.executable, str(HELPER), action, '--home', str(self.home),
'--manifest', str(ROOT / 'adapters/plugins.tsv'), *arguments],
capture_output=True, text=True, timeout=10)
def save(self, *arguments):
result = self.run_state('write', '--platform', 'windows', '--client', 'both', *arguments)
self.assertEqual(result.returncode, 0, result.stderr)
def test_missing_selection_fails_without_creating_state(self):
result = self.run_state('read')
self.assertNotEqual(result.returncode, 0)
self.assertIn('without Quick', result.stderr)
self.assertFalse(self.path.exists())
def test_selection_round_trip_and_last_successful_write(self):
self.save('--selected', 'Superpowers', 'Anthropic Frontend Design', '--deselected', 'Context7')
original = self.path.read_bytes()
state = json.loads(self.run_state('read').stdout)
self.assertEqual(state['platform'], 'windows')
self.assertEqual(state['client'], 'both')
self.assertEqual(state['selected'], ['Superpowers', 'Anthropic Frontend Design'])
self.assertEqual(state['deselected'], ['Context7'])
self.assertEqual(self.path.read_bytes(), original)
self.assertIn('selected\tAnthropic Frontend Design', self.run_state('read', '--format', 'tsv').stdout)
self.save('--selected', 'Context7')
self.assertEqual(json.loads(self.run_state('read').stdout)['selected'], ['Context7'])
def test_empty_selection_does_not_select_everything(self):
self.save()
self.assertEqual(json.loads(self.run_state('read').stdout)['selected'], [])
def test_invalid_write_preserves_last_selection(self):
self.save('--selected', 'Superpowers')
original = self.path.read_bytes()
result = self.run_state('write', '--platform', 'linux', '--client', 'codex', '--selected', 'Unknown')
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.path.read_bytes(), original)
def test_corrupt_or_conflicting_state_is_rejected(self):
self.path.parent.mkdir()
for state in ('{', '[]', json.dumps(dict(version=1, platform='windows', client='both',
selected=['Context7'], deselected=['Context7']))):
self.path.write_text(state)
self.assertNotEqual(self.run_state('read').returncode, 0)
def test_removed_plugins_are_ignored_and_new_plugins_not_selected(self):
self.save('--selected', 'Superpowers')
state = json.loads(self.path.read_text())
state['selected'].append('Removed plugin')
self.path.write_text(json.dumps(state))
self.assertEqual(json.loads(self.run_state('read').stdout)['selected'], ['Superpowers'])
def test_powershell_quick_preview_uses_saved_selection_without_writes(self):
shell = shutil.which('powershell') or shutil.which('pwsh')
if not shell:
self.skipTest('PowerShell unavailable')
environment = dict(os.environ, TEST_SELECTION_HOME=str(self.home), TEST_REPOSITORY_ROOT=str(ROOT))
save = subprocess.run([shell, '-NoProfile', '-Command',
"$ErrorActionPreference = 'Stop'; . (Join-Path $env:TEST_REPOSITORY_ROOT 'scripts/lib/selection-state.ps1'); "
"Save-UpdateSelection -HomePath $env:TEST_SELECTION_HOME -RepositoryRoot $env:TEST_REPOSITORY_ROOT "
"-Platform Windows -Client Both -Plugins @{ Selected = @([pscustomobject]@{name='Superpowers'}); Deselected = @() }"],
env=environment, capture_output=True, text=True, timeout=10)
self.assertEqual(save.returncode, 0, save.stdout + save.stderr)
original = self.path.read_bytes()
source = (ROOT / 'scripts/update.ps1').read_text(encoding='utf-8-sig')
source = source.replace("[Environment]::GetFolderPath('UserProfile')", '$env:TEST_SELECTION_HOME')
with tempfile.NamedTemporaryFile(mode='w', suffix='.tmp.ps1', dir=ROOT / 'scripts',
encoding='utf-8', delete=False) as stream:
stream.write(source)
entry = Path(stream.name)
self.addCleanup(entry.unlink)
result = subprocess.run([shell, '-NoProfile', '-File', str(entry), '-Quick', '-DryRun'],
env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertNotIn('Select plugins', result.stdout)
self.assertIn('codex plugin add superpowers@openai-curated-remote', result.stdout)
self.assertNotIn('codex plugin add context7', result.stdout)
self.assertEqual(self.path.read_bytes(), original)
self.assertFalse((self.home / '.codex').exists())
failed = subprocess.run([shell, '-NoProfile', '-File', str(entry), '-Quick'], env=environment,
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=10)
self.assertNotEqual(failed.returncode, 0)
self.assertEqual(self.path.read_bytes(), original)
def test_bash_quick_preview_uses_saved_selection_without_writes(self):
shell = shutil.which('bash')
if os.name == 'nt':
candidate = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe'
shell = str(candidate) if candidate.exists() else None
if not shell:
self.skipTest('Bash unavailable')
save = subprocess.run([shell, '-c',
'set -euo pipefail; root=$1; home_path=$2; platform=windows; client=both; '
'selected_plugins=(Superpowers); deselected_plugins=(); '
'. "$root/scripts/lib/selection-state.sh"; save_update_selection',
'test', ROOT.as_posix(), self.home.as_posix()],
capture_output=True, text=True, timeout=10)
self.assertEqual(save.returncode, 0, save.stdout + save.stderr)
original = self.path.read_bytes()
environment = dict(os.environ, HOME=self.home.as_posix())
result = subprocess.run([shell, str(ROOT / 'scripts/update.sh'), '--quick', '--dry-run'],
env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertNotIn('Select plugins', result.stdout)
self.assertIn('codex plugin add superpowers@openai-curated-remote', result.stdout)
self.assertNotIn('codex plugin add context7', result.stdout)
self.assertEqual(self.path.read_bytes(), original)
self.assertFalse((self.home / '.codex').exists())
if __name__ == '__main__':
unittest.main(argv=[sys.argv[0]], verbosity=1)
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
case "${1:-}" in ''|--summary) ;; *) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;; esac
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
python3 "$root/scripts/test-quick-update.py"
printf 'Tests: PASS | quick update selection\n'
+26 -2
View File
@@ -1,6 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[switch]$DryRun, [switch]$DryRun,
[switch]$Quick,
[ValidateSet('Codex','Claude','Both')][string]$Client, [ValidateSet('Codex','Claude','Both')][string]$Client,
[ValidateSet('Windows','Linux')][string]$Platform, [ValidateSet('Windows','Linux')][string]$Platform,
[switch]$Summary [switch]$Summary
@@ -10,12 +11,19 @@ $root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/plugins.ps1') . (Join-Path $PSScriptRoot 'lib/plugins.ps1')
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $PSScriptRoot 'lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') . (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
$homePath = [Environment]::GetFolderPath('UserProfile')
if ($Quick) {
$saved = Read-UpdateSelection -HomePath $homePath -RepositoryRoot $root
if (-not $Platform) { $Platform = $saved.platform }
if (-not $Client) { $Client = $saved.client }
}
$Platform = Read-InstallPlatform -Platform $Platform $Platform = Read-InstallPlatform -Platform $Platform
$platform = $Platform.ToLowerInvariant() $platform = $Platform.ToLowerInvariant()
$Client = Read-InstallClient -Client $Client $Client = Read-InstallClient -Client $Client
$homePath = [Environment]::GetFolderPath('UserProfile')
if (-not $DryRun -and -not (Test-AllManifestsPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) { if (-not $DryRun -and -not (Test-AllManifestsPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) {
throw 'Not installed, use the install script.' throw 'Not installed, use the install script.'
} }
@@ -31,13 +39,29 @@ $shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File'
$windowsShellCommand = 'powershell -NoProfile -ExecutionPolicy Bypass -File' $windowsShellCommand = 'powershell -NoProfile -ExecutionPolicy Bypass -File'
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { $shellCommand = $windowsShellCommand } if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { $shellCommand = $windowsShellCommand }
$plugins = if ($Quick) {
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $root)
@{ Selected = @($entries | Where-Object { $saved.selected -contains $_.name }); Deselected = @($entries | Where-Object { $saved.deselected -contains $_.name }) }
} else {
Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Update' -DryRun:$DryRun
}
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Platform $platform -Client $Client)) { foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Platform $platform -Client $Client)) {
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp ` Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun -Summary:$Summary -AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun -Summary:$Summary
} }
$plugins = Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Update' -DryRun:$DryRun $previousPluginNonInteractive = $script:PluginNonInteractive
$previousCI = $env:CI
try {
$script:PluginNonInteractive = [bool]$Quick
if ($Quick) { $env:CI = 'true' }
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Update -Summary:$Summary -Entries $plugins.Selected Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Update -Summary:$Summary -Entries $plugins.Selected
Uninstall-DeselectedPlugins -Entries $plugins.Deselected -Client $Client -DryRun:$DryRun -Summary:$Summary Uninstall-DeselectedPlugins -Entries $plugins.Deselected -Client $Client -DryRun:$DryRun -Summary:$Summary
} finally {
$script:PluginNonInteractive = $previousPluginNonInteractive
$env:CI = $previousCI
}
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Platform $Platform -Client $Client -Plugins $plugins }
if ($Summary) { Write-Output "Update: PASS | $Client, $Platform$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS update dry-run' } else { 'PASS update' })) } if ($Summary) { Write-Output "Update: PASS | $Client, $Platform$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS update dry-run' } else { 'PASS update' })) }
exit 0 exit 0
+18 -4
View File
@@ -2,12 +2,14 @@
set -euo pipefail set -euo pipefail
dry_run=false dry_run=false
quick=false
client="" client=""
platform="" platform=""
summary=false summary=false
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--dry-run) dry_run=true; shift ;; --dry-run) dry_run=true; shift ;;
--quick) quick=true; shift ;;
--summary) summary=true; shift ;; --summary) summary=true; shift ;;
--client) client=${2:?--client requires a value}; shift 2 ;; --client) client=${2:?--client requires a value}; shift 2 ;;
--platform) platform=${2:?--platform requires a value}; shift 2 ;; --platform) platform=${2:?--platform requires a value}; shift 2 ;;
@@ -19,11 +21,16 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh" . "$root/scripts/lib/install-targets.sh"
. "$root/scripts/lib/selection-state.sh"
home_path=${HOME:?HOME is required}
selected_plugins=()
deselected_plugins=()
if [ "$quick" = true ]; then read_update_selection; fi
platform=$(read_install_platform "$platform") platform=$(read_install_platform "$platform")
client=$(read_install_client "$client") client=$(read_install_client "$client")
home_path=${HOME:?HOME is required}
if [ "$dry_run" = false ]; then if [ "$dry_run" = false ]; then
mapfile -t destinations < <(get_install_destinations "$home_path" "$client") mapfile -t destinations < <(get_install_destinations "$home_path" "$client")
if ! all_manifests_present "${destinations[@]}"; then if ! all_manifests_present "${destinations[@]}"; then
@@ -42,17 +49,24 @@ stamp=$(date +%Y%m%d-%H%M%S)
shell_command=bash shell_command=bash
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File' windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
if [ "$quick" = false ]; then
select_configured_plugins "$root" "$client" Update "$dry_run" selected_plugins deselected_plugins
fi
while IFS='|' read -r source destination; do while IFS='|' read -r source destination; do
ai_config_root=$destination ai_config_root=$destination
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination") command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$windows_shell_command" "$dry_run" "$summary" sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$windows_shell_command" "$dry_run" "$summary"
done < <(get_install_targets "$generated" "$home_path" "$platform" "$client") done < <(get_install_targets "$generated" "$home_path" "$platform" "$client")
selected_plugins=() if [ "$quick" = true ]; then
deselected_plugins=() CI=true install_configured_plugins "$root" "$client" "$dry_run" true selected_plugins "$summary" </dev/null
select_configured_plugins "$root" "$client" Update "$dry_run" selected_plugins deselected_plugins CI=true uninstall_deselected_plugins "$root" "$client" "$dry_run" deselected_plugins "$summary" </dev/null
else
install_configured_plugins "$root" "$client" "$dry_run" true selected_plugins "$summary" install_configured_plugins "$root" "$client" "$dry_run" true selected_plugins "$summary"
uninstall_deselected_plugins "$root" "$client" "$dry_run" deselected_plugins "$summary" uninstall_deselected_plugins "$root" "$client" "$dry_run" deselected_plugins "$summary"
fi
if [ "$dry_run" = false ]; then save_update_selection; fi
if [ "$summary" = true ]; then if [ "$summary" = true ]; then
suffix= suffix=
+4 -2
View File
@@ -1,5 +1,7 @@
# Hooks # Hooks
Hooks are conservative entry points shared by clients. They validate obvious command hazards, check repository-local configuration inputs, and provide an optional post-change verification command. 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.
Only the `Stop` hook (`flashbang`) is registered in the generated client configuration. `Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are utility scripts, not active hooks; nothing in this repository invokes them automatically. Wire one up as a client-native `PreToolUse`/equivalent hook yourself if you want it enforced, and do not assume it runs otherwise. 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`). These use `.ai-session/` under the installed configuration root: the first records a compaction timestamp, and the second points to an existing `state.json`. They do not create a task summary.
`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.
+4 -5
View File
@@ -7,10 +7,9 @@ A task is only considered done when all applicable requirements below are fulfil
* The implementation fully satisfies the requested behavior. * The implementation fully satisfies the requested behavior.
* All relevant acceptance criteria are implemented. * All relevant acceptance criteria are implemented.
* The work item description is fully addressed. * The work item description is fully addressed.
* The code builds successfully. * Verification is sufficient for the change's risk and scope, following `general.md`'s Verification rules. A build, test run, new test, or independent review is not required for every change.
* Relevant tests pass. * Checks selected on that basis, and checks explicitly required by the user or project, have passed. Material gaps and pre-existing failures are reported without claiming unverified success.
* New or changed behavior is covered by tests where appropriate. * No known unresolved defects or regressions introduced by the change remain.
* No known errors, warnings, regressions, or broken behavior remain.
* The implementation follows the project's coding, architecture, security, and performance guidelines. * The implementation follows the project's coding, architecture, security, and performance guidelines.
* No unrelated changes are included. * No unrelated changes are included.
* No temporary code, debug output, commented-out code, placeholders, or TODOs remain unless explicitly intended. * No temporary code, debug output, commented-out code, placeholders, or TODOs remain unless explicitly intended.
@@ -39,7 +38,7 @@ Before declaring a task complete:
* Re-read all acceptance criteria. * Re-read all acceptance criteria.
* Compare each requirement against the implemented result. * Compare each requirement against the implemented result.
* Verify the relevant code paths. * Verify the relevant code paths.
* Run applicable builds and tests. * Choose and complete the smallest sufficient verification; expand only when evidence or risk warrants it.
* Check for incomplete or unrelated changes. * Check for incomplete or unrelated changes.
* Confirm that the final state matches the requested outcome. * Confirm that the final state matches the requested outcome.
+11
View File
@@ -19,6 +19,17 @@ Project instructions may specialize global defaults, but must not weaken securit
* Ask about business behavior, user-visible behavior, UI, UX, APIs, database models, configuration formats, or compatibility behavior only when it is missing, ambiguous, contradictory, or open to interpretation; implement explicitly specified behavior without asking again. * Ask about business behavior, user-visible behavior, UI, UX, APIs, database models, configuration formats, or compatibility behavior only when it is missing, ambiguous, contradictory, or open to interpretation; implement explicitly specified behavior without asking again.
* For deeper guidance on ambiguous requirements or a meaningful technical choice, consult `decision-rule.md` (Codex: read `rules/decision-rule.md`; Claude: invoke the `rules-decision-rule` skill). * For deeper guidance on ambiguous requirements or a meaningful technical choice, consult `decision-rule.md` (Codex: read `rules/decision-rule.md`; Claude: invoke the `rules-decision-rule` skill).
## Verification
* Decide independently whether and how much verification the change needs. Use the smallest set of checks that provides sufficient confidence, based on changed behavior, affected callers, failure impact, reversibility, and remaining uncertainty.
* A focused inspection can be sufficient for a small, low-risk change. Do not automatically run builds or tests, add tests, or request a separate review for every change.
* When execution adds useful evidence, start with a targeted syntax/type check, affected project build, focused test, or manual check. Choose the check that can detect the likely failure; these are alternatives, not a mandatory sequence.
* Expand to integration tests, a full application build, a wider test suite, or an independent review when cross-component effects, security or data-loss risks, failures, or unresolved uncertainty justify the cost. Change size alone does not determine risk.
* Add or update tests when they protect meaningful changed behavior or a plausible regression. Avoid tests that only mirror implementation details or assert wording and formatting.
* Stop once the relevant risks are covered. Repeat or broaden successful checks only after changes that affect their results, new failures, or new evidence of risk.
* Explicit user requests and applicable project or CI requirements remain binding. Generic skill workflows requiring review, builds, tests, or test-first development for every change do not override this proportional approach; no permission is needed to skip unnecessary workflow steps.
* Report the checks actually performed and material limitations concisely. Inspection is not a passing build or test run; do not claim evidence you did not obtain or hide unresolved failures.
## Documentation ## Documentation
* Never modify existing documentation automatically. * Never modify existing documentation automatically.
+3 -3
View File
@@ -94,9 +94,9 @@
After a refactoring: After a refactoring:
* Build the affected project or solution. * Choose verification using `general.md`'s Verification rules, based on the affected behavior and callers.
* Run relevant tests. * Build or run tests when they add needed confidence; a full solution build or test suite is not automatic.
* Check for new warnings. * Investigate new warnings from the selected checks.
* Verify affected code paths. * Verify affected code paths.
* Review the diff for accidental behavior changes. * Review the diff for accidental behavior changes.
* Check that unrelated files were not modified unnecessarily. * Check that unrelated files were not modified unnecessarily.
+24 -9
View File
@@ -1,28 +1,43 @@
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$invariantCulture = [System.Globalization.CultureInfo]::InvariantCulture $invariantCulture = [System.Globalization.CultureInfo]::InvariantCulture
function Format-TokenCount {
param($Value)
if ($null -eq $Value) { return '-' }
$count = [double]$Value
if ($count -ge 1000000) { return ($count / 1000000).ToString('0.#', $invariantCulture) + 'M' }
if ($count -ge 1000) { return ($count / 1000).ToString('0.#', $invariantCulture) + 'k' }
return $count.ToString('0', $invariantCulture)
}
$data = ($input | Out-String) | ConvertFrom-Json $data = ($input | Out-String) | ConvertFrom-Json
$model = if ($data.model.display_name) { $data.model.display_name } else { '-' } $model = if ($data.model.display_name) { $data.model.display_name } else { '-' }
$effort = if ($data.effort.level) { $data.effort.level } else { '-' } $effort = if ($data.effort.level) { $data.effort.level } else { '-' }
$directory = if ($data.workspace.current_dir) { $data.workspace.current_dir } else { $data.cwd } $directory = if ($data.workspace.current_dir) { $data.workspace.current_dir } else { $data.cwd }
$repo = if ($data.workspace.repo.name) { $data.workspace.repo.name } elseif ($directory) { Split-Path $directory -Leaf } else { '-' } $repo = if ($data.workspace.repo.name) { $data.workspace.repo.name } elseif ($directory) { Split-Path $directory -Leaf } else { '-' }
if (-not $data.workspace.repo.name -and $directory -and (Get-Command git -ErrorAction SilentlyContinue)) {
$repoRoot = & git -C $directory rev-parse --show-toplevel 2>$null | Select-Object -First 1
if ($repoRoot) { $repo = Split-Path $repoRoot -Leaf }
}
$branch = if ($directory -and (Get-Command git -ErrorAction SilentlyContinue)) { (& git -C $directory branch --show-current 2>$null | Select-Object -First 1) } else { $null } $branch = if ($directory -and (Get-Command git -ErrorAction SilentlyContinue)) { (& git -C $directory branch --show-current 2>$null | Select-Object -First 1) } else { $null }
if (-not $branch) { $branch = '-' } if (-not $branch) { $branch = '-' }
$usedPercentage = if ($null -ne $data.context_window.used_percentage) { [double]$data.context_window.used_percentage } else { $null } $usedPercentage = if ($null -ne $data.context_window.used_percentage) { [math]::Round([double]$data.context_window.used_percentage, [MidpointRounding]::AwayFromZero) } else { $null }
$usedContext = if ($null -ne $usedPercentage) { "$([math]::Round($usedPercentage, [MidpointRounding]::AwayFromZero).ToString($invariantCulture))%" } else { '-' } $usedContext = if ($null -ne $usedPercentage) { "$([math]::Round($usedPercentage, [MidpointRounding]::AwayFromZero).ToString($invariantCulture))%" } else { '-' }
$usedTokens = [long]$data.context_window.total_input_tokens + [long]$data.context_window.total_output_tokens $usedTokens = [long]$data.context_window.total_input_tokens + [long]$data.context_window.total_output_tokens
$usedTokensDisplay = if ($usedTokens -ge 1000) { ($usedTokens / 1000).ToString('0.0', $invariantCulture) + 'k' } else { $usedTokens.ToString($invariantCulture) } $usedTokensDisplay = Format-TokenCount $usedTokens
$contextWindow = Format-TokenCount $data.context_window.context_window_size
$esc = [char]27 $esc = [char]27
$reset = "$esc[0m" $reset = "$esc[0m"
$dim = "$esc[90m" $dim = "$esc[90m"
$cyan = "$esc[36m" $cyan = "$esc[96m"
$magenta = "$esc[35m" $magenta = "$esc[95m"
$blue = "$esc[34m" $blue = "$esc[94m"
$green = "$esc[32m" $green = "$esc[92m"
$contextColor = if ($null -eq $usedPercentage) { $dim } elseif ($usedPercentage -ge 85) { "$esc[31m" } elseif ($usedPercentage -ge 60) { "$esc[33m" } else { $green } $contextColor = if ($null -eq $usedPercentage) { $dim } elseif ($usedPercentage -ge 85) { "$esc[31m" } elseif ($usedPercentage -ge 60) { "$esc[33m" } else { $green }
$middot = [char]0x00B7 $middot = [char]0x00B7
$sep = "$dim$middot$reset" $sep = "$dim$middot$reset"
$line1 = "$cyan$model$reset $sep $magenta$effort$reset" $line1 = "$cyan$model$reset $sep Effort $magenta$effort$reset $sep $blue$repo$reset @ $green$branch$reset"
$line2 = "$blue$repo$reset$dim on$reset $green$branch$reset $sep $contextColor$usedContext ctx$reset $dim($usedTokensDisplay tok)$reset" $line2 = "Ctx $cyan$contextWindow$reset $sep Used $contextColor$usedContext$reset $sep Tokens $magenta$usedTokensDisplay$reset"
Write-Output "$line1`n$line2" $outputText = "$line1`n$line2"
if ($env:NO_COLOR) { $outputText = [regex]::Replace($outputText, [char]27 + '\[[0-9;]*m', '') }
Write-Output $outputText
+31 -15
View File
@@ -4,19 +4,24 @@ set -euo pipefail
command -v jq >/dev/null 2>&1 || exit 0 command -v jq >/dev/null 2>&1 || exit 0
input= input=
while IFS= read -r line || [ -n "$line" ]; do input+="$line"; done while IFS= read -r line || [ -n "$line" ]; do input+="$line"; done
IFS=$'\t' read -r model effort repo directory used_percentage used_tokens < <( IFS=$'\t' read -r model effort repo directory used_percentage used_tokens context_window < <(
printf '%s' "$input" | jq -r '[ printf '%s' "$input" | jq -r '[
(.model.display_name // "-"), (.model.display_name // "-"),
(.effort.level // "-"), (.effort.level // "-"),
(.workspace.repo.name // "-"), (.workspace.repo.name // "-"),
(.workspace.current_dir // .cwd // "."), (.workspace.current_dir // .cwd // "-"),
(if .context_window.used_percentage == null then "-" else (.context_window.used_percentage | round) end), (if .context_window.used_percentage == null then "-" else (.context_window.used_percentage | round) end),
((.context_window.total_input_tokens // 0) + (.context_window.total_output_tokens // 0)) ((.context_window.total_input_tokens // 0) + (.context_window.total_output_tokens // 0)),
(.context_window.context_window_size // "-")
] | @tsv' ] | @tsv'
) )
used_percentage=${used_percentage%$'\r'} used_percentage=${used_percentage%$'\r'}
used_tokens=${used_tokens%$'\r'} used_tokens=${used_tokens%$'\r'}
[ "$repo" != - ] || repo=$(basename -- "$directory") context_window=${context_window%$'\r'}
if [ "$repo" = - ]; then
repo_root=$(git -C "$directory" rev-parse --show-toplevel 2>/dev/null || true)
repo=$(basename -- "${repo_root:-$directory}")
fi
[ -n "$repo" ] || repo=- [ -n "$repo" ] || repo=-
branch=$(git -C "$directory" branch --show-current 2>/dev/null || true) branch=$(git -C "$directory" branch --show-current 2>/dev/null || true)
[ -n "$branch" ] || branch=- [ -n "$branch" ] || branch=-
@@ -26,19 +31,27 @@ if [ "$used_percentage" != - ]; then
else else
used_context=- used_context=-
fi fi
if [ "$used_tokens" -ge 1000 ]; then format_token_count() {
used_tokens_display=$(awk -v t="$used_tokens" 'BEGIN { printf "%.1fk", t / 1000 }') LC_ALL=C awk -v t="$1" 'BEGIN {
else if (t == "-") { printf "-"; exit }
used_tokens_display=$used_tokens suffix = ""
fi if (t >= 1000000) { t /= 1000000; suffix = "M" }
else if (t >= 1000) { t /= 1000; suffix = "k" }
value = sprintf("%.1f", t)
sub(/\.0$/, "", value)
printf "%s%s", value, suffix
}'
}
used_tokens_display=$(format_token_count "$used_tokens")
context_window_display=$(format_token_count "$context_window")
esc=$'\033' esc=$'\033'
reset="$esc[0m" reset="$esc[0m"
dim="$esc[90m" dim="$esc[90m"
cyan="$esc[36m" cyan="$esc[96m"
magenta="$esc[35m" magenta="$esc[95m"
blue="$esc[34m" blue="$esc[94m"
green="$esc[32m" green="$esc[92m"
if [ "$used_percentage" = - ]; then if [ "$used_percentage" = - ]; then
context_color=$dim context_color=$dim
elif [ "$used_percentage" -ge 85 ]; then elif [ "$used_percentage" -ge 85 ]; then
@@ -48,7 +61,10 @@ elif [ "$used_percentage" -ge 60 ]; then
else else
context_color=$green context_color=$green
fi fi
if [ -n "${NO_COLOR:-}" ]; then
reset= dim= cyan= magenta= blue= green= context_color=
fi
sep="${dim}·${reset}" sep="${dim}·${reset}"
printf "${cyan}%s${reset} ${sep} ${magenta}%s${reset}\n${blue}%s${reset}${dim} on${reset} ${green}%s${reset} ${sep} ${context_color}%s ctx${reset} ${dim}(%s tok)${reset}\n" \ printf "${cyan}%s${reset} ${sep} Effort ${magenta}%s${reset} ${sep} ${blue}%s${reset} @ ${green}%s${reset}\nCtx ${cyan}%s${reset} ${sep} Used ${context_color}%s${reset} ${sep} Tokens ${magenta}%s${reset}\n" \
"$model" "$effort" "$repo" "$branch" "$used_context" "$used_tokens_display" "$model" "$effort" "$repo" "$branch" "$context_window_display" "$used_context" "$used_tokens_display"