Compare commits

..
37 Commits
Author SHA1 Message Date
Julian lechner c6c8bf7105 feat: add keyboard plugin selection
CI / linux (push) Failing after 11s
2026-09-16 17:37:53 +02:00
Julian lechner 9b5e862d8a feat: persist compact session state 2026-09-16 17:37:48 +02:00
Julian lechner a509509192 feat: add local session telemetry 2026-09-16 17:37:43 +02:00
Julian lechner 9b82f3eecd feat: cache repository context 2026-09-16 17:37:40 +02:00
Julian lechner c7b25b63c9 ci: add prompt budget checks 2026-09-16 17:37:23 +02:00
Julian lechner c8674a5059 chore(codex): add local hook configuration 2026-09-16 16:56:18 +02:00
Julian lechner 77a829aff0 docs(plan): record token efficiency implementation 2026-09-16 16:55:32 +02:00
Julian lechner 137af86de3 docs(plan): add token efficiency design 2026-09-16 16:55:22 +02:00
Julian lechner 13c3f56aa5 ci: run compact script checks 2026-09-16 16:55:11 +02:00
Julian lechner 67c28d4b08 test(packages): verify compact output and rule deduplication 2026-09-16 16:55:02 +02:00
Julian lechner 2ce60a86ab test(plugins): cover selection and install guards 2026-09-16 16:54:53 +02:00
Julian lechner 1c359729a8 test(scripts): add summary modes to validation tests 2026-09-16 16:54:43 +02:00
Julian lechner 545c9841b2 feat(install): add compact install and update summaries 2026-09-16 16:54:33 +02:00
Julian lechner 5b50f32f4b refactor(install): share target selection helpers 2026-09-16 16:54:23 +02:00
Julian lechner 0c0062d955 fix(doctor): quote TOML parser command 2026-09-16 16:54:10 +02:00
Julian lechner edd874945e test(config): validate generated skill frontmatter 2026-09-16 16:54:01 +02:00
Julian lechner ab3007cf48 feat(build): add compact summary mode 2026-09-16 16:53:51 +02:00
Julian lechner 1355cbe3cd feat(plugins): compact command output and preserve diagnostics 2026-09-16 16:53:40 +02:00
Julian lechner 453da10b61 feat(output): summarize managed file sync 2026-09-16 16:53:28 +02:00
Julian lechner bfd3093776 refactor(rules): narrow ambiguity and output guidance 2026-09-16 16:53:15 +02:00
Julian lechner 847038d1e0 style(statusline): compact Claude status display 2026-09-16 16:53:03 +02:00
Julian lechner 9d23c4455e docs: document summary output and update workflow 2026-09-16 16:52:52 +02:00
Julian lechnerandClaude Sonnet 5 e02b6cfed2 test: cover embedded general.md dedup and split-rule reference files
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 16:04:35 +02:00
Julian lechnerandClaude Haiku 4.5 c979f47621 refactor(rules): point definition-of-done at general.md instead of restating it
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 15:50:08 +02:00
Julian lechnerandClaude Haiku 4.5 89de58ff24 refactor(rules): de-duplicate priority/decision/documentation guidance into general.md
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 15:46:20 +02:00
Julian lechnerandClaude Sonnet 5 3fa0d74ad8 feat(build): embed general.md into AGENTS.md, not just CLAUDE.md
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 15:42:36 +02:00
Julian lechnerandClaude Sonnet 5 3a01d28506 feat(build): support directory-shaped rules with references/
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 15:38:28 +02:00
Julian lechnerandClaude Haiku 4.5 01220cebd4 refactor(rules): split ui-ux.md into index + references
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 15:34:43 +02:00
Julian lechnerandClaude Haiku 4.5 a2b9a9f5c4 refactor(rules): split wpf.md into index + references
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 15:29:33 +02:00
Julian lechner 29d26b0918 refactor(rules): split angular.md into index + references
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 15:24:03 +02:00
Julian lechner 0a76bd5e54 feat(install,update): thread --summary/-Summary through sync and plugin steps
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 15:14:17 +02:00
Julian lechnerandClaude Haiku 4.5 77dab28c9a fix(doctor): implement buffered PASS output in summary mode
Implement proper buffering for PASS lines in summary mode:
- PASS lines are buffered when -Summary/--summary is active
- WARN and FAIL lines print immediately (never buffered)
- On success: buffer is discarded, print tally line only
- On failure: buffer is flushed before tally line

Revert out-of-scope try/catch around Python command (not part of Task 4).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 15:06:55 +02:00
Julian lechnerandClaude Haiku 4.5 3fab891383 feat(doctor): add --summary/-Summary output mode
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 14:49:49 +02:00
Julian lechner c94dfdcb30 feat(build): accept --summary/-Summary for interface parity
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 14:39:22 +02:00
Julian lechnerandClaude Haiku 4.5 0cc6909057 feat(scripts): add summary mode to plugin install/uninstall
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 14:32:17 +02:00
Julian lechner 18b6a7e390 feat(scripts): add summary mode to managed-file sync
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-16 14:24:39 +02:00
Julian lechnerandClaude Sonnet 5 5f75de4eb1 chore: ignore .superpowers/ scratch workspace
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 14:20:49 +02:00
79 changed files with 4058 additions and 1214 deletions

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|apply_patch",
"hooks": [
{
"type": "command",
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
"commandWindows": "if exist \"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" (\"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" hook & exit /b)",
"timeout": 5,
"statusMessage": "Checking UI changes"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "[ ! -f 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' ] || 'C:\\Users\\j.lechner\\.agents\\skills\\impeccable\\scripts\\impeccable' hook",
"commandWindows": "if exist \"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" (\"C:\\\\Users\\\\j.lechner\\\\.agents\\\\skills\\\\impeccable\\\\scripts\\\\impeccable.cmd\" hook & exit /b)",
"timeout": 30,
"statusMessage": "Design deep pass"
}
]
}
]
}
}
+28 -6
View File
@@ -17,20 +17,33 @@ jobs:
- name: Bash syntax - name: Bash syntax
run: find scripts shared/hooks -name '*.sh' -print0 | xargs -0 -n1 bash -n run: find scripts shared/hooks -name '*.sh' -print0 | xargs -0 -n1 bash -n
- name: Build - name: Build
run: REQUIRE_CODEX_SCHEMA=true ./scripts/build.sh run: REQUIRE_CODEX_SCHEMA=true ./scripts/build.sh --summary
- name: Prompt budget
run: ./scripts/prompt-budget.sh --summary
- name: Repository context cache
run: ./scripts/repository-context.sh --summary
- name: Doctor - name: Doctor
run: ./scripts/doctor.sh run: ./scripts/doctor.sh --summary
- name: Platform package tests - name: Platform package tests
run: ./scripts/test-platform-packages.sh run: ./scripts/test-platform-packages.sh --summary
- name: Managed manifest tests - name: Managed manifest tests
run: ./scripts/test-managed-install.sh run: ./scripts/test-managed-install.sh --summary
- name: Build validation tests - name: Build validation tests
run: ./scripts/test-build-validation.sh run: ./scripts/test-build-validation.sh --summary
- name: Install guard tests
run: ./scripts/test-install-guards.sh --summary
- name: Plugin selection tests
run: ./scripts/test-plugin-selection.sh --summary
- name: Install dry run - name: Install dry run
run: | run: |
export HOME="$RUNNER_TEMP/isolated-home" export HOME="$RUNNER_TEMP/isolated-home"
mkdir -p "$HOME" mkdir -p "$HOME"
./scripts/install.sh --dry-run --client both --platform linux ./scripts/install.sh --summary --dry-run --client both --platform linux
- name: Update dry run
run: |
export HOME="$RUNNER_TEMP/isolated-home"
mkdir -p "$HOME"
./scripts/update.sh --summary --dry-run --client both --platform linux
# windows: # windows:
# runs-on: windows-latest # runs-on: windows-latest
@@ -65,6 +78,15 @@ jobs:
# - name: Build validation tests # - name: Build validation tests
# shell: powershell # shell: powershell
# run: .\scripts\test-build-validation.ps1 # run: .\scripts\test-build-validation.ps1
# - name: Install guard tests
# shell: powershell
# run: .\scripts\test-install-guards.ps1
# - name: Plugin selection tests
# shell: powershell
# run: .\scripts\test-plugin-selection.ps1
# - name: Install dry run # - name: Install dry run
# shell: powershell # shell: powershell
# run: .\scripts\install.ps1 -DryRun -Client Both -Platform Windows # run: .\scripts\install.ps1 -DryRun -Client Both -Platform Windows
# - name: Update dry run
# shell: powershell
# run: .\scripts\update.ps1 -DryRun -Client Both -Platform Windows
+2
View File
@@ -1,8 +1,10 @@
generated/ generated/
.superpowers/
*.log *.log
*.tmp *.tmp
*.bak *.bak
backups/ backups/
.ai-session/
.env .env
.env.* .env.*
auth.json auth.json
+37 -14
View File
@@ -25,9 +25,11 @@ into platform-specific packages that can be inspected before they are installed.
or deleting them. or deleting them.
- Dry-run support (no writes, no backups, no plugin changes) and timestamped, per-run backup directories. - Dry-run support (no writes, no backups, no plugin changes) and timestamped, per-run backup directories.
- Shared safety, notification, validation, and status-line hooks. - Shared safety, notification, validation, and status-line hooks.
- User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, - Separate install and update scripts: install refuses to run against an already-installed destination, update refuses
and Anthropic Frontend Design; updates only with to run against one that is not installed yet.
`-UpdatePlugins`/`--update-plugins`. - User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, Humanizer, Impeccable, and
Anthropic Frontend Design, with an interactive per-plugin toggle during both install and update; update pre-checks
plugins that are already installed, so unchecking one uninstalls it and checking one installs or updates it.
## Architecture ## Architecture
@@ -40,7 +42,7 @@ shared definitions -> client adapters -> generated packages -> user installation
| `shared/` | Client-independent rules, agents, skills, hooks, status lines, and global instructions. | | `shared/` | Client-independent rules, agents, skills, hooks, status lines, and global instructions. |
| `adapters/` | Codex and Claude Code formats, settings, templates, and the plugin manifest. | | `adapters/` | Codex and Claude Code formats, settings, templates, and the plugin manifest. |
| `generated/` | Reproducible Windows and Linux packages. This directory is ignored by Git. | | `generated/` | Reproducible Windows and Linux packages. This directory is ignored by Git. |
| `scripts/` | Build, installation, diagnosis, and platform validation entry points. | | `scripts/` | Build, installation, update, diagnosis, and platform validation entry points. |
| `docs/` | Focused documentation about the configuration design. | | `docs/` | Focused documentation about the configuration design. |
The build creates: The build creates:
@@ -82,9 +84,10 @@ Every repository change must pass the build before completion. Building only wri
## Installation ## Installation
The installer runs a fresh build and then asks for the target platform and client, unless they are passed as The installer asks for the target platform and client, unless they are passed as parameters, then refuses to continue
parameters. Selecting Linux or Windows chooses the generated package format; installation always targets the current if any selected destination is already installed (`Already installed, use the update script.`). Selecting Linux or
user's home directory. Windows chooses the generated package format; installation always targets the current user's home directory. This
check is skipped for a dry run, so previewing always works regardless of prior install state.
Preview an installation without changing user files, backups, or plugins: Preview an installation without changing user files, backups, or plugins:
@@ -131,11 +134,30 @@ and left in place with a warning instead. A file the installer never tracked is
same name is now part of the package. Backups land in a single timestamped directory per run under same name is now part of the package. Backups land in a single timestamped directory per run under
`backups/<timestamp>/` inside each destination. `backups/<timestamp>/` inside each destination.
Selecting Codex also installs shared skills to `.agents/skills`. A normal installation only installs plugins that are Selecting Codex also installs shared skills to `.agents/skills`. Before plugins are installed, an interactive list lets
missing; add `-UpdatePlugins`/`--update-plugins` to also update and enable plugins that are already installed. 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.
Run the same installation command whenever the repository configuration should be updated. There is no separate update ## Update
script.
Once a destination is installed, use the update script (not the installer) to refresh its files and plugins. It refuses
to continue if any selected destination is not installed yet (`Not installed, use the install script.`), also skipped
for a dry run.
Powershell: (Windows)
```powershell
.\scripts\update.ps1
```
Bash: (Linux)
```bash
./scripts/update.sh
```
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
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.
## Doctor ## Doctor
@@ -151,7 +173,8 @@ Bash: (Linux)
./scripts/doctor.sh ./scripts/doctor.sh
``` ```
The doctor reports concise `PASS`, `WARN`, and `FAIL` results without reading or printing credentials. 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.
## Configuration ## Configuration
@@ -159,8 +182,8 @@ Codex receives global instructions, rules, skills, agent TOML files, hooks, and
workspace-scoped and use automatic review for eligible escalation requests. Every rule ships as a plain file under workspace-scoped and use automatic review for eligible escalation requests. Every rule ships as a plain file under
`rules/`, and `AGENTS.md` tells Codex to load the matching one by path. `rules/`, and `AGENTS.md` tells Codex to load the matching one by path.
Claude Code receives the equivalent global instructions, agents, hooks, and `settings.json`. Only `general.md` ships as Claude Code receives the equivalent global instructions, agents, hooks, and `settings.json`. General rules are embedded
an always-applied rule file (its content is also embedded directly in `CLAUDE.md`); every technology- or once in `CLAUDE.md` (and in Codex's `AGENTS.md`); every technology- or
situation-specific rule (`adapters/claude/rule-skills.tsv`) is generated as a skill under `skills/rules/` instead, so situation-specific rule (`adapters/claude/rule-skills.tsv`) is generated as a skill under `skills/rules/` instead, so
only its name and description are permanently visible and the full rule text loads only when Claude invokes it. Its only its name and description are permanently visible and the full rule text loads only when Claude invokes it. Its
generated settings use the supported automatic permission mode where available. generated settings use the supported automatic permission mode where available.
+2
View File
@@ -19,6 +19,8 @@
"command": "__STATUSLINE_COMMAND__ \"__AI_CONFIG_ROOT__/statusline/__STATUSLINE_SCRIPT__\"" "command": "__STATUSLINE_COMMAND__ \"__AI_CONFIG_ROOT__/statusline/__STATUSLINE_SCRIPT__\""
}, },
"hooks": { "hooks": {
"PreCompact": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__COMPACT_SCRIPT__\"", "timeout": 4}]}],
"SessionStart": [{"hooks": [{"type": "command", "command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/scripts/__SESSION_POINTER_SCRIPT__\"", "timeout": 4}]}],
"Stop": [ "Stop": [
{ {
"hooks": [ "hooks": [
+4 -4
View File
@@ -1,7 +1,7 @@
rule_file skill_name trigger rule_file skill_name trigger
angular.md rules-angular Angular component, template, or RxJS/signal work angular rules-angular Angular component, template, or RxJS/signal work
csharp.md rules-csharp C# or .NET implementation, review, or refactoring work csharp.md rules-csharp C# or .NET implementation, review, or refactoring work
decision-rule.md rules-decision-rule evaluating requirements, ambiguous behavior, or technical choices that need a decision decision-rule.md rules-decision-rule unresolved requirements, ambiguous behavior, or a meaningful technical choice
definition-of-done.md rules-definition-of-done validating that a task is complete or ready for review definition-of-done.md rules-definition-of-done validating that a task is complete or ready for review
git.md rules-git Git operations such as commits, branches, or merges git.md rules-git Git operations such as commits, branches, or merges
microsoft.md rules-microsoft Microsoft 365, Azure DevOps, or Teams operations microsoft.md rules-microsoft Microsoft 365, Azure DevOps, or Teams operations
@@ -16,5 +16,5 @@ security-network.md rules-security-network network, URL fetch, proxy, outbound r
security-crypto.md rules-security-crypto cryptography, secrets, credentials, key, or token work security-crypto.md rules-security-crypto cryptography, secrets, credentials, key, or token work
security-supply-chain.md rules-security-supply-chain dependency, package, plugin, build, deployment, or CI supply-chain work security-supply-chain.md rules-security-supply-chain dependency, package, plugin, build, deployment, or CI supply-chain work
typescript.md rules-typescript TypeScript or modern ECMAScript implementation work typescript.md rules-typescript TypeScript or modern ECMAScript implementation work
ui-ux.md rules-ui-ux UI or UX design and interaction decisions ui-ux rules-ui-ux UI or UX design and interaction decisions
wpf.md rules-wpf WPF, XAML, or MVVM desktop UI work wpf rules-wpf WPF, XAML, or MVVM desktop UI work
1 rule_file skill_name trigger
2 angular.md angular rules-angular Angular component, template, or RxJS/signal work
3 csharp.md rules-csharp C# or .NET implementation, review, or refactoring work
4 decision-rule.md rules-decision-rule evaluating requirements, ambiguous behavior, or technical choices that need a decision unresolved requirements, ambiguous behavior, or a meaningful technical choice
5 definition-of-done.md rules-definition-of-done validating that a task is complete or ready for review
6 git.md rules-git Git operations such as commits, branches, or merges
7 microsoft.md rules-microsoft Microsoft 365, Azure DevOps, or Teams operations
16 security-crypto.md rules-security-crypto cryptography, secrets, credentials, key, or token work
17 security-supply-chain.md rules-security-supply-chain dependency, package, plugin, build, deployment, or CI supply-chain work
18 typescript.md rules-typescript TypeScript or modern ECMAScript implementation work
19 ui-ux.md ui-ux rules-ui-ux UI or UX design and interaction decisions
20 wpf.md wpf rules-wpf WPF, XAML, or MVVM desktop UI work
+1
View File
@@ -0,0 +1 @@
{"Claude permanent context":5030,"Codex permanent context":4828}
+2
View File
@@ -0,0 +1,2 @@
Claude 5030
Codex 4828
1 Claude 5030
2 Codex 4828
+6 -2
View File
@@ -2,9 +2,13 @@
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. The install script operates at user scope and is idempotent: a normal run only installs missing plugins (a disabled but already-installed Claude plugin is re-enabled). Pass `-UpdatePlugins`/`--update-plugins` to also update plugins that are already installed, so a configuration update never silently updates third-party plugin code. 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).
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. `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.`).
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).
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.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,149 @@
# Token-Efficiency Improvements — Design
Status: approved by user (chat), 2026-09-16. Implementation via writing-plans.
## Goal
Reduce unnecessary token consumption in both Claude Code and Codex — script
output copied into agent context, rule/skill content loaded when not needed,
duplicated instructions loaded on every task, and delegation that duplicates
context — without weakening correctness, security, or verification quality.
## 1. Quiet output mode for scripts
Add `-Summary` (PowerShell) / `--summary` (Bash) to `build`, `doctor`,
`install`, `update`, and the `test-*` scripts.
- Purely an output-verbosity switch. Orthogonal to `-Client`/`-Platform`
(interactivity is unaffected — a quiet run without `-Client`/`-Platform`
still prompts) and to `-DryRun` (dry-run keeps its existing defaults;
`-Summary -DryRun` just makes the preview more compact).
- On success: suppress the low-value per-item lines (`SOURCE`, `CREATE`,
`UPDATE`, `UNCHANGED`, per-plugin `PASS ... ensured/already installed`,
per-doctor-check `PASS`) and print one summary line instead:
- `Build: PASS | 4 packages`
- `Doctor: PASS | <n> checks`
- `Tests: PASS | <n>/<n>`
- `Install: PASS | <client>, <platform>` / `Update: PASS | <client>, <platform>`
- Never suppress: `WARN`, `FAIL`, thrown errors, interactive prompts, or the
plugin toggle list. Warnings/errors always print in full regardless of the
flag.
- Implementation: each script gains a `$Summary`/`summary` bool threaded into
the existing `Write-Output`/`printf` call sites in `scripts/lib/manifest.*`
and `scripts/lib/plugins.*` (guard the per-item lines), plus a tally +
one-line footer at the end of `build.*`, `doctor.*`, `install.*`,
`update.*`, and each `test-*.*`.
## 2. Progressive disclosure for large rules
Split only the three rules that are 2-3x larger than the rest and have
clearly separable, independently-invokable subtopics:
- `angular.md` (297 lines) → components, templates, signals, rxjs,
dependency-injection-and-services, forms-and-routing, testing
- `wpf.md` (375 lines) → mvvm-and-binding, commands-and-async,
collections-and-virtualization, xaml-and-layout, resources-and-lifecycle,
testing-and-quality
- `ui-ux.md` (306 lines) → navigation-and-actions, forms, content-and-feedback,
visual-and-accessibility, tables-search-dialogs, destructive-and-error-prevention
`git.md`, `typescript.md`, `csharp.md`, `refactoring.md`, `microsoft.md`
(116-167 lines) stay single-file — splitting them adds a read hop without
materially cutting tokens.
Structure per split rule: `shared/rules/<name>/index.md` (small: universal/
high-value rules + a short table telling the agent which `references/*.md`
to open for which subtopic) + `shared/rules/<name>/references/*.md`.
- Claude: `skills/rules/<skill_name>/SKILL.md` = generated frontmatter (from
`rule-skills.tsv`, unchanged mechanism) + `index.md` body; `references/`
copied alongside into the same skill directory (already documented Anthropic
pattern; `Copy-Directory` already ships arbitrary sibling files, so no new
copy mechanism needed — only the generation loop needs to read `index.md`
instead of a flat file when `rule_file` in the manifest names a directory).
- Codex: `rules/<name>/index.md` + `rules/<name>/references/*.md`; the path
in `AGENTS.md`'s rule-loading heredoc (`scripts/build.ps1`/`build.sh`)
changes from `rules/<name>.md` to `rules/<name>/index.md` for these three.
- `adapters/claude/rule-skills.tsv` stays the single source of truth for
trigger wording — no new duplication.
- Build-script change: detect whether `shared/rules/<rule_file>` resolves to
a file or a directory; directory ⇒ read `<dir>/index.md` as the body and
copy the whole `<dir>/` (including `references/`) instead of a single file.
## 3+4. Narrower triggers and canonical rule locations (same fix)
Root cause found in the current build: `general.md` is embedded in full into
`CLAUDE.md` (`build.ps1`/`build.sh`, unconditional, every task) but for Codex
is only *referenced by path* ("Always load `rules/general.md`"—Codex must
actively read it). `shared/global-instructions.md` separately re-paraphrases
Priority, Documentation/Comments, and decision-making content inline — so
`decision-rule.md`'s substance is already permanently loaded for both
clients today, never actually lazy, defeating its own skill/file-based
lazy-loading.
Changes:
- Embed `general.md` in full into `AGENTS.md` too (mirror what `CLAUDE.md`
already does). Remove the "Always load `rules/general.md`" instruction —
no longer needed. This is strictly safer, not riskier, for Codex even
though subagent AGENTS.md inheritance is undocumented/unconfirmed
(researched): today, if a Codex subagent doesn't inherit AGENTS.md, it
already can't see the *instruction* to go read `general.md` either, so
embedding changes nothing for that case, and strictly helps the case where
it does inherit (guaranteed-present vs. requires-a-read-action).
- Trim `shared/global-instructions.md` to just the rule-loading block and the
(rewritten, see §5) orchestration paragraph. Delete the restated
Priority/Documentation/Comments/decision-making prose — `general.md` (now
embedded for both clients, always) is the single canonical source.
- `general.md` gains one short pointer line to the decision-rule skill/file
for actual ambiguity, replacing the removed inline paragraph — keeps
decision-making guidance genuinely lazy (loads only when ambiguity is
actually present), which is exactly the item-3 goal for `decision-rule.md`.
- `definition-of-done.md`'s two bullets that restate the documentation rule
(lines 19-20) collapse to one line referencing `general.md`'s Documentation
section instead of repeating it.
- `shared/agents/*/instructions.md` (the 3-line per-role files) stay
unchanged. Researched: Claude subagents fully inherit CLAUDE.md (redundant
there), but Codex subagent AGENTS.md inheritance is unconfirmed/
undocumented — these files feed both clients' agent definitions from one
shared source, and for Codex the one-line "no comments" reminder inside
`developer_instructions` is the only guaranteed-present copy if inheritance
turns out not to hold. Cost is 3 lines; not worth the risk to trim.
- `security.md`'s broad trigger ("any software development task") is left
as-is — it's already the documented "short universal baseline" design
(`docs/configuration.md`), 11 lines, and narrowing it would just relocate
the same content behind a less reliable trigger. The actual "effectively
always loaded" problem was the *duplicated* Priority/decision content in
`global-instructions.md`, fixed above.
## 5. Token-aware subagent orchestration guidance
Replace the "Use one implementer for small, contained tasks. Add researcher
only for material uncertainty..." paragraph in `global-instructions.md` with:
> Delegate only when the expected improvement in correctness, independence,
> specialization, or parallelism outweighs the duplicated context and tool
> cost. Do not delegate simple repository exploration the main agent can do
> directly. Do not create a researcher for information the main agent can
> obtain cheaply itself. Use one implementer for small and medium contained
> tasks. Run reviewer and verifier together only when their responsibilities
> are materially different for the current task. Avoid multiple agents
> independently reading the same large set of files. Use parallel agents
> only for genuinely independent work. Do not let subagents create further
> subagents.
`config.toml`'s `max_concurrent_threads_per_session = 5` / Claude's five-
operation concurrency stay untouched — only the decision guidance changes,
not the ceiling.
## Validation
- `scripts/build.ps1`/`.sh`, `scripts/doctor.ps1`/`.sh` pass.
- `scripts/test-platform-packages.*` (agent/skill/AGENTS.md/CLAUDE.md content
assertions) updated for: embedded `general.md` in `AGENTS.md`, the split
rule skill/reference structure, trimmed `global-instructions.md`.
- New/updated assertions for `-Summary`/`--summary` output shape on at least
one script per language (build + doctor), covering both the summary line
and that `WARN`/`FAIL` still surface unsummarized.
- Manual read-through of the three split rules to confirm no content was
dropped, only relocated.
+29 -20
View File
@@ -1,5 +1,5 @@
[CmdletBinding()] [CmdletBinding()]
param() param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path $PSScriptRoot -Parent
$shared = Join-Path $root 'shared' $shared = Join-Path $root 'shared'
@@ -79,7 +79,7 @@ function Test-CodexSchema($configPath) {
} }
} }
& (Join-Path $root 'shared/hooks/scripts/Test-SessionConfig.ps1') -RepositoryRoot $root & (Join-Path $root 'shared/hooks/scripts/Test-SessionConfig.ps1') -RepositoryRoot $root | Where-Object { -not $Summary -or $_ -ne 'PASS session configuration' }
$sourceAgentDirs = @(Get-ChildItem (Join-Path $shared 'agents') -Directory | Sort-Object Name) $sourceAgentDirs = @(Get-ChildItem (Join-Path $shared 'agents') -Directory | Sort-Object Name)
if ($sourceAgentDirs.Count -eq 0) { throw 'No agents found under shared/agents.' } if ($sourceAgentDirs.Count -eq 0) { throw 'No agents found under shared/agents.' }
@@ -90,10 +90,11 @@ foreach ($agentDir in $sourceAgentDirs) {
$agentNames += $name $agentNames += $name
} }
# Codex keeps the original rule-loading text unchanged: every rule ships as a plain # Codex: general.md is embedded directly into AGENTS.md, the same way Claude embeds
# file and AGENTS.md tells Codex to load the matching one by path. # it into CLAUDE.md, instead of only being referenced by path — guaranteed present
# either way, and safe even if a subagent's AGENTS.md inheritance is not guaranteed.
$codexRuleLoading = @' $codexRuleLoading = @'
Always load and apply `rules/general.md` before starting any task. General rules are embedded below.
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations. When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
@@ -109,16 +110,16 @@ Load rule files when their subject applies:
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF. - rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens. - rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI. - rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
- rules/angular.md for Angular work. - rules/angular/index.md for Angular work.
- rules/typescript.md for TypeScript work. - rules/typescript.md for TypeScript work.
- rules/csharp.md for C# or .NET work. - rules/csharp.md for C# or .NET work.
- rules/wpf.md for WPF work. - rules/wpf/index.md for WPF work.
- rules/ui-ux.md for UI or UX decisions. - rules/ui-ux/index.md for UI or UX decisions.
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work. - rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
- rules/git.md for Git operations. - rules/git.md for Git operations.
- rules/refactoring.md for refactoring work. - rules/refactoring.md for refactoring work.
- rules/definition-of-done.md when validating completion. - rules/definition-of-done.md when validating completion.
- rules/decision-rule.md when requirements, behavior, or technical choices need to be evaluated. - rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains.
'@ '@
foreach ($platform in @('windows','linux')) { foreach ($platform in @('windows','linux')) {
@@ -131,28 +132,34 @@ foreach ($platform in @('windows','linux')) {
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$platform/hooks") Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$platform/hooks")
Copy-Directory (Join-Path $shared 'statusline') (Join-Path $output "claude-$platform/statusline") Copy-Directory (Join-Path $shared 'statusline') (Join-Path $output "claude-$platform/statusline")
# Claude: general.md stays a plain, always-applied rule file. Every technology- or
# situation-specific rule becomes a skill instead, so only its name and description
# sit permanently in context; the full text loads only when the skill is invoked.
$claudeRulesDir = Join-Path $output "claude-$platform/rules" $claudeRulesDir = Join-Path $output "claude-$platform/rules"
New-Item $claudeRulesDir -ItemType Directory -Force | Out-Null New-Item $claudeRulesDir -ItemType Directory -Force | Out-Null
Copy-Item (Join-Path $shared 'rules/general.md') (Join-Path $claudeRulesDir 'general.md') -Force
$claudeRuleSkillLines = @() $claudeRuleSkillLines = @()
foreach ($entry in $ruleSkillEntries) { foreach ($entry in $ruleSkillEntries) {
$ruleSourcePath = Join-Path $shared "rules/$($entry.rule_file)"
$isDirectory = Test-Path -LiteralPath $ruleSourcePath -PathType Container
$ruleBodyPath = if ($isDirectory) { Join-Path $ruleSourcePath 'index.md' } else { $ruleSourcePath }
$skillDir = Join-Path $output "claude-$platform/skills/rules/$($entry.skill_name)" $skillDir = Join-Path $output "claude-$platform/skills/rules/$($entry.skill_name)"
New-Item $skillDir -ItemType Directory -Force | Out-Null New-Item $skillDir -ItemType Directory -Force | Out-Null
$ruleContent = Get-Content (Join-Path $shared "rules/$($entry.rule_file)") -Raw $ruleContent = Get-Content $ruleBodyPath -Raw
$skillBody = "---`r`nname: $($entry.skill_name)`r`ndescription: Use for $($entry.trigger).`r`n---`r`n`r`n$ruleContent" $skillBody = "---`r`nname: $($entry.skill_name)`r`ndescription: $(Quote-Toml "Use for $($entry.trigger).")`r`n---`r`n`r`n$ruleContent"
Set-Content (Join-Path $skillDir 'SKILL.md') $skillBody -Encoding UTF8 Set-Content (Join-Path $skillDir 'SKILL.md') $skillBody -Encoding UTF8
if ($isDirectory) {
$referencesSource = Join-Path $ruleSourcePath 'references'
if (Test-Path -LiteralPath $referencesSource) { Copy-Directory $referencesSource (Join-Path $skillDir 'references') }
}
$claudeRuleSkillLines += "- $($entry.skill_name) for $($entry.trigger)." $claudeRuleSkillLines += "- $($entry.skill_name) for $($entry.trigger)."
} }
$claudeRuleLoading = "Always apply ``rules/general.md`` before starting any task, embedded below.`r`n`r`nDetect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply.`r`n`r`nInvoke rule skills when their subject applies:`r`n`r`n" + ($claudeRuleSkillLines -join "`r`n") $claudeRuleLoading = "General rules are embedded below.`r`n`r`nDetect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply.`r`n`r`nInvoke rule skills when their subject applies:`r`n`r`n" + ($claudeRuleSkillLines -join "`r`n")
$sharedTemplate = Get-Content (Join-Path $shared 'global-instructions.md') -Raw $sharedTemplate = Get-Content (Join-Path $shared 'global-instructions.md') -Raw
Set-Content (Join-Path $output "codex-$platform/AGENTS.md") ($sharedTemplate.Replace('__RULE_LOADING__', $codexRuleLoading)) -Encoding UTF8
$generalContent = Get-Content (Join-Path $shared 'rules/general.md') -Raw $generalContent = Get-Content (Join-Path $shared 'rules/general.md') -Raw
$agentsContent = $sharedTemplate.Replace('__RULE_LOADING__', $codexRuleLoading)
$agentsContent = $agentsContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
Set-Content (Join-Path $output "codex-$platform/AGENTS.md") $agentsContent -Encoding UTF8
$claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading) $claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading)
$claudeContent = $claudeContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent" $claudeContent = $claudeContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
Set-Content (Join-Path $output "claude-$platform/CLAUDE.md") $claudeContent -Encoding UTF8 Set-Content (Join-Path $output "claude-$platform/CLAUDE.md") $claudeContent -Encoding UTF8
@@ -183,7 +190,9 @@ foreach ($platform in @('windows','linux')) {
$script = if ($platform -eq 'windows') { 'flashbang.ps1' } else { 'flashbang.sh' } $script = if ($platform -eq 'windows') { 'flashbang.ps1' } else { 'flashbang.sh' }
$statusLineScript = if ($platform -eq 'windows') { 'statusline.ps1' } else { 'statusline.sh' } $statusLineScript = if ($platform -eq 'windows') { 'statusline.ps1' } else { 'statusline.sh' }
$content = Get-Content -LiteralPath $path -Raw $content = Get-Content -LiteralPath $path -Raw
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__WINDOWS_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__WINDOWS_HOOK_SCRIPT__', $script).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript) $compactScript = if ($platform -eq 'windows') { 'Record-Compact.ps1' } else { 'record-compact.sh' }
$pointerScript = if ($platform -eq 'windows') { 'Show-SessionStatePointer.ps1' } else { 'show-session-state-pointer.sh' }
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__WINDOWS_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__WINDOWS_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript)
Set-Content -LiteralPath $path -Value $content -Encoding UTF8 Set-Content -LiteralPath $path -Value $content -Encoding UTF8
} }
@@ -203,5 +212,5 @@ $leftoverPlaceholders = @(Get-ChildItem $output -File -Recurse | ForEach-Object
}) })
if ($leftoverPlaceholders.Count) { throw "Unresolved template placeholders in: $($leftoverPlaceholders -join ', ')" } if ($leftoverPlaceholders.Count) { throw "Unresolved template placeholders in: $($leftoverPlaceholders -join ', ')" }
Write-Output 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux' if ($Summary) { Write-Output 'Build: PASS | 4 packages' } else { Write-Output 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux' }
exit 0 exit 0
+42 -19
View File
@@ -1,5 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--summary) summary=true; shift ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
shared="$root/shared" shared="$root/shared"
@@ -27,11 +34,12 @@ rule_skill_files=()
rule_skill_names=() rule_skill_names=()
rule_skill_triggers=() rule_skill_triggers=()
while IFS=$'\t' read -r rule_file skill_name trigger; do while IFS=$'\t' read -r rule_file skill_name trigger; do
trigger=${trigger%$'\r'}
[ "$rule_file" != rule_file ] || continue [ "$rule_file" != rule_file ] || continue
[ -n "$rule_file" ] || continue [ -n "$rule_file" ] || continue
[ -n "$skill_name" ] || { printf 'Missing skill_name in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; } [ -n "$skill_name" ] || { printf 'Missing skill_name in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; }
[ -n "$trigger" ] || { printf 'Missing trigger in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; } [ -n "$trigger" ] || { printf 'Missing trigger in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; }
[ -f "$shared/rules/$rule_file" ] || { printf 'Rule-skill manifest references a missing rule file: %s\n' "$rule_file" >&2; exit 1; } [ -f "$shared/rules/$rule_file" ] || [ -d "$shared/rules/$rule_file" ] || { printf 'Rule-skill manifest references a missing rule file: %s\n' "$rule_file" >&2; exit 1; }
[ -z "${rule_skill_seen[$skill_name]+x}" ] || { printf 'Duplicate rule-skill name in manifest: %s\n' "$skill_name" >&2; exit 1; } [ -z "${rule_skill_seen[$skill_name]+x}" ] || { printf 'Duplicate rule-skill name in manifest: %s\n' "$skill_name" >&2; exit 1; }
rule_skill_seen[$skill_name]=1 rule_skill_seen[$skill_name]=1
rule_skill_files+=("$rule_file") rule_skill_files+=("$rule_file")
@@ -62,7 +70,12 @@ quote_toml() {
printf '"%s"' "$value" printf '"%s"' "$value"
} }
bash "$shared/hooks/scripts/test-session-config.sh" "$root" session_output=$(bash "$shared/hooks/scripts/test-session-config.sh" "$root") || { printf '%s\n' "$session_output" >&2; exit 1; }
if [ "$summary" = true ]; then
printf '%s\n' "$session_output" | sed '/^PASS session configuration$/d'
else
printf '%s\n' "$session_output"
fi
rm -rf -- "$output" rm -rf -- "$output"
agent_dirs=("$shared"/agents/*/) agent_dirs=("$shared"/agents/*/)
@@ -73,10 +86,11 @@ for agent_dir in "${agent_dirs[@]}"; do
agent_seen[$name]=1 agent_seen[$name]=1
done done
# Codex keeps the original rule-loading text unchanged: every rule ships as a plain # Codex: general.md is embedded directly into AGENTS.md, the same way Claude embeds
# file and AGENTS.md tells Codex to load the matching one by path. # it into CLAUDE.md, instead of only being referenced by path — guaranteed present
# either way, and safe even if a subagent's AGENTS.md inheritance is not guaranteed.
read -r -d '' codex_rule_loading <<'BLOCK' || true read -r -d '' codex_rule_loading <<'BLOCK' || true
Always load and apply `rules/general.md` before starting any task. General rules are embedded below.
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations. When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
@@ -92,16 +106,16 @@ Load rule files when their subject applies:
- rules/security-network.md for network access, URLs, TLS, proxies, or SSRF. - rules/security-network.md for network access, URLs, TLS, proxies, or SSRF.
- rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens. - rules/security-crypto.md for cryptography, secrets, credentials, keys, or tokens.
- rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI. - rules/security-supply-chain.md for dependencies, packages, plugins, builds, deployments, or CI.
- rules/angular.md for Angular work. - rules/angular/index.md for Angular work.
- rules/typescript.md for TypeScript work. - rules/typescript.md for TypeScript work.
- rules/csharp.md for C# or .NET work. - rules/csharp.md for C# or .NET work.
- rules/wpf.md for WPF work. - rules/wpf/index.md for WPF work.
- rules/ui-ux.md for UI or UX decisions. - rules/ui-ux/index.md for UI or UX decisions.
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work. - rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
- rules/git.md for Git operations. - rules/git.md for Git operations.
- rules/refactoring.md for refactoring work. - rules/refactoring.md for refactoring work.
- rules/definition-of-done.md when validating completion. - rules/definition-of-done.md when validating completion.
- rules/decision-rule.md when requirements, behavior, or technical choices need to be evaluated. - rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains.
BLOCK BLOCK
for platform in windows linux; do for platform in windows linux; do
@@ -113,24 +127,26 @@ copy_directory "$shared/hooks" "$output/codex-$platform/hooks"
copy_directory "$shared/hooks" "$output/claude-$platform/hooks" copy_directory "$shared/hooks" "$output/claude-$platform/hooks"
copy_directory "$shared/statusline" "$output/claude-$platform/statusline" copy_directory "$shared/statusline" "$output/claude-$platform/statusline"
# Claude: general.md stays a plain, always-applied rule file. Every technology- or
# situation-specific rule becomes a skill instead, so only its name and description
# sit permanently in context; the full text loads only when the skill is invoked.
mkdir -p "$output/claude-$platform/rules" mkdir -p "$output/claude-$platform/rules"
cp "$shared/rules/general.md" "$output/claude-$platform/rules/general.md"
claude_rule_loading_list="" claude_rule_loading_list=""
for i in "${!rule_skill_names[@]}"; do for i in "${!rule_skill_names[@]}"; do
rule_file=${rule_skill_files[$i]} rule_file=${rule_skill_files[$i]}
skill_name=${rule_skill_names[$i]} skill_name=${rule_skill_names[$i]}
trigger=${rule_skill_triggers[$i]} trigger=${rule_skill_triggers[$i]}
rule_source="$shared/rules/$rule_file"
rule_body="$rule_source"
[ -d "$rule_source" ] && rule_body="$rule_source/index.md"
skill_dir="$output/claude-$platform/skills/rules/$skill_name" skill_dir="$output/claude-$platform/skills/rules/$skill_name"
mkdir -p "$skill_dir" mkdir -p "$skill_dir"
{ printf -- '---\nname: %s\ndescription: Use for %s.\n---\n\n' "$skill_name" "$trigger"; cat "$shared/rules/$rule_file"; } > "$skill_dir/SKILL.md" { printf -- '---\nname: %s\ndescription: %s\n---\n\n' "$skill_name" "$(quote_toml "Use for $trigger.")"; cat "$rule_body"; } > "$skill_dir/SKILL.md"
if [ -d "$rule_source/references" ]; then
copy_directory "$rule_source/references" "$skill_dir/references"
fi
claude_rule_loading_list="${claude_rule_loading_list}- ${skill_name} for ${trigger}. claude_rule_loading_list="${claude_rule_loading_list}- ${skill_name} for ${trigger}.
" "
done done
claude_rule_loading="Always apply \`rules/general.md\` before starting any task, embedded below. claude_rule_loading="General rules are embedded below.
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply. Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply.
@@ -139,10 +155,11 @@ Invoke rule skills when their subject applies:
${claude_rule_loading_list%$'\n'}" ${claude_rule_loading_list%$'\n'}"
shared_template=$(<"$shared/global-instructions.md") shared_template=$(<"$shared/global-instructions.md")
agents_content=${shared_template//__RULE_LOADING__/$codex_rule_loading}
printf '%s\n' "$agents_content" > "$output/codex-$platform/AGENTS.md"
general_content=$(<"$shared/rules/general.md") general_content=$(<"$shared/rules/general.md")
agents_content=${shared_template//__RULE_LOADING__/$codex_rule_loading}
printf '%s\n\n---\n\n%s\n' "${agents_content%$'\n'}" "$general_content" > "$output/codex-$platform/AGENTS.md"
claude_content=${shared_template//__RULE_LOADING__/$claude_rule_loading} claude_content=${shared_template//__RULE_LOADING__/$claude_rule_loading}
printf '%s\n\n---\n\n%s\n' "${claude_content%$'\n'}" "$general_content" > "$output/claude-$platform/CLAUDE.md" printf '%s\n\n---\n\n%s\n' "${claude_content%$'\n'}" "$general_content" > "$output/claude-$platform/CLAUDE.md"
@@ -171,10 +188,14 @@ for client in codex claude; do
command=bash command=bash
script=flashbang.sh script=flashbang.sh
statusline_script=statusline.sh statusline_script=statusline.sh
compact_script=record-compact.sh
pointer_script=show-session-state-pointer.sh
if [ "$platform" = windows ]; then if [ "$platform" = windows ]; then
command='powershell -NoProfile -ExecutionPolicy Bypass -File' command='powershell -NoProfile -ExecutionPolicy Bypass -File'
script=flashbang.ps1 script=flashbang.ps1
statusline_script=statusline.ps1 statusline_script=statusline.ps1
compact_script=Record-Compact.ps1
pointer_script=Show-SessionStatePointer.ps1
fi fi
path="$output/$client-$platform/$file" path="$output/$client-$platform/$file"
content=$(<"$path") content=$(<"$path")
@@ -182,6 +203,8 @@ for client in codex claude; do
content=${content//__WINDOWS_HOOK_COMMAND__/$command} content=${content//__WINDOWS_HOOK_COMMAND__/$command}
content=${content//__HOOK_SCRIPT__/$script} content=${content//__HOOK_SCRIPT__/$script}
content=${content//__WINDOWS_HOOK_SCRIPT__/$script} content=${content//__WINDOWS_HOOK_SCRIPT__/$script}
content=${content//__COMPACT_SCRIPT__/$compact_script}
content=${content//__SESSION_POINTER_SCRIPT__/$pointer_script}
content=${content//__STATUSLINE_COMMAND__/$command} content=${content//__STATUSLINE_COMMAND__/$command}
content=${content//__STATUSLINE_SCRIPT__/$statusline_script} content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
printf '%s\n' "$content" > "$path" printf '%s\n' "$content" > "$path"
@@ -213,4 +236,4 @@ if [ -n "$leftover_tokens" ]; then
exit 1 exit 1
fi fi
printf 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux\n' if [ "$summary" = true ]; then printf 'Build: PASS | 4 packages\n'; else printf 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux\n'; fi
+25 -6
View File
@@ -1,11 +1,21 @@
[CmdletBinding()] [CmdletBinding()]
param() param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $PSScriptRoot 'lib/manifest.ps1')
$homePath = [Environment]::GetFolderPath('UserProfile') $homePath = [Environment]::GetFolderPath('UserProfile')
$fail = $false $script:fail = $false
function Result($state, $message) { Write-Output ("$state $message"); if ($state -eq 'FAIL') { $script:fail = $true } } $script:checkCount = 0
$script:passBuffer = @()
function Result($state, $message) {
$script:checkCount++
if ($state -eq 'FAIL') { $script:fail = $true }
if ($Summary -and $state -eq 'PASS') {
$script:passBuffer += "$state $message"
} else {
Write-Output ("$state $message")
}
}
foreach ($tool in @('codex','claude')) { if (Get-Command $tool -ErrorAction SilentlyContinue) { $version = & $tool --version 2>&1 | Select-Object -First 1; Result 'PASS' "$tool available ($version)" } else { Result 'WARN' "$tool unavailable" } } foreach ($tool in @('codex','claude')) { if (Get-Command $tool -ErrorAction SilentlyContinue) { $version = & $tool --version 2>&1 | Select-Object -First 1; Result 'PASS' "$tool available ($version)" } else { Result 'WARN' "$tool unavailable" } }
if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude status line' } else { Result 'WARN' 'jq unavailable; Claude status line is disabled' } } if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude status line' } else { Result 'WARN' 'jq unavailable; Claude status line is disabled' } }
@@ -42,7 +52,7 @@ if (Test-Path -LiteralPath $settingsPath) {
} }
$configTomlPath = Join-Path $homePath '.codex/config.toml' $configTomlPath = Join-Path $homePath '.codex/config.toml'
if (Test-Path -LiteralPath $configTomlPath) { if (Test-Path -LiteralPath $configTomlPath) {
$parseResult = & python -c 'import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8-sig"))' $configTomlPath 2>&1 $parseResult = & python -c "import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8-sig'))" $configTomlPath 2>&1
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $parseResult" } if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $parseResult" }
if (Get-Command codex -ErrorAction SilentlyContinue) { if (Get-Command codex -ErrorAction SilentlyContinue) {
$previousCodexHome = $env:CODEX_HOME $previousCodexHome = $env:CODEX_HOME
@@ -77,6 +87,15 @@ foreach ($client in @('codex','claude')) {
if ($registered) { Result 'PASS' "$client Stop hook is registered" } else { Result 'WARN' "$client ships hook utilities, but no Stop hook is registered" } if ($registered) { Result 'PASS' "$client Stop hook is registered" } else { Result 'WARN' "$client ships hook utilities, but no Stop hook is registered" }
} }
if ($fail) { exit 1 } if ($Summary) {
Write-Output 'PASS doctor' if ($script:fail) {
$script:passBuffer | ForEach-Object { Write-Output $_ }
Write-Output "Doctor: FAIL | $script:checkCount checks"
exit 1
}
Write-Output "Doctor: PASS | $script:checkCount checks"
} else {
if ($script:fail) { exit 1 }
Write-Output 'PASS doctor'
}
exit 0 exit 0
+27 -3
View File
@@ -1,14 +1,29 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--summary) summary=true; shift ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
home_path=${HOME:?HOME is required} home_path=${HOME:?HOME is required}
failed=false failed=false
check_count=0
pass_buffer=()
result() { result() {
printf '%s %s\n' "$1" "$2" check_count=$((check_count + 1))
[ "$1" != FAIL ] || failed=true [ "$1" != FAIL ] || failed=true
if [ "$summary" = true ] && [ "$1" = PASS ]; then
pass_buffer+=("$1 $2")
else
printf '%s %s\n' "$1" "$2"
fi
} }
for tool in codex claude; do for tool in codex claude; do
@@ -85,5 +100,14 @@ for path in shared/rules shared/skills shared/agents shared/hooks/scripts; do
[ -e "$root/$path" ] && result PASS "source $path" || result FAIL "missing $path" [ -e "$root/$path" ] && result PASS "source $path" || result FAIL "missing $path"
done done
"$failed" && exit 1 if [ "$summary" = true ]; then
printf 'PASS doctor\n' if [ "$failed" = true ]; then
for line in "${pass_buffer[@]}"; do printf '%s\n' "$line"; done
printf 'Doctor: FAIL | %s checks\n' "$check_count"
exit 1
fi
printf 'Doctor: PASS | %s checks\n' "$check_count"
else
[ "$failed" = false ] || exit 1
printf 'PASS doctor\n'
fi
+18 -29
View File
@@ -3,51 +3,40 @@ param(
[switch]$DryRun, [switch]$DryRun,
[ValidateSet('Codex','Claude','Both')][string]$Client, [ValidateSet('Codex','Claude','Both')][string]$Client,
[ValidateSet('Windows','Linux')][string]$Platform, [ValidateSet('Windows','Linux')][string]$Platform,
[switch]$UpdatePlugins [switch]$Summary
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $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')
$Platform = Read-InstallPlatform -Platform $Platform
$platform = $Platform.ToLowerInvariant()
$Client = Read-InstallClient -Client $Client
$homePath = [Environment]::GetFolderPath('UserProfile')
if (-not $DryRun -and (Test-AnyManifestPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) {
throw 'Already installed, use the update script.'
}
$buildScript = Join-Path $PSScriptRoot 'build.ps1' $buildScript = Join-Path $PSScriptRoot 'build.ps1'
& $buildScript & $buildScript -Summary:$Summary
if (-not $?) { throw 'Build failed. Installation was not started.' } if (-not $?) { throw 'Build failed. Installation was not started.' }
$generated = Join-Path $root 'generated' $generated = Join-Path $root 'generated'
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' } if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
if (-not $Platform) {
Write-Output 'Select target platform:'
Write-Output '1) Windows'
Write-Output '2) Linux'
do { $platformSelection = Read-Host 'Selection [1-2]' } while ($platformSelection -notin @('1','2'))
$Platform = @{'1'='Windows'; '2'='Linux'}[$platformSelection]
}
$platform = $Platform.ToLowerInvariant()
if (-not $Client) {
Write-Output 'Select installation target:'
Write-Output '1) Codex'
Write-Output '2) Claude'
Write-Output '3) Both'
do { $selection = Read-Host 'Selection [1-3]' } while ($selection -notin @('1','2','3'))
$Client = @{'1'='Codex'; '2'='Claude'; '3'='Both'}[$selection]
}
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$homePath = [Environment]::GetFolderPath('UserProfile')
$shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File' $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 }
$targets = @()
if ($Client -in @('Codex','Both')) { $targets += @{ Source=(Join-Path $generated "codex-$platform"); Destination=(Join-Path $homePath '.codex') } }
if ($Client -in @('Codex','Both')) { $targets += @{ Source=(Join-Path $generated "codex-$platform/skills"); Destination=(Join-Path $homePath '.agents/skills') } }
if ($Client -in @('Claude','Both')) { $targets += @{ Source=(Join-Path $generated "claude-$platform"); Destination=(Join-Path $homePath '.claude') } }
foreach ($item in $targets) { 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 -AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun -Summary:$Summary
} }
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Update:$UpdatePlugins $plugins = Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Install' -DryRun:$DryRun
Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected
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
+31 -48
View File
@@ -4,11 +4,11 @@ set -euo pipefail
dry_run=false dry_run=false
client="" client=""
platform="" platform=""
update_plugins=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 ;;
--update-plugins) update_plugins=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 ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;; *) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
@@ -18,60 +18,43 @@ done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
bash "$root/scripts/build.sh" . "$root/scripts/lib/install-targets.sh"
platform=$(read_install_platform "$platform")
client=$(read_install_client "$client")
home_path=${HOME:?HOME is required}
if [ "$dry_run" = false ]; then
mapfile -t destinations < <(get_install_destinations "$home_path" "$client")
if any_manifest_present "${destinations[@]}"; then
printf 'Already installed, use the update script.\n' >&2
exit 1
fi
fi
build_args=()
[ "$summary" = false ] || build_args+=(--summary)
bash "$root/scripts/build.sh" "${build_args[@]}"
generated="$root/generated" generated="$root/generated"
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; } [ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
if [ -z "$platform" ]; then
printf 'Select target platform:\n1) Windows\n2) Linux\n'
while :; do
read -r -p 'Selection [1-2] ' platform_selection
case "$platform_selection" in 1) platform=windows; break;; 2) platform=linux; break;; esac
done
else
case "$platform" in
windows|Windows) platform=windows ;;
linux|Linux) platform=linux ;;
*) printf 'Unknown --platform value: %s (expected windows or linux)\n' "$platform" >&2; exit 1 ;;
esac
fi
if [ -z "$client" ]; then
printf 'Select installation target:\n1) Codex\n2) Claude\n3) Both\n'
while :; do
read -r -p 'Selection [1-3] ' selection
case "$selection" in 1) client=codex; break;; 2) client=claude; break;; 3) client=both; break;; esac
done
else
case "$client" in
codex|Codex) client=codex ;;
claude|Claude) client=claude ;;
both|Both) client=both ;;
*) printf 'Unknown --client value: %s (expected codex, claude, or both)\n' "$client" >&2; exit 1 ;;
esac
fi
stamp=$(date +%Y%m%d-%H%M%S) stamp=$(date +%Y%m%d-%H%M%S)
home_path=${HOME:?HOME is required}
shell_command=bash shell_command=bash
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File' windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
targets=() while IFS='|' read -r source destination; do
case "$client" in
codex|both) targets+=("$generated/codex-$platform|$home_path/.codex" "$generated/codex-$platform/skills|$home_path/.agents/skills");;
esac
case "$client" in
claude|both) targets+=("$generated/claude-$platform|$home_path/.claude");;
esac
for target_pair in "${targets[@]}"; do
source=${target_pair%%|*}
destination=${target_pair#*|}
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" sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$windows_shell_command" "$dry_run" "$summary"
done done < <(get_install_targets "$generated" "$home_path" "$platform" "$client")
install_configured_plugins "$root" "$client" "$dry_run" "$update_plugins" 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"
if "$dry_run"; then printf 'PASS install dry-run\n'; else printf 'PASS install\n'; fi if [ "$summary" = true ]; then
suffix=
[ "$dry_run" = false ] || suffix=', dry-run'
printf 'Install: PASS | %s, %s%s\n' "$client" "$platform" "$suffix"
elif "$dry_run"; then printf 'PASS install dry-run\n'; else printf 'PASS install\n'; fi
+35
View File
@@ -0,0 +1,35 @@
function Read-InstallPlatform {
param([string]$Platform)
if ($Platform) { return $Platform }
return Read-InteractiveSingle -Title 'Select shell' -Names @('PowerShell','Bash') -Values @('Windows','Linux')
}
function Read-InstallClient {
param([string]$Client)
if ($Client) { return $Client }
return Read-InteractiveSingle -Title 'Select client' -Names @('Codex','Claude','Both') -Values @('Codex','Claude','Both')
}
function Clear-InteractiveScreen { if (-not [Console]::IsInputRedirected -and $env:CI -ne 'true' -and $env:AI_CONFIG_NO_INTERACTIVE -ne '1') { Clear-Host } }
function Read-InteractiveSingle {
param([string]$Title,[string[]]$Names,[string[]]$Values)
if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') {
Write-Host $Title; for($i=0;$i -lt $Names.Count;$i++){ Write-Host ('{0}) {1}' -f ($i+1),$Names[$i]) }
do { $answer=Read-Host 'Selection' } while($answer -notmatch ('^[1-{0}]$' -f $Names.Count)); return $Values[[int]$answer-1]
}
$index=0
while($true) {
Clear-InteractiveScreen; Write-Host $Title
for($i=0;$i -lt $Names.Count;$i++){ $prefix=if($i -eq $index){'>'}else{' '}; if($i -eq $index -and $env:NO_COLOR -ne '1'){Write-Host ('{0} {1}' -f $prefix,$Names[$i]) -ForegroundColor Cyan}else{Write-Host ('{0} {1}' -f $prefix,$Names[$i])} }
Write-Host 'Up/Down Navigate 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 'Enter'){ Clear-InteractiveScreen; return $Values[$index] }
}
}
function Get-InstallDestinations {
param([Parameter(Mandatory=$true)][string]$HomePath,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client)
$destinations=@(); if($Client -in @('Codex','Both')){$destinations+=Join-Path $HomePath '.codex'}; if($Client -in @('Claude','Both')){$destinations+=Join-Path $HomePath '.claude'}; return $destinations
}
function Get-InstallTargets {
param([Parameter(Mandatory=$true)][string]$Generated,[Parameter(Mandatory=$true)][string]$HomePath,[Parameter(Mandatory=$true)][string]$Platform,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client)
$targets=@(); if($Client -in @('Codex','Both')){$targets+=@{Source=(Join-Path $Generated "codex-$Platform");Destination=(Join-Path $HomePath '.codex')};$targets+=@{Source=(Join-Path $Generated "codex-$Platform/skills");Destination=(Join-Path $HomePath '.agents/skills')}}; if($Client -in @('Claude','Both')){$targets+=@{Source=(Join-Path $Generated "claude-$Platform");Destination=(Join-Path $HomePath '.claude')};}; return $targets
}
function Test-AnyManifestPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{Test-Path(Get-ManagedManifestPath $_)}).Count) }
function Test-AllManifestsPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{-not(Test-Path(Get-ManagedManifestPath $_))}).Count -eq 0) }
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
read_install_platform() {
local platform=$1
if [ -n "$platform" ]; then
case "$platform" in
windows|Windows) printf 'windows\n'; return 0 ;;
linux|Linux) printf 'linux\n'; return 0 ;;
*) printf 'Unknown --platform value: %s (expected windows or linux)\n' "$platform" >&2; return 1 ;;
esac
fi
read_single_selection 'Select shell' 'PowerShell' 'windows' 'Bash' 'linux'
}
read_install_client() {
local client=$1
if [ -n "$client" ]; then
case "$client" in
codex|Codex) printf 'codex\n'; return 0 ;;
claude|Claude) printf 'claude\n'; return 0 ;;
both|Both) printf 'both\n'; return 0 ;;
*) printf 'Unknown --client value: %s (expected codex, claude, or both)\n' "$client" >&2; return 1 ;;
esac
fi
read_single_selection 'Select client' 'Codex' 'codex' 'Claude' 'claude' 'Both' 'both'
}
clear_interactive() { [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ] && printf '\033[2J\033[H'; }
read_single_selection() {
local title=$1; shift; local labels=() values=() label value index=0 key
while [ $# -gt 0 ]; do labels+=("$1"); values+=("$2"); shift 2; done
if [ ! -t 0 ] || [ "${CI:-}" = true ] || [ "${AI_CONFIG_NO_INTERACTIVE:-}" = 1 ]; then printf '%s\n' "$title" >&2; for ((index=0;index<${#labels[@]};index++)); do printf '%d) %s\n' "$((index+1))" "${labels[$index]}" >&2; done; while read -r -p 'Selection ' value; do [[ "$value" =~ ^[1-9][0-9]*$ ]] && [ "$value" -le "${#labels[@]}" ] && { printf '%s\n' "${values[$((value-1))]}"; return; }; done; return 1; fi
local selected=0; while :; do clear_interactive; printf '%s\n' "$title"; for ((index=0;index<${#labels[@]};index++)); do [ "$index" -eq "$selected" ] && printf '> %s\n' "${labels[$index]}" || printf ' %s\n' "${labels[$index]}"; done; printf '↑/↓ Navigate Enter Confirm\n'; IFS= read -rsn1 key || return 1; case "$key" in $'\x1b') read -rsn2 key; case "$key" in '[A') selected=$(( (selected+${#labels[@]}-1)%${#labels[@]} ));; '[B') selected=$(( (selected+1)%${#labels[@]} ));; esac;; '') clear_interactive; printf '%s\n' "${values[$selected]}"; return;; esac; done
}
# get_install_destinations <home> <client> -- prints one destination per line
get_install_destinations() {
local home_path=$1 client=$2
case "$client" in codex|both) printf '%s/.codex\n' "$home_path" ;; esac
case "$client" in claude|both) printf '%s/.claude\n' "$home_path" ;; esac
}
# get_install_targets <generated> <home> <platform> <client> -- prints "source|dest" per line
get_install_targets() {
local generated=$1 home_path=$2 platform=$3 client=$4
case "$client" in
codex|both)
printf '%s/codex-%s|%s/.codex\n' "$generated" "$platform" "$home_path"
printf '%s/codex-%s/skills|%s/.agents/skills\n' "$generated" "$platform" "$home_path"
;;
esac
case "$client" in claude|both) printf '%s/claude-%s|%s/.claude\n' "$generated" "$platform" "$home_path" ;; esac
}
# any_manifest_present <destination>... -- true (0) if at least one has a manifest
any_manifest_present() {
local destination
for destination in "$@"; do
[ -f "$(manifest_path "$destination")" ] && return 0
done
return 1
}
# all_manifests_present <destination>... -- true (0) only if every destination has a manifest
all_manifests_present() {
local destination
for destination in "$@"; do
[ -f "$(manifest_path "$destination")" ] || return 1
done
return 0
}
+16 -10
View File
@@ -56,13 +56,15 @@ function Sync-ManagedDestination {
[string]$AiConfigRoot, [string]$AiConfigRoot,
[string]$ShellCommand, [string]$ShellCommand,
[string]$WindowsShellCommand, [string]$WindowsShellCommand,
[switch]$DryRun [switch]$DryRun,
[switch]$Summary
) )
Write-Output "SOURCE $Source -> $Destination" if (-not $Summary) { Write-Output "SOURCE $Source -> $Destination" }
$manifestPath = Get-ManagedManifestPath $Destination $manifestPath = Get-ManagedManifestPath $Destination
$oldManifest = Read-ManagedManifest $manifestPath $oldManifest = Read-ManagedManifest $manifestPath
$newManifest = @{} $newManifest = @{}
$backupRoot = Join-Path $Destination "backups/$Stamp" $backupRoot = Join-Path $Destination "backups/$Stamp"
$tally = @{ Created = 0; Updated = 0; Unchanged = 0; Removed = 0; Warned = 0 }
Get-ChildItem $Source -File -Recurse | ForEach-Object { Get-ChildItem $Source -File -Recurse | ForEach-Object {
$relative = $_.FullName.Substring($Source.Length).TrimStart([char[]]@('\','/')).Replace('\','/') $relative = $_.FullName.Substring($Source.Length).TrimStart([char[]]@('\','/')).Replace('\','/')
@@ -79,38 +81,42 @@ function Sync-ManagedDestination {
$newManifest[$relative] = $newHash $newManifest[$relative] = $newHash
$exists = Test-Path -LiteralPath $target $exists = Test-Path -LiteralPath $target
if ($exists -and (Get-Sha256Hash $target) -eq $newHash) { Write-Output "UNCHANGED $target"; return } if ($exists -and (Get-Sha256Hash $target) -eq $newHash) { $tally.Unchanged++; if (-not $Summary) { Write-Output "UNCHANGED $target" }; return }
$action = if ($exists) { 'UPDATE' } else { 'CREATE' } $action = if ($exists) { 'UPDATE' } else { 'CREATE' }
if ($DryRun) { Write-Output "DRYRUN $action $target"; return } if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN $action $target" }; if ($action -eq 'CREATE') { $tally.Created++ } else { $tally.Updated++ }; return }
if ($exists) { if ($exists) {
$wasManaged = $oldManifest.ContainsKey($relative) $wasManaged = $oldManifest.ContainsKey($relative)
$backup = Join-Path $backupRoot $relative $backup = Join-Path $backupRoot $relative
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
Copy-Item -LiteralPath $target $backup -Force Copy-Item -LiteralPath $target $backup -Force
Write-Output "BACKUP $target -> $backup" if (-not $Summary) { Write-Output "BACKUP $target -> $backup" }
if (-not $wasManaged) { Write-Output "WARN $target existed before this installation but was not tracked by a previous run; it was backed up before being overwritten." } if (-not $wasManaged) { Write-Output "WARN $target existed before this installation but was not tracked by a previous run; it was backed up before being overwritten."; $tally.Warned++ }
} }
New-Item (Split-Path $target -Parent) -ItemType Directory -Force | Out-Null New-Item (Split-Path $target -Parent) -ItemType Directory -Force | Out-Null
[System.IO.File]::WriteAllBytes($target, $newBytes) [System.IO.File]::WriteAllBytes($target, $newBytes)
Write-Output "$action $target" if (-not $Summary) { Write-Output "$action $target" }
if ($action -eq 'CREATE') { $tally.Created++ } else { $tally.Updated++ }
} }
foreach ($relative in @($oldManifest.Keys | Where-Object { -not $newManifest.ContainsKey($_) })) { foreach ($relative in @($oldManifest.Keys | Where-Object { -not $newManifest.ContainsKey($_) })) {
$target = Join-Path $Destination $relative $target = Join-Path $Destination $relative
if (-not (Test-Path -LiteralPath $target)) { continue } if (-not (Test-Path -LiteralPath $target)) { continue }
if ($DryRun) { Write-Output "DRYRUN REMOVE $target"; continue } if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN REMOVE $target" }; $tally.Removed++; continue }
$backup = Join-Path $backupRoot $relative $backup = Join-Path $backupRoot $relative
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
Copy-Item -LiteralPath $target $backup -Force Copy-Item -LiteralPath $target $backup -Force
Write-Output "BACKUP $target -> $backup" if (-not $Summary) { Write-Output "BACKUP $target -> $backup" }
if ((Get-Sha256Hash $target) -eq $oldManifest[$relative]) { if ((Get-Sha256Hash $target) -eq $oldManifest[$relative]) {
Remove-Item -LiteralPath $target -Force Remove-Item -LiteralPath $target -Force
Write-Output "REMOVE $target" if (-not $Summary) { Write-Output "REMOVE $target" }
$tally.Removed++
} else { } else {
Write-Output "WARN $target was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed." Write-Output "WARN $target was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed."
$tally.Warned++
} }
} }
if (-not $DryRun) { Write-ManagedManifest $manifestPath $newManifest } if (-not $DryRun) { Write-ManagedManifest $manifestPath $newManifest }
if ($Summary) { Write-Output ("SYNC {0}: {1} created, {2} updated, {3} unchanged, {4} removed, {5} warnings" -f $Destination, $tally.Created, $tally.Updated, $tally.Unchanged, $tally.Removed, $tally.Warned) }
} }
+27 -10
View File
@@ -49,16 +49,17 @@ write_managed_manifest() {
} > "$path" } > "$path"
} }
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <windows-shell-command> <dry-run: true|false> # sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <windows-shell-command> <dry-run: true|false> [summary: true|false]
sync_managed_destination() { sync_managed_destination() {
local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 windows_shell_command=$6 dry_run=$7 local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 windows_shell_command=$6 dry_run=$7 summary=${8:-false}
printf 'SOURCE %s -> %s\n' "$source" "$destination" [ "$summary" = true ] || printf 'SOURCE %s -> %s\n' "$source" "$destination"
local manifest local manifest
manifest=$(manifest_path "$destination") manifest=$(manifest_path "$destination")
declare -A old_manifest declare -A old_manifest
read_managed_manifest "$manifest" old_manifest read_managed_manifest "$manifest" old_manifest
declare -A new_manifest declare -A new_manifest
local backup_root="$destination/backups/$stamp" local backup_root="$destination/backups/$stamp"
local created=0 updated=0 unchanged=0 removed=0 warned=0
while IFS= read -r -d '' source_file; do while IFS= read -r -d '' source_file; do
local relative target content needs_sub new_hash exists current_hash action backup local relative target content needs_sub new_hash exists current_hash action backup
@@ -84,7 +85,8 @@ sync_managed_destination() {
if [ "$exists" = true ]; then if [ "$exists" = true ]; then
current_hash=$(sha256_of_file "$target") current_hash=$(sha256_of_file "$target")
if [ "$current_hash" = "$new_hash" ]; then if [ "$current_hash" = "$new_hash" ]; then
printf 'UNCHANGED %s\n' "$target" unchanged=$((unchanged + 1))
[ "$summary" = true ] || printf 'UNCHANGED %s\n' "$target"
continue continue
fi fi
action=UPDATE action=UPDATE
@@ -92,15 +94,20 @@ sync_managed_destination() {
action=CREATE action=CREATE
fi fi
if [ "$dry_run" = true ]; then printf 'DRYRUN %s %s\n' "$action" "$target"; continue; fi if [ "$dry_run" = true ]; then
[ "$summary" = true ] || printf 'DRYRUN %s %s\n' "$action" "$target"
[ "$action" = CREATE ] && created=$((created + 1)) || updated=$((updated + 1))
continue
fi
if [ "$exists" = true ]; then if [ "$exists" = true ]; then
backup="$backup_root/$relative" backup="$backup_root/$relative"
mkdir -p "$(dirname -- "$backup")" mkdir -p "$(dirname -- "$backup")"
cp -- "$target" "$backup" cp -- "$target" "$backup"
printf 'BACKUP %s -> %s\n' "$target" "$backup" [ "$summary" = true ] || printf 'BACKUP %s -> %s\n' "$target" "$backup"
if [ -z "${old_manifest[$relative]+x}" ]; then if [ -z "${old_manifest[$relative]+x}" ]; then
printf 'WARN %s existed before this installation but was not tracked by a previous run; it was backed up before being overwritten.\n' "$target" printf 'WARN %s existed before this installation but was not tracked by a previous run; it was backed up before being overwritten.\n' "$target"
warned=$((warned + 1))
fi fi
fi fi
mkdir -p "$(dirname -- "$target")" mkdir -p "$(dirname -- "$target")"
@@ -109,7 +116,8 @@ sync_managed_destination() {
else else
cp -- "$source_file" "$target" cp -- "$source_file" "$target"
fi fi
printf '%s %s\n' "$action" "$target" [ "$summary" = true ] || printf '%s %s\n' "$action" "$target"
[ "$action" = CREATE ] && created=$((created + 1)) || updated=$((updated + 1))
done < <(find "$source" -type f -print0) done < <(find "$source" -type f -print0)
local relative target current_hash backup local relative target current_hash backup
@@ -117,19 +125,28 @@ sync_managed_destination() {
[ -z "${new_manifest[$relative]+x}" ] || continue [ -z "${new_manifest[$relative]+x}" ] || continue
target="$destination/$relative" target="$destination/$relative"
[ -f "$target" ] || continue [ -f "$target" ] || continue
if [ "$dry_run" = true ]; then printf 'DRYRUN REMOVE %s\n' "$target"; continue; fi if [ "$dry_run" = true ]; then
[ "$summary" = true ] || printf 'DRYRUN REMOVE %s\n' "$target"
removed=$((removed + 1))
continue
fi
backup="$backup_root/$relative" backup="$backup_root/$relative"
mkdir -p "$(dirname -- "$backup")" mkdir -p "$(dirname -- "$backup")"
cp -- "$target" "$backup" cp -- "$target" "$backup"
printf 'BACKUP %s -> %s\n' "$target" "$backup" [ "$summary" = true ] || printf 'BACKUP %s -> %s\n' "$target" "$backup"
current_hash=$(sha256_of_file "$target") current_hash=$(sha256_of_file "$target")
if [ "$current_hash" = "${old_manifest[$relative]}" ]; then if [ "$current_hash" = "${old_manifest[$relative]}" ]; then
rm -f -- "$target" rm -f -- "$target"
printf 'REMOVE %s\n' "$target" [ "$summary" = true ] || printf 'REMOVE %s\n' "$target"
removed=$((removed + 1))
else else
printf 'WARN %s was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed.\n' "$target" printf 'WARN %s was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed.\n' "$target"
warned=$((warned + 1))
fi fi
done done
if [ "$dry_run" != true ]; then write_managed_manifest "$manifest" new_manifest; fi if [ "$dry_run" != true ]; then write_managed_manifest "$manifest" new_manifest; fi
if [ "$summary" = true ]; then
printf 'SYNC %s: %s created, %s updated, %s unchanged, %s removed, %s warnings\n' "$destination" "$created" "$updated" "$unchanged" "$removed" "$warned"
fi
} }
+132 -21
View File
@@ -2,12 +2,25 @@ function Invoke-PluginCommand {
param( param(
[Parameter(Mandatory=$true)][string]$Command, [Parameter(Mandatory=$true)][string]$Command,
[Parameter(Mandatory=$true)][string[]]$Arguments, [Parameter(Mandatory=$true)][string[]]$Arguments,
[switch]$DryRun [switch]$DryRun,
[switch]$Summary
) )
$display = "$Command $($Arguments -join ' ')" $display = "$Command $($Arguments -join ' ')"
if ($DryRun) { Write-Output "DRYRUN $display"; return } if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN $display" }; return }
& $Command @Arguments $resolvedCommand = Get-Command $Command -ErrorAction Stop
if (-not $Summary) {
& $resolvedCommand @Arguments
if ($LASTEXITCODE -ne 0) { throw "Plugin command failed: $display" } if ($LASTEXITCODE -ne 0) { throw "Plugin command failed: $display" }
return
}
$previousPreference = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
$output = @(& $resolvedCommand @Arguments 2>&1)
$exitCode = $LASTEXITCODE
} 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 ($exitCode -ne 0) { throw "Plugin command failed: $display" }
} }
function Get-ConfiguredPluginEntries { function Get-ConfiguredPluginEntries {
@@ -37,10 +50,11 @@ function Ensure-CodexMarketplace {
param( param(
[Parameter(Mandatory=$true)][string]$Source, [Parameter(Mandatory=$true)][string]$Source,
[Parameter(Mandatory=$true)][string]$MarketplaceName, [Parameter(Mandatory=$true)][string]$MarketplaceName,
[switch]$DryRun [switch]$DryRun,
[switch]$Summary
) )
if ($DryRun) { if ($DryRun) {
Write-Output "DRYRUN codex plugin marketplace add $Source" if (-not $Summary) { Write-Output "DRYRUN codex plugin marketplace add $Source" }
return return
} }
$previousErrorActionPreference = $ErrorActionPreference $previousErrorActionPreference = $ErrorActionPreference
@@ -54,31 +68,119 @@ function Ensure-CodexMarketplace {
} finally { } finally {
$ErrorActionPreference = $previousErrorActionPreference $ErrorActionPreference = $previousErrorActionPreference
} }
$output | ForEach-Object { Write-Output $_ } 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 ($exitCode -eq 0) { return } if ($exitCode -eq 0) { return }
$text = $output -join [Environment]::NewLine $text = $output -join [Environment]::NewLine
if ($text -match 'marketplace .* already added from a different source') { if ($text -match 'marketplace .* already added from a different source') {
Write-Output "WARN Codex marketplace '$MarketplaceName' exists from another source; replacing it with: $Source" Write-Output "WARN Codex marketplace '$MarketplaceName' exists from another source; replacing it with: $Source"
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','remove',$MarketplaceName) Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','remove',$MarketplaceName) -Summary:$Summary
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','add',$Source) Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','add',$Source) -Summary:$Summary
return return
} }
throw "Plugin command failed: codex plugin marketplace add $Source" throw "Plugin command failed: codex plugin marketplace add $Source"
} }
function Read-PluginToggleSelection {
param(
[Parameter(Mandatory=$true)][string[]]$Names,
[Parameter(Mandatory=$true)][bool[]]$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]} } }
$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} }
}
function Update-CodexMarketplace {
param([Parameter(Mandatory=$true)][string]$MarketplaceName,[Parameter(Mandatory=$true)][string]$Source,[switch]$DryRun,[switch]$Summary)
try { Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','upgrade',$MarketplaceName) -DryRun:$DryRun -Summary:$Summary }
catch { Ensure-CodexMarketplace -Source $Source -MarketplaceName $MarketplaceName -DryRun:$DryRun -Summary:$Summary }
}
function Select-ConfiguredPlugins {
<#
.SYNOPSIS
Lets the user toggle each configured plugin on or off. Install mode starts
with everything checked; update mode starts checked to match what the
reference client (Claude, or Codex when only Codex is targeted) already
has installed, so unchecking a currently installed plugin uninstalls it.
#>
param(
[Parameter(Mandatory=$true)][string]$RepositoryRoot,
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[ValidateSet('Install','Update')][Parameter(Mandatory=$true)][string]$Mode,
[switch]$DryRun
)
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot)
$toggleable = @($entries | Where-Object { $Client -in @('Claude','Both') -or $_.codex_method -eq 'plugin' })
$fixed = @($entries | Where-Object { $toggleable -notcontains $_ })
if ($DryRun -or $toggleable.Count -eq 0) { return @{ Selected = $entries; Deselected = @() } }
$referenceClient = if ($Client -eq 'Codex') { 'Codex' } else { 'Claude' }
$installed = if ($Mode -eq 'Update') { @(Get-InstalledPlugins -Client $referenceClient) } else { @() }
$idField = if ($referenceClient -eq 'Claude') { 'id' } else { 'pluginId' }
$checked = @($toggleable | ForEach-Object {
if ($Mode -eq 'Install') { $true; return }
$selector = if ($referenceClient -eq 'Claude') { $_.claude_plugin } else { $_.codex_plugin }
[bool](@($installed | Where-Object { $_.$idField -eq $selector }).Count)
})
$names = @($toggleable | ForEach-Object { $_.name })
$checked = Read-PluginToggleSelection -Names $names -Checked $checked
$selected = @()
$deselected = @()
for ($i = 0; $i -lt $toggleable.Count; $i++) {
if ($checked[$i]) { $selected += $toggleable[$i] } else { $deselected += $toggleable[$i] }
}
return @{ Selected = ($selected + $fixed); Deselected = $deselected }
}
function Uninstall-DeselectedPlugins {
param(
[Parameter(Mandatory=$true)][AllowEmptyCollection()][object[]]$Entries,
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[switch]$DryRun,
[switch]$Summary
)
if ($Entries.Count -eq 0 -and $PSBoundParameters.ContainsKey('Entries')) { return }
$clients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) }
foreach ($selectedClient in $clients) {
$installed = if ($DryRun) { @() } else { @(Get-InstalledPlugins -Client $selectedClient) }
$idField = if ($selectedClient -eq 'Claude') { 'id' } else { 'pluginId' }
$removedCount = 0
foreach ($entry in $Entries) {
$plugin = if ($selectedClient -eq 'Claude') { $entry.claude_plugin } else { $entry.codex_plugin }
if ([string]::IsNullOrWhiteSpace($plugin)) { continue }
$isInstalled = [bool](@($installed | Where-Object { $_.$idField -eq $plugin }).Count)
if (-not $isInstalled) { continue }
if ($selectedClient -eq 'Claude') {
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','uninstall',$plugin,'--scope','user') -DryRun:$DryRun -Summary:$Summary
} else {
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','remove',$plugin) -DryRun:$DryRun -Summary:$Summary
}
if (-not $Summary) { Write-Output "PASS $selectedClient plugin removed: $plugin" }
$removedCount++
}
if ($Summary -and $removedCount -gt 0) { Write-Output "PLUGINS ${selectedClient}: $removedCount removed" }
}
}
function Install-ConfiguredPlugins { function Install-ConfiguredPlugins {
param( param(
[Parameter(Mandatory=$true)][string]$RepositoryRoot, [Parameter(Mandatory=$true)][string]$RepositoryRoot,
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[switch]$DryRun, [switch]$DryRun,
[switch]$Update [switch]$Update,
[switch]$Summary,
[object[]]$Entries
) )
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot) $entries = if ($PSBoundParameters.ContainsKey('Entries')) { $Entries } else { @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot) }
if ($Entries.Count -eq 0 -and $PSBoundParameters.ContainsKey('Entries')) { return }
$clients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) } $clients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) }
foreach ($selectedClient in $clients) { foreach ($selectedClient in $clients) {
$installed = if ($DryRun) { @() } else { @(Get-InstalledPlugins -Client $selectedClient) } $installed = if ($DryRun) { @() } else { @(Get-InstalledPlugins -Client $selectedClient) }
$tally = @{ Ensured = 0; AlreadyInstalled = 0; Updated = 0 }
foreach ($entry in $entries) { foreach ($entry in $entries) {
$marketplace = if ($selectedClient -eq 'Claude') { $entry.claude_marketplace.Trim() } else { $entry.codex_marketplace.Trim() } $marketplace = if ($selectedClient -eq 'Claude') { $entry.claude_marketplace.Trim() } else { $entry.codex_marketplace.Trim() }
if ($marketplace -eq '-') { $marketplace = '' } if ($marketplace -eq '-') { $marketplace = '' }
@@ -94,40 +196,49 @@ function Install-ConfiguredPlugins {
} else { } else {
throw "Unknown Codex install method '$($entry.codex_method)' for $($entry.name)." throw "Unknown Codex install method '$($entry.codex_method)' for $($entry.name)."
} }
Invoke-PluginCommand -Command 'npx' -Arguments $arguments -DryRun:$DryRun Invoke-PluginCommand -Command 'npx' -Arguments $arguments -DryRun:$DryRun -Summary:$Summary
Write-Output "PASS Codex skill ensured: $($entry.name)" if (-not $Summary) { Write-Output "PASS Codex skill ensured: $($entry.name)" }
$tally.Ensured++
continue continue
} }
$installedItem = if ($selectedClient -eq 'Claude') { $installed | Where-Object { $_.id -eq $plugin } | Select-Object -First 1 } else { $installed | Where-Object { $_.pluginId -eq $plugin } | Select-Object -First 1 } $installedItem = if ($selectedClient -eq 'Claude') { $installed | Where-Object { $_.id -eq $plugin } | Select-Object -First 1 } else { $installed | Where-Object { $_.pluginId -eq $plugin } | Select-Object -First 1 }
if ($Update -and $selectedClient -eq 'Claude' -and $null -ne $installedItem) { if ($Update -and $selectedClient -eq 'Claude' -and $null -ne $installedItem) {
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','update',$plugin) -DryRun:$DryRun Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','update',$plugin) -DryRun:$DryRun -Summary:$Summary
if (-not $installedItem.enabled) { if (-not $installedItem.enabled) {
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun -Summary:$Summary
} }
$tally.Updated++
continue continue
} }
if (-not $Update -and $null -ne $installedItem) { if (-not $Update -and $null -ne $installedItem) {
if ($selectedClient -eq 'Claude' -and -not $installedItem.enabled) { if ($selectedClient -eq 'Claude' -and -not $installedItem.enabled) {
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun -Summary:$Summary
} }
Write-Output "PASS $selectedClient plugin already installed: $plugin" if (-not $Summary) { Write-Output "PASS $selectedClient plugin already installed: $plugin" }
$tally.AlreadyInstalled++
continue continue
} }
if ($selectedClient -eq 'Claude') { if ($selectedClient -eq 'Claude') {
if (-not [string]::IsNullOrWhiteSpace($marketplace)) { if (-not [string]::IsNullOrWhiteSpace($marketplace)) {
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','marketplace','add',$marketplace) -DryRun:$DryRun Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','marketplace','add',$marketplace) -DryRun:$DryRun -Summary:$Summary
} }
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','install',$plugin,'--scope','user') -DryRun:$DryRun Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','install',$plugin,'--scope','user') -DryRun:$DryRun -Summary:$Summary
} else { } else {
if ($Update -and -not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') {
Update-CodexMarketplace -MarketplaceName (($plugin -split '@')[-1]) -Source $marketplace -DryRun:$DryRun -Summary:$Summary
}
if (-not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') { if (-not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') {
$marketplaceName = ($plugin -split '@')[-1] $marketplaceName = ($plugin -split '@')[-1]
Ensure-CodexMarketplace -Source $marketplace -MarketplaceName $marketplaceName -DryRun:$DryRun Ensure-CodexMarketplace -Source $marketplace -MarketplaceName $marketplaceName -DryRun:$DryRun -Summary:$Summary
} }
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','add',$plugin) -DryRun:$DryRun Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','add',$plugin) -DryRun:$DryRun -Summary:$Summary
} }
Write-Output "PASS $selectedClient plugin ensured: $plugin" if (-not $Summary) { Write-Output "PASS $selectedClient plugin ensured: $plugin" }
if ($Update -and $null -ne $installedItem) { $tally.Updated++ } else { $tally.Ensured++ }
} }
if ($Summary) { Write-Output ("PLUGINS {0}: {1} ensured, {2} already installed, {3} updated" -f $selectedClient, $tally.Ensured, $tally.AlreadyInstalled, $tally.Updated) }
} }
} }
+171 -10
View File
@@ -14,16 +14,34 @@ run_plugin_command() {
local dry_run=$1 local dry_run=$1
shift shift
if [ "$dry_run" = true ]; then if [ "$dry_run" = true ]; then
printf 'DRYRUN %s\n' "$*" [ "${summary:-false}" = true ] || printf 'DRYRUN %s\n' "$*"
return 0
elif [ "${summary:-false}" = true ]; then
local output status
if output=$("$@"); then
printf '%s\n' "$output" | grep -Ei 'warn|error|fail|deprecat' || true
else
status=$?
[ "${AI_CONFIG_VERBOSE:-}" = 1 ] && printf '%s\n' "$output" >&2
printf 'Plugin command failed: %s\n' "$*" >&2
return "$status"
fi
else else
"$@" "$@"
fi fi
} }
update_codex_marketplace() {
local dry_run=$1 marketplace_name=$2 source=$3
if ! run_plugin_command "$dry_run" codex plugin marketplace upgrade "$marketplace_name"; then
ensure_codex_marketplace "$dry_run" "$source" "$marketplace_name"
fi
}
ensure_codex_marketplace() { ensure_codex_marketplace() {
local dry_run=$1 source=$2 marketplace_name=$3 output local dry_run=$1 source=$2 marketplace_name=$3 output
if [ "$dry_run" = true ]; then if [ "$dry_run" = true ]; then
printf 'DRYRUN codex plugin marketplace add %s\n' "$source" [ "${summary:-false}" = true ] || printf 'DRYRUN codex plugin marketplace add %s\n' "$source"
return 0 return 0
fi fi
if output=$(codex plugin marketplace add "$source" 2>&1); then if output=$(codex plugin marketplace add "$source" 2>&1); then
@@ -33,18 +51,150 @@ ensure_codex_marketplace() {
printf '%s\n' "$output" >&2 printf '%s\n' "$output" >&2
if printf '%s\n' "$output" | grep -Eiq 'marketplace .* already added from a different source'; then if printf '%s\n' "$output" | grep -Eiq 'marketplace .* already added from a different source'; then
printf "WARN Codex marketplace '%s' exists from another source; replacing it with: %s\n" "$marketplace_name" "$source" printf "WARN Codex marketplace '%s' exists from another source; replacing it with: %s\n" "$marketplace_name" "$source"
codex plugin marketplace remove "$marketplace_name" run_plugin_command false codex plugin marketplace remove "$marketplace_name"
codex plugin marketplace add "$source" run_plugin_command false codex plugin marketplace add "$source"
return 0 return 0
fi fi
return 1 return 1
} }
get_plugin_names() {
local root=$1 name rest
while IFS=$'\t' read -r name rest; do
[ "$name" = name ] && continue
[ -n "$name" ] || continue
printf '%s\n' "$name"
done < "$root/adapters/plugins.tsv"
}
# plugin_field <root> <name> <field> -- field is one of the plugins.tsv column names
plugin_field() {
local root=$1 target=$2 field=$3
local name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill
while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do
[ "$name" = "$target" ] || continue
printf '%s\n' "${!field}"
return 0
done < "$root/adapters/plugins.tsv"
return 1
}
plugin_toggleable() {
local root=$1 name=$2 client=$3
case "$client" in claude|both) return 0;; esac
[ "$(plugin_field "$root" "$name" codex_method)" = plugin ]
}
# read_plugin_toggle_selection <names-array-name> <checked-array-name>
read_plugin_toggle_selection() {
local -n names_ref=$1
local -n checked_ref=$2
local answer i mark index=0 key
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
fi
while :; do
printf 'Select plugins (enter a number to toggle, "done" to confirm):\n'
for ((i = 0; i < ${#names_ref[@]}; i++)); do
mark=' '
[ "${checked_ref[$i]}" = true ] && mark=x
printf ' %d) [%s] %s\n' "$((i + 1))" "$mark" "${names_ref[$i]}"
done
read -r -p 'Toggle number or "done" ' answer || { printf 'Plugin selection cancelled: input closed.\n' >&2; return 1; }
{ [ -z "$answer" ] || [ "$answer" = done ]; } && return 0
if [[ "$answer" =~ ^[0-9]+$ ]] && [ "$answer" -ge 1 ] && [ "$answer" -le "${#names_ref[@]}" ]; then
i=$((10#$answer - 1))
if [ "${checked_ref[$i]}" = true ]; then checked_ref[$i]=false; else checked_ref[$i]=true; fi
fi
done
}
# select_configured_plugins <root> <client> <mode: Install|Update> <dry_run> <selected-array-name> <deselected-array-name>
select_configured_plugins() {
local root=$1 client=$2 mode=$3 dry_run=$4
local -n selected_ref=$5
local -n deselected_ref=$6
selected_ref=()
deselected_ref=()
local name all_names=() toggle_names=() fixed_names=()
while IFS= read -r name; do
all_names+=("$name")
if plugin_toggleable "$root" "$name" "$client"; then toggle_names+=("$name"); else fixed_names+=("$name"); fi
done < <(get_plugin_names "$root")
if [ "$dry_run" = true ] || [ ${#toggle_names[@]} -eq 0 ]; then
selected_ref=("${all_names[@]}")
return 0
fi
local reference_client=claude
[ "$client" = codex ] && reference_client=codex
local checked=() selector
for name in "${toggle_names[@]}"; do
if [ "$mode" = Install ]; then
checked+=(true)
continue
fi
if [ "$reference_client" = claude ]; then selector=$(plugin_field "$root" "$name" claude_plugin); else selector=$(plugin_field "$root" "$name" codex_plugin); fi
if plugin_installed "$reference_client" "$selector"; then checked+=(true); else checked+=(false); fi
done
read_plugin_toggle_selection toggle_names checked
local i
for ((i = 0; i < ${#toggle_names[@]}; i++)); do
if [ "${checked[$i]}" = true ]; then selected_ref+=("${toggle_names[$i]}"); else deselected_ref+=("${toggle_names[$i]}"); fi
done
selected_ref+=("${fixed_names[@]}")
}
# uninstall_deselected_plugins <root> <client> <dry_run> <deselected-array-name> [summary]
uninstall_deselected_plugins() {
local root=$1 client_selection=$2 dry_run=$3
local -n names_ref=$4
local summary=${5:-false}
local clients=()
case "$client_selection" in
codex) clients=(codex);;
claude) clients=(claude);;
both) clients=(claude codex);;
esac
local client name plugin removed
for client in "${clients[@]}"; do
removed=0
for name in "${names_ref[@]}"; do
if [ "$client" = claude ]; then plugin=$(plugin_field "$root" "$name" claude_plugin); else plugin=$(plugin_field "$root" "$name" codex_plugin); fi
[ -n "$plugin" ] || continue
if [ "$dry_run" = false ] && plugin_installed "$client" "$plugin"; then
if [ "$client" = claude ]; then
run_plugin_command "$dry_run" claude plugin uninstall "$plugin" --scope user
else
run_plugin_command "$dry_run" codex plugin remove "$plugin"
fi
[ "$summary" = true ] || printf 'PASS %s plugin removed: %s\n' "$client" "$plugin"
removed=$((removed + 1))
fi
done
if [ "$summary" = true ] && [ "$removed" -gt 0 ]; then printf 'PLUGINS %s: %s removed\n' "$client" "$removed"; fi
done
}
# install_configured_plugins <root> <client> <dry_run> <update> [selected-names-array-name] [summary]
install_configured_plugins() { install_configured_plugins() {
local root=$1 client_selection=$2 dry_run=$3 update=$4 local root=$1 client_selection=$2 dry_run=$3 update=$4 selected_name=${5:-} summary=${6:-false}
local manifest="$root/adapters/plugins.tsv" local manifest="$root/adapters/plugins.tsv"
[ -f "$manifest" ] || { printf 'Plugin manifest is missing: %s\n' "$manifest" >&2; return 1; } [ -f "$manifest" ] || { printf 'Plugin manifest is missing: %s\n' "$manifest" >&2; return 1; }
local -A selected_set=()
if [ -n "$selected_name" ]; then
local -n selected_names_ref=$selected_name
local sn
for sn in "${selected_names_ref[@]}"; do selected_set[$sn]=1; done
fi
local ensured=0 already_installed=0 updated_count=0
local clients=() local clients=()
case "$client_selection" in case "$client_selection" in
codex) clients=(codex);; codex) clients=(codex);;
@@ -54,12 +204,14 @@ install_configured_plugins() {
esac esac
local client name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill marketplace plugin local client name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill marketplace plugin
for client in "${clients[@]}"; do
ensured=0 already_installed=0 updated_count=0
while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do
[ "$name" = name ] && continue [ "$name" = name ] && continue
[ -n "$name" ] || continue [ -n "$name" ] || continue
[ -n "$selected_name" ] && [ -z "${selected_set[$name]+x}" ] && continue
[ "$claude_marketplace" = - ] && claude_marketplace= [ "$claude_marketplace" = - ] && claude_marketplace=
[ "$codex_marketplace" = - ] && codex_marketplace= [ "$codex_marketplace" = - ] && codex_marketplace=
for client in "${clients[@]}"; do
if [ "$client" = claude ]; then marketplace=$claude_marketplace; plugin=$claude_plugin; else marketplace=$codex_marketplace; plugin=$codex_plugin; fi if [ "$client" = claude ]; then marketplace=$claude_marketplace; plugin=$claude_plugin; else marketplace=$codex_marketplace; plugin=$codex_plugin; fi
[ -n "$plugin" ] || { printf 'Plugin selector missing for %s: %s\n' "$client" "$name" >&2; return 1; } [ -n "$plugin" ] || { printf 'Plugin selector missing for %s: %s\n' "$client" "$name" >&2; return 1; }
if [ "$client" = codex ] && [ "$codex_method" != plugin ]; then if [ "$client" = codex ] && [ "$codex_method" != plugin ]; then
@@ -76,17 +228,22 @@ install_configured_plugins() {
;; ;;
*) printf 'Unknown Codex install method for %s: %s\n' "$client" "$name" >&2; return 1;; *) printf 'Unknown Codex install method for %s: %s\n' "$client" "$name" >&2; return 1;;
esac esac
printf 'PASS Codex skill ensured: %s\n' "$name" [ "$summary" = true ] || printf 'PASS Codex skill ensured: %s\n' "$name"
ensured=$((ensured + 1))
continue continue
fi fi
if [ "$dry_run" = false ] && plugin_installed "$client" "$plugin"; then if [ "$dry_run" = false ] && plugin_installed "$client" "$plugin"; then
if [ "$update" = true ] && [ "$client" = claude ]; then if [ "$update" = true ] && [ "$client" = claude ]; then
run_plugin_command "$dry_run" claude plugin update "$plugin" run_plugin_command "$dry_run" claude plugin update "$plugin"
run_plugin_command "$dry_run" claude plugin enable "$plugin" run_plugin_command "$dry_run" claude plugin enable "$plugin"
updated_count=$((updated_count + 1))
elif [ "$update" = true ] && [ "$client" = codex ]; then elif [ "$update" = true ] && [ "$client" = codex ]; then
[ -z "$marketplace" ] || [ "$marketplace" = openai-curated-remote ] || update_codex_marketplace "$dry_run" "${plugin##*@}" "$marketplace"
run_plugin_command "$dry_run" codex plugin add "$plugin" run_plugin_command "$dry_run" codex plugin add "$plugin"
updated_count=$((updated_count + 1))
else else
printf 'PASS %s plugin already installed: %s\n' "$client" "$plugin" [ "$summary" = true ] || printf 'PASS %s plugin already installed: %s\n' "$client" "$plugin"
already_installed=$((already_installed + 1))
fi fi
continue continue
fi fi
@@ -100,7 +257,11 @@ install_configured_plugins() {
fi fi
run_plugin_command "$dry_run" codex plugin add "$plugin" run_plugin_command "$dry_run" codex plugin add "$plugin"
fi fi
printf 'PASS %s plugin ensured: %s\n' "$client" "$plugin" [ "$summary" = true ] || printf 'PASS %s plugin ensured: %s\n' "$client" "$plugin"
done ensured=$((ensured + 1))
done < "$manifest" done < "$manifest"
if [ "$summary" = true ]; then
printf 'PLUGINS %s: %s ensured, %s already installed, %s updated\n' "$client" "$ensured" "$already_installed" "$updated_count"
fi
done
} }
+25
View File
@@ -0,0 +1,25 @@
[CmdletBinding()]
param([switch]$Summary)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$generated = Join-Path $root 'generated'
if (-not (Test-Path (Join-Path $generated 'claude-linux/CLAUDE.md'))) { throw 'Generated output is missing; run build first.' }
function Measure-Tree([string]$path) {
if (-not (Test-Path $path)) { return 0 }
return [int64]((Get-ChildItem $path -File -Recurse | ForEach-Object { $_.Length } | Measure-Object -Sum).Sum)
}
function Tokens([int64]$bytes) { return [int][Math]::Ceiling($bytes / 4.0) }
$claude = (Get-Item (Join-Path $generated 'claude-linux/CLAUDE.md')).Length
$codex = (Get-Item (Join-Path $generated 'codex-linux/AGENTS.md')).Length
$skills = Measure-Tree (Join-Path $generated 'claude-linux/skills')
$triggers = (Get-Item (Join-Path $root 'adapters/claude/rule-skills.tsv')).Length
$lazyItem = Get-ChildItem (Join-Path $root 'shared/rules') -File -Recurse | Sort-Object Length -Descending | Select-Object -First 1
$lazy = if ($lazyItem) { [int64]$lazyItem.Length } else { 0 }
$baseline = Join-Path $root 'adapters/prompt-budget-baseline.json'
$old = if (Test-Path $baseline) { Get-Content $baseline -Raw | ConvertFrom-Json } else { $null }
$rows = @([pscustomobject]@{Name='Claude permanent context';Bytes=$claude;Tokens=(Tokens $claude)},[pscustomobject]@{Name='Codex permanent context';Bytes=$codex;Tokens=(Tokens $codex)},[pscustomobject]@{Name='Claude skill metadata';Bytes=$skills;Tokens=(Tokens $skills)},[pscustomobject]@{Name='Rule trigger metadata';Bytes=$triggers;Tokens=(Tokens $triggers)},[pscustomobject]@{Name='Largest lazy rule';Bytes=$lazy;Tokens=(Tokens $lazy)})
$fail = $false
if ($old) { foreach ($row in $rows | Select-Object -First 2) { $previous = [double]$old.($row.Name); if ($previous -gt 0 -and $row.Bytes -gt $previous * 1.15) { $fail = $true } } }
if ($Summary) { Write-Output 'Prompt Budget'; $rows | ForEach-Object { Write-Output ('{0,-28} {1,8} bytes {2,6} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } else { $rows | ForEach-Object { Write-Output ('{0}: {1} bytes / {2} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } }
if ($fail) { Write-Error 'Prompt budget: permanent context increased by more than 15 percent.'; exit 1 }
exit 0
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
summary=false
for arg in "$@"; do case "$arg" in --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
generated="$root/generated"
[ -f "$generated/claude-linux/CLAUDE.md" ] || { printf 'Generated output is missing; run build first.\n' >&2; exit 1; }
bytes() { wc -c < "$1" | tr -d ' '; }
tree_bytes() { find "$1" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {print s+0}'; }
tokens() { awk -v b="$1" 'BEGIN { printf "%d", (b + 3) / 4 }'; }
claude=$(bytes "$generated/claude-linux/CLAUDE.md"); codex=$(bytes "$generated/codex-linux/AGENTS.md"); skills=$(tree_bytes "$generated/claude-linux/skills"); triggers=$(bytes "$root/adapters/claude/rule-skills.tsv"); lazy=$(find "$root/shared/rules" -type f -printf '%s %p\n' | sort -nr | awk 'NR==1 {print $1+0}'); fail=false
if [ -f "$root/adapters/prompt-budget-baseline.tsv" ]; then while IFS=$'\t' read -r name old; do current=$([ "$name" = Claude ] && printf '%s' "$claude" || printf '%s' "$codex"); awk -v n="$current" -v o="$old" 'BEGIN { exit !(o > 0 && n > o * 1.15) }' && fail=true; done < "$root/adapters/prompt-budget-baseline.tsv"; fi
if [ "$summary" = true ]; then printf 'Prompt Budget\nClaude permanent context %8s bytes %6s tokens\nCodex permanent context %8s bytes %6s tokens\nClaude skill metadata %8s bytes %6s tokens\nRule trigger metadata %8s bytes %6s tokens\nLargest lazy rule %8s bytes %6s tokens\n' "$claude" "$(tokens "$claude")" "$codex" "$(tokens "$codex")" "$skills" "$(tokens "$skills")" "$triggers" "$(tokens "$triggers")" "$lazy" "$(tokens "$lazy")"; else printf 'Prompt budget: Claude %s bytes, Codex %s bytes, skills %s bytes, triggers %s bytes, largest lazy rule %s bytes\n' "$claude" "$codex" "$skills" "$triggers" "$lazy"; fi
$fail && { printf 'Prompt budget: permanent context increased by more than 15 percent.\n' >&2; exit 1; }
+23
View File
@@ -0,0 +1,23 @@
[CmdletBinding()]
param([switch]$Force,[switch]$Summary)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$cacheDir = Join-Path $root '.ai-session'
$cachePath = Join-Path $cacheDir 'repository-context.json'
$safeRoot = $root.Replace('\','/')
$head = (& git -c "safe.directory=$safeRoot" -C $root rev-parse HEAD 2>$null).Trim()
$inputs = @('AI-Instructions.md','adapters/plugins.tsv','adapters/claude/rule-skills.tsv','adapters/codex/config/config.toml','adapters/claude/config/settings.json')
$inputs += @(Get-ChildItem $root -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj|vendor|\.ai-session)\\' -and $_.Extension -in @('.sln','.csproj','.fsproj','.ts','.tsx','.json','.toml','.yaml','.yml','.cs','.fs','.py','.sh','.ps1') } | ForEach-Object { $_.FullName.Substring($root.Length + 1) } | Sort-Object)
$keyText = $head + "`n" + (($inputs | Sort-Object -Unique | ForEach-Object { $_ + ':' + (Get-FileHash (Join-Path $root $_) -Algorithm SHA256).Hash }) -join "`n")
$key = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($keyText)).TrimEnd('=')
if (-not $Force -and (Test-Path $cachePath)) { try { $existing = Get-Content $cachePath -Raw | ConvertFrom-Json; if ($existing.invalidationKey -eq $key) { if ($Summary) { Write-Output 'Repository context: unchanged' }; exit 0 } } catch {} }
$extensions = @(Get-ChildItem $root -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj|vendor|\.ai-session)\\' } | Group-Object Extension | Sort-Object Count -Descending)
$languages = @(); foreach ($item in $extensions) { switch ($item.Name.ToLowerInvariant()) { '.cs' { $languages += 'csharp' }; '.ts' { $languages += 'typescript' }; '.tsx' { $languages += 'typescript' }; '.py' { $languages += 'python' }; '.ps1' { $languages += 'powershell' }; '.sh' { $languages += 'bash' } } }
$languages = @($languages | Sort-Object -Unique)
$solutions = @(Get-ChildItem $root -Filter *.sln -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git)\\' } | ForEach-Object { $_.BaseName } | Sort-Object -Unique)
$tests = @(Get-ChildItem $root -File -Recurse | Where-Object { $_.Name -match '(?i)test' -and $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj)\\' } | ForEach-Object { $_.BaseName } | Sort-Object -Unique)
$packageManager = if (Test-Path (Join-Path $root 'pnpm-lock.yaml')) { 'pnpm' } elseif (Test-Path (Join-Path $root 'package-lock.json')) { 'npm' } elseif (Test-Path (Join-Path $root 'yarn.lock')) { 'yarn' } else { $null }
$data = [ordered]@{ invalidationKey=$key; head=$head; languages=$languages; frameworks=@(); solutions=$solutions; testProjects=$tests; packageManager=$packageManager; buildCommands=[ordered]@{}; generatedAt=(Get-Date).ToUniversalTime().ToString('o') }
New-Item $cacheDir -ItemType Directory -Force | Out-Null
$data | ConvertTo-Json -Depth 5 | Set-Content $cachePath -Encoding UTF8
if ($Summary) { Write-Output ('Repository context: refreshed | {0} languages | {1} solutions | {2} tests' -f $languages.Count,$solutions.Count,$tests.Count) } else { Write-Output $cachePath }
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
force=false; summary=false
for arg in "$@"; do case "$arg" in --force) force=true;; --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); cache_dir="$root/.ai-session"; cache_path="$cache_dir/repository-context.json"
head=$(git -c "safe.directory=$root" -C "$root" rev-parse HEAD 2>/dev/null || true); key_input=$head
while IFS= read -r file; do key_input+="\n$file:$(sha256sum "$file" | awk '{print $1}')"; done < <(find "$root" -type f \( -name AI-Instructions.md -o -path '*/adapters/*' \) ! -path '*/generated/*' ! -path '*/.git/*' ! -path '*/.ai-session/*' | sort)
key=$(printf '%s' "$key_input" | sha256sum | awk '{print $1}')
if [ "$force" = false ] && [ -f "$cache_path" ] && grep -Fq "\"invalidationKey\": \"$key\"" "$cache_path"; then [ "$summary" = true ] && printf 'Repository context: unchanged\n'; exit 0; fi
mapfile -t languages < <(find "$root" -type f ! -path '*/generated/*' ! -path '*/.git/*' ! -path '*/node_modules/*' ! -path '*/.ai-session/*' | awk -F. 'NF>1 {print $NF}' | sort -u | awk 'BEGIN{a["cs"]="csharp";a["ts"]="typescript";a["tsx"]="typescript";a["py"]="python";a["ps1"]="powershell";a["sh"]="bash"} a[$1]!=""{print a[$1]}' | sort -u)
solutions=$(find "$root" -type f -name '*.sln' ! -path '*/generated/*' -printf '"%f",' | sed 's/,$//'); tests=$(find "$root" -type f ! -path '*/generated/*' ! -path '*/.git/*' -iname '*test*' -printf '"%f",' | sed 's/,$//'); manager=null; [ -f "$root/pnpm-lock.yaml" ] && manager='"pnpm"'; [ -f "$root/package-lock.json" ] && manager='"npm"'; [ -f "$root/yarn.lock" ] && manager='"yarn"'
mkdir -p "$cache_dir"; printf '{\n "invalidationKey": "%s",\n "head": "%s",\n "languages": [%s],\n "frameworks": [],\n "solutions": [%s],\n "testProjects": [%s],\n "packageManager": %s,\n "buildCommands": {}\n}\n' "$key" "$head" "$(printf '"%s",' "${languages[@]}" | sed 's/,$//')" "$solutions" "$tests" "$manager" > "$cache_path"
if [ "$summary" = true ]; then printf 'Repository context: refreshed | %s languages | %s solutions\n' "${#languages[@]}" "$(find "$root" -type f -name '*.sln' ! -path '*/generated/*' | wc -l)"; else printf '%s\n' "$cache_path"; fi
+8
View File
@@ -0,0 +1,8 @@
[CmdletBinding()]
param([string]$Goal,[string[]]$ChangedFiles=@(),[string[]]$Verified=@(),[string[]]$Pending=@(),[string[]]$ImportantFindings=@(),[switch]$Pointer)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent; $dir = Join-Path $root '.ai-session'; $path = Join-Path $dir 'state.json'
if ($Pointer) { if (Test-Path $path) { Write-Output 'Task state is available in .ai-session/state.json; read it only if needed.' }; exit 0 }
function Limit([string[]]$items) { @($items | Where-Object { $_ } | Select-Object -First 12 | ForEach-Object { if ($_.Length -gt 180) { $_.Substring(0,180) } else { $_ } }) }
New-Item $dir -ItemType Directory -Force | Out-Null
[ordered]@{ goal=if($Goal){$Goal.Substring(0,[Math]::Min(240,$Goal.Length))}else{''}; changedFiles=Limit $ChangedFiles; verified=Limit $Verified; pending=Limit $Pending; importantFindings=Limit $ImportantFindings; updatedAt=(Get-Date).ToUniversalTime().ToString('o') } | ConvertTo-Json -Depth 4 | Set-Content $path -Encoding UTF8
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); dir="$root/.ai-session"; path="$dir/state.json"
if [ "${1:-}" = --pointer ]; then [ -f "$path" ] && printf 'Task state is available in .ai-session/state.json; read it only if needed.\n'; exit 0; fi
goal=${AI_TASK_GOAL:-}; goal=${goal:0:240}; goal_json=$(printf '%s' "$goal" | sed 's/[\\]/\\\\/g; s/"/\\"/g'); mkdir -p "$dir"
printf '{"goal":"%s","changedFiles":[],"verified":[],"pending":[],"importantFindings":[],"updatedAt":"%s"}\n' "$goal_json" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$path"
+8
View File
@@ -0,0 +1,8 @@
[CmdletBinding()]
param([ValidateSet('record','summary','rotate')][string]$Action='record',[string]$Event,[string]$Agent,[string]$Role,[int]$DurationMs,[int]$ToolCalls,[string]$CommandClass,[int64]$Bytes,[int]$Count)
$ErrorActionPreference = 'Stop'; if ($env:AI_CONFIG_TELEMETRY -eq '0') { exit 0 }
$root = Split-Path $PSScriptRoot -Parent; $dir = Join-Path $root '.ai-session'; $path = Join-Path $dir 'telemetry.jsonl'; New-Item $dir -ItemType Directory -Force | Out-Null
if ($Action -eq 'rotate') { if (Test-Path $path) { Move-Item $path ($path + '.1') -Force }; exit 0 }
if ($Action -eq 'summary') { if (-not (Test-Path $path)) { Write-Output 'Telemetry: no events'; exit 0 }; $events=@(Get-Content $path | ForEach-Object { try { $_ | ConvertFrom-Json } catch {} }); $agents=@($events | Where-Object event -eq 'subagent-stop'); Write-Output ('Telemetry: {0} events | {1} subagents' -f $events.Count,$agents.Count); exit 0 }
if ([string]::IsNullOrWhiteSpace($Event)) { throw 'Event is required.' }
$record=[ordered]@{event=$Event;timestamp=(Get-Date).ToUniversalTime().ToString('o')}; foreach($pair in @(@('agent',$Agent),@('role',$Role),@('durationMs',$DurationMs),@('toolCalls',$ToolCalls),@('commandClass',$CommandClass),@('bytes',$Bytes),@('count',$Count))){if($pair[1]){$record[$pair[0]]=$pair[1]}}; ($record|ConvertTo-Json -Compress)|Add-Content $path; if((Get-Item $path).Length -gt 1048576){Move-Item $path ($path+'.1') -Force}
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
[ "${AI_CONFIG_TELEMETRY:-1}" = 0 ] && exit 0
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); dir="$root/.ai-session"; path="$dir/telemetry.jsonl"; mkdir -p "$dir"
case "${1:-record}" in
rotate) [ -f "$path" ] && mv -f "$path" "$path.1"; exit 0;;
summary) [ -f "$path" ] || { printf 'Telemetry: no events\n'; exit 0; }; count=$(wc -l < "$path" | tr -d ' '); agents=$(grep -c '"event":"subagent-stop"' "$path" || true); printf 'Telemetry: %s events | %s subagents\n' "$count" "$agents"; exit 0;;
record) event=${2:?event required};; *) printf 'Unknown action: %s\n' "$1" >&2; exit 1;;
esac
printf '{"event":"%s","timestamp":"%s"}\n' "$event" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$path"; [ "$(wc -c < "$path")" -le 1048576 ] || mv -f "$path" "$path.1"
+2 -2
View File
@@ -1,5 +1,5 @@
[CmdletBinding()] [CmdletBinding()]
param() param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path $PSScriptRoot -Parent
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-build-validation-" + [Guid]::NewGuid()) $work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-build-validation-" + [Guid]::NewGuid())
@@ -48,5 +48,5 @@ Test-BuildFails 'leftover template placeholder' {
} }
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
Write-Output 'PASS build validation: invalid JSON, duplicate agent, duplicate plugin, and leftover placeholders are all rejected' if ($Summary) { Write-Output 'Tests: PASS | build validation' } else { Write-Output 'PASS build validation: invalid JSON, duplicate agent, duplicate plugin, and leftover placeholders are all rejected' }
exit 0 exit 0
+8 -1
View File
@@ -1,5 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--summary) summary=true; shift ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
work=$(mktemp -d) work=$(mktemp -d)
@@ -45,4 +52,4 @@ mutate_leftover_placeholder() {
} }
test_build_fails 'leftover template placeholder' mutate_leftover_placeholder test_build_fails 'leftover template placeholder' mutate_leftover_placeholder
printf 'PASS build validation: invalid JSON, duplicate agent, duplicate plugin, and leftover placeholders are all rejected\n' if [ "$summary" = true ]; then printf 'Tests: PASS | build validation\n'; else printf 'PASS build validation: invalid JSON, duplicate agent, duplicate plugin, and leftover placeholders are all rejected\n'; fi
+24
View File
@@ -0,0 +1,24 @@
[CmdletBinding()]
param([switch]$Summary)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-install-guard-test-" + [Guid]::NewGuid())
try {
$installed = Join-Path $work 'installed'
$missing = Join-Path $work 'missing'
New-Item $installed -ItemType Directory -Force | Out-Null
New-Item (Get-ManagedManifestPath $installed) -ItemType File -Force | Out-Null
if (-not (Test-AnyManifestPresent @($installed, $missing))) { throw 'Expected an installed destination to be detected.' }
if (Test-AnyManifestPresent @($missing)) { throw 'A destination without a manifest must not count as installed.' }
if (Test-AllManifestsPresent @($installed, $missing)) { throw 'Update guard must fail when any selected destination is not installed.' }
if (-not (Test-AllManifestsPresent @($installed))) { throw 'Update guard must pass when every selected destination is installed.' }
if ($Summary) { Write-Output 'Tests: PASS | install guards' } else { Write-Output 'PASS install guards: already-installed and not-installed detection' }
} finally {
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
}
exit 0
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--summary) summary=true; shift ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh"
work=$(mktemp -d)
trap 'rm -rf -- "$work"' EXIT
installed="$work/installed"
missing="$work/missing"
mkdir -p "$installed"
: > "$(manifest_path "$installed")"
any_manifest_present "$installed" "$missing" || { printf 'Expected an installed destination to be detected.\n' >&2; exit 1; }
! any_manifest_present "$missing" || { printf 'A destination without a manifest must not count as installed.\n' >&2; exit 1; }
! all_manifests_present "$installed" "$missing" || { printf 'Update guard must fail when any selected destination is not installed.\n' >&2; exit 1; }
all_manifests_present "$installed" || { printf 'Update guard must pass when every selected destination is installed.\n' >&2; exit 1; }
if [ "$summary" = true ]; then printf 'Tests: PASS | install guards\n'; else printf 'PASS install guards: already-installed and not-installed detection\n'; fi
+6 -2
View File
@@ -1,5 +1,5 @@
[CmdletBinding()] [CmdletBinding()]
param() param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/manifest.ps1') . (Join-Path $PSScriptRoot 'lib/manifest.ps1')
@@ -44,7 +44,11 @@ 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 '; ')" }
Write-Output 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free' $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 { $_ -match '^SYNC .* unchanged' }).Count -ne 1) { throw "Summary mode must print exactly one SYNC tally line: $($outSummary -join '; ')" }
if ($Summary) { Write-Output 'Tests: PASS | managed install' } else { Write-Output 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free' }
} finally { } finally {
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
} }
+18 -1
View File
@@ -1,5 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--summary) summary=true; shift ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/manifest.sh"
@@ -44,4 +51,14 @@ 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; }
printf 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free\n' out_summary=$(sync_managed_destination "$source_dir" "$destination" "summary" "" "" "" false true)
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
exit 1
fi
if [ "$(printf '%s\n' "$out_summary" | grep -c '^SYNC .* unchanged')" -ne 1 ]; then
printf 'Summary mode must print exactly one SYNC tally line: %s\n' "$out_summary" >&2
exit 1
fi
if [ "$summary" = true ]; then printf 'Tests: PASS | managed install\n'; else printf 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free\n'; fi
+30 -5
View File
@@ -1,12 +1,19 @@
$ErrorActionPreference = 'Stop' [CmdletBinding()]
param([switch]$Summary)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path $PSScriptRoot -Parent
$powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw $powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw
$bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw $bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw
if ($powerShellHook -notmatch '\$HoldMs\s*=\s*250' -or $powerShellHook -notmatch '\$FadeMs\s*=\s*250' -or ([regex]::Matches($bashHook, 'default=250').Count -lt 2)) { throw 'Flashbang defaults must total 500 milliseconds on Windows and Linux.' } if ($powerShellHook -notmatch '\$HoldMs\s*=\s*250' -or $powerShellHook -notmatch '\$FadeMs\s*=\s*250' -or ([regex]::Matches($bashHook, 'default=250').Count -lt 2)) { throw 'Flashbang defaults must total 500 milliseconds on Windows and Linux.' }
$statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"' + $root.Replace('\','\\') + '","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' $statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"' + $root.Replace('\','\\') + '","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}'
$branch = & git -C $root branch --show-current $branch = & git -C $root branch --show-current
$statusOutput = $statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1') $buildSummaryOutput = & (Join-Path $root 'scripts/build.ps1') -Summary
if ($statusOutput -ne "Model: Test Model | Effort: high | Repo: TestRepo | Branch: $branch | Max Context: 200000 | Used Context: 9% | Used Tokens: 16700") { throw 'PowerShell status line output is incorrect.' } 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"
$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)"
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.' }
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count $sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count
$sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse).Count $sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse).Count
$ruleSkillCount = @(Import-Csv -LiteralPath (Join-Path $root 'adapters/claude/rule-skills.tsv') -Delimiter ([char]9)).Count $ruleSkillCount = @(Import-Csv -LiteralPath (Join-Path $root 'adapters/claude/rule-skills.tsv') -Delimiter ([char]9)).Count
@@ -34,6 +41,15 @@ foreach ($platform in @('windows','linux')) {
} elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })") { } elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })") {
throw "Claude status line is incorrect in $package" throw "Claude status line is incorrect in $package"
} }
$docFile = if ($client -eq 'codex') { 'AGENTS.md' } else { 'CLAUDE.md' }
$docContent = Get-Content -LiteralPath (Join-Path $package $docFile) -Raw
if (([regex]::Matches($docContent, [regex]::Escape('Apply instructions in this order'))).Count -ne 1) { throw "$client-$platform must embed the priority rule exactly once" }
if ($client -eq 'codex' -and $docContent -match 'Always load and apply `rules/general\.md`') { throw "$package must not still instruct loading general.md by path" }
if ($client -eq 'claude' -and (Test-Path (Join-Path $package 'rules/general.md'))) { throw 'Claude must not automatically load a second copy of general rules.' }
foreach ($splitRule in @('angular','wpf','ui-ux')) {
$referencesDir = if ($client -eq 'codex') { Join-Path $package "rules/$splitRule/references" } else { Join-Path $package "skills/rules/rules-$splitRule/references" }
if (-not (Test-Path $referencesDir) -or (Get-ChildItem $referencesDir -Filter '*.md').Count -eq 0) { throw "$package is missing reference files for $splitRule" }
}
} }
} }
foreach ($platformSelection in @('1','2')) { foreach ($platformSelection in @('1','2')) {
@@ -42,12 +58,21 @@ foreach ($platformSelection in @('1','2')) {
$script:answers.Enqueue($platformSelection) $script:answers.Enqueue($platformSelection)
$script:answers.Enqueue($clientSelection) $script:answers.Enqueue($clientSelection)
function Read-Host { param($Prompt) $answers.Dequeue() } function Read-Host { param($Prompt) $answers.Dequeue() }
$output = & "$PSScriptRoot/install.ps1" -DryRun $output = & "$PSScriptRoot/install.ps1" -DryRun 6>$null
if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' } if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' }
$platform = if ($platformSelection -eq '1') { 'windows' } else { 'linux' } $platform = if ($platformSelection -eq '1') { 'windows' } else { 'linux' }
if (($output -join "`n") -notmatch "-$platform") { throw 'Incorrect selected package.' } if (($output -join "`n") -notmatch "-$platform") { throw 'Incorrect selected package.' }
} }
} }
Write-Output 'PASS four platform packages and six PowerShell selections' $doctorSummaryOutput = & (Join-Path $root 'scripts/doctor.ps1') -Summary
if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' }
if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" }
foreach ($entryPoint in @('install','update')) {
$preview = @(& "$PSScriptRoot/$entryPoint.ps1" -Summary -DryRun -Client Both -Platform Windows)
if ($preview -match '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) ') { throw "$entryPoint summary leaked per-item output." }
if (@($preview | Where-Object { $_ -match ('^' + $entryPoint + ': PASS') }).Count -ne 1) { throw "$entryPoint summary is missing." }
}
$doctorSummaryOutput | Where-Object { $_ -match '^WARN ' }
if ($Summary) { Write-Output 'Tests: PASS | platform packages' } else { Write-Output 'PASS four platform packages and six PowerShell selections' }
exit 0 exit 0
+61 -3
View File
@@ -1,12 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--summary) summary=true; shift ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
grep -Eq '\$HoldMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1" grep -Eq '\$HoldMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1" grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
[ "$(grep -c 'default=250' "$root/shared/hooks/flashbang.sh")" -ge 2 ] [ "$(grep -c 'default=250' "$root/shared/hooks/flashbang.sh")" -ge 2 ]
branch=$(git -C "$root" branch --show-current) branch=$(git -C "$root" branch --show-current)
build_summary_output=$(bash "$root/scripts/build.sh" --summary)
if ! printf '%s\n' "$build_summary_output" | grep -qx 'Build: PASS | 4 packages'; then
printf 'build.sh --summary must still print the PASS line: %s\n' "$build_summary_output" >&2
exit 1
fi
[ "$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")
[ "$status_output" = "Model: Test Model | Effort: high | Repo: TestRepo | Branch: $branch | Max Context: 200000 | Used Context: 9% | Used Tokens: 16700" ] 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)'
[ "$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)
rule_skill_count=$(($(wc -l < "$root/adapters/claude/rule-skills.tsv") - 1)) rule_skill_count=$(($(wc -l < "$root/adapters/claude/rule-skills.tsv") - 1))
@@ -43,16 +58,59 @@ for platform in windows linux; do
grep -q "statusline.$statusline_extension" "$package/$file" grep -q "statusline.$statusline_extension" "$package/$file"
fi fi
if grep -Eq '__HOOK_|__WINDOWS_HOOK_' "$package/$file"; then exit 1; fi if grep -Eq '__HOOK_|__WINDOWS_HOOK_' "$package/$file"; then exit 1; fi
doc_file=AGENTS.md
[ "$client" != claude ] || doc_file=CLAUDE.md
doc_content=$(cat "$package/$doc_file")
occurrences=$(printf '%s' "$doc_content" | grep -o 'Apply instructions in this order' | wc -l)
[ "$occurrences" -eq 1 ] || { printf '%s-%s must embed the priority rule exactly once\n' "$client" "$platform" >&2; exit 1; }
if [ "$client" = codex ] && printf '%s' "$doc_content" | grep -q 'Always load and apply `rules/general\.md`'; then
printf '%s must not still instruct loading general.md by path\n' "$package" >&2
exit 1
fi
if [ "$client" = claude ] && [ -f "$package/rules/general.md" ]; then
printf 'Claude must not automatically load a second copy of general rules.\n' >&2
exit 1
fi
for split_rule in angular wpf ui-ux; do
if [ "$client" = codex ]; then
references_dir="$package/rules/$split_rule/references"
else
references_dir="$package/skills/rules/rules-$split_rule/references"
fi
if [ ! -d "$references_dir" ] || [ -z "$(find "$references_dir" -maxdepth 1 -name '*.md' -print -quit)" ]; then
printf '%s is missing reference files for %s\n' "$package" "$split_rule" >&2
exit 1
fi
done
done done
done done
for platform_selection in 1 2; do for platform_selection in 1 2; do
for client_selection in 1 2 3; do for client_selection in 1 2 3; do
output=$(printf '%s\n%s\n' "$platform_selection" "$client_selection" | bash "$root/scripts/install.sh" --dry-run) output=$(printf '%s\n%s\n' "$platform_selection" "$client_selection" | bash "$root/scripts/install.sh" --dry-run 2>&1) || { printf '%s\n' "$output" >&2; exit 1; }
printf '%s\n' "$output" | grep -E 'WARN|FAIL|WARNING|ERROR' || true
[[ "$output" == *'PASS install dry-run'* ]] [[ "$output" == *'PASS install dry-run'* ]]
platform=windows platform=windows
[ "$platform_selection" != 2 ] || platform=linux [ "$platform_selection" != 2 ] || platform=linux
[[ "$output" == *"-$platform"* ]] [[ "$output" == *"-$platform"* ]]
done done
done done
printf 'PASS four platform packages and six Bash selections\n' doctor_summary_output=$(bash "$root/scripts/doctor.sh" --summary)
if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then
printf 'Doctor summary mode must not print individual PASS lines.\n' >&2
exit 1
fi
if ! printf '%s\n' "$doctor_summary_output" | grep -Eq '^Doctor: PASS \| [0-9]+ checks$'; then
printf "Doctor summary mode must print one 'Doctor: PASS | N checks' line: %s\n" "$doctor_summary_output" >&2
exit 1
fi
for entry_point in install update; do
preview=$(bash "$root/scripts/$entry_point.sh" --summary --dry-run --client both --platform linux)
if printf '%s\n' "$preview" | grep -Eq '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) '; then
printf '%s summary leaked per-item output.\n' "$entry_point" >&2
exit 1
fi
printf '%s\n' "$preview" | grep -Eiq "^$entry_point: PASS"
done
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
+51
View File
@@ -0,0 +1,51 @@
[CmdletBinding()]
param([switch]$Summary)
$ErrorActionPreference = 'Stop'
function Write-Host { param($Object) if (-not $Summary) { Microsoft.PowerShell.Utility\Write-Host $Object } }
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
$script:answers = [System.Collections.Generic.Queue[string]]::new()
$script:answers.Enqueue('1')
$script:answers.Enqueue('2')
$script:answers.Enqueue('done')
function Read-Host { param($Prompt) $answers.Dequeue() }
$result = Read-PluginToggleSelection -Names @('A','B','C') -Checked @($true, $false, $true)
if ($result[0] -ne $false) { throw 'Toggling checked entry 1 must uncheck it.' }
if ($result[1] -ne $true) { throw 'Toggling unchecked entry 2 must check it.' }
if ($result[2] -ne $true) { throw 'Entry 3 was never toggled and must stay unchanged.' }
$script:answers.Enqueue('')
$confirmed = @(Read-PluginToggleSelection -Names @('A') -Checked @($true))
if ($confirmed.Count -ne 1 -or -not $confirmed[0]) { throw 'Enter must confirm without changing the selection.' }
function Test-PluginOutput {
param([string]$Mode)
Write-Output 'download progress'
Write-Output 'WARN test warning'
$global:LASTEXITCODE = if ($Mode -eq 'fail') { 7 } else { 0 }
}
$compact = @(Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('success') -Summary)
if ($compact.Count -ne 1 -or $compact[0] -ne 'WARN test warning') { throw 'Plugin summary must retain warnings and suppress progress.' }
$detailed = @(Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('success'))
if ($detailed.Count -ne 2 -or $detailed[0] -ne 'download progress') { throw 'Detailed output must retain progress.' }
$diagnostics = [System.Collections.Generic.List[string]]::new()
$failed = $false
$env:AI_CONFIG_VERBOSE = '1'
try { Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('fail') -Summary | ForEach-Object { $diagnostics.Add("$_") } }
catch { $failed = $true }
$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.' }
$global:LASTEXITCODE = 0
function Get-InstalledPlugins { [pscustomobject]@{ pluginId = 'test@market' } }
$script:commands = @()
function Invoke-PluginCommand { param($Command, $Arguments, [switch]$DryRun, [switch]$Summary) $script:commands += "$Command $($Arguments -join ' ')" }
$entry = [pscustomobject]@{ name = 'Test'; codex_method = 'plugin'; codex_marketplace = '-'; codex_plugin = 'test@market' }
$updated = @(Install-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Update -Summary -Entries @($entry))
if ($commands.Count -ne 1 -or $commands[0] -ne 'codex plugin add test@market' -or $updated[0] -ne 'PLUGINS Codex: 0 ensured, 0 already installed, 1 updated') { throw 'Updating an installed Codex plugin must report one update.' }
function Get-InstalledPlugins { throw 'Empty selection must not query installed plugins.' }
function Invoke-PluginCommand { throw 'Empty selection must not run plugin commands.' }
Install-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Both -Entries @()
Uninstall-DeselectedPlugins -Client Both -Entries @()
if ($Summary) { Write-Output 'Tests: PASS | plugin selection' } else { Write-Output 'PASS plugin toggle selection: toggle and confirm' }
exit 0
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--summary) summary=true; shift ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/plugins.sh"
names=(A B C)
checked=(true false true)
toggle_log=$(mktemp)
trap 'rm -f -- "$toggle_log"' EXIT
read_plugin_toggle_selection names checked <<< $'1\n2\ndone' > "$toggle_log"
[ "$summary" = true ] || cat "$toggle_log"
[ "${checked[0]}" = false ] || { printf 'Toggling checked entry 1 must uncheck it.\n' >&2; exit 1; }
[ "${checked[1]}" = true ] || { printf 'Toggling unchecked entry 2 must check it.\n' >&2; exit 1; }
[ "${checked[2]}" = true ] || { printf 'Entry 3 was never toggled and must stay unchanged.\n' >&2; exit 1; }
read_plugin_toggle_selection names checked <<< '' > "$toggle_log"
[ "${checked[0]}" = false ] && [ "${checked[1]}" = true ]
test_plugin_output() {
printf 'download progress\nWARN test warning\n'
[ "$1" != fail ]
}
compact=$(summary=true run_plugin_command false test_plugin_output success)
[ "$compact" = 'WARN test warning' ] || { printf 'Plugin summary lost warnings or leaked progress.\n' >&2; exit 1; }
if diagnostics=$(summary=true run_plugin_command false test_plugin_output fail 2>&1); then
printf 'Plugin failure must fail.\n' >&2
exit 1
fi
[[ "$diagnostics" == *'download progress'* ]] && [[ "$diagnostics" == *'WARN test warning'* ]]
selected=()
plugin_installed() { printf 'Empty selection must not query installed plugins.\n' >&2; exit 1; }
run_plugin_command() { printf 'Empty selection must not run commands.\n' >&2; exit 1; }
install_configured_plugins "$root" both false false selected false
uninstall_deselected_plugins "$root" both false selected false
if read_plugin_toggle_selection names checked </dev/null > "$toggle_log" 2>&1; then
printf 'Closed input must fail instead of looping.\n' >&2
exit 1
fi
if [ "$summary" = true ]; then printf 'Tests: PASS | plugin selection\n'; else printf 'PASS plugin toggle selection: toggle and confirm\n'; fi
+43
View File
@@ -0,0 +1,43 @@
[CmdletBinding()]
param(
[switch]$DryRun,
[ValidateSet('Codex','Claude','Both')][string]$Client,
[ValidateSet('Windows','Linux')][string]$Platform,
[switch]$Summary
)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
$Platform = Read-InstallPlatform -Platform $Platform
$platform = $Platform.ToLowerInvariant()
$Client = Read-InstallClient -Client $Client
$homePath = [Environment]::GetFolderPath('UserProfile')
if (-not $DryRun -and -not (Test-AllManifestsPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) {
throw 'Not installed, use the install script.'
}
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
& $buildScript -Summary:$Summary
if (-not $?) { throw 'Build failed. Update was not started.' }
$generated = Join-Path $root 'generated'
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File'
$windowsShellCommand = 'powershell -NoProfile -ExecutionPolicy Bypass -File'
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { $shellCommand = $windowsShellCommand }
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Platform $platform -Client $Client)) {
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun -Summary:$Summary
}
$plugins = Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Update' -DryRun:$DryRun
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
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
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
dry_run=false
client=""
platform=""
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) dry_run=true; shift ;;
--summary) summary=true; shift ;;
--client) client=${2:?--client requires a value}; shift 2 ;;
--platform) platform=${2:?--platform requires a value}; shift 2 ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh"
platform=$(read_install_platform "$platform")
client=$(read_install_client "$client")
home_path=${HOME:?HOME is required}
if [ "$dry_run" = false ]; then
mapfile -t destinations < <(get_install_destinations "$home_path" "$client")
if ! all_manifests_present "${destinations[@]}"; then
printf 'Not installed, use the install script.\n' >&2
exit 1
fi
fi
build_args=()
[ "$summary" = false ] || build_args+=(--summary)
bash "$root/scripts/build.sh" "${build_args[@]}"
generated="$root/generated"
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
stamp=$(date +%Y%m%d-%H%M%S)
shell_command=bash
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
while IFS='|' read -r source destination; do
ai_config_root=$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"
done < <(get_install_targets "$generated" "$home_path" "$platform" "$client")
selected_plugins=()
deselected_plugins=()
select_configured_plugins "$root" "$client" Update "$dry_run" selected_plugins deselected_plugins
install_configured_plugins "$root" "$client" "$dry_run" true selected_plugins "$summary"
uninstall_deselected_plugins "$root" "$client" "$dry_run" deselected_plugins "$summary"
if [ "$summary" = true ]; then
suffix=
[ "$dry_run" = false ] || suffix=', dry-run'
printf 'Update: PASS | %s, %s%s\n' "$client" "$platform" "$suffix"
elif "$dry_run"; then printf 'PASS update dry-run\n'; else printf 'PASS update\n'; fi
+11
View File
@@ -58,12 +58,23 @@ def validate_claude(path: Path) -> None:
fail(f"hooks.{event} contains an unsupported command hook") fail(f"hooks.{event} contains an unsupported command hook")
def validate_rule_skills(package: Path) -> None:
for path in (package / "skills" / "rules").glob("*/SKILL.md"):
lines = path.read_text(encoding="utf-8-sig").splitlines()
if len(lines) < 4 or lines[0] != "---" or lines[3] != "---" or not lines[1].startswith("name: ") or not lines[2].startswith("description: "):
fail(f"{path}: invalid generated skill frontmatter")
description = json.loads(lines[2].removeprefix("description: "))
if not isinstance(description, str) or not description.strip():
fail(f"{path}: skill description must be a quoted, nonempty string")
def main() -> int: def main() -> int:
if len(sys.argv) != 3: if len(sys.argv) != 3:
print("usage: validate-config.py <codex-toml> <claude-json>", file=sys.stderr) print("usage: validate-config.py <codex-toml> <claude-json>", file=sys.stderr)
return 2 return 2
tomllib.loads(Path(sys.argv[1]).read_text(encoding="utf-8-sig")) tomllib.loads(Path(sys.argv[1]).read_text(encoding="utf-8-sig"))
validate_claude(Path(sys.argv[2])) validate_claude(Path(sys.argv[2]))
validate_rule_skills(Path(sys.argv[2]).parent)
return 0 return 0
+1 -9
View File
@@ -4,12 +4,4 @@ This instruction set is generated into the client-specific `AGENTS.md` and `CLAU
__RULE_LOADING__ __RULE_LOADING__
Apply instructions in this order: direct current user instructions; security and data-loss protections; applicable project instructions; these global rules; existing project conventions; agent preferences. Delegate only when the expected improvement in correctness, independence, specialization, or parallelism outweighs the duplicated context and tool cost. Do not delegate simple repository exploration the main agent can do directly. Do not create a researcher for information the main agent can obtain cheaply itself. Use one implementer for small and medium contained tasks. Run reviewer and verifier together only when their responsibilities are materially different for the current task. Avoid multiple agents independently reading the same large set of files. Use parallel agents only for genuinely independent work. Do not let subagents create further subagents.
Make evidence-based, reversible technical decisions independently. Ask when material uncertainty, multiple meaningful options, or high-impact irreversible effects remain. Ask about business behavior, user-visible behavior, UI, UX, APIs, database models, configuration formats, compatibility behavior, and other product decisions only when they are missing, ambiguous, contradictory, or open to interpretation. Implement explicitly specified behavior without asking again.
Never modify existing documentation automatically. Ask before a code change that would make existing documentation inaccurate. Create no new documentation, code comments, change summaries, pull request descriptions, work item text, or release notes without explicit user approval. If a comment appears necessary, ask first.
Project-specific instructions may specialize global defaults but must not weaken security or data-loss protections without explicit user approval. Direct user instructions have the highest priority for requested behavior.
Use one implementer for small, contained tasks. Add researcher only for material uncertainty, architect only for architecture or boundary decisions, reviewer only for independent quality review, and verifier only for explicit acceptance criteria, tests, or generated artifacts. Do not start reviewer and verifier by default or let subagents create further subagents. Parallelize only genuinely independent work.
+7
View File
@@ -0,0 +1,7 @@
[CmdletBinding()]
param()
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$stateDir = Join-Path $root '.ai-session'
New-Item $stateDir -ItemType Directory -Force | Out-Null
('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content (Join-Path $stateDir 'telemetry.jsonl')
exit 0
@@ -0,0 +1,4 @@
[CmdletBinding()]
param()
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
if (Test-Path (Join-Path $root '.ai-session/state.json')) { Write-Output 'Task state is available in .ai-session/state.json; read it only if needed.' }
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
mkdir -p "$root/.ai-session"
printf '{"event":"compact","timestamp":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$root/.ai-session/telemetry.jsonl"
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
[ -f "$root/.ai-session/state.json" ] && printf 'Task state is available in .ai-session/state.json; read it only if needed.\n'
-297
View File
@@ -1,297 +0,0 @@
# Angular Code Guidelines
* Use modern Angular patterns and current framework conventions.
* Prefer standalone components, directives and pipes for new code.
* Avoid introducing `NgModule` unless the existing project architecture requires it.
* Keep components small and focused.
* Keep templates simple.
* Move complex logic out of templates.
* Keep business logic out of components where practical.
* Prefer services, facades or dedicated domain logic for non-trivial behavior.
* Avoid god components and god services.
* Keep dependencies explicit.
* Prefer composition over inheritance.
* Avoid unnecessary abstractions.
* Avoid unnecessary wrapper components.
* Follow the existing project architecture unless there is a strong reason to change it.
## Components
* Use `ChangeDetectionStrategy.OnPush` unless there is a concrete reason not to.
* Prefer signal-based state for local component state.
* Prefer `computed` for derived state.
* Prefer `effect` only for actual side effects.
* Do not use `effect` as a replacement for normal data flow.
* Keep component state minimal.
* Avoid duplicating derived state.
* Prefer immutable state updates.
* Avoid direct mutation of shared state.
* Keep inputs and outputs strongly typed.
* Prefer signal-based inputs and outputs when consistent with the project.
* Avoid unnecessary two-way binding.
* Avoid unnecessary component-to-component coupling.
* Prefer explicit data flow from parent to child and explicit events upward.
* Do not expose internal component state unnecessarily.
* Use lifecycle hooks only when required.
* Avoid complex initialization logic inside constructors.
## Templates
* Keep expressions short and readable.
* Do not execute expensive methods from templates.
* Avoid function calls in templates when their result can be derived or cached.
* Avoid complex conditions directly in markup.
* Prefer derived state in TypeScript over repeated template expressions.
* Use Angular control flow syntax such as `@if`, `@for` and `@switch` for new code.
* Always provide stable tracking for repeated collections.
* Avoid deeply nested template structures.
* Split large templates into focused components when it improves maintainability.
* Avoid unnecessary DOM elements.
* Use semantic HTML.
* Prefer native HTML behavior before custom implementations.
* Keep accessibility in mind when choosing elements and interactions.
## Signals and State
* Prefer signals for local and synchronous reactive state.
* Prefer `computed` for derived values.
* Keep signals private when external mutation is not intended.
* Expose readonly state where practical.
* Avoid storing values that can be derived from existing state.
* Avoid circular signal dependencies.
* Avoid side effects inside `computed`.
* Keep effects small and deterministic.
* Clean up resources created by effects.
* Do not use global state unless multiple unrelated parts of the application genuinely need it.
* Do not introduce a state-management library without a clear requirement.
## RxJS
* Use RxJS for asynchronous streams, events and complex reactive flows.
* Do not use RxJS when simple synchronous signals are sufficient.
* Prefer declarative observable pipelines.
* Avoid nested `subscribe`.
* Avoid manual subscriptions where `async`, signals or framework helpers are sufficient.
* Prefer `takeUntilDestroyed` or equivalent Angular lifecycle integration for manual subscriptions.
* Always consider subscription lifetime.
* Avoid memory leaks from unmanaged subscriptions.
* Use the correct flattening operator for the required semantics.
* Do not use `switchMap`, `mergeMap`, `concatMap` or `exhaustMap` interchangeably.
* Avoid unnecessary `Subject` usage.
* Prefer exposing observables instead of exposing mutable subjects.
* Prefer `BehaviorSubject` only when a current value is actually required.
* Avoid excessive observable chains for simple state.
* Avoid converting repeatedly between signals and observables without need.
## Dependency Injection
* Prefer Angular dependency injection for application services.
* Keep injected dependencies minimal.
* Avoid service locator patterns.
* Prefer `inject()` where it improves readability and is consistent with the project.
* Do not inject services that are not used.
* Scope services intentionally.
* Prefer application-wide singletons only for truly shared stateless or coordinated state.
* Avoid storing request-specific or component-specific mutable state in root services.
* Use injection tokens for configuration or abstractions where appropriate.
* Do not create interfaces only to satisfy dependency injection when no abstraction is needed.
## Services
* Keep services focused on a clear responsibility.
* Avoid large catch-all services.
* Separate API access, domain logic and UI state where practical.
* Avoid leaking transport models throughout the application.
* Do not expose mutable internal state directly.
* Prefer readonly APIs for state consumers.
* Avoid hidden side effects.
* Keep service methods predictable.
* Do not use services merely to move poorly structured component code elsewhere.
## HTTP and APIs
* Keep API access centralized and strongly typed.
* Prefer dedicated API clients or data-access services.
* Do not use `any` for API responses.
* Treat all server responses as untrusted at runtime.
* Validate external data when the contract is security- or correctness-critical.
* Keep DTOs separate from domain models when their responsibilities differ.
* Avoid duplicate API calls.
* Cache only when the invalidation strategy is clear.
* Avoid manual URL string construction when typed helpers or centralized endpoints are available.
* Use interceptors only for cross-cutting concerns.
* Do not put unrelated business logic into HTTP interceptors.
* Handle cancellation where appropriate.
* Handle expected error states explicitly.
## Forms
* Prefer reactive forms for non-trivial forms.
* Keep forms strongly typed.
* Avoid untyped forms.
* Keep validation rules centralized and reusable where practical.
* Validate on both client and server where required.
* Never treat client-side validation as a security boundary.
* Keep form state separate from domain state when useful.
* Avoid large components that combine layout, validation, persistence and business logic.
* Display validation errors consistently.
* Preserve user input when recoverable errors occur.
## Routing
* Keep route definitions explicit and predictable.
* Prefer lazy loading for larger feature areas.
* Avoid loading large feature bundles eagerly without need.
* Use route guards only for navigation behavior, not as a security boundary.
* Authorization must still be enforced on the backend.
* Keep resolvers focused.
* Avoid expensive work during navigation unless required.
* Keep route parameters strongly typed at application boundaries where practical.
* Do not rely on client-side routing state for sensitive authorization decisions.
## Performance
* Use `OnPush`.
* Use stable tracking in loops.
* Avoid unnecessary change detection triggers.
* Avoid repeated expensive template calculations.
* Avoid unnecessary subscriptions.
* Avoid unnecessary DOM rendering.
* Lazy-load large features and heavy dependencies where appropriate.
* Avoid large initial bundles.
* Avoid importing full libraries when only a small part is required.
* Avoid unnecessary deep cloning.
* Avoid unnecessary object and array recreation in hot paths.
* Keep state updates targeted.
* Avoid unnecessary global state updates.
* Measure bundle size and runtime performance before micro-optimizing.
* Prefer architectural improvements over template-level micro-optimizations.
## Security
* Treat all external data as untrusted.
* Never bypass Angular sanitization without a verified reason.
* Avoid direct DOM manipulation.
* Avoid `innerHTML` with untrusted content.
* Avoid `bypassSecurityTrust*` unless explicitly required and the input is fully controlled.
* Never expose secrets in frontend code.
* Never store sensitive credentials in the Angular application.
* Do not rely on frontend checks for authorization.
* Enforce authentication and authorization on the backend.
* Avoid leaking sensitive information through logs or error messages.
* Use secure cookie or token handling according to the application's authentication architecture.
* Avoid storing sensitive tokens in insecure browser storage without a deliberate security decision.
* Do not disable security controls for convenience.
## DOM and Browser APIs
* Prefer Angular APIs and declarative templates over direct DOM manipulation.
* Use `Renderer2` only when direct rendering abstraction is actually needed.
* Avoid accessing globals directly when it harms testability or SSR compatibility.
* Consider SSR and hydration compatibility for browser-specific code.
* Guard access to `window`, `document`, `localStorage` and similar browser-only APIs when SSR is supported.
* Clean up event listeners, observers and timers.
* Avoid layout thrashing in performance-sensitive code.
## Accessibility
* Use semantic HTML.
* Preserve keyboard navigation.
* Ensure interactive elements are actually interactive elements.
* Do not replace buttons with clickable `div` elements.
* Provide labels for form controls.
* Maintain visible focus behavior.
* Use ARIA only when native HTML semantics are insufficient.
* Avoid unnecessary custom widgets.
* Keep accessible names and states synchronized with UI state.
## Styling
* Keep styles scoped and predictable.
* Avoid unnecessary global CSS.
* Follow the project's styling strategy consistently.
* Avoid excessive specificity.
* Avoid `!important` unless there is a concrete reason.
* Prefer reusable design tokens and shared styles over duplicated magic values.
* Keep component styles focused on component concerns.
* Avoid styling based on fragile DOM structure.
* Do not mix multiple styling approaches without need.
## Testing
* Test behavior, not implementation details.
* Prefer focused component tests.
* Test critical user interactions.
* Test services and domain logic independently where useful.
* Avoid brittle tests coupled to internal structure.
* Avoid excessive mocking.
* Mock external boundaries rather than internal implementation details.
* Keep tests deterministic.
* Avoid time-dependent tests without controlled clocks.
* Avoid network-dependent tests without proper isolation.
* Keep test setup minimal and readable.
* Update tests when refactoring changes structure but not behavior.
## Architecture
* Keep UI, application, domain and data-access concerns separated where practical.
* Organize code by feature rather than by technical file type when it improves cohesion.
* Avoid circular dependencies.
* Avoid cross-feature imports without a clear contract.
* Keep shared code genuinely shared.
* Do not move feature-specific logic into generic shared modules.
* Avoid dumping unrelated helpers into global utility files.
* Keep public APIs of feature areas small.
* Avoid exposing internal implementation details across feature boundaries.
* Prefer local ownership of state and logic.
## Code Quality
* Use strict TypeScript settings.
* Avoid `any`.
* Avoid unnecessary type assertions.
* Avoid non-null assertions unless safety is guaranteed.
* Prefer modern, readable syntax.
* Use descriptive names.
* Avoid vague names such as `data`, `item`, `value` or `temp` when better names exist.
* Remove dead code.
* Remove unused imports and dependencies.
* Remove commented-out code instead of keeping it in source files.
* Do not add comments unless the user explicitly requests them.
* Treat Angular, TypeScript and lint warnings seriously.
* Do not suppress diagnostics without a concrete reason.
* Prefer correctness, security and maintainability over cleverness.
## Dependency Rules
* Prefer Angular and browser platform capabilities before adding third-party libraries.
* Do not add dependencies without a clear benefit.
* Avoid libraries that duplicate existing framework functionality.
* Avoid abandoned or poorly maintained packages.
* Keep Angular package versions aligned.
* Do not mix incompatible Angular package versions.
* Respect the existing lockfile and package manager.
* Avoid unrelated dependency upgrades.
## Performance Priority
Prioritize Angular performance work in this order:
* Application architecture
* Network and API behavior
* Bundle size and lazy loading
* State architecture
* Change detection
* Rendering and DOM complexity
* RxJS and subscription behavior
* Memory allocations
* CPU micro-optimizations
Never optimize based purely on assumptions.
## Safety Rule
* Angular frontend code is never a security boundary.
* Client-side validation, guards, hidden controls and disabled UI elements do not replace backend enforcement.
* Do not bypass Angular's built-in security mechanisms without a verified and explicit reason.
* Prefer safe framework defaults over custom low-level behavior.
+38
View File
@@ -0,0 +1,38 @@
# Angular Code Guidelines
* Use modern Angular patterns and current framework conventions.
* Prefer standalone components, directives and pipes for new code.
* Avoid introducing `NgModule` unless the existing project architecture requires it.
* Keep components small and focused.
* Keep templates simple.
* Move complex logic out of templates.
* Keep business logic out of components where practical.
* Prefer services, facades or dedicated domain logic for non-trivial behavior.
* Avoid god components and god services.
* Keep dependencies explicit.
* Prefer composition over inheritance.
* Avoid unnecessary abstractions.
* Avoid unnecessary wrapper components.
* Follow the existing project architecture unless there is a strong reason to change it.
## Safety Rule
* Angular frontend code is never a security boundary.
* Client-side validation, guards, hidden controls and disabled UI elements do not replace backend enforcement.
* Do not bypass Angular's built-in security mechanisms without a verified and explicit reason.
* Prefer safe framework defaults over custom low-level behavior.
## Detailed guidance
Read the matching reference file before working in that area:
| Read this reference for... | ...this kind of work |
| --- | --- |
| `references/components-and-state.md` | Component structure, signal-based state |
| `references/templates-and-styling.md` | Template markup, CSS/styling |
| `references/rxjs.md` | RxJS observables and subscriptions |
| `references/services-and-http.md` | Dependency injection, services, HTTP/API access |
| `references/forms-and-routing.md` | Reactive forms, routing |
| `references/security-and-accessibility.md` | Security, DOM/browser APIs, accessibility |
| `references/performance.md` | Performance work and its priority order |
| `references/testing-and-architecture.md` | Testing, architecture, code quality, dependency rules |
@@ -0,0 +1,33 @@
## Components
* Use `ChangeDetectionStrategy.OnPush` unless there is a concrete reason not to.
* Prefer signal-based state for local component state.
* Prefer `computed` for derived state.
* Prefer `effect` only for actual side effects.
* Do not use `effect` as a replacement for normal data flow.
* Keep component state minimal.
* Avoid duplicating derived state.
* Prefer immutable state updates.
* Avoid direct mutation of shared state.
* Keep inputs and outputs strongly typed.
* Prefer signal-based inputs and outputs when consistent with the project.
* Avoid unnecessary two-way binding.
* Avoid unnecessary component-to-component coupling.
* Prefer explicit data flow from parent to child and explicit events upward.
* Do not expose internal component state unnecessarily.
* Use lifecycle hooks only when required.
* Avoid complex initialization logic inside constructors.
## Signals and State
* Prefer signals for local and synchronous reactive state.
* Prefer `computed` for derived values.
* Keep signals private when external mutation is not intended.
* Expose readonly state where practical.
* Avoid storing values that can be derived from existing state.
* Avoid circular signal dependencies.
* Avoid side effects inside `computed`.
* Keep effects small and deterministic.
* Clean up resources created by effects.
* Do not use global state unless multiple unrelated parts of the application genuinely need it.
* Do not introduce a state-management library without a clear requirement.
@@ -0,0 +1,24 @@
## Forms
* Prefer reactive forms for non-trivial forms.
* Keep forms strongly typed.
* Avoid untyped forms.
* Keep validation rules centralized and reusable where practical.
* Validate on both client and server where required.
* Never treat client-side validation as a security boundary.
* Keep form state separate from domain state when useful.
* Avoid large components that combine layout, validation, persistence and business logic.
* Display validation errors consistently.
* Preserve user input when recoverable errors occur.
## Routing
* Keep route definitions explicit and predictable.
* Prefer lazy loading for larger feature areas.
* Avoid loading large feature bundles eagerly without need.
* Use route guards only for navigation behavior, not as a security boundary.
* Authorization must still be enforced on the backend.
* Keep resolvers focused.
* Avoid expensive work during navigation unless required.
* Keep route parameters strongly typed at application boundaries where practical.
* Do not rely on client-side routing state for sensitive authorization decisions.
@@ -0,0 +1,33 @@
## Performance
* Use `OnPush`.
* Use stable tracking in loops.
* Avoid unnecessary change detection triggers.
* Avoid repeated expensive template calculations.
* Avoid unnecessary subscriptions.
* Avoid unnecessary DOM rendering.
* Lazy-load large features and heavy dependencies where appropriate.
* Avoid large initial bundles.
* Avoid importing full libraries when only a small part is required.
* Avoid unnecessary deep cloning.
* Avoid unnecessary object and array recreation in hot paths.
* Keep state updates targeted.
* Avoid unnecessary global state updates.
* Measure bundle size and runtime performance before micro-optimizing.
* Prefer architectural improvements over template-level micro-optimizations.
## Performance Priority
Prioritize Angular performance work in this order:
* Application architecture
* Network and API behavior
* Bundle size and lazy loading
* State architecture
* Change detection
* Rendering and DOM complexity
* RxJS and subscription behavior
* Memory allocations
* CPU micro-optimizations
Never optimize based purely on assumptions.
+17
View File
@@ -0,0 +1,17 @@
## RxJS
* Use RxJS for asynchronous streams, events and complex reactive flows.
* Do not use RxJS when simple synchronous signals are sufficient.
* Prefer declarative observable pipelines.
* Avoid nested `subscribe`.
* Avoid manual subscriptions where `async`, signals or framework helpers are sufficient.
* Prefer `takeUntilDestroyed` or equivalent Angular lifecycle integration for manual subscriptions.
* Always consider subscription lifetime.
* Avoid memory leaks from unmanaged subscriptions.
* Use the correct flattening operator for the required semantics.
* Do not use `switchMap`, `mergeMap`, `concatMap` or `exhaustMap` interchangeably.
* Avoid unnecessary `Subject` usage.
* Prefer exposing observables instead of exposing mutable subjects.
* Prefer `BehaviorSubject` only when a current value is actually required.
* Avoid excessive observable chains for simple state.
* Avoid converting repeatedly between signals and observables without need.
@@ -0,0 +1,37 @@
## Security
* Treat all external data as untrusted.
* Never bypass Angular sanitization without a verified reason.
* Avoid direct DOM manipulation.
* Avoid `innerHTML` with untrusted content.
* Avoid `bypassSecurityTrust*` unless explicitly required and the input is fully controlled.
* Never expose secrets in frontend code.
* Never store sensitive credentials in the Angular application.
* Do not rely on frontend checks for authorization.
* Enforce authentication and authorization on the backend.
* Avoid leaking sensitive information through logs or error messages.
* Use secure cookie or token handling according to the application's authentication architecture.
* Avoid storing sensitive tokens in insecure browser storage without a deliberate security decision.
* Do not disable security controls for convenience.
## DOM and Browser APIs
* Prefer Angular APIs and declarative templates over direct DOM manipulation.
* Use `Renderer2` only when direct rendering abstraction is actually needed.
* Avoid accessing globals directly when it harms testability or SSR compatibility.
* Consider SSR and hydration compatibility for browser-specific code.
* Guard access to `window`, `document`, `localStorage` and similar browser-only APIs when SSR is supported.
* Clean up event listeners, observers and timers.
* Avoid layout thrashing in performance-sensitive code.
## Accessibility
* Use semantic HTML.
* Preserve keyboard navigation.
* Ensure interactive elements are actually interactive elements.
* Do not replace buttons with clickable `div` elements.
* Provide labels for form controls.
* Maintain visible focus behavior.
* Use ARIA only when native HTML semantics are insufficient.
* Avoid unnecessary custom widgets.
* Keep accessible names and states synchronized with UI state.
@@ -0,0 +1,40 @@
## Dependency Injection
* Prefer Angular dependency injection for application services.
* Keep injected dependencies minimal.
* Avoid service locator patterns.
* Prefer `inject()` where it improves readability and is consistent with the project.
* Do not inject services that are not used.
* Scope services intentionally.
* Prefer application-wide singletons only for truly shared stateless or coordinated state.
* Avoid storing request-specific or component-specific mutable state in root services.
* Use injection tokens for configuration or abstractions where appropriate.
* Do not create interfaces only to satisfy dependency injection when no abstraction is needed.
## Services
* Keep services focused on a clear responsibility.
* Avoid large catch-all services.
* Separate API access, domain logic and UI state where practical.
* Avoid leaking transport models throughout the application.
* Do not expose mutable internal state directly.
* Prefer readonly APIs for state consumers.
* Avoid hidden side effects.
* Keep service methods predictable.
* Do not use services merely to move poorly structured component code elsewhere.
## HTTP and APIs
* Keep API access centralized and strongly typed.
* Prefer dedicated API clients or data-access services.
* Do not use `any` for API responses.
* Treat all server responses as untrusted at runtime.
* Validate external data when the contract is security- or correctness-critical.
* Keep DTOs separate from domain models when their responsibilities differ.
* Avoid duplicate API calls.
* Cache only when the invalidation strategy is clear.
* Avoid manual URL string construction when typed helpers or centralized endpoints are available.
* Use interceptors only for cross-cutting concerns.
* Do not put unrelated business logic into HTTP interceptors.
* Handle cancellation where appropriate.
* Handle expected error states explicitly.
@@ -0,0 +1,27 @@
## Templates
* Keep expressions short and readable.
* Do not execute expensive methods from templates.
* Avoid function calls in templates when their result can be derived or cached.
* Avoid complex conditions directly in markup.
* Prefer derived state in TypeScript over repeated template expressions.
* Use Angular control flow syntax such as `@if`, `@for` and `@switch` for new code.
* Always provide stable tracking for repeated collections.
* Avoid deeply nested template structures.
* Split large templates into focused components when it improves maintainability.
* Avoid unnecessary DOM elements.
* Use semantic HTML.
* Prefer native HTML behavior before custom implementations.
* Keep accessibility in mind when choosing elements and interactions.
## Styling
* Keep styles scoped and predictable.
* Avoid unnecessary global CSS.
* Follow the project's styling strategy consistently.
* Avoid excessive specificity.
* Avoid `!important` unless there is a concrete reason.
* Prefer reusable design tokens and shared styles over duplicated magic values.
* Keep component styles focused on component concerns.
* Avoid styling based on fragile DOM structure.
* Do not mix multiple styling approaches without need.
@@ -0,0 +1,55 @@
## Testing
* Test behavior, not implementation details.
* Prefer focused component tests.
* Test critical user interactions.
* Test services and domain logic independently where useful.
* Avoid brittle tests coupled to internal structure.
* Avoid excessive mocking.
* Mock external boundaries rather than internal implementation details.
* Keep tests deterministic.
* Avoid time-dependent tests without controlled clocks.
* Avoid network-dependent tests without proper isolation.
* Keep test setup minimal and readable.
* Update tests when refactoring changes structure but not behavior.
## Architecture
* Keep UI, application, domain and data-access concerns separated where practical.
* Organize code by feature rather than by technical file type when it improves cohesion.
* Avoid circular dependencies.
* Avoid cross-feature imports without a clear contract.
* Keep shared code genuinely shared.
* Do not move feature-specific logic into generic shared modules.
* Avoid dumping unrelated helpers into global utility files.
* Keep public APIs of feature areas small.
* Avoid exposing internal implementation details across feature boundaries.
* Prefer local ownership of state and logic.
## Code Quality
* Use strict TypeScript settings.
* Avoid `any`.
* Avoid unnecessary type assertions.
* Avoid non-null assertions unless safety is guaranteed.
* Prefer modern, readable syntax.
* Use descriptive names.
* Avoid vague names such as `data`, `item`, `value` or `temp` when better names exist.
* Remove dead code.
* Remove unused imports and dependencies.
* Remove commented-out code instead of keeping it in source files.
* Do not add comments unless the user explicitly requests them.
* Treat Angular, TypeScript and lint warnings seriously.
* Do not suppress diagnostics without a concrete reason.
* Prefer correctness, security and maintainability over cleverness.
## Dependency Rules
* Prefer Angular and browser platform capabilities before adding third-party libraries.
* Do not add dependencies without a clear benefit.
* Avoid libraries that duplicate existing framework functionality.
* Avoid abandoned or poorly maintained packages.
* Keep Angular package versions aligned.
* Do not mix incompatible Angular package versions.
* Respect the existing lockfile and package manager.
* Avoid unrelated dependency upgrades.
+1 -2
View File
@@ -16,8 +16,7 @@ A task is only considered done when all applicable requirements below are fulfil
* 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.
* Error handling and edge cases are handled appropriately. * Error handling and edge cases are handled appropriately.
* The implementation is readable, maintainable, and production-ready. * The implementation is readable, maintainable, and production-ready.
* Existing documentation was not changed automatically. If the code makes it inaccurate, the user was asked before proceeding. * Documentation and comments follow `general.md`'s Documentation and Comments rules (not changed automatically, none created without explicit instruction).
* No new documentation, repository comments, pull request text, or work-item text was created without explicit user instruction.
* Any delegated research, review, or verification is complete and its relevant findings are resolved or reported. * Any delegated research, review, or verification is complete and its relevant findings are resolved or reported.
## Work Items ## Work Items
+12 -2
View File
@@ -13,19 +13,29 @@ Apply instructions in this order:
Project instructions may specialize global defaults, but must not weaken security or data-loss protections without explicit user approval. Project instructions may specialize global defaults, but must not weaken security or data-loss protections without explicit user approval.
## Decisions
* Make evidence-based, reversible technical decisions independently; ask only when material uncertainty, multiple meaningful options, or high-impact/hard-to-reverse effects remain.
* 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).
## Documentation ## Documentation
* Never modify existing documentation automatically. * Never modify existing documentation automatically.
* If a code change would make existing documentation inaccurate or outdated, ask the user before changing the code or documentation.
* Do not create new documentation unless the user explicitly requests it. * Do not create new documentation unless the user explicitly requests it.
* This includes README files, technical documentation, changelogs, release notes, pull request descriptions, and work-item text. * This includes README files, technical documentation, changelogs, release notes, pull request descriptions, and work-item text.
* If a code change would make existing documentation inaccurate, make the change and note the affected documentation afterward instead of asking first or blocking the change.
## Comments ## Comments
* Do not add new code comments without the user's explicit approval. * Do not add new code comments without the user's explicit approval.
* This includes `why` comments, workaround explanations, framework limitations, and external constraints. * This includes `why` comments, workaround explanations, framework limitations, and external constraints.
* If a comment appears technically useful or necessary, ask the user before adding it.
* Prefer self-explanatory code and clear names. * Prefer self-explanatory code and clear names.
* Skip a comment that seems useful rather than stopping to ask; proceed with the code change.
## Tool output
* Prefer a script's documented summary mode for routine agent runs; keep warnings and failure diagnostics visible. Use detailed output when investigating a failure or when requested.
## Language ## Language
-306
View File
@@ -1,306 +0,0 @@
# UI & UX Guidelines
* Design for clarity first.
* Prefer simple, modern and predictable interfaces.
* Optimize for fast understanding and low cognitive load.
* Every screen should have a clear primary purpose.
* Every important action should be easy to discover.
* Prefer familiar interaction patterns over novel ones.
* Do not require users to learn unnecessary custom behavior.
* Keep layouts visually calm and structured.
* Use consistent spacing, typography, colors and interaction patterns.
* Avoid visual noise.
* Avoid decorative elements that do not improve usability.
* Avoid unnecessary complexity in navigation and workflows.
* Minimize the number of steps required to complete common tasks.
* Keep related information and actions close together.
* Group content logically.
* Use clear visual hierarchy.
* Make primary actions visually distinct from secondary actions.
* Avoid giving multiple actions equal visual priority when one is clearly more important.
* Use whitespace intentionally.
* Prefer readable line lengths and sufficient spacing.
* Avoid dense walls of content.
* Avoid oversized empty areas that reduce information efficiency.
* Keep important information visible without unnecessary scrolling where practical.
## Navigation
* Navigation must be predictable and consistent.
* Users should always understand where they are.
* Users should always understand how to go back.
* Avoid hidden navigation when visible navigation is practical.
* Keep navigation depth shallow where possible.
* Use clear and descriptive navigation labels.
* Do not use ambiguous icons without labels when meaning is not obvious.
* Preserve navigation state when users return to a previous view where practical.
* Avoid resetting filters, search state or scroll position unnecessarily.
* Avoid unexpected redirects.
* Do not change navigation behavior between similar screens without a clear reason.
## Actions
* Use clear action labels.
* Prefer verbs that describe the actual outcome.
* Avoid vague labels such as `OK`, `Submit`, `Proceed` or `Execute` when a more specific label is possible.
* Make destructive actions clearly distinguishable.
* Keep dangerous actions away from common actions.
* Require confirmation for destructive or difficult-to-reverse actions.
* Do not require unnecessary confirmation for safe, reversible actions.
* Disable unavailable actions when the reason is obvious.
* Explain why an action is unavailable when it is not obvious.
* Avoid actions that appear clickable but are not.
* Provide immediate visual feedback after user actions.
* Prevent accidental duplicate submissions.
* Make loading, success and failure states visible.
## Forms
* Keep forms as short as practical.
* Ask only for information that is actually required.
* Group related fields together.
* Use clear field labels.
* Do not rely on placeholders as the only label.
* Use appropriate input controls for the expected value.
* Provide useful defaults where safe and predictable.
* Preserve user input when validation or network errors occur.
* Validate fields at useful moments without interrupting normal input.
* Show validation errors close to the affected field.
* Explain how to fix invalid input.
* Avoid technical validation messages.
* Clearly mark optional fields where useful.
* Avoid asking the same information multiple times.
* Use autocomplete and autofill where appropriate.
* Make keyboard navigation logical.
* Use sensible tab order.
* Support Enter and Escape behavior where users expect it.
## Content and Language
* Use plain, concise language.
* Prefer user terminology over internal technical terminology.
* Avoid jargon unless the target users are expected to understand it.
* Use consistent naming for the same concepts.
* Do not rename concepts between screens.
* Keep button labels, headings and descriptions concise.
* Put the most important information first.
* Avoid unnecessary explanatory text.
* Add explanation only where users may reasonably be confused.
* Error messages should explain what happened and what the user can do next.
* Avoid blaming the user.
* Avoid exposing stack traces, exception names or internal implementation details.
## Feedback and System State
* Always communicate important system state.
* Show loading states for operations that are not immediate.
* Avoid indefinite loading indicators without context.
* Use progress indicators for longer operations where progress can be measured.
* Clearly distinguish loading, empty, error and success states.
* Do not leave blank screens where an empty state is expected.
* Empty states should explain what the user can do next.
* Show success feedback when the result is not otherwise obvious.
* Keep transient notifications concise.
* Do not hide important errors in temporary notifications.
* Preserve actionable errors until the user has had a chance to understand them.
* Do not silently fail.
* Do not silently discard user input.
## Responsiveness
* The interface should feel responsive at all times.
* Provide immediate feedback after interaction.
* Avoid blocking the entire interface when only one section is loading.
* Prefer localized loading states.
* Use optimistic UI only when failures can be handled safely.
* Avoid layout jumps during loading.
* Reserve space for content where practical.
* Keep animations short and functional.
* Do not use animations that delay task completion.
* Respect reduced-motion preferences.
* Avoid unnecessary transitions and visual effects.
## Visual Design
* Prefer modern, clean and restrained visual design.
* Use a consistent design system.
* Keep typography hierarchy clear.
* Maintain sufficient contrast.
* Use color intentionally.
* Do not rely on color alone to communicate state.
* Keep the number of accent colors limited.
* Avoid excessive gradients, shadows and decorative effects.
* Use rounded corners, borders and elevation consistently.
* Keep icons stylistically consistent.
* Use familiar icons for familiar actions.
* Pair unclear icons with text labels.
* Avoid tiny click targets.
* Keep interactive targets large enough for comfortable use.
* Maintain consistent alignment.
* Avoid arbitrary spacing values.
* Prefer reusable spacing and sizing tokens.
## Accessibility
* Accessibility is part of the default design, not an optional enhancement.
* Use semantic controls and elements.
* Ensure full keyboard usability.
* Maintain visible focus indicators.
* Use sufficient color contrast.
* Do not rely solely on hover interactions.
* Do not rely solely on color to indicate status.
* Provide accessible names for controls.
* Keep focus order logical.
* Move focus intentionally after dialogs, navigation or major state changes.
* Support screen readers where applicable.
* Use ARIA only when native semantics are insufficient.
* Respect reduced-motion and system accessibility preferences.
* Avoid flashing or distracting content.
## Responsive Design
* Design for the available viewport instead of fixed screen sizes.
* Prioritize important content on smaller screens.
* Avoid horizontal scrolling unless the content genuinely requires it.
* Keep controls usable on touch devices.
* Do not simply shrink desktop layouts.
* Adapt navigation and layout intentionally for smaller screens.
* Preserve core functionality across supported screen sizes.
* Avoid hiding important actions solely because space is limited.
* Test common breakpoints and extreme content lengths.
## Tables and Data-Dense Views
* Use tables only for genuinely tabular data.
* Keep column names concise.
* Align numeric data consistently.
* Make sorting and filtering discoverable.
* Preserve filter and sort state where useful.
* Do not overload tables with too many actions.
* Prefer a clear primary row action and secondary contextual actions.
* Keep important columns visible.
* Allow horizontal scrolling only when necessary.
* Use pagination, virtualization or incremental loading for large data sets.
* Provide clear empty states.
* Avoid showing unnecessary columns by default.
* Use sensible formatting for dates, numbers and units.
## Search and Filtering
* Search should behave predictably.
* Make active filters visible.
* Make filters easy to remove.
* Provide a clear way to reset filters.
* Preserve search and filter state when navigating back where practical.
* Avoid requiring users to configure many filters before seeing results.
* Use sensible defaults.
* Clearly distinguish no results from loading or errors.
* Suggest recovery options when no results are found.
## Dialogs and Modals
* Use dialogs only when interruption is justified.
* Avoid stacking dialogs.
* Keep dialogs focused on one decision or task.
* Use clear titles.
* Keep primary and secondary actions obvious.
* Make cancellation easy.
* Support Escape where appropriate.
* Do not use dialogs for information that belongs inline.
* Avoid large workflows inside modal dialogs.
* Do not close dialogs unexpectedly while users are entering data.
## Destructive Actions
* Clearly label destructive actions.
* Use confirmations for irreversible or high-impact operations.
* Explain the consequence before confirmation.
* Prefer reversible actions when possible.
* Offer undo where practical.
* Do not use generic confirmation text for destructive operations.
* Make the safe action visually distinct.
* Do not preselect destructive choices.
## Error Prevention
* Prevent invalid actions before they occur where practical.
* Use constraints and suitable controls instead of relying only on validation errors.
* Provide sensible defaults.
* Confirm only high-impact decisions.
* Warn users before losing unsaved changes.
* Do not clear entered data unexpectedly.
* Avoid ambiguous state transitions.
* Make dependencies between fields or actions visible.
## Consistency
* Similar problems should have similar UI solutions.
* Similar actions should use the same wording and placement.
* Do not introduce custom patterns when an existing pattern already solves the problem.
* Reuse components and design tokens.
* Keep interaction behavior consistent across the application.
* Preserve platform conventions unless there is a strong reason not to.
## User Control
* Keep users in control of important actions.
* Do not perform surprising destructive actions automatically.
* Do not change user data without a clear action or established behavior.
* Make automatic behavior visible when it materially affects the user.
* Allow users to cancel long-running operations where practical.
* Allow recovery from mistakes where possible.
* Avoid dark patterns.
* Never manipulate users into actions they did not intend.
## Progressive Disclosure
* Show the most important options first.
* Hide advanced complexity until it is needed.
* Do not overwhelm users with every possible setting at once.
* Keep advanced options discoverable.
* Avoid forcing expert workflows on normal users.
* Avoid hiding frequently used functionality behind excessive menus.
## Performance Perception
* Optimize perceived performance as well as actual performance.
* Show useful content as early as possible.
* Avoid blocking initial rendering on non-critical data.
* Use skeletons only when they improve comprehension.
* Avoid fake progress.
* Do not use loading animations to mask avoidable slowness.
* Keep interactions responsive even while background work continues.
## Defaults
* Choose safe and sensible defaults.
* Defaults should match the most common expected user intent.
* Avoid defaults with destructive consequences.
* Preserve user preferences when appropriate.
* Do not force repeated configuration for common workflows.
## Design Decision Rule
* Every UI element should have a clear purpose.
* Every additional interaction adds cognitive cost.
* Prefer removing complexity over explaining unnecessary complexity.
* Prefer familiar patterns over clever ones.
* Prefer fewer clear choices over many ambiguous choices.
* Prefer predictable behavior over surprising automation.
* If a design decision introduces ambiguity, unnecessary complexity, or multiple equally valid UX directions, ask the user instead of deciding autonomously.
## Priority Order
Prioritize UI/UX decisions in this order:
* Correctness
* Clarity
* Safety
* Accessibility
* User control
* Task efficiency
* Consistency
* Responsiveness
* Visual polish
Visual polish must never reduce usability.
+64
View File
@@ -0,0 +1,64 @@
# UI & UX Guidelines
* Design for clarity first.
* Prefer simple, modern and predictable interfaces.
* Optimize for fast understanding and low cognitive load.
* Every screen should have a clear primary purpose.
* Every important action should be easy to discover.
* Prefer familiar interaction patterns over novel ones.
* Do not require users to learn unnecessary custom behavior.
* Keep layouts visually calm and structured.
* Use consistent spacing, typography, colors and interaction patterns.
* Avoid visual noise.
* Avoid decorative elements that do not improve usability.
* Avoid unnecessary complexity in navigation and workflows.
* Minimize the number of steps required to complete common tasks.
* Keep related information and actions close together.
* Group content logically.
* Use clear visual hierarchy.
* Make primary actions visually distinct from secondary actions.
* Avoid giving multiple actions equal visual priority when one is clearly more important.
* Use whitespace intentionally.
* Prefer readable line lengths and sufficient spacing.
* Avoid dense walls of content.
* Avoid oversized empty areas that reduce information efficiency.
* Keep important information visible without unnecessary scrolling where practical.
## Design Decision Rule
* Every UI element should have a clear purpose.
* Every additional interaction adds cognitive cost.
* Prefer removing complexity over explaining unnecessary complexity.
* Prefer familiar patterns over clever ones.
* Prefer fewer clear choices over many ambiguous choices.
* Prefer predictable behavior over surprising automation.
* If a design decision introduces ambiguity, unnecessary complexity, or multiple equally valid UX directions, ask the user instead of deciding autonomously.
## Priority Order
Prioritize UI/UX decisions in this order:
* Correctness
* Clarity
* Safety
* Accessibility
* User control
* Task efficiency
* Consistency
* Responsiveness
* Visual polish
Visual polish must never reduce usability.
## Detailed guidance
Read the matching reference file before working in that area:
| Read this reference for... | ...this kind of work |
| --- | --- |
| `references/navigation-and-actions.md` | Navigation structure, action labeling and placement |
| `references/forms.md` | Form design and validation UX |
| `references/content-and-feedback.md` | Copy, loading/empty/error states, responsiveness |
| `references/visual-and-accessibility.md` | Visual design, accessibility, responsive layout |
| `references/tables-search-dialogs.md` | Data-dense views, search/filtering, dialogs/modals |
| `references/destructive-and-error-prevention.md` | Destructive actions, error prevention, consistency, user control, progressive disclosure, perceived performance, defaults |
@@ -0,0 +1,44 @@
## Content and Language
* Use plain, concise language.
* Prefer user terminology over internal technical terminology.
* Avoid jargon unless the target users are expected to understand it.
* Use consistent naming for the same concepts.
* Do not rename concepts between screens.
* Keep button labels, headings and descriptions concise.
* Put the most important information first.
* Avoid unnecessary explanatory text.
* Add explanation only where users may reasonably be confused.
* Error messages should explain what happened and what the user can do next.
* Avoid blaming the user.
* Avoid exposing stack traces, exception names or internal implementation details.
## Feedback and System State
* Always communicate important system state.
* Show loading states for operations that are not immediate.
* Avoid indefinite loading indicators without context.
* Use progress indicators for longer operations where progress can be measured.
* Clearly distinguish loading, empty, error and success states.
* Do not leave blank screens where an empty state is expected.
* Empty states should explain what the user can do next.
* Show success feedback when the result is not otherwise obvious.
* Keep transient notifications concise.
* Do not hide important errors in temporary notifications.
* Preserve actionable errors until the user has had a chance to understand them.
* Do not silently fail.
* Do not silently discard user input.
## Responsiveness
* The interface should feel responsive at all times.
* Provide immediate feedback after interaction.
* Avoid blocking the entire interface when only one section is loading.
* Prefer localized loading states.
* Use optimistic UI only when failures can be handled safely.
* Avoid layout jumps during loading.
* Reserve space for content where practical.
* Keep animations short and functional.
* Do not use animations that delay task completion.
* Respect reduced-motion preferences.
* Avoid unnecessary transitions and visual effects.
@@ -0,0 +1,68 @@
## Destructive Actions
* Clearly label destructive actions.
* Use confirmations for irreversible or high-impact operations.
* Explain the consequence before confirmation.
* Prefer reversible actions when possible.
* Offer undo where practical.
* Do not use generic confirmation text for destructive operations.
* Make the safe action visually distinct.
* Do not preselect destructive choices.
## Error Prevention
* Prevent invalid actions before they occur where practical.
* Use constraints and suitable controls instead of relying only on validation errors.
* Provide sensible defaults.
* Confirm only high-impact decisions.
* Warn users before losing unsaved changes.
* Do not clear entered data unexpectedly.
* Avoid ambiguous state transitions.
* Make dependencies between fields or actions visible.
## Consistency
* Similar problems should have similar UI solutions.
* Similar actions should use the same wording and placement.
* Do not introduce custom patterns when an existing pattern already solves the problem.
* Reuse components and design tokens.
* Keep interaction behavior consistent across the application.
* Preserve platform conventions unless there is a strong reason not to.
## User Control
* Keep users in control of important actions.
* Do not perform surprising destructive actions automatically.
* Do not change user data without a clear action or established behavior.
* Make automatic behavior visible when it materially affects the user.
* Allow users to cancel long-running operations where practical.
* Allow recovery from mistakes where possible.
* Avoid dark patterns.
* Never manipulate users into actions they did not intend.
## Progressive Disclosure
* Show the most important options first.
* Hide advanced complexity until it is needed.
* Do not overwhelm users with every possible setting at once.
* Keep advanced options discoverable.
* Avoid forcing expert workflows on normal users.
* Avoid hiding frequently used functionality behind excessive menus.
## Performance Perception
* Optimize perceived performance as well as actual performance.
* Show useful content as early as possible.
* Avoid blocking initial rendering on non-critical data.
* Use skeletons only when they improve comprehension.
* Avoid fake progress.
* Do not use loading animations to mask avoidable slowness.
* Keep interactions responsive even while background work continues.
## Defaults
* Choose safe and sensible defaults.
* Defaults should match the most common expected user intent.
* Avoid defaults with destructive consequences.
* Preserve user preferences when appropriate.
* Do not force repeated configuration for common workflows.
+20
View File
@@ -0,0 +1,20 @@
## Forms
* Keep forms as short as practical.
* Ask only for information that is actually required.
* Group related fields together.
* Use clear field labels.
* Do not rely on placeholders as the only label.
* Use appropriate input controls for the expected value.
* Provide useful defaults where safe and predictable.
* Preserve user input when validation or network errors occur.
* Validate fields at useful moments without interrupting normal input.
* Show validation errors close to the affected field.
* Explain how to fix invalid input.
* Avoid technical validation messages.
* Clearly mark optional fields where useful.
* Avoid asking the same information multiple times.
* Use autocomplete and autofill where appropriate.
* Make keyboard navigation logical.
* Use sensible tab order.
* Support Enter and Escape behavior where users expect it.
@@ -0,0 +1,29 @@
## Navigation
* Navigation must be predictable and consistent.
* Users should always understand where they are.
* Users should always understand how to go back.
* Avoid hidden navigation when visible navigation is practical.
* Keep navigation depth shallow where possible.
* Use clear and descriptive navigation labels.
* Do not use ambiguous icons without labels when meaning is not obvious.
* Preserve navigation state when users return to a previous view where practical.
* Avoid resetting filters, search state or scroll position unnecessarily.
* Avoid unexpected redirects.
* Do not change navigation behavior between similar screens without a clear reason.
## Actions
* Use clear action labels.
* Prefer verbs that describe the actual outcome.
* Avoid vague labels such as `OK`, `Submit`, `Proceed` or `Execute` when a more specific label is possible.
* Make destructive actions clearly distinguishable.
* Keep dangerous actions away from common actions.
* Require confirmation for destructive or difficult-to-reverse actions.
* Do not require unnecessary confirmation for safe, reversible actions.
* Disable unavailable actions when the reason is obvious.
* Explain why an action is unavailable when it is not obvious.
* Avoid actions that appear clickable but are not.
* Provide immediate visual feedback after user actions.
* Prevent accidental duplicate submissions.
* Make loading, success and failure states visible.
@@ -0,0 +1,40 @@
## Tables and Data-Dense Views
* Use tables only for genuinely tabular data.
* Keep column names concise.
* Align numeric data consistently.
* Make sorting and filtering discoverable.
* Preserve filter and sort state where useful.
* Do not overload tables with too many actions.
* Prefer a clear primary row action and secondary contextual actions.
* Keep important columns visible.
* Allow horizontal scrolling only when necessary.
* Use pagination, virtualization or incremental loading for large data sets.
* Provide clear empty states.
* Avoid showing unnecessary columns by default.
* Use sensible formatting for dates, numbers and units.
## Search and Filtering
* Search should behave predictably.
* Make active filters visible.
* Make filters easy to remove.
* Provide a clear way to reset filters.
* Preserve search and filter state when navigating back where practical.
* Avoid requiring users to configure many filters before seeing results.
* Use sensible defaults.
* Clearly distinguish no results from loading or errors.
* Suggest recovery options when no results are found.
## Dialogs and Modals
* Use dialogs only when interruption is justified.
* Avoid stacking dialogs.
* Keep dialogs focused on one decision or task.
* Use clear titles.
* Keep primary and secondary actions obvious.
* Make cancellation easy.
* Support Escape where appropriate.
* Do not use dialogs for information that belongs inline.
* Avoid large workflows inside modal dialogs.
* Do not close dialogs unexpectedly while users are entering data.
@@ -0,0 +1,48 @@
## Visual Design
* Prefer modern, clean and restrained visual design.
* Use a consistent design system.
* Keep typography hierarchy clear.
* Maintain sufficient contrast.
* Use color intentionally.
* Do not rely on color alone to communicate state.
* Keep the number of accent colors limited.
* Avoid excessive gradients, shadows and decorative effects.
* Use rounded corners, borders and elevation consistently.
* Keep icons stylistically consistent.
* Use familiar icons for familiar actions.
* Pair unclear icons with text labels.
* Avoid tiny click targets.
* Keep interactive targets large enough for comfortable use.
* Maintain consistent alignment.
* Avoid arbitrary spacing values.
* Prefer reusable spacing and sizing tokens.
## Accessibility
* Accessibility is part of the default design, not an optional enhancement.
* Use semantic controls and elements.
* Ensure full keyboard usability.
* Maintain visible focus indicators.
* Use sufficient color contrast.
* Do not rely solely on hover interactions.
* Do not rely solely on color to indicate status.
* Provide accessible names for controls.
* Keep focus order logical.
* Move focus intentionally after dialogs, navigation or major state changes.
* Support screen readers where applicable.
* Use ARIA only when native semantics are insufficient.
* Respect reduced-motion and system accessibility preferences.
* Avoid flashing or distracting content.
## Responsive Design
* Design for the available viewport instead of fixed screen sizes.
* Prioritize important content on smaller screens.
* Avoid horizontal scrolling unless the content genuinely requires it.
* Keep controls usable on touch devices.
* Do not simply shrink desktop layouts.
* Adapt navigation and layout intentionally for smaller screens.
* Preserve core functionality across supported screen sizes.
* Avoid hiding important actions solely because space is limited.
* Test common breakpoints and extreme content lengths.
-375
View File
@@ -1,375 +0,0 @@
# WPF Code Guidelines
* Use modern C# and current .NET/WPF conventions.
* Keep UI, application logic and domain logic clearly separated.
* Prefer MVVM for non-trivial views.
* Keep code-behind minimal.
* Use code-behind only for view-specific behavior that does not belong in the ViewModel.
* Avoid business logic in views.
* Avoid direct service access from controls.
* Keep dependencies explicit and testable.
* Prefer composition over inheritance.
* Avoid unnecessary abstractions and framework wrappers.
* Follow the existing project architecture unless there is a strong reason to change it.
## MVVM
* Keep ViewModels focused on presentation state and commands.
* Do not put UI element references into ViewModels.
* Do not access `Window`, `UserControl`, `DispatcherObject` or visual tree elements from ViewModels.
* Avoid direct navigation, dialogs or message boxes from ViewModels without an abstraction when testability matters.
* Prefer explicit application services for navigation, dialogs and external interactions.
* Keep domain logic outside ViewModels.
* Do not use ViewModels as general-purpose service containers.
* Keep ViewModel dependencies minimal.
* Prefer immutable or readonly state where practical.
* Avoid duplicated derived state.
* Keep state transitions predictable.
## Data Binding
* Prefer data binding over manual UI synchronization.
* Use strongly typed properties.
* Use appropriate binding modes.
* Prefer `OneWay` when two-way updates are not required.
* Use `TwoWay` only when the UI must modify the source.
* Avoid unnecessary `UpdateSourceTrigger=PropertyChanged` for expensive operations.
* Keep binding paths simple and stable.
* Avoid deeply nested binding paths.
* Do not hide broken bindings.
* Treat binding errors as real defects.
* Use `FallbackValue` and `TargetNullValue` only when they reflect intended behavior.
* Avoid excessive converters.
* Prefer exposing correctly shaped state from the ViewModel over complex converter logic.
* Avoid `MultiBinding` when simpler state modeling is clearer.
* Do not put business logic into converters.
## Property Change Notifications
* Implement property change notifications consistently.
* Avoid raising `PropertyChanged` unnecessarily.
* Raise notifications only when values actually change.
* Keep dependent property notifications explicit.
* Avoid hidden notification chains that are difficult to reason about.
* Prefer reusable base implementations only when they reduce duplication without obscuring behavior.
* Do not use reflection-based notification mechanisms unless there is a concrete benefit.
## Commands
* Prefer commands over click handlers for ViewModel-driven actions.
* Keep command execution focused.
* Avoid putting large workflows directly inside command implementations.
* Keep `CanExecute` logic cheap.
* Refresh command availability only when relevant state changes.
* Avoid command implementations with hidden side effects.
* Handle async commands safely.
* Prevent accidental duplicate execution when an async action must not run concurrently.
* Never use `async void` except for true event handlers.
## Async and Threading
* Keep async operations asynchronous throughout the call chain.
* Avoid `.Result`, `.Wait()` and other synchronous blocking of async work.
* Do not block the UI thread.
* Run I/O asynchronously.
* Move expensive CPU work off the UI thread only when necessary.
* Marshal UI updates back to the UI thread when required.
* Keep Dispatcher usage minimal and explicit.
* Do not use the Dispatcher to hide incorrect threading design.
* Support `CancellationToken` for cancellable long-running operations.
* Cancel obsolete work when views or selections change.
* Prevent race conditions caused by overlapping async operations.
* Handle exceptions from background operations explicitly.
## UI Thread
* Treat the UI thread as a scarce resource.
* Avoid expensive loops, parsing, serialization, database access or network calls on the UI thread.
* Avoid excessive Dispatcher invocations.
* Batch UI updates where practical.
* Do not update UI-bound collections excessively in tight loops.
* Avoid synchronous waits on background work.
* Keep rendering-related callbacks lightweight.
## Collections
* Use `ObservableCollection<T>` only when collection change notifications are required.
* Do not use `ObservableCollection<T>` as a general-purpose collection.
* Use `List<T>` or other suitable collections for non-observable internal data.
* Avoid replacing entire observable collections when incremental updates are sufficient.
* Avoid thousands of individual collection notifications when a bulk update is more appropriate.
* Keep collection mutations on the correct thread.
* Avoid exposing mutable internal collections unnecessarily.
* Prefer read-only views where consumers should not modify data.
## XAML
* Keep XAML readable and focused.
* Avoid excessively large view files.
* Split complex views into focused controls when it improves maintainability.
* Use resources for repeated values and styles.
* Avoid magic numbers in XAML.
* Keep styles and templates consistent.
* Avoid excessive inline styling.
* Prefer reusable resources over duplicated markup.
* Avoid deeply nested panels.
* Keep visual trees as shallow as practical.
* Use semantic control choices.
* Avoid unnecessary containers.
* Do not overuse triggers when simpler state representation is possible.
* Keep templates understandable.
* Avoid XAML tricks that make behavior hard to discover.
## Layout
* Prefer `Grid` for structured layouts.
* Use `StackPanel` only where its layout behavior is actually appropriate.
* Avoid unnecessary nested panels.
* Avoid fixed sizes unless the design requires them.
* Prefer responsive sizing with `Auto` and `*`.
* Avoid layout configurations that cause excessive measure/arrange passes.
* Be careful with large visual trees inside scrolling containers.
* Avoid disabling virtualization accidentally.
## Virtualization
* Keep UI virtualization enabled for large collections.
* Do not use container layouts that disable virtualization without a reason.
* Avoid wrapping large virtualized item lists in additional scrolling containers.
* Use virtualizing panels where appropriate.
* Verify virtualization when displaying large data sets.
* Avoid rendering thousands of controls simultaneously.
* Prefer paging, incremental loading or virtualization for very large data sets.
## Resources and Styles
* Use shared resources for repeated styles, brushes, templates and dimensions.
* Keep resource dictionaries focused.
* Avoid giant global resource dictionaries.
* Prefer feature-level resources when styles are not truly global.
* Use `StaticResource` by default.
* Use `DynamicResource` only when runtime resource replacement is required.
* Avoid excessive dynamic resource lookups.
* Keep theme resources separated from application logic.
* Avoid hardcoded colors and dimensions when design tokens or resources already exist.
## Dependency Properties
* Use dependency properties only when WPF property-system behavior is required.
* Do not use dependency properties as a replacement for normal CLR properties.
* Keep metadata and callbacks lightweight.
* Avoid expensive work in property-changed callbacks.
* Avoid callbacks with hidden side effects.
* Validate or coerce values only when the control contract requires it.
* Do not expose mutable default values in dependency property metadata.
## Attached Properties and Behaviors
* Use attached properties and behaviors for reusable view-specific behavior.
* Do not use them to hide business logic.
* Keep behaviors focused and predictable.
* Clean up event handlers when behaviors are detached.
* Avoid global attached-property state.
* Prefer explicit behavior over complex XAML hacks.
## Events
* Avoid unnecessary event subscriptions.
* Unsubscribe from long-lived publishers when required.
* Prevent memory leaks from event handlers.
* Prefer weak events where publisher lifetime significantly exceeds subscriber lifetime.
* Keep event handlers small.
* Do not use events as hidden global communication channels.
* Avoid excessive event chaining.
## Memory Management
* Watch for memory leaks caused by:
* Event subscriptions
* Timers
* Static references
* Long-lived services
* Cached views
* Closures
* Collection views
* Binding-related references
* Dispose resources correctly.
* Stop timers and background work when no longer needed.
* Avoid retaining closed windows or unloaded views.
* Do not keep unnecessary references to visual elements.
* Profile memory when long-running applications continuously grow.
## Windows and Dialogs
* Keep window lifecycle explicit.
* Avoid creating duplicate windows unintentionally.
* Do not keep hidden windows alive without a reason.
* Separate dialog logic from business logic.
* Prefer dialog services when ViewModels need to request user interaction.
* Keep ownership relationships explicit.
* Avoid application shutdown behavior that depends on accidental window lifetime.
## Navigation
* Keep navigation state explicit.
* Avoid coupling ViewModels directly to concrete views.
* Avoid service locator patterns for view resolution.
* Keep navigation history intentional.
* Dispose or release navigation targets when they are no longer required.
* Do not retain entire navigation graphs unnecessarily.
## Validation
* Validate user input at the appropriate boundary.
* Keep validation logic out of XAML when it becomes complex.
* Prefer reusable validation rules or ViewModel validation for non-trivial cases.
* Display validation errors consistently.
* Do not rely on UI validation as a security boundary.
* Validate again at backend or domain boundaries when required.
* Preserve user input on recoverable errors.
## Performance
* Keep the UI thread responsive.
* Minimize visual tree depth.
* Keep virtualization enabled.
* Avoid unnecessary bindings.
* Avoid unnecessary converters.
* Avoid unnecessary resource lookups.
* Avoid unnecessary property change notifications.
* Avoid frequent full collection refreshes.
* Avoid loading large datasets eagerly.
* Avoid loading large images at full resolution when unnecessary.
* Freeze `Freezable` objects where practical.
* Reuse immutable resources where appropriate.
* Avoid unnecessary animations.
* Avoid high-frequency timers on the UI thread.
* Measure startup time, UI responsiveness, memory usage and rendering performance before micro-optimizing.
## Images and Media
* Load images at an appropriate resolution.
* Avoid decoding images significantly larger than their displayed size.
* Release streams and file handles correctly.
* Avoid locking image files unintentionally.
* Cache images only when there is a clear memory strategy.
* Avoid keeping large bitmaps alive unnecessarily.
* Prefer frozen image resources when they are immutable.
## Security
* Treat all external input as untrusted.
* Never expose secrets, credentials or tokens in UI code.
* Do not store secrets in configuration files committed to source control.
* Avoid leaking sensitive information through logs, dialogs or exception messages.
* Validate file paths and external resources.
* Do not execute external processes or files without explicit validation.
* Avoid insecure deserialization.
* Do not weaken TLS, authentication or certificate validation.
* Prefer secure defaults.
* Never disable security controls for convenience.
## File and Process Access
* Validate file paths before use.
* Avoid assuming files or directories exist.
* Handle permissions and locked files gracefully.
* Avoid arbitrary shell execution.
* Quote and validate process arguments safely.
* Do not concatenate untrusted input into shell commands.
* Avoid elevated privileges unless explicitly required.
* Clean up temporary files when appropriate.
## Error Handling
* Handle expected errors explicitly.
* Do not swallow exceptions.
* Preserve useful exception context.
* Avoid showing raw technical exceptions directly to end users.
* Log enough information for diagnosis without exposing sensitive data.
* Keep user-facing error messages clear and actionable.
* Avoid using exceptions for normal control flow.
* Ensure background exceptions are observed.
## Logging
* Use structured logging.
* Avoid excessive logging on the UI thread.
* Do not log every property change or binding update.
* Never log secrets, tokens, credentials or sensitive user data.
* Include relevant context for operational failures.
* Avoid logging the same exception repeatedly at multiple layers.
## Architecture
* Keep presentation, application, domain and infrastructure concerns separated.
* Avoid direct database access from views or ViewModels.
* Avoid direct HTTP access from controls.
* Keep external integrations behind focused services.
* Avoid circular dependencies.
* Keep feature boundaries clear.
* Do not create shared utility classes for unrelated functionality.
* Keep shared code genuinely reusable.
* Minimize global state.
* Avoid static service access.
## Dependency Injection
* Use dependency injection where lifecycle management, substitution or testability benefits from it.
* Avoid service locator patterns.
* Keep constructor dependencies minimal.
* Do not inject dependencies that are not used.
* Choose service lifetimes intentionally.
* Do not make stateful services global without a concrete reason.
* Avoid introducing interfaces solely for dependency injection when no abstraction is needed.
## Testing
* Keep ViewModels testable without a UI thread where practical.
* Test behavior rather than implementation details.
* Test domain and application logic independently from WPF.
* Avoid brittle tests coupled to XAML structure.
* Keep UI automation tests focused on critical flows.
* Mock external boundaries rather than internal details.
* Keep tests deterministic.
* Avoid real network, database or filesystem dependencies in unit tests unless explicitly intended.
## Code Quality
* Use nullable reference types.
* Prefer modern, readable C# syntax.
* Avoid unnecessary interfaces and abstractions.
* Prefer concrete types where no abstraction is required.
* Use descriptive names.
* Remove dead code.
* Remove commented-out code.
* Do not add comments unless the user explicitly requests them.
* Treat compiler, analyzer and binding warnings seriously.
* Do not suppress warnings without a concrete reason.
* Prefer correctness, responsiveness, security and maintainability over cleverness.
## Performance Priority
Prioritize WPF performance work in this order:
* UI architecture
* UI thread blocking
* Data loading and I/O
* Virtualization
* Visual tree complexity
* Binding and notification frequency
* Rendering and images
* Memory retention and leaks
* Allocations
* CPU micro-optimizations
Never optimize based purely on assumptions.
## Safety Rule
* The UI must remain responsive.
* Do not block the UI thread with I/O or expensive CPU work.
* Do not bypass validation or security controls for convenience.
* Do not introduce hidden cross-thread access.
* Prefer predictable, explicit and testable UI behavior.
+34
View File
@@ -0,0 +1,34 @@
# WPF Code Guidelines
* Use modern C# and current .NET/WPF conventions.
* Keep UI, application logic and domain logic clearly separated.
* Prefer MVVM for non-trivial views.
* Keep code-behind minimal.
* Use code-behind only for view-specific behavior that does not belong in the ViewModel.
* Avoid business logic in views.
* Avoid direct service access from controls.
* Keep dependencies explicit and testable.
* Prefer composition over inheritance.
* Avoid unnecessary abstractions and framework wrappers.
* Follow the existing project architecture unless there is a strong reason to change it.
## Safety Rule
* The UI must remain responsive.
* Do not block the UI thread with I/O or expensive CPU work.
* Do not bypass validation or security controls for convenience.
* Do not introduce hidden cross-thread access.
* Prefer predictable, explicit and testable UI behavior.
## Detailed guidance
Read the matching reference file before working in that area:
| Read this reference for... | ...this kind of work |
| --- | --- |
| `references/mvvm-and-binding.md` | MVVM, data binding, property change notifications, dependency/attached properties |
| `references/commands-and-async.md` | Commands, async/threading, UI-thread work, events |
| `references/collections-and-xaml.md` | Collections, virtualization, layout, XAML |
| `references/resources-and-lifecycle.md` | Resources/styles, windows/dialogs, navigation, memory, images/media |
| `references/security-and-errors.md` | Security, file/process access, error handling, logging, validation |
| `references/architecture-and-quality.md` | Architecture, DI, testing, code quality, performance |
@@ -0,0 +1,82 @@
## Architecture
* Keep presentation, application, domain and infrastructure concerns separated.
* Avoid direct database access from views or ViewModels.
* Avoid direct HTTP access from controls.
* Keep external integrations behind focused services.
* Avoid circular dependencies.
* Keep feature boundaries clear.
* Do not create shared utility classes for unrelated functionality.
* Keep shared code genuinely reusable.
* Minimize global state.
* Avoid static service access.
## Dependency Injection
* Use dependency injection where lifecycle management, substitution or testability benefits from it.
* Avoid service locator patterns.
* Keep constructor dependencies minimal.
* Do not inject dependencies that are not used.
* Choose service lifetimes intentionally.
* Do not make stateful services global without a concrete reason.
* Avoid introducing interfaces solely for dependency injection when no abstraction is needed.
## Testing
* Keep ViewModels testable without a UI thread where practical.
* Test behavior rather than implementation details.
* Test domain and application logic independently from WPF.
* Avoid brittle tests coupled to XAML structure.
* Keep UI automation tests focused on critical flows.
* Mock external boundaries rather than internal details.
* Keep tests deterministic.
* Avoid real network, database or filesystem dependencies in unit tests unless explicitly intended.
## Code Quality
* Use nullable reference types.
* Prefer modern, readable C# syntax.
* Avoid unnecessary interfaces and abstractions.
* Prefer concrete types where no abstraction is required.
* Use descriptive names.
* Remove dead code.
* Remove commented-out code.
* Do not add comments unless the user explicitly requests them.
* Treat compiler, analyzer and binding warnings seriously.
* Do not suppress warnings without a concrete reason.
* Prefer correctness, responsiveness, security and maintainability over cleverness.
## Performance
* Keep the UI thread responsive.
* Minimize visual tree depth.
* Keep virtualization enabled.
* Avoid unnecessary bindings.
* Avoid unnecessary converters.
* Avoid unnecessary resource lookups.
* Avoid unnecessary property change notifications.
* Avoid frequent full collection refreshes.
* Avoid loading large datasets eagerly.
* Avoid loading large images at full resolution when unnecessary.
* Freeze `Freezable` objects where practical.
* Reuse immutable resources where appropriate.
* Avoid unnecessary animations.
* Avoid high-frequency timers on the UI thread.
* Measure startup time, UI responsiveness, memory usage and rendering performance before micro-optimizing.
## Performance Priority
Prioritize WPF performance work in this order:
* UI architecture
* UI thread blocking
* Data loading and I/O
* Virtualization
* Visual tree complexity
* Binding and notification frequency
* Rendering and images
* Memory retention and leaks
* Allocations
* CPU micro-optimizations
Never optimize based purely on assumptions.
@@ -0,0 +1,49 @@
## Collections
* Use `ObservableCollection<T>` only when collection change notifications are required.
* Do not use `ObservableCollection<T>` as a general-purpose collection.
* Use `List<T>` or other suitable collections for non-observable internal data.
* Avoid replacing entire observable collections when incremental updates are sufficient.
* Avoid thousands of individual collection notifications when a bulk update is more appropriate.
* Keep collection mutations on the correct thread.
* Avoid exposing mutable internal collections unnecessarily.
* Prefer read-only views where consumers should not modify data.
## XAML
* Keep XAML readable and focused.
* Avoid excessively large view files.
* Split complex views into focused controls when it improves maintainability.
* Use resources for repeated values and styles.
* Avoid magic numbers in XAML.
* Keep styles and templates consistent.
* Avoid excessive inline styling.
* Prefer reusable resources over duplicated markup.
* Avoid deeply nested panels.
* Keep visual trees as shallow as practical.
* Use semantic control choices.
* Avoid unnecessary containers.
* Do not overuse triggers when simpler state representation is possible.
* Keep templates understandable.
* Avoid XAML tricks that make behavior hard to discover.
## Layout
* Prefer `Grid` for structured layouts.
* Use `StackPanel` only where its layout behavior is actually appropriate.
* Avoid unnecessary nested panels.
* Avoid fixed sizes unless the design requires them.
* Prefer responsive sizing with `Auto` and `*`.
* Avoid layout configurations that cause excessive measure/arrange passes.
* Be careful with large visual trees inside scrolling containers.
* Avoid disabling virtualization accidentally.
## Virtualization
* Keep UI virtualization enabled for large collections.
* Do not use container layouts that disable virtualization without a reason.
* Avoid wrapping large virtualized item lists in additional scrolling containers.
* Use virtualizing panels where appropriate.
* Verify virtualization when displaying large data sets.
* Avoid rendering thousands of controls simultaneously.
* Prefer paging, incremental loading or virtualization for very large data sets.
@@ -0,0 +1,46 @@
## Commands
* Prefer commands over click handlers for ViewModel-driven actions.
* Keep command execution focused.
* Avoid putting large workflows directly inside command implementations.
* Keep `CanExecute` logic cheap.
* Refresh command availability only when relevant state changes.
* Avoid command implementations with hidden side effects.
* Handle async commands safely.
* Prevent accidental duplicate execution when an async action must not run concurrently.
* Never use `async void` except for true event handlers.
## Async and Threading
* Keep async operations asynchronous throughout the call chain.
* Avoid `.Result`, `.Wait()` and other synchronous blocking of async work.
* Do not block the UI thread.
* Run I/O asynchronously.
* Move expensive CPU work off the UI thread only when necessary.
* Marshal UI updates back to the UI thread when required.
* Keep Dispatcher usage minimal and explicit.
* Do not use the Dispatcher to hide incorrect threading design.
* Support `CancellationToken` for cancellable long-running operations.
* Cancel obsolete work when views or selections change.
* Prevent race conditions caused by overlapping async operations.
* Handle exceptions from background operations explicitly.
## UI Thread
* Treat the UI thread as a scarce resource.
* Avoid expensive loops, parsing, serialization, database access or network calls on the UI thread.
* Avoid excessive Dispatcher invocations.
* Batch UI updates where practical.
* Do not update UI-bound collections excessively in tight loops.
* Avoid synchronous waits on background work.
* Keep rendering-related callbacks lightweight.
## Events
* Avoid unnecessary event subscriptions.
* Unsubscribe from long-lived publishers when required.
* Prevent memory leaks from event handlers.
* Prefer weak events where publisher lifetime significantly exceeds subscriber lifetime.
* Keep event handlers small.
* Do not use events as hidden global communication channels.
* Avoid excessive event chaining.
@@ -0,0 +1,60 @@
## MVVM
* Keep ViewModels focused on presentation state and commands.
* Do not put UI element references into ViewModels.
* Do not access `Window`, `UserControl`, `DispatcherObject` or visual tree elements from ViewModels.
* Avoid direct navigation, dialogs or message boxes from ViewModels without an abstraction when testability matters.
* Prefer explicit application services for navigation, dialogs and external interactions.
* Keep domain logic outside ViewModels.
* Do not use ViewModels as general-purpose service containers.
* Keep ViewModel dependencies minimal.
* Prefer immutable or readonly state where practical.
* Avoid duplicated derived state.
* Keep state transitions predictable.
## Data Binding
* Prefer data binding over manual UI synchronization.
* Use strongly typed properties.
* Use appropriate binding modes.
* Prefer `OneWay` when two-way updates are not required.
* Use `TwoWay` only when the UI must modify the source.
* Avoid unnecessary `UpdateSourceTrigger=PropertyChanged` for expensive operations.
* Keep binding paths simple and stable.
* Avoid deeply nested binding paths.
* Do not hide broken bindings.
* Treat binding errors as real defects.
* Use `FallbackValue` and `TargetNullValue` only when they reflect intended behavior.
* Avoid excessive converters.
* Prefer exposing correctly shaped state from the ViewModel over complex converter logic.
* Avoid `MultiBinding` when simpler state modeling is clearer.
* Do not put business logic into converters.
## Property Change Notifications
* Implement property change notifications consistently.
* Avoid raising `PropertyChanged` unnecessarily.
* Raise notifications only when values actually change.
* Keep dependent property notifications explicit.
* Avoid hidden notification chains that are difficult to reason about.
* Prefer reusable base implementations only when they reduce duplication without obscuring behavior.
* Do not use reflection-based notification mechanisms unless there is a concrete benefit.
## Dependency Properties
* Use dependency properties only when WPF property-system behavior is required.
* Do not use dependency properties as a replacement for normal CLR properties.
* Keep metadata and callbacks lightweight.
* Avoid expensive work in property-changed callbacks.
* Avoid callbacks with hidden side effects.
* Validate or coerce values only when the control contract requires it.
* Do not expose mutable default values in dependency property metadata.
## Attached Properties and Behaviors
* Use attached properties and behaviors for reusable view-specific behavior.
* Do not use them to hide business logic.
* Keep behaviors focused and predictable.
* Clean up event handlers when behaviors are detached.
* Avoid global attached-property state.
* Prefer explicit behavior over complex XAML hacks.
@@ -0,0 +1,58 @@
## Resources and Styles
* Use shared resources for repeated styles, brushes, templates and dimensions.
* Keep resource dictionaries focused.
* Avoid giant global resource dictionaries.
* Prefer feature-level resources when styles are not truly global.
* Use `StaticResource` by default.
* Use `DynamicResource` only when runtime resource replacement is required.
* Avoid excessive dynamic resource lookups.
* Keep theme resources separated from application logic.
* Avoid hardcoded colors and dimensions when design tokens or resources already exist.
## Windows and Dialogs
* Keep window lifecycle explicit.
* Avoid creating duplicate windows unintentionally.
* Do not keep hidden windows alive without a reason.
* Separate dialog logic from business logic.
* Prefer dialog services when ViewModels need to request user interaction.
* Keep ownership relationships explicit.
* Avoid application shutdown behavior that depends on accidental window lifetime.
## Navigation
* Keep navigation state explicit.
* Avoid coupling ViewModels directly to concrete views.
* Avoid service locator patterns for view resolution.
* Keep navigation history intentional.
* Dispose or release navigation targets when they are no longer required.
* Do not retain entire navigation graphs unnecessarily.
## Memory Management
* Watch for memory leaks caused by:
* Event subscriptions
* Timers
* Static references
* Long-lived services
* Cached views
* Closures
* Collection views
* Binding-related references
* Dispose resources correctly.
* Stop timers and background work when no longer needed.
* Avoid retaining closed windows or unloaded views.
* Do not keep unnecessary references to visual elements.
* Profile memory when long-running applications continuously grow.
## Images and Media
* Load images at an appropriate resolution.
* Avoid decoding images significantly larger than their displayed size.
* Release streams and file handles correctly.
* Avoid locking image files unintentionally.
* Cache images only when there is a clear memory strategy.
* Avoid keeping large bitmaps alive unnecessarily.
* Prefer frozen image resources when they are immutable.
@@ -0,0 +1,53 @@
## Security
* Treat all external input as untrusted.
* Never expose secrets, credentials or tokens in UI code.
* Do not store secrets in configuration files committed to source control.
* Avoid leaking sensitive information through logs, dialogs or exception messages.
* Validate file paths and external resources.
* Do not execute external processes or files without explicit validation.
* Avoid insecure deserialization.
* Do not weaken TLS, authentication or certificate validation.
* Prefer secure defaults.
* Never disable security controls for convenience.
## File and Process Access
* Validate file paths before use.
* Avoid assuming files or directories exist.
* Handle permissions and locked files gracefully.
* Avoid arbitrary shell execution.
* Quote and validate process arguments safely.
* Do not concatenate untrusted input into shell commands.
* Avoid elevated privileges unless explicitly required.
* Clean up temporary files when appropriate.
## Error Handling
* Handle expected errors explicitly.
* Do not swallow exceptions.
* Preserve useful exception context.
* Avoid showing raw technical exceptions directly to end users.
* Log enough information for diagnosis without exposing sensitive data.
* Keep user-facing error messages clear and actionable.
* Avoid using exceptions for normal control flow.
* Ensure background exceptions are observed.
## Logging
* Use structured logging.
* Avoid excessive logging on the UI thread.
* Do not log every property change or binding update.
* Never log secrets, tokens, credentials or sensitive user data.
* Include relevant context for operational failures.
* Avoid logging the same exception repeatedly at multiple layers.
## Validation
* Validate user input at the appropriate boundary.
* Keep validation logic out of XAML when it becomes complex.
* Prefer reusable validation rules or ViewModel validation for non-trivial cases.
* Display validation errors consistently.
* Do not rely on UI validation as a security boundary.
* Validate again at backend or domain boundaries when required.
* Preserve user input on recoverable errors.
+20 -3
View File
@@ -1,3 +1,5 @@
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$invariantCulture = [System.Globalization.CultureInfo]::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 { '-' }
@@ -5,7 +7,22 @@ $directory = if ($data.workspace.current_dir) { $data.workspace.current_dir } el
$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 { '-' }
$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 = '-' }
$maxContext = if ($data.context_window.context_window_size) { [long]$data.context_window.context_window_size } else { 0 } $usedPercentage = if ($null -ne $data.context_window.used_percentage) { [double]$data.context_window.used_percentage } else { $null }
$usedContext = if ($null -ne $data.context_window.used_percentage) { "$([math]::Round([double]$data.context_window.used_percentage, [MidpointRounding]::AwayFromZero))%" } 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
Write-Output "Model: $model | Effort: $effort | Repo: $repo | Branch: $branch | Max Context: $maxContext | Used Context: $usedContext | Used Tokens: $usedTokens" $usedTokensDisplay = if ($usedTokens -ge 1000) { ($usedTokens / 1000).ToString('0.0', $invariantCulture) + 'k' } else { $usedTokens.ToString($invariantCulture) }
$esc = [char]27
$reset = "$esc[0m"
$dim = "$esc[90m"
$cyan = "$esc[36m"
$magenta = "$esc[35m"
$blue = "$esc[34m"
$green = "$esc[32m"
$contextColor = if ($null -eq $usedPercentage) { $dim } elseif ($usedPercentage -ge 85) { "$esc[31m" } elseif ($usedPercentage -ge 60) { "$esc[33m" } else { $green }
$middot = [char]0x00B7
$sep = "$dim$middot$reset"
$line1 = "$cyan$model$reset $sep $magenta$effort$reset"
$line2 = "$blue$repo$reset$dim on$reset $green$branch$reset $sep $contextColor$usedContext ctx$reset $dim($usedTokensDisplay tok)$reset"
Write-Output "$line1`n$line2"
+36 -4
View File
@@ -4,19 +4,51 @@ 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 max_context used_context used_tokens < <( IFS=$'\t' read -r model effort repo directory used_percentage used_tokens < <(
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 // "."),
(.context_window.context_window_size // 0), (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 | tostring) + "%") 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))
] | @tsv' ] | @tsv'
) )
used_percentage=${used_percentage%$'\r'}
used_tokens=${used_tokens%$'\r'}
[ "$repo" != - ] || repo=$(basename -- "$directory") [ "$repo" != - ] || repo=$(basename -- "$directory")
[ -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=-
printf 'Model: %s | Effort: %s | Repo: %s | Branch: %s | Max Context: %s | Used Context: %s | Used Tokens: %s\n' "$model" "$effort" "$repo" "$branch" "$max_context" "$used_context" "$used_tokens"
if [ "$used_percentage" != - ]; then
used_context="${used_percentage}%"
else
used_context=-
fi
if [ "$used_tokens" -ge 1000 ]; then
used_tokens_display=$(awk -v t="$used_tokens" 'BEGIN { printf "%.1fk", t / 1000 }')
else
used_tokens_display=$used_tokens
fi
esc=$'\033'
reset="$esc[0m"
dim="$esc[90m"
cyan="$esc[36m"
magenta="$esc[35m"
blue="$esc[34m"
green="$esc[32m"
if [ "$used_percentage" = - ]; then
context_color=$dim
elif [ "$used_percentage" -ge 85 ]; then
context_color="$esc[31m"
elif [ "$used_percentage" -ge 60 ]; then
context_color="$esc[33m"
else
context_color=$green
fi
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" \
"$model" "$effort" "$repo" "$branch" "$used_context" "$used_tokens_display"