Compare commits

..
25 Commits
Author SHA1 Message Date
Julian lechnerandClaude Sonnet 5 6ebb13d51a docs(readme): document rule skills, managed manifest, and installer params
CI / linux (push) Failing after 3s
CI / windows (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:44:32 +02:00
Julian lechnerandClaude Sonnet 5 0d60aa6f5c docs(plugins): document the opt-in plugin update behavior
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:44:28 +02:00
Julian lechnerandClaude Sonnet 5 2fd59f3fd8 docs(skills): document the generated Claude skills/rules category
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:44:24 +02:00
Julian lechnerandClaude Sonnet 5 b815d16084 docs(configuration): document Claude rule skills and the Codex key rename
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:44:20 +02:00
Julian lechnerandClaude Sonnet 5 cd6647eb7b ci: add build and test workflow for Linux and Windows
Runs build, doctor, the platform-package tests, the managed-manifest
tests, the build-validation tests, and an install dry run on both
ubuntu-latest and windows-latest for every push to main and every pull
request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:44:15 +02:00
Julian lechnerandClaude Sonnet 5 1abb3a2d32 docs(hooks): clarify which hook scripts are actually registered
Only the Stop hook (flashbang) is wired into the generated client
configuration. Validate-CommandSafety and Invoke-PostChangeVerification
are utility scripts nothing here invokes automatically; the previous
wording could be read as implying otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:44:09 +02:00
Julian lechnerandClaude Sonnet 5 077eaa55be fix(hooks): handle missing tkinter cleanly in flashbang.sh
tkinter was imported at module level, before the try/except that was
meant to catch a missing display or toolkit. On a Python install without
the (often separately packaged) tkinter module, the script crashed
instead of falling back to notify-send. The import is now wrapped in its
own try/except with the same fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:44:04 +02:00
Julian lechnerandClaude Sonnet 5 b59d355e99 perf(hooks): reduce Stop hook timeout from 20s to 8s
The Stop hook only runs a ~500ms visual flash; 20 seconds was far more
than any realistic process-start latency needs, including the one-time
native shim compile on Windows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:43:57 +02:00
Julian lechnerandClaude Sonnet 5 ae421ce93b fix(codex): rename max_threads to max_concurrent_threads_per_session
max_threads is a legacy alias; the current Codex config sample only
documents max_concurrent_threads_per_session under [agents]. Confirmed
against the official sample configuration before renaming.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:43:47 +02:00
Julian lechnerandClaude Sonnet 5 eff814b245 test: add build-time validation regression tests (Bash)
Bash equivalent of the PowerShell build-validation test, using a tar-based
repo copy per case to check the same four rejected-defect scenarios.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:43:36 +02:00
Julian lechnerandClaude Sonnet 5 3d6c19814e test: add build-time validation regression tests (PowerShell)
Runs the build against temporary full-repo copies, each with one injected
defect, and asserts the build rejects it: invalid Claude settings.json,
a duplicate agent name, a duplicate plugin name, and a leftover template
placeholder.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:43:31 +02:00
Julian lechnerandClaude Sonnet 5 f56366969d test: add managed-manifest installer scenarios (Bash)
Bash equivalent of the PowerShell managed-manifest test, covering the same
idempotency, stale-removal, modified-file-protection, foreign-file, and
side-effect-free dry-run scenarios against sync_managed_destination.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:43:26 +02:00
Julian lechnerandClaude Sonnet 5 0e9de14d46 test: add managed-manifest installer scenarios (PowerShell)
Exercises Sync-ManagedDestination directly against a throwaway
source/destination pair: idempotent second install (no writes, no
backups), removal of a stale-and-untouched file, protection of a
stale-but-locally-modified file (backed up, left in place, warned), a
foreign file that was never installed staying untouched, and a dry run
that reports the planned removal without any side effects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:43:21 +02:00
Julian lechnerandClaude Sonnet 5 0fc87fc6b3 feat(doctor): validate managed files, counts, and config syntax (Bash)
Bash equivalent of the PowerShell doctor change: dynamic agent/skill count
checks, JSON validation of installed settings.json (jq, falling back to a
functional python3 probe), a config.toml balance check, a fast tree-wide
placeholder scan, and managed-manifest missing/modified-file reporting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:43:15 +02:00
Julian lechnerandClaude Sonnet 5 ec354ca5f6 feat(doctor): validate managed files, counts, and config syntax (PowerShell)
Doctor now compares generated agent/skill counts against the source
directories and the rule-skills manifest, parses installed settings.json
as JSON, sanity-checks installed config.toml for balanced quotes and
brackets, scans generated output for leftover template placeholders, and
reports missing or locally modified files from each destination's managed
manifest. All checks stay local and file-based; no network calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:43:10 +02:00
Julian lechnerandClaude Sonnet 5 e617a2477a feat(installer): install via managed manifest with non-interactive params (Bash)
Bash equivalent of the PowerShell installer change: delegates to
sync_managed_destination, adds --client/--platform for non-interactive
runs (falling back to the existing interactive prompts when omitted), and
requires --update-plugins to update already-installed plugins instead of
doing so by default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:43:04 +02:00
Julian lechnerandClaude Sonnet 5 f72af0ee39 feat(installer): install via managed manifest with non-interactive params (PowerShell)
install.ps1 now delegates every destination to Sync-ManagedDestination
instead of a blind copy-and-backup loop, so repeated installs are cheap
and safe by default.

Adds -Client and -Platform parameters so CI or scripts can run fully
non-interactively (install.ps1 -Client Both -Platform Windows); both fall
back to the existing interactive prompts when omitted. Plugin updates now
require an explicit -UpdatePlugins switch instead of always updating
already-installed plugins, so a configuration update never silently
changes third-party plugin code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:42:59 +02:00
Julian lechnerandClaude Sonnet 5 80cff169f4 feat(installer): add managed-file manifest module (Bash)
Bash equivalent of the PowerShell manifest module: sync_managed_destination
plus its read/write/hash helpers, using sha256sum and a TSV manifest so
the same idempotent-install, stale-detection, and local-modification
protection behavior is available without a PowerShell dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:42:53 +02:00
Julian lechnerandClaude Sonnet 5 3f27cd0778 feat(installer): add managed-file manifest module (PowerShell)
Adds Sync-ManagedDestination and its manifest helpers: a per-destination
TSV of path -> SHA-256 that makes installation idempotent (unchanged files
are neither rewritten nor backed up), detects files this setup previously
installed that the source no longer ships (removed after backup, unless
changed locally, in which case they are backed up and left in place with
a warning), and never touches a file it never installed. Backups for a
run land together under backups/<stamp>/ instead of one suffix per file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:42:48 +02:00
Julian lechnerandClaude Sonnet 5 83b260533a test: derive expected agent and skill counts from source (Bash)
Bash equivalent of the PowerShell test change. Also relaxes the platform
dry-run assertion to match on "-$platform" instead of "-$platform/", since
the installer now reports one destination summary line per sync instead of
one line per copied file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:42:41 +02:00
Julian lechnerandClaude Sonnet 5 b1d5a7be42 test: derive expected agent and skill counts from source (PowerShell)
Replaces the hardcoded 5-agent/22-skill expectations with counts computed
from shared/agents, shared/skills, and the new rule-skills manifest, so
adding an agent, skill, or Claude rule skill no longer requires touching
an unrelated magic number in this test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:42:36 +02:00
Julian lechnerandClaude Sonnet 5 1807137daa feat(build): generate Claude rule skills and validate build output (Bash)
Bash equivalent of the PowerShell build change: Claude gets general.md
inlined into CLAUDE.md plus one generated skill per rule-skills.tsv entry;
Codex keeps every rule as a plain file with unchanged AGENTS.md text.

Adds the same build-time validation as the PowerShell script: duplicate
agent/plugin/rule-skill names, JSON validity (jq, falling back to a
functional python3 probe), balanced TOML quotes/brackets, and a
tree-wide (not per-file) leftover-placeholder scan for speed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:42:30 +02:00
Julian lechnerandClaude Sonnet 5 11b319c2ea feat(build): generate Claude rule skills and validate build output (PowerShell)
Claude's package now gets general.md inlined into CLAUDE.md plus one
generated skill per rule-skills.tsv entry under skills/rules/, instead of
copying every rule file into the always-loaded rules/ directory. Codex is
unaffected: it still receives every rule as a plain file and the original
rule-loading text, byte-for-byte.

Also adds build-time validation: duplicate agent/plugin/rule-skill names
fail the build, generated settings.json must parse as JSON, generated
config.toml must have balanced quotes/brackets, and no __PLACEHOLDER__
tokens may remain unresolved (__AI_CONFIG_ROOT__ excepted, since it is
only resolved at install time).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:42:23 +02:00
Julian lechnerandClaude Sonnet 5 24e4d3f96b feat(rules): define Claude rule-skill manifest and template placeholder
Adds adapters/claude/rule-skills.tsv, mapping each technology- or
situation-specific rule to a Claude skill name and trigger description.
Replaces the hardcoded rule-loading block in the shared instructions
template with a __RULE_LOADING__ placeholder so each client's build step
can substitute its own loading mechanism without duplicating the shared
orchestration text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:42:16 +02:00
Julian lechnerandClaude Sonnet 5 3a3c66f290 chore: add .gitattributes for consistent line endings
Ensures shell scripts keep LF and PowerShell scripts keep CRLF across
Windows and Linux checkouts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:42:10 +02:00
26 changed files with 1071 additions and 182 deletions

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
# Auto-detect text files and normalize line endings to LF in the repo
* text=auto eol=lf
# Scripts must keep their platform-native line endings
*.sh text eol=lf
*.ps1 text eol=crlf
# Documentation and config
*.md text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.toml text eol=lf
*.tsv text eol=lf
# Never let Git touch line endings in these
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
+54
View File
@@ -0,0 +1,54 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: ./scripts/build.sh
- name: Doctor
run: ./scripts/doctor.sh
- name: Platform package tests
run: ./scripts/test-platform-packages.sh
- name: Managed manifest tests
run: ./scripts/test-managed-install.sh
- name: Build validation tests
run: ./scripts/test-build-validation.sh
- name: Install dry run (isolated HOME)
run: |
export HOME="$RUNNER_TEMP/isolated-home"
mkdir -p "$HOME"
./scripts/install.sh --dry-run --client both --platform linux
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Build
shell: powershell
run: .\scripts\build.ps1
- name: Doctor
shell: powershell
run: .\scripts\doctor.ps1
- name: Platform package tests
shell: powershell
run: .\scripts\test-platform-packages.ps1
- name: Managed manifest tests
shell: powershell
run: .\scripts\test-managed-install.ps1
- name: Build validation tests
shell: powershell
run: .\scripts\test-build-validation.ps1
- name: Install dry run
shell: powershell
# -DryRun performs no writes, so it is safe to run against the runner's
# real profile path. [Environment]::GetFolderPath('UserProfile') reads
# from the Windows profile API on PowerShell 5.1, not $env:USERPROFILE,
# so overriding that variable would not relocate it anyway.
run: .\scripts\install.ps1 -DryRun -Client Both -Platform Windows
+49 -15
View File
@@ -16,10 +16,17 @@ into platform-specific packages that can be inspected before they are installed.
- One shared source of truth for Codex and Claude Code.
- Separate generated packages for Windows and Linux.
- Native PowerShell 5.1 and Bash scripts.
- Interactive installation for Codex, Claude Code, or both.
- Dry-run support and timestamped backups of replaced managed files.
- Interactive or non-interactive (`-Client`/`-Platform`) installation for Codex, Claude Code, or both.
- Claude Code loads technology- and situation-specific rules as skills, so only their name and description sit
permanently in context; the full rule text loads only when the skill is invoked. Codex keeps loading rules as plain
files, unchanged.
- A managed-file manifest (path and SHA-256 per destination) makes installation idempotent, detects files this setup
previously installed but no longer ships, and protects files a user changed locally instead of silently overwriting
or deleting them.
- Dry-run support (no writes, no backups, no plugin changes) and timestamped, per-run backup directories.
- Shared safety, notification, validation, and status-line hooks.
- User-level plugin installation and updates for Ponytail, i-have-adhd, Superpowers, and Context7.
- User-level plugin installation for Ponytail, i-have-adhd, Superpowers, and Context7; updates only with
`-UpdatePlugins`/`--update-plugins`.
## Architecture
@@ -74,10 +81,11 @@ Every repository change must pass the build before completion. Building only wri
## Installation
The installer runs a fresh build and then asks for the target platform and client. Selecting Linux or Windows chooses
the generated package format; installation always targets the current user's home directory.
The installer runs a fresh build and then asks for the target platform and client, unless they are passed as
parameters. Selecting Linux or Windows chooses the generated package format; installation always targets the current
user's home directory.
Preview an installation without changing user files or plugins:
Preview an installation without changing user files, backups, or plugins:
Powershell: (Windows)
```powershell
@@ -89,7 +97,7 @@ Bash: (Linux)
./scripts/install.sh --dry-run
```
Install the selected configuration:
Install the selected configuration interactively:
Powershell: (Windows)
```powershell
@@ -101,9 +109,29 @@ Bash: (Linux)
./scripts/install.sh
```
Existing managed files receive timestamped backups before replacement. Unrelated files are left untouched. Selecting
Codex also installs shared skills to `.agents/skills`. A normal installation ensures configured plugins are present and
updates plugins that are already installed.
Install non-interactively, for scripting or CI:
Powershell: (Windows)
```powershell
.\scripts\install.ps1 -Client Both -Platform Windows
```
Bash: (Linux)
```bash
./scripts/install.sh --client both --platform linux
```
`-Platform`/`--platform` defaults to the current platform when omitted together with `-Client`/`--client`.
Every installed file is tracked in a per-destination manifest (`.ai-config-manifest.tsv`). A second run of the same
installation is a no-op for unchanged files (no write, no backup). A file this setup installed before but no longer
ships is backed up and removed, unless it was changed locally since the last install, in which case it is backed up
and left in place with a warning instead. A file the installer never tracked is never touched, even if a file of the
same name is now part of the package. Backups land in a single timestamped directory per run under
`backups/<timestamp>/` inside each destination.
Selecting Codex also installs shared skills to `.agents/skills`. A normal installation only installs plugins that are
missing; add `-UpdatePlugins`/`--update-plugins` to also update and enable plugins that are already installed.
Run the same installation command whenever the repository configuration should be updated. There is no separate update
script.
@@ -127,10 +155,14 @@ The doctor reports concise `PASS`, `WARN`, and `FAIL` results without reading or
## Configuration
Codex receives global instructions, rules, skills, agent TOML files, hooks, and `config.toml`. Its defaults keep writes
workspace-scoped and use automatic review for eligible escalation requests.
workspace-scoped and use automatic review for eligible escalation requests. Every rule ships as a plain file under
`rules/`, and `AGENTS.md` tells Codex to load the matching one by path.
Claude Code receives the equivalent global instructions, rules, skills, agents, hooks, and `settings.json`. Its generated
settings use the supported automatic permission mode where available.
Claude Code receives the equivalent global instructions, agents, hooks, and `settings.json`. Only `general.md` ships as
an always-applied rule file (its content is also embedded directly in `CLAUDE.md`); every technology- or
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
generated settings use the supported automatic permission mode where available.
Both clients receive the same logical agent roles:
@@ -147,8 +179,10 @@ when research, design, independent review, or focused verification adds value.
### Add a rule
Create a focused Markdown file in `shared/rules/`. Add its loading condition to `shared/global-instructions.md` when the
rule applies only to a language, framework, tool, or change area.
Create a focused Markdown file in `shared/rules/`. For a rule that applies only to a language, framework, tool, or
change area, add a row to `adapters/claude/rule-skills.tsv` (Claude generates it as a skill) and add its loading
condition to the Codex rule-loading text in `scripts/build.ps1`/`scripts/build.sh` (Codex loads it as a plain file, by
path — unchanged from before). A rule that should always apply, like `general.md`, needs neither.
### Add a skill
+1 -1
View File
@@ -25,7 +25,7 @@
{
"type": "command",
"command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__\"",
"timeout": 20
"timeout": 8
}
]
}
+12
View File
@@ -0,0 +1,12 @@
rule_file skill_name trigger
angular.md rules-angular Angular component, template, or RxJS/signal 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
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
microsoft.md rules-microsoft Microsoft 365, Azure DevOps, or Teams operations
refactoring.md rules-refactoring refactoring existing code without changing behavior
security.md rules-security any software development task: implementing, modifying, debugging, reviewing, testing, or configuring code, scripts, hooks, or infrastructure
typescript.md rules-typescript TypeScript or modern ECMAScript implementation work
ui-ux.md rules-ui-ux UI or UX design and interaction decisions
wpf.md rules-wpf WPF, XAML, or MVVM desktop UI work
1 rule_file skill_name trigger
2 angular.md 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
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
8 refactoring.md rules-refactoring refactoring existing code without changing behavior
9 security.md rules-security any software development task: implementing, modifying, debugging, reviewing, testing, or configuring code, scripts, hooks, or infrastructure
10 typescript.md rules-typescript TypeScript or modern ECMAScript implementation work
11 ui-ux.md rules-ui-ux UI or UX design and interaction decisions
12 wpf.md rules-wpf WPF, XAML, or MVVM desktop UI work
+2 -2
View File
@@ -7,7 +7,7 @@ status_line = ["model", "reasoning", "project-name", "git-branch", "context-wind
[agents]
enabled = true
max_threads = 5
max_concurrent_threads_per_session = 5
[features]
hooks = true
@@ -17,4 +17,4 @@ hooks = true
type = "command"
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__"'
command_windows = '__WINDOWS_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__WINDOWS_HOOK_SCRIPT__"'
timeout = 20
timeout = 8
+3 -1
View File
@@ -1,6 +1,8 @@
# Configuration
Codex receives a generated global `AGENTS.md`, agent TOML files, skills, and a `config.toml` adapter with workspace sandboxing, interactive approval boundaries, automatic approval review, enabled sub-agents, and a five-thread concurrency limit. Claude Code receives a generated global `CLAUDE.md`, agent Markdown files, skills, and `settings.json` with the `auto` permission mode, explicit `Agent` permission, and a five-operation tool/sub-agent concurrency limit. The exact installed locations are resolved from the user profile at install time; no machine-specific absolute path is stored in the repository.
Codex receives a generated global `AGENTS.md`, agent TOML files, skills, rule files, and a `config.toml` adapter with workspace sandboxing, interactive approval boundaries, automatic approval review, enabled sub-agents, and a five-thread concurrency limit (`max_concurrent_threads_per_session`, the current canonical key; the legacy `max_threads` alias is not emitted). Claude Code receives a generated global `CLAUDE.md`, agent Markdown files, skills, and `settings.json` with the `auto` permission mode, explicit `Agent` permission, and a five-operation tool/sub-agent concurrency limit. The exact installed locations are resolved from the user profile at install time; no machine-specific absolute path is stored in the repository.
Claude's rule loading differs from Codex's by design: `general.md` is the only rule shipped as a plain, always-applied file (its text is also embedded directly in `CLAUDE.md`). Every technology- or situation-specific rule in `shared/rules/` is instead generated as a skill under `skills/rules/`, driven by `adapters/claude/rule-skills.tsv` (rule file, skill name, trigger description). Only a skill's name and description are ever permanently in context; Claude loads the full rule text only when it invokes the skill. Codex is unaffected — it keeps receiving every rule as a plain file and is told to load the matching one by path, exactly as before.
Neither current client configuration model exposes a supported global `max_depth` setting. The repository does not emit an unsupported key that would be ignored or cause configuration validation errors.
+1 -1
View File
@@ -2,7 +2,7 @@
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, and Context7. The install script operates at user scope and is idempotent: existing Claude plugins are enabled and updated, and Codex plugins are updated or ensured through the Codex plugin command.
The current baseline includes Ponytail, i-have-adhd, Superpowers, and Context7. 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.
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.
+5
View File
@@ -13,3 +13,8 @@ The current structure is organized by purpose:
- `git/`: pull request responses and reviews
The files intentionally define only a small baseline. Detailed personal workflows and standards can be added later without changing the structure.
Claude's generated package additionally has `skills/rules/`: one skill per technology- or situation-specific rule in
`shared/rules/` (driven by `adapters/claude/rule-skills.tsv`), so the full rule text loads only when Claude invokes the
matching skill instead of sitting permanently in context. This category does not exist for Codex, which keeps loading
every rule as a plain file instead. See `docs/configuration.md`.
+137 -40
View File
@@ -1,20 +1,40 @@
[CmdletBinding()]
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$shared = Join-Path $root 'shared'
$output = Join-Path $root 'generated'
$pluginManifest = Join-Path $root 'adapters/plugins.tsv'
$ruleSkillManifest = Join-Path $root 'adapters/claude/rule-skills.tsv'
if (-not (Test-Path -LiteralPath $pluginManifest)) { throw "Missing plugin manifest: $pluginManifest" }
$pluginEntries = @(Import-Csv -LiteralPath $pluginManifest -Delimiter ([char]9))
if ($pluginEntries.Count -ne 4) { throw 'Plugin manifest must define exactly four plugins.' }
if ($pluginEntries.Count -eq 0) { throw 'Plugin manifest must define at least one plugin.' }
$pluginNames = @()
foreach ($pluginEntry in $pluginEntries) {
foreach ($field in @('claude_plugin','codex_plugin')) {
foreach ($field in @('name','claude_plugin','codex_plugin')) {
if ([string]::IsNullOrWhiteSpace($pluginEntry.$field)) { throw "Missing $field for plugin $($pluginEntry.name)." }
}
if ($pluginNames -contains $pluginEntry.name) { throw "Duplicate plugin name in manifest: $($pluginEntry.name)" }
$pluginNames += $pluginEntry.name
}
if (-not (Test-Path -LiteralPath $ruleSkillManifest)) { throw "Missing rule-skill manifest: $ruleSkillManifest" }
$ruleSkillEntries = @(Import-Csv -LiteralPath $ruleSkillManifest -Delimiter ([char]9) | Sort-Object skill_name)
if ($ruleSkillEntries.Count -eq 0) { throw 'Rule-skill manifest must define at least one entry.' }
$ruleSkillNames = @()
foreach ($entry in $ruleSkillEntries) {
foreach ($field in @('rule_file','skill_name','trigger')) {
if ([string]::IsNullOrWhiteSpace($entry.$field)) { throw "Missing $field in rule-skill manifest row for $($entry.rule_file)." }
}
if (-not (Test-Path -LiteralPath (Join-Path $shared "rules/$($entry.rule_file)"))) { throw "Rule-skill manifest references a missing rule file: $($entry.rule_file)" }
if ($ruleSkillNames -contains $entry.skill_name) { throw "Duplicate rule-skill name in manifest: $($entry.skill_name)" }
$ruleSkillNames += $entry.skill_name
}
Remove-Item $output -Recurse -Force -ErrorAction SilentlyContinue
New-Item $output -ItemType Directory -Force | Out-Null
function Copy-Directory($source, $destination) {
New-Item $destination -ItemType Directory -Force | Out-Null
Get-ChildItem $source -File -Recurse | ForEach-Object {
@@ -32,47 +52,124 @@ function Read-Field($path, $name) {
function Quote-Toml($value) {
return ('"' + $value.Replace('\','\\').Replace('"','\"').Replace("`r",'').Replace("`n",'\n') + '"')
}
& (Join-Path $root 'shared/hooks/scripts/Test-SessionConfig.ps1') -RepositoryRoot $root
$sourceAgentDirs = @(Get-ChildItem (Join-Path $shared 'agents') -Directory | Sort-Object Name)
if ($sourceAgentDirs.Count -eq 0) { throw 'No agents found under shared/agents.' }
$agentNames = @()
foreach ($agentDir in $sourceAgentDirs) {
$name = Read-Field (Join-Path $agentDir.FullName 'agent.yml') 'name'
if ($agentNames -contains $name) { throw "Duplicate agent name: $name" }
$agentNames += $name
}
# Codex keeps the original rule-loading text unchanged: every rule ships as a plain
# file and AGENTS.md tells Codex to load the matching one by path.
$codexRuleLoading = @'
Always load and apply `rules/general.md` before starting any task.
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Load every applicable rule file before editing. Load all matching files when multiple technologies apply.
Load rule files when their subject applies:
- rules/angular.md for Angular work.
- rules/typescript.md for TypeScript work.
- rules/csharp.md for C# or .NET work.
- rules/wpf.md for WPF work.
- rules/ui-ux.md for UI or UX decisions.
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
- rules/git.md for Git operations.
- rules/refactoring.md for refactoring work.
- rules/definition-of-done.md when validating completion.
- rules/decision-rule.md when requirements, behavior, or technical choices need to be evaluated.
'@
foreach ($platform in @('windows','linux')) {
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "codex-$platform/skills")
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "claude-$platform/skills")
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "codex-$platform/rules")
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "claude-$platform/rules")
New-Item (Join-Path $output "codex-$platform") -ItemType Directory -Force | Out-Null
New-Item (Join-Path $output "claude-$platform") -ItemType Directory -Force | Out-Null
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "codex-$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-Item (Join-Path $shared 'global-instructions.md') (Join-Path $output "codex-$platform/AGENTS.md") -Force
Copy-Item (Join-Path $shared 'global-instructions.md') (Join-Path $output "claude-$platform/CLAUDE.md") -Force
Copy-Item (Join-Path $root 'adapters/codex/config/config.toml') (Join-Path $output "codex-$platform/config.toml") -Force
Copy-Item (Join-Path $root 'adapters/claude/config/settings.json') (Join-Path $output "claude-$platform/settings.json") -Force
foreach ($client in @('codex','claude')) {
$agentsOutput = Join-Path $output "$client-$platform/agents"
New-Item $agentsOutput -ItemType Directory -Force | Out-Null
Get-ChildItem (Join-Path $shared 'agents') -Directory | Sort-Object Name | ForEach-Object {
$metadata = Join-Path $_.FullName 'agent.yml'
$name = Read-Field $metadata 'name'
$description = Read-Field $metadata 'description'
$instructions = Get-Content (Join-Path $_.FullName 'instructions.md') -Raw
if ($client -eq 'codex') {
Set-Content (Join-Path $agentsOutput "$name.toml") "name = $(Quote-Toml $name)`r`ndescription = $(Quote-Toml $description)`r`ndeveloper_instructions = $(Quote-Toml $instructions)" -Encoding UTF8
} else {
Set-Content (Join-Path $agentsOutput "$name.md") "---`r`nname: $name`r`ndescription: $description`r`n---`r`n`r`n$instructions" -Encoding UTF8
New-Item (Join-Path $output "codex-$platform") -ItemType Directory -Force | Out-Null
New-Item (Join-Path $output "claude-$platform") -ItemType Directory -Force | Out-Null
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "codex-$platform/skills")
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "claude-$platform/skills")
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "codex-$platform/rules")
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "codex-$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")
# 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"
New-Item $claudeRulesDir -ItemType Directory -Force | Out-Null
Copy-Item (Join-Path $shared 'rules/general.md') (Join-Path $claudeRulesDir 'general.md') -Force
$claudeRuleSkillLines = @()
foreach ($entry in $ruleSkillEntries) {
$skillDir = Join-Path $output "claude-$platform/skills/rules/$($entry.skill_name)"
New-Item $skillDir -ItemType Directory -Force | Out-Null
$ruleContent = Get-Content (Join-Path $shared "rules/$($entry.rule_file)") -Raw
$skillBody = "---`r`nname: $($entry.skill_name)`r`ndescription: Use for $($entry.trigger).`r`n---`r`n`r`n$ruleContent"
Set-Content (Join-Path $skillDir 'SKILL.md') $skillBody -Encoding UTF8
$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")
$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
$claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading)
$claudeContent = $claudeContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
Set-Content (Join-Path $output "claude-$platform/CLAUDE.md") $claudeContent -Encoding UTF8
Copy-Item (Join-Path $root 'adapters/codex/config/config.toml') (Join-Path $output "codex-$platform/config.toml") -Force
Copy-Item (Join-Path $root 'adapters/claude/config/settings.json') (Join-Path $output "claude-$platform/settings.json") -Force
foreach ($client in @('codex','claude')) {
$agentsOutput = Join-Path $output "$client-$platform/agents"
New-Item $agentsOutput -ItemType Directory -Force | Out-Null
foreach ($agentDir in $sourceAgentDirs) {
$metadata = Join-Path $agentDir.FullName 'agent.yml'
$name = Read-Field $metadata 'name'
$description = Read-Field $metadata 'description'
$instructions = Get-Content (Join-Path $agentDir.FullName 'instructions.md') -Raw
if ($client -eq 'codex') {
Set-Content (Join-Path $agentsOutput "$name.toml") "name = $(Quote-Toml $name)`r`ndescription = $(Quote-Toml $description)`r`ndeveloper_instructions = $(Quote-Toml $instructions)" -Encoding UTF8
} else {
Set-Content (Join-Path $agentsOutput "$name.md") "---`r`nname: $name`r`ndescription: $description`r`n---`r`n`r`n$instructions" -Encoding UTF8
}
}
}
foreach ($client in @('codex','claude')) {
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
$path = Join-Path $output "$client-$platform/$file"
$command = if ($platform -eq 'windows') { 'powershell -NoProfile -ExecutionPolicy Bypass -File' } else { 'bash' }
$script = if ($platform -eq 'windows') { 'flashbang.ps1' } else { 'flashbang.sh' }
$statusLineScript = if ($platform -eq 'windows') { 'statusline.ps1' } else { 'statusline.sh' }
$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)
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
}
$tomlPath = Join-Path $output "codex-$platform/config.toml"
$tomlContent = Get-Content -LiteralPath $tomlPath -Raw
if (([regex]::Matches($tomlContent, '(?<!\\)"')).Count % 2 -ne 0) { throw "Generated Codex config.toml has unbalanced quotes: $tomlPath" }
if (([regex]::Matches($tomlContent, '\[')).Count -ne ([regex]::Matches($tomlContent, '\]')).Count) { throw "Generated Codex config.toml has unbalanced brackets: $tomlPath" }
$settingsPath = Join-Path $output "claude-$platform/settings.json"
try { Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json | Out-Null }
catch { throw "Generated Claude settings.json is not valid JSON: $settingsPath" }
}
foreach ($client in @('codex','claude')) {
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
$path = Join-Path $output "$client-$platform/$file"
$command = if ($platform -eq 'windows') { 'powershell -NoProfile -ExecutionPolicy Bypass -File' } else { 'bash' }
$script = if ($platform -eq 'windows') { 'flashbang.ps1' } else { 'flashbang.sh' }
$statusLineScript = if ($platform -eq 'windows') { 'statusline.ps1' } else { 'statusline.sh' }
$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)
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
}
}
$leftoverPlaceholders = @(Get-ChildItem $output -File -Recurse | ForEach-Object {
# __AI_CONFIG_ROOT__ is resolved at install time, once the destination is known; it is
# expected to remain in generated output. -cmatch keeps this case-sensitive so Python
# dunder names (__main__, __name__) in flashbang.sh are not mistaken for placeholders.
$content = (Get-Content -LiteralPath $_.FullName -Raw) -replace '__AI_CONFIG_ROOT__', ''
if ($content -cmatch '__[A-Z0-9_]+__') { $_.FullName }
})
if ($leftoverPlaceholders.Count) { throw "Unresolved template placeholders in: $($leftoverPlaceholders -join ', ')" }
Write-Output 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux'
+129 -9
View File
@@ -5,8 +5,40 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
shared="$root/shared"
output="$root/generated"
plugin_manifest="$root/adapters/plugins.tsv"
rule_skill_manifest="$root/adapters/claude/rule-skills.tsv"
[ -f "$plugin_manifest" ] || { printf 'Missing plugin manifest: %s\n' "$plugin_manifest" >&2; exit 1; }
[ "$(awk 'NR > 1 && NF == 5 { count++ } END { print count + 0 }' "$plugin_manifest")" -eq 4 ] || { printf 'Plugin manifest must define exactly four plugins.\n' >&2; exit 1; }
declare -A plugin_seen
plugin_count=0
while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin; do
[ "$name" != name ] || continue
[ -n "$name" ] || continue
plugin_count=$((plugin_count + 1))
[ -n "$claude_plugin" ] || { printf 'Missing claude_plugin for plugin %s.\n' "$name" >&2; exit 1; }
[ -n "$codex_plugin" ] || { printf 'Missing codex_plugin for plugin %s.\n' "$name" >&2; exit 1; }
[ -z "${plugin_seen[$name]+x}" ] || { printf 'Duplicate plugin name in manifest: %s\n' "$name" >&2; exit 1; }
plugin_seen[$name]=1
done < "$plugin_manifest"
[ "$plugin_count" -gt 0 ] || { printf 'Plugin manifest must define at least one plugin.\n' >&2; exit 1; }
[ -f "$rule_skill_manifest" ] || { printf 'Missing rule-skill manifest: %s\n' "$rule_skill_manifest" >&2; exit 1; }
declare -A rule_skill_seen
rule_skill_files=()
rule_skill_names=()
rule_skill_triggers=()
while IFS=$'\t' read -r rule_file skill_name trigger; do
[ "$rule_file" != 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 "$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; }
[ -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_files+=("$rule_file")
rule_skill_names+=("$skill_name")
rule_skill_triggers+=("$trigger")
done < "$rule_skill_manifest"
[ "${#rule_skill_names[@]}" -gt 0 ] || { printf 'Rule-skill manifest must define at least one entry.\n' >&2; exit 1; }
copy_directory() {
local source=$1 destination=$2
@@ -32,29 +64,91 @@ quote_toml() {
bash "$shared/hooks/scripts/test-session-config.sh" "$root"
rm -rf -- "$output"
agent_dirs=("$shared"/agents/*/)
declare -A agent_seen
for agent_dir in "${agent_dirs[@]}"; do
name=$(read_field "${agent_dir}agent.yml" name)
[ -z "${agent_seen[$name]+x}" ] || { printf 'Duplicate agent name: %s\n' "$name" >&2; exit 1; }
agent_seen[$name]=1
done
# Codex keeps the original rule-loading text unchanged: every rule ships as a plain
# file and AGENTS.md tells Codex to load the matching one by path.
read -r -d '' codex_rule_loading <<'BLOCK' || true
Always load and apply `rules/general.md` before starting any task.
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Load every applicable rule file before editing. Load all matching files when multiple technologies apply.
Load rule files when their subject applies:
- rules/angular.md for Angular work.
- rules/typescript.md for TypeScript work.
- rules/csharp.md for C# or .NET work.
- rules/wpf.md for WPF work.
- rules/ui-ux.md for UI or UX decisions.
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
- rules/git.md for Git operations.
- rules/refactoring.md for refactoring work.
- rules/definition-of-done.md when validating completion.
- rules/decision-rule.md when requirements, behavior, or technical choices need to be evaluated.
BLOCK
for platform in windows linux; do
mkdir -p "$output/codex-$platform" "$output/claude-$platform"
copy_directory "$shared/skills" "$output/codex-$platform/skills"
copy_directory "$shared/skills" "$output/claude-$platform/skills"
copy_directory "$shared/rules" "$output/codex-$platform/rules"
copy_directory "$shared/rules" "$output/claude-$platform/rules"
copy_directory "$shared/hooks" "$output/codex-$platform/hooks"
copy_directory "$shared/hooks" "$output/claude-$platform/hooks"
copy_directory "$shared/statusline" "$output/claude-$platform/statusline"
cp "$shared/global-instructions.md" "$output/codex-$platform/AGENTS.md"
cp "$shared/global-instructions.md" "$output/claude-$platform/CLAUDE.md"
# 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"
cp "$shared/rules/general.md" "$output/claude-$platform/rules/general.md"
claude_rule_loading_list=""
for i in "${!rule_skill_names[@]}"; do
rule_file=${rule_skill_files[$i]}
skill_name=${rule_skill_names[$i]}
trigger=${rule_skill_triggers[$i]}
skill_dir="$output/claude-$platform/skills/rules/$skill_name"
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"
claude_rule_loading_list="${claude_rule_loading_list}- ${skill_name} for ${trigger}.
"
done
claude_rule_loading="Always apply \`rules/general.md\` before starting any task, 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.
Invoke rule skills when their subject applies:
${claude_rule_loading_list%$'\n'}"
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")
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"
cp "$root/adapters/codex/config/config.toml" "$output/codex-$platform/config.toml"
cp "$root/adapters/claude/config/settings.json" "$output/claude-$platform/settings.json"
for client in codex claude; do
agents_output="$output/$client-$platform/agents"
mkdir -p "$agents_output"
for agent in "$shared"/agents/*; do
metadata="$agent/agent.yml"
for agent_dir in "${agent_dirs[@]}"; do
metadata="${agent_dir}agent.yml"
name=$(read_field "$metadata" name)
description=$(read_field "$metadata" description)
instructions=$(<"$agent/instructions.md")
instructions=$(<"${agent_dir}instructions.md")
if [ "$client" = codex ]; then
printf 'name = %s\ndescription = %s\ndeveloper_instructions = %s\n' "$(quote_toml "$name")" "$(quote_toml "$description")" "$(quote_toml "$instructions")" > "$agents_output/$name.toml"
else
@@ -84,6 +178,32 @@ for client in codex claude; do
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
printf '%s\n' "$content" > "$path"
done
done
printf 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux\n'
toml_path="$output/codex-$platform/config.toml"
quote_count=$(grep -o '"' "$toml_path" | wc -l)
[ $((quote_count % 2)) -eq 0 ] || { printf 'Generated Codex config.toml has unbalanced quotes: %s\n' "$toml_path" >&2; exit 1; }
open_brackets=$(grep -o '\[' "$toml_path" | wc -l)
close_brackets=$(grep -o '\]' "$toml_path" | wc -l)
[ "$open_brackets" -eq "$close_brackets" ] || { printf 'Generated Codex config.toml has unbalanced brackets: %s\n' "$toml_path" >&2; exit 1; }
settings_path="$output/claude-$platform/settings.json"
if command -v jq >/dev/null 2>&1; then
jq empty "$settings_path" >/dev/null 2>&1 || { printf 'Generated Claude settings.json is not valid JSON: %s\n' "$settings_path" >&2; exit 1; }
elif command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; then
# On Windows, `python3` can resolve to a non-functional Microsoft Store app-execution
# alias that still passes `command -v`; probe it with a real invocation before trusting it.
python3 -c 'import json,sys; json.load(open(sys.argv[1], encoding="utf-8"))' "$settings_path" || { printf 'Generated Claude settings.json is not valid JSON: %s\n' "$settings_path" >&2; exit 1; }
fi
done
# __AI_CONFIG_ROOT__ is resolved at install time, once the destination is known; it is
# expected to remain in generated output, so it is excluded from the leftover check. A
# single tree-wide grep (rather than one process per file) keeps this fast.
leftover_tokens=$(grep -RohE '__[A-Z0-9_]+__' "$output" 2>/dev/null | sort -u | grep -v '^__AI_CONFIG_ROOT__$' || true)
if [ -n "$leftover_tokens" ]; then
offending_files=$(grep -RlE '__[A-Z0-9_]+__' "$output" 2>/dev/null | tr '\n' ' ')
printf 'Unresolved template placeholders (%s) in: %s\n' "$(printf '%s' "$leftover_tokens" | tr '\n' ' ')" "$offending_files" >&2
exit 1
fi
printf 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux\n'
+54 -3
View File
@@ -2,16 +2,67 @@
param()
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
$homePath = [Environment]::GetFolderPath('UserProfile')
$fail = $false
function Result($state, $message) { Write-Output ("$state $message"); if ($state -eq 'FAIL') { $script:fail = $true } }
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' } }
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory -ErrorAction SilentlyContinue).Count
$sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse -ErrorAction SilentlyContinue).Count
$ruleSkillManifestPath = Join-Path $root 'adapters/claude/rule-skills.tsv'
$ruleSkillCount = if (Test-Path -LiteralPath $ruleSkillManifestPath) { @(Import-Csv -LiteralPath $ruleSkillManifestPath -Delimiter ([char]9)).Count } else { 0 }
foreach ($platform in @('windows','linux')) {
if (Test-Path (Join-Path $root "generated/codex-$platform/AGENTS.md")) { Result 'PASS' 'generated output present' } else { Result 'FAIL' 'generated output missing; run build' }
if ((Test-Path (Join-Path $root "generated/codex-$platform/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/codex-$platform/hooks/scripts/validate-command-safety.sh")) -and (Test-Path (Join-Path $root "generated/claude-$platform/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/claude-$platform/hooks/scripts/validate-command-safety.sh"))) { Result 'PASS' 'generated hooks present' } else { Result 'FAIL' 'generated hooks missing; run build' }
if (Test-Path (Join-Path $root "generated/codex-$platform/AGENTS.md")) { Result 'PASS' "generated output present ($platform)" } else { Result 'FAIL' "generated output missing ($platform); run build" }
if ((Test-Path (Join-Path $root "generated/codex-$platform/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/codex-$platform/hooks/scripts/validate-command-safety.sh")) -and (Test-Path (Join-Path $root "generated/claude-$platform/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/claude-$platform/hooks/scripts/validate-command-safety.sh"))) { Result 'PASS' "generated hooks present ($platform)" } else { Result 'FAIL' "generated hooks missing ($platform); run build" }
foreach ($client in @('codex','claude')) {
$package = Join-Path $root "generated/$client-$platform"
if (-not (Test-Path $package)) { continue }
$agentCount = @(Get-ChildItem (Join-Path $package 'agents') -File -ErrorAction SilentlyContinue).Count
if ($agentCount -eq $sourceAgentCount) { Result 'PASS' "$client-$platform agent count matches source ($agentCount)" } else { Result 'FAIL' "$client-$platform agent count $agentCount does not match source ($sourceAgentCount)" }
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
$skillCount = @(Get-ChildItem (Join-Path $package 'skills') -Filter 'SKILL.md' -Recurse -ErrorAction SilentlyContinue).Count
if ($skillCount -eq $expectedSkillCount) { Result 'PASS' "$client-$platform skill count matches source ($skillCount)" } else { Result 'FAIL' "$client-$platform skill count $skillCount does not match source ($expectedSkillCount)" }
$placeholderHits = @(Get-ChildItem $package -File -Recurse | Where-Object { ((Get-Content -LiteralPath $_.FullName -Raw) -replace '__AI_CONFIG_ROOT__', '') -cmatch '__[A-Z0-9_]+__' })
if ($placeholderHits.Count -eq 0) { Result 'PASS' "$client-$platform has no unresolved placeholders" } else { Result 'FAIL' "$client-$platform has unresolved placeholders: $($placeholderHits.FullName -join ', ')" }
}
}
foreach ($path in @('.codex/AGENTS.md','.codex/config.toml','.claude/CLAUDE.md','.claude/settings.json')) { if (Test-Path (Join-Path $homePath $path)) { Result 'PASS' "installed $path" } else { Result 'WARN' "not installed $path" } }
foreach ($path in @('.codex/AGENTS.md','.codex/config.toml','.claude/CLAUDE.md','.claude/settings.json')) {
if (Test-Path (Join-Path $homePath $path)) { Result 'PASS' "installed $path" } else { Result 'WARN' "not installed $path" }
}
$settingsPath = Join-Path $homePath '.claude/settings.json'
if (Test-Path -LiteralPath $settingsPath) {
try { Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json | Out-Null; Result 'PASS' 'installed .claude/settings.json is valid JSON' }
catch { Result 'FAIL' 'installed .claude/settings.json is not valid JSON' }
}
$configTomlPath = Join-Path $homePath '.codex/config.toml'
if (Test-Path -LiteralPath $configTomlPath) {
$tomlContent = Get-Content -LiteralPath $configTomlPath -Raw
$balanced = (([regex]::Matches($tomlContent, '(?<!\\)"')).Count % 2 -eq 0) -and (([regex]::Matches($tomlContent, '\[')).Count -eq ([regex]::Matches($tomlContent, '\]')).Count)
if ($balanced) { Result 'PASS' 'installed .codex/config.toml looks structurally valid' } else { Result 'FAIL' 'installed .codex/config.toml has unbalanced quotes or brackets' }
}
foreach ($destination in @((Join-Path $homePath '.codex'), (Join-Path $homePath '.agents/skills'), (Join-Path $homePath '.claude'))) {
$manifestPath = Get-ManagedManifestPath $destination
if (-not (Test-Path -LiteralPath $manifestPath)) { continue }
$manifest = Read-ManagedManifest $manifestPath
$missing = @()
$modified = @()
foreach ($relative in $manifest.Keys) {
$target = Join-Path $destination $relative
if (-not (Test-Path -LiteralPath $target)) { $missing += $relative; continue }
if ((Get-Sha256Hash $target) -ne $manifest[$relative]) { $modified += $relative }
}
if ($missing.Count) { Result 'WARN' "$destination is missing managed files: $($missing -join ', ')" } else { Result 'PASS' "$destination has no missing managed files" }
if ($modified.Count) { Result 'WARN' "$destination has locally modified managed files: $($modified -join ', ')" } else { Result 'PASS' "$destination has no locally modified managed files" }
}
foreach ($path in @('shared/rules','shared/skills','shared/agents','shared/hooks/scripts')) { if (Test-Path (Join-Path $root $path)) { Result 'PASS' "source $path" } else { Result 'FAIL' "missing $path" } }
if ($fail) { exit 1 }
Write-Output 'PASS doctor'
+63 -2
View File
@@ -2,6 +2,7 @@
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/manifest.sh"
home_path=${HOME:?HOME is required}
failed=false
@@ -14,15 +15,75 @@ for tool in codex claude; do
if command -v "$tool" >/dev/null; then result PASS "$tool available ($($tool --version 2>&1 | head -n 1))"; else result WARN "$tool unavailable"; fi
done
command -v jq >/dev/null && result PASS 'jq available for Claude status line' || result WARN 'jq unavailable; Claude status line is disabled'
source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
source_skill_count=$(find "$root/shared/skills" -name SKILL.md 2>/dev/null | wc -l)
rule_skill_manifest="$root/adapters/claude/rule-skills.tsv"
rule_skill_count=0
[ -f "$rule_skill_manifest" ] && rule_skill_count=$(($(wc -l < "$rule_skill_manifest") - 1))
for platform in windows linux; do
[ -f "$root/generated/codex-$platform/AGENTS.md" ] && result PASS 'generated output present' || result FAIL 'generated output missing; run build'
[ -f "$root/generated/codex-$platform/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/codex-$platform/hooks/scripts/validate-command-safety.sh" ] && [ -f "$root/generated/claude-$platform/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/claude-$platform/hooks/scripts/validate-command-safety.sh" ] && result PASS 'generated hooks present' || result FAIL 'generated hooks missing; run build'
[ -f "$root/generated/codex-$platform/AGENTS.md" ] && result PASS "generated output present ($platform)" || result FAIL "generated output missing ($platform); run build"
[ -f "$root/generated/codex-$platform/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/codex-$platform/hooks/scripts/validate-command-safety.sh" ] && [ -f "$root/generated/claude-$platform/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/claude-$platform/hooks/scripts/validate-command-safety.sh" ] && result PASS "generated hooks present ($platform)" || result FAIL "generated hooks missing ($platform); run build"
for client in codex claude; do
package="$root/generated/$client-$platform"
[ -d "$package" ] || continue
agent_count=$(find "$package/agents" -type f 2>/dev/null | wc -l)
if [ "$agent_count" -eq "$source_agent_count" ]; then result PASS "$client-$platform agent count matches source ($agent_count)"; else result FAIL "$client-$platform agent count $agent_count does not match source ($source_agent_count)"; fi
expected_skill_count=$source_skill_count
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
skill_count=$(find "$package/skills" -name SKILL.md 2>/dev/null | wc -l)
if [ "$skill_count" -eq "$expected_skill_count" ]; then result PASS "$client-$platform skill count matches source ($skill_count)"; else result FAIL "$client-$platform skill count $skill_count does not match source ($expected_skill_count)"; fi
leftover_tokens=$(grep -RohE '__[A-Z0-9_]+__' "$package" 2>/dev/null | sort -u | grep -v '^__AI_CONFIG_ROOT__$' || true)
if [ -n "$leftover_tokens" ]; then result FAIL "$client-$platform has unresolved placeholders"; else result PASS "$client-$platform has no unresolved placeholders"; fi
done
done
for path in .codex/AGENTS.md .codex/config.toml .claude/CLAUDE.md .claude/settings.json; do
[ -f "$home_path/$path" ] && result PASS "installed $path" || result WARN "not installed $path"
done
if [ -f "$home_path/.claude/settings.json" ]; then
if command -v jq >/dev/null 2>&1; then
if jq empty "$home_path/.claude/settings.json" >/dev/null 2>&1; then result PASS 'installed .claude/settings.json is valid JSON'; else result FAIL 'installed .claude/settings.json is not valid JSON'; fi
elif command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; then
if python3 -c 'import json,sys; json.load(open(sys.argv[1], encoding="utf-8"))' "$home_path/.claude/settings.json" 2>/dev/null; then
result PASS 'installed .claude/settings.json is valid JSON'
else
result FAIL 'installed .claude/settings.json is not valid JSON'
fi
fi
fi
if [ -f "$home_path/.codex/config.toml" ]; then
quote_count=$(grep -o '"' "$home_path/.codex/config.toml" | wc -l)
open_brackets=$(grep -o '\[' "$home_path/.codex/config.toml" | wc -l)
close_brackets=$(grep -o '\]' "$home_path/.codex/config.toml" | wc -l)
if [ $((quote_count % 2)) -eq 0 ] && [ "$open_brackets" -eq "$close_brackets" ]; then
result PASS 'installed .codex/config.toml looks structurally valid'
else
result FAIL 'installed .codex/config.toml has unbalanced quotes or brackets'
fi
fi
for destination in "$home_path/.codex" "$home_path/.agents/skills" "$home_path/.claude"; do
manifest=$(manifest_path "$destination")
[ -f "$manifest" ] || continue
declare -A managed
read_managed_manifest "$manifest" managed
missing=()
modified=()
for relative in "${!managed[@]}"; do
target="$destination/$relative"
if [ ! -f "$target" ]; then missing+=("$relative"); continue; fi
[ "$(sha256_of_file "$target")" = "${managed[$relative]}" ] || modified+=("$relative")
done
if [ "${#missing[@]}" -eq 0 ]; then result PASS "$destination has no missing managed files"; else result WARN "$destination is missing managed files: ${missing[*]}"; fi
if [ "${#modified[@]}" -eq 0 ]; then result PASS "$destination has no locally modified managed files"; else result WARN "$destination has locally modified managed files: ${modified[*]}"; fi
done
for path in shared/rules shared/skills shared/agents shared/hooks/scripts; do
[ -e "$root/$path" ] && result PASS "source $path" || result FAIL "missing $path"
done
"$failed" && exit 1
printf 'PASS doctor\n'
+31 -37
View File
@@ -1,24 +1,38 @@
[CmdletBinding()]
param([switch]$DryRun)
param(
[switch]$DryRun,
[ValidateSet('Codex','Claude','Both')][string]$Client,
[ValidateSet('Windows','Linux')][string]$Platform,
[switch]$UpdatePlugins
)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
& $buildScript
if (-not $?) { throw 'Build failed. Installation was not started.' }
$generated = Join-Path $root 'generated'
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
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]
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]
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'
$homePath = [Environment]::GetFolderPath('UserProfile')
$shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File'
@@ -28,31 +42,11 @@ $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) {
$backupRoot = Join-Path $item.Destination 'backups'
Get-ChildItem $item.Source -File -Recurse | ForEach-Object {
$relative = $_.FullName.Substring($item.Source.Length).TrimStart([char[]]@('\','/'))
$target = Join-Path $item.Destination $relative
if ($DryRun) { Write-Output "DRYRUN $($_.FullName) -> $target"; return }
if (Test-Path $target) {
$backup = Join-Path $backupRoot $relative
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
Copy-Item $target "$backup.$stamp" -Force
}
New-Item (Split-Path $target -Parent) -ItemType Directory -Force | Out-Null
$content = Get-Content $_.FullName -Raw
if ($content.Contains('__AI_CONFIG_ROOT__') -or $content.Contains('__HOOK_COMMAND__') -or $content.Contains('__WINDOWS_HOOK_COMMAND__')) {
$replacement = $item.Destination.Replace('\','/')
$content = $content.Replace('__AI_CONFIG_ROOT__', $replacement)
$content = $content.Replace('__HOOK_COMMAND__', $shellCommand)
$content = $content.Replace('__WINDOWS_HOOK_COMMAND__', $windowsShellCommand)
$content = $content.Replace('__HOOK_SCRIPT__', 'flashbang.ps1')
$content = $content.Replace('__WINDOWS_HOOK_SCRIPT__', 'flashbang.ps1')
[System.IO.File]::WriteAllText($target, $content, (New-Object System.Text.UTF8Encoding($false)))
}
else { Copy-Item $_.FullName $target -Force }
Write-Output "INSTALL $target"
}
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun
}
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Update
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Update:$UpdatePlugins
Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' }))
+52 -47
View File
@@ -2,71 +2,76 @@
set -euo pipefail
dry_run=false
for argument in "$@"; do
[ "$argument" != "--dry-run" ] || dry_run=true
client=""
platform=""
update_plugins=false
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) dry_run=true; shift ;;
--update-plugins) update_plugins=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"
bash "$root/scripts/build.sh"
generated="$root/generated"
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
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
printf 'Select installation target:\n1) Codex\n2) Claude\n3) Both\n'
while :; do
read -r -p 'Selection [1-3] ' selection
case "$selection" in 1|2|3) break;; esac
done
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)
home_path=${HOME:?HOME is required}
shell_command=bash
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
targets=()
case "$selection" in
1|3) targets+=("$generated/codex-$platform|$home_path/.codex" "$generated/codex-$platform/skills|$home_path/.agents/skills");;
case "$client" in
codex|both) targets+=("$generated/codex-$platform|$home_path/.codex" "$generated/codex-$platform/skills|$home_path/.agents/skills");;
esac
case "$selection" in
2|3) targets+=("$generated/claude-$platform|$home_path/.claude");;
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#*|}
while IFS= read -r -d '' source_file; do
relative=${source_file#"$source/"}
target="$destination/$relative"
if "$dry_run"; then printf 'DRYRUN %s -> %s\n' "$source_file" "$target"; continue; fi
if [ -f "$target" ]; then
backup="$destination/backups/$relative.$stamp"
mkdir -p "$(dirname -- "$backup")"
cp "$target" "$backup"
fi
mkdir -p "$(dirname -- "$target")"
content=$(<"$source_file")
if [[ "$content" == *'__AI_CONFIG_ROOT__'* || "$content" == *'__HOOK_COMMAND__'* || "$content" == *'__WINDOWS_HOOK_COMMAND__'* ]]; then
replacement=$destination
if command -v cygpath >/dev/null; then replacement=$(cygpath -m "$destination"); fi
content=${content//__AI_CONFIG_ROOT__/$replacement}
content=${content//__HOOK_COMMAND__/bash}
content=${content//__WINDOWS_HOOK_COMMAND__/bash}
content=${content//__HOOK_SCRIPT__/flashbang.sh}
content=${content//__WINDOWS_HOOK_SCRIPT__/flashbang.sh}
printf '%s' "$content" > "$target"
else
cp "$source_file" "$target"
fi
printf 'INSTALL %s\n' "$target"
done < <(find "$source" -type f -print0)
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"
done
client_selection=both
case "$selection" in
1) client_selection=codex;;
2) client_selection=claude;;
esac
install_configured_plugins "$root" "$client_selection" "$dry_run" true
install_configured_plugins "$root" "$client" "$dry_run" "$update_plugins"
if "$dry_run"; then printf 'PASS install dry-run\n'; else printf 'PASS install\n'; fi
+108
View File
@@ -0,0 +1,108 @@
function Get-ManagedManifestPath {
param([Parameter(Mandatory=$true)][string]$Destination)
return (Join-Path $Destination '.ai-config-manifest.tsv')
}
function Get-Sha256Hash {
param([Parameter(Mandatory=$true)][string]$Path)
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
}
function Get-Sha256HashOfBytes {
param([Parameter(Mandatory=$true)][byte[]]$Bytes)
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try { return ([System.BitConverter]::ToString($sha256.ComputeHash($Bytes))).Replace('-', '') }
finally { $sha256.Dispose() }
}
function Read-ManagedManifest {
param([Parameter(Mandatory=$true)][string]$ManifestPath)
$entries = @{}
if (Test-Path -LiteralPath $ManifestPath) {
Import-Csv -LiteralPath $ManifestPath -Delimiter ([char]9) | ForEach-Object { $entries[$_.path] = $_.sha256 }
}
return $entries
}
function Write-ManagedManifest {
param([Parameter(Mandatory=$true)][string]$ManifestPath, [Parameter(Mandatory=$true)][hashtable]$Entries)
$lines = @("path`tsha256")
foreach ($path in ($Entries.Keys | Sort-Object)) { $lines += "$path`t$($Entries[$path])" }
New-Item (Split-Path $ManifestPath -Parent) -ItemType Directory -Force | Out-Null
Set-Content -LiteralPath $ManifestPath -Value $lines -Encoding UTF8
}
function Sync-ManagedDestination {
<#
.SYNOPSIS
Installs one generated source tree into a destination directory, tracking
every installed file in a manifest so a later run can detect files this
setup previously managed that were since removed from the source (stale)
or changed on disk by the user (locally modified), without ever touching
a file it never installed.
#>
param(
[Parameter(Mandatory=$true)][string]$Source,
[Parameter(Mandatory=$true)][string]$Destination,
[Parameter(Mandatory=$true)][string]$Stamp,
[string]$AiConfigRoot,
[string]$ShellCommand,
[string]$WindowsShellCommand,
[switch]$DryRun
)
Write-Output "SOURCE $Source -> $Destination"
$manifestPath = Get-ManagedManifestPath $Destination
$oldManifest = Read-ManagedManifest $manifestPath
$newManifest = @{}
$backupRoot = Join-Path $Destination "backups/$Stamp"
Get-ChildItem $Source -File -Recurse | ForEach-Object {
$relative = $_.FullName.Substring($Source.Length).TrimStart([char[]]@('\','/')).Replace('\','/')
$target = Join-Path $Destination $relative
$rawContent = Get-Content -LiteralPath $_.FullName -Raw
$needsSubstitution = $rawContent.Contains('__AI_CONFIG_ROOT__') -or $rawContent.Contains('__HOOK_COMMAND__') -or $rawContent.Contains('__WINDOWS_HOOK_COMMAND__')
if ($needsSubstitution) {
$finalContent = $rawContent.Replace('__AI_CONFIG_ROOT__', $AiConfigRoot).Replace('__HOOK_COMMAND__', $ShellCommand).Replace('__WINDOWS_HOOK_COMMAND__', $WindowsShellCommand).Replace('__HOOK_SCRIPT__', 'flashbang.ps1').Replace('__WINDOWS_HOOK_SCRIPT__', 'flashbang.ps1')
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($finalContent)
} else {
$newBytes = [System.IO.File]::ReadAllBytes($_.FullName)
}
$newHash = Get-Sha256HashOfBytes $newBytes
$newManifest[$relative] = $newHash
$exists = Test-Path -LiteralPath $target
if ($exists -and (Get-Sha256Hash $target) -eq $newHash) { Write-Output "UNCHANGED $target"; return }
$action = if ($exists) { 'UPDATE' } else { 'CREATE' }
if ($DryRun) { Write-Output "DRYRUN $action $target"; return }
if ($exists) {
$wasManaged = $oldManifest.ContainsKey($relative)
$backup = Join-Path $backupRoot $relative
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
Copy-Item -LiteralPath $target $backup -Force
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." }
}
New-Item (Split-Path $target -Parent) -ItemType Directory -Force | Out-Null
[System.IO.File]::WriteAllBytes($target, $newBytes)
Write-Output "$action $target"
}
foreach ($relative in @($oldManifest.Keys | Where-Object { -not $newManifest.ContainsKey($_) })) {
$target = Join-Path $Destination $relative
if (-not (Test-Path -LiteralPath $target)) { continue }
if ($DryRun) { Write-Output "DRYRUN REMOVE $target"; continue }
$backup = Join-Path $backupRoot $relative
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
Copy-Item -LiteralPath $target $backup -Force
Write-Output "BACKUP $target -> $backup"
if ((Get-Sha256Hash $target) -eq $oldManifest[$relative]) {
Remove-Item -LiteralPath $target -Force
Write-Output "REMOVE $target"
} 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."
}
}
if (-not $DryRun) { Write-ManagedManifest $manifestPath $newManifest }
}
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# Managed-file manifest: tracks every file this setup previously installed into a
# destination so a later run can detect stale files (removed from the source) or
# locally modified files (changed on disk since the last install) without ever
# touching a file it never installed itself.
sha256_of_file() {
sha256sum -- "$1" | cut -d' ' -f1
}
sha256_of_string() {
printf '%s' "$1" | sha256sum | cut -d' ' -f1
}
manifest_path() {
printf '%s/.ai-config-manifest.tsv' "$1"
}
# read_managed_manifest <manifest-file> <assoc-array-name>
read_managed_manifest() {
local path=$1
local -n out_ref=$2
out_ref=()
[ -f "$path" ] || return 0
local rel hash first=1
while IFS=$'\t' read -r rel hash; do
if [ "$first" = 1 ]; then first=0; [ "$rel" = path ] && continue; fi
[ -n "$rel" ] || continue
out_ref[$rel]=$hash
done < "$path"
}
# write_managed_manifest <manifest-file> <assoc-array-name>
write_managed_manifest() {
local path=$1
local -n in_ref=$2
mkdir -p "$(dirname -- "$path")"
{
printf 'path\tsha256\n'
for rel in $(printf '%s\n' "${!in_ref[@]}" | sort); do
printf '%s\t%s\n' "$rel" "${in_ref[$rel]}"
done
} > "$path"
}
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <windows-shell-command> <dry-run: true|false>
sync_managed_destination() {
local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 windows_shell_command=$6 dry_run=$7
printf 'SOURCE %s -> %s\n' "$source" "$destination"
local manifest
manifest=$(manifest_path "$destination")
declare -A old_manifest
read_managed_manifest "$manifest" old_manifest
declare -A new_manifest
local backup_root="$destination/backups/$stamp"
while IFS= read -r -d '' source_file; do
local relative target content needs_sub new_hash exists current_hash action backup
relative=${source_file#"$source/"}
target="$destination/$relative"
content=$(<"$source_file")
needs_sub=false
if [[ "$content" == *'__AI_CONFIG_ROOT__'* || "$content" == *'__HOOK_COMMAND__'* || "$content" == *'__WINDOWS_HOOK_COMMAND__'* ]]; then
needs_sub=true
content=${content//__AI_CONFIG_ROOT__/$ai_config_root}
content=${content//__HOOK_COMMAND__/$shell_command}
content=${content//__WINDOWS_HOOK_COMMAND__/$windows_shell_command}
content=${content//__HOOK_SCRIPT__/flashbang.sh}
content=${content//__WINDOWS_HOOK_SCRIPT__/flashbang.sh}
new_hash=$(sha256_of_string "$content")
else
new_hash=$(sha256_of_file "$source_file")
fi
new_manifest[$relative]=$new_hash
exists=false
[ -f "$target" ] && exists=true
if [ "$exists" = true ]; then
current_hash=$(sha256_of_file "$target")
if [ "$current_hash" = "$new_hash" ]; then
printf 'UNCHANGED %s\n' "$target"
continue
fi
action=UPDATE
else
action=CREATE
fi
if [ "$dry_run" = true ]; then printf 'DRYRUN %s %s\n' "$action" "$target"; continue; fi
if [ "$exists" = true ]; then
backup="$backup_root/$relative"
mkdir -p "$(dirname -- "$backup")"
cp -- "$target" "$backup"
printf 'BACKUP %s -> %s\n' "$target" "$backup"
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"
fi
fi
mkdir -p "$(dirname -- "$target")"
if [ "$needs_sub" = true ]; then
printf '%s' "$content" > "$target"
else
cp -- "$source_file" "$target"
fi
printf '%s %s\n' "$action" "$target"
done < <(find "$source" -type f -print0)
local relative target current_hash backup
for relative in "${!old_manifest[@]}"; do
[ -z "${new_manifest[$relative]+x}" ] || continue
target="$destination/$relative"
[ -f "$target" ] || continue
if [ "$dry_run" = true ]; then printf 'DRYRUN REMOVE %s\n' "$target"; continue; fi
backup="$backup_root/$relative"
mkdir -p "$(dirname -- "$backup")"
cp -- "$target" "$backup"
printf 'BACKUP %s -> %s\n' "$target" "$backup"
current_hash=$(sha256_of_file "$target")
if [ "$current_hash" = "${old_manifest[$relative]}" ]; then
rm -f -- "$target"
printf 'REMOVE %s\n' "$target"
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"
fi
done
if [ "$dry_run" != true ]; then write_managed_manifest "$manifest" new_manifest; fi
}
+51
View File
@@ -0,0 +1,51 @@
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-build-validation-" + [Guid]::NewGuid())
New-Item $work -ItemType Directory -Force | Out-Null
function New-RepoCopy {
$copy = Join-Path $work ("case-" + [Guid]::NewGuid())
New-Item $copy -ItemType Directory -Force | Out-Null
Get-ChildItem $root -Force | Where-Object { $_.Name -notin @('.git','generated') } | ForEach-Object {
Copy-Item $_.FullName (Join-Path $copy $_.Name) -Recurse -Force
}
return $copy
}
function Test-BuildFails([string]$Label, [scriptblock]$Mutate) {
$copy = New-RepoCopy
& $Mutate $copy
$failed = $false
try { & (Join-Path $copy 'scripts/build.ps1') 2>&1 | Out-Null }
catch { $failed = $true }
Remove-Item $copy -Recurse -Force -ErrorAction SilentlyContinue
if (-not $failed) { throw "Expected build to fail for case '$Label' but it succeeded." }
}
Test-BuildFails 'invalid Claude settings.json' {
param($copy)
Add-Content (Join-Path $copy 'adapters/claude/config/settings.json') '} this is not json {'
}
Test-BuildFails 'duplicate agent name' {
param($copy)
$architect = Join-Path $copy 'shared/agents/architect/agent.yml'
(Get-Content $architect -Raw) -replace 'name:\s*\S+', 'name: implementer' | Set-Content $architect -NoNewline
}
Test-BuildFails 'duplicate plugin name' {
param($copy)
$manifest = Join-Path $copy 'adapters/plugins.tsv'
$lines = Get-Content $manifest
Add-Content $manifest $lines[1]
}
Test-BuildFails 'leftover template placeholder' {
param($copy)
Add-Content (Join-Path $copy 'shared/global-instructions.md') "`n__NOT_A_REAL_PLACEHOLDER__"
}
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
Write-Output 'PASS build validation: invalid JSON, duplicate agent, duplicate plugin, and leftover placeholders are all rejected'
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
work=$(mktemp -d)
cleanup() { rm -rf -- "$work"; }
trap cleanup EXIT
new_repo_copy() {
local copy
copy=$(mktemp -d "$work/case-XXXXXX")
(cd "$root" && tar -cf - --exclude='.git' --exclude='generated' .) | (cd "$copy" && tar -xf -)
printf '%s' "$copy"
}
test_build_fails() {
local label=$1 mutate_fn=$2 copy
copy=$(new_repo_copy)
"$mutate_fn" "$copy"
if bash "$copy/scripts/build.sh" >/dev/null 2>&1; then
printf 'Expected build to fail for case "%s" but it succeeded.\n' "$label" >&2
exit 1
fi
rm -rf -- "$copy"
}
mutate_invalid_json() {
printf '} this is not json {' >> "$1/adapters/claude/config/settings.json"
}
test_build_fails 'invalid Claude settings.json' mutate_invalid_json
mutate_duplicate_agent() {
sed -i 's/^name:.*/name: implementer/' "$1/shared/agents/architect/agent.yml"
}
test_build_fails 'duplicate agent name' mutate_duplicate_agent
mutate_duplicate_plugin() {
local manifest="$1/adapters/plugins.tsv"
sed -n '2p' "$manifest" >> "$manifest"
}
test_build_fails 'duplicate plugin name' mutate_duplicate_plugin
mutate_leftover_placeholder() {
printf '\n__NOT_A_REAL_PLACEHOLDER__\n' >> "$1/shared/global-instructions.md"
}
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'
+50
View File
@@ -0,0 +1,50 @@
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-manifest-test-" + [Guid]::NewGuid())
New-Item $work -ItemType Directory -Force | Out-Null
try {
$source = Join-Path $work 'source'
$destination = Join-Path $work 'destination'
New-Item $source -ItemType Directory -Force | Out-Null
Set-Content (Join-Path $source 'a.md') 'A' -Encoding UTF8 -NoNewline
Set-Content (Join-Path $source 'b.md') 'B' -Encoding UTF8 -NoNewline
Set-Content (Join-Path $source 'c.md') 'C' -Encoding UTF8 -NoNewline
$out1 = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'run1')
if (@($out1 | Where-Object { $_ -like 'CREATE*' }).Count -ne 3) { throw "Expected three CREATE lines on first install: $($out1 -join '; ')" }
$out2 = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'run2')
if (@($out2 | Where-Object { $_ -notlike 'UNCHANGED*' -and $_ -notlike 'SOURCE *' }).Count -ne 0) { throw "Second identical install was not a no-op: $($out2 -join '; ')" }
if (Test-Path (Join-Path $destination 'backups/run2')) { throw 'Idempotent install must not create a backup.' }
Set-Content (Join-Path $destination 'foreign.md') 'mine' -Encoding UTF8 -NoNewline
Remove-Item (Join-Path $source 'b.md') -Force
Remove-Item (Join-Path $source 'c.md') -Force
Set-Content (Join-Path $destination 'c.md') 'locally changed' -Encoding UTF8 -NoNewline
$out3 = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'run3')
if (@($out3 | Where-Object { $_ -match 'REMOVE.*b\.md' }).Count -ne 1) { throw "Expected b.md to be removed: $($out3 -join '; ')" }
if (Test-Path (Join-Path $destination 'b.md')) { throw 'b.md should have been removed: it was untouched by the user and dropped from source.' }
if (@($out3 | Where-Object { $_ -match 'WARN.*c\.md.*changed locally' }).Count -ne 1) { throw "Expected c.md to be flagged as locally modified: $($out3 -join '; ')" }
if (-not (Test-Path (Join-Path $destination 'c.md'))) { throw 'A locally modified stale file must not be removed.' }
if ((Get-Content (Join-Path $destination 'c.md') -Raw) -ne 'locally changed') { throw 'c.md content must be preserved exactly.' }
if ((Get-Content (Join-Path $destination 'foreign.md') -Raw) -ne 'mine') { throw 'A file this setup never installed must never be touched.' }
if (-not (Test-Path (Join-Path $destination 'backups/run3/b.md'))) { throw 'b.md should have been backed up before removal.' }
if (-not (Test-Path (Join-Path $destination 'backups/run3/c.md'))) { throw 'c.md should have been backed up even though it was left in place.' }
Remove-Item (Join-Path $source 'a.md') -Force
$beforeDry = @(Get-ChildItem $destination -Recurse -File | Sort-Object FullName | ForEach-Object FullName)
$outDry = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'dry' -DryRun)
$afterDry = @(Get-ChildItem $destination -Recurse -File | Sort-Object FullName | ForEach-Object FullName)
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 '; ')" }
Write-Output 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free'
} finally {
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
}
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/manifest.sh"
work=$(mktemp -d)
cleanup() { rm -rf -- "$work"; }
trap cleanup EXIT
source_dir="$work/source"
destination="$work/destination"
mkdir -p "$source_dir"
printf 'A' > "$source_dir/a.md"
printf 'B' > "$source_dir/b.md"
printf 'C' > "$source_dir/c.md"
out1=$(sync_managed_destination "$source_dir" "$destination" run1 "$destination" bash bash false)
[ "$(printf '%s\n' "$out1" | grep -c '^CREATE')" -eq 3 ] || { printf 'Expected three CREATE lines on first install:\n%s\n' "$out1" >&2; exit 1; }
out2=$(sync_managed_destination "$source_dir" "$destination" run2 "$destination" bash bash false)
[ -z "$(printf '%s\n' "$out2" | grep -v '^UNCHANGED' | grep -v '^SOURCE ')" ] || { printf 'Second identical install was not a no-op:\n%s\n' "$out2" >&2; exit 1; }
[ ! -d "$destination/backups/run2" ] || { printf 'Idempotent install must not create a backup.\n' >&2; exit 1; }
printf 'mine' > "$destination/foreign.md"
rm -f "$source_dir/b.md" "$source_dir/c.md"
printf 'locally changed' > "$destination/c.md"
out3=$(sync_managed_destination "$source_dir" "$destination" run3 "$destination" bash bash false)
printf '%s\n' "$out3" | grep -q '^REMOVE .*b\.md$' || { printf 'Expected b.md to be removed:\n%s\n' "$out3" >&2; exit 1; }
[ ! -f "$destination/b.md" ] || { printf 'b.md should have been removed.\n' >&2; exit 1; }
printf '%s\n' "$out3" | grep -q 'WARN .*c\.md.*changed locally' || { printf 'Expected c.md to be flagged as locally modified:\n%s\n' "$out3" >&2; exit 1; }
[ -f "$destination/c.md" ] || { printf 'A locally modified stale file must not be removed.\n' >&2; exit 1; }
[ "$(cat "$destination/c.md")" = 'locally changed' ] || { printf 'c.md content must be preserved exactly.\n' >&2; exit 1; }
[ "$(cat "$destination/foreign.md")" = 'mine' ] || { printf 'A file this setup never installed must never be touched.\n' >&2; exit 1; }
[ -f "$destination/backups/run3/b.md" ] || { printf 'b.md should have been backed up before removal.\n' >&2; exit 1; }
[ -f "$destination/backups/run3/c.md" ] || { printf 'c.md should have been backed up even though it was left in place.\n' >&2; exit 1; }
rm -f "$source_dir/a.md"
before_dry=$(find "$destination" -type f | sort)
out_dry=$(sync_managed_destination "$source_dir" "$destination" dry "$destination" bash bash true)
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; }
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'
+6 -2
View File
@@ -7,14 +7,18 @@ $statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"}
$branch = & git -C $root branch --show-current
$statusOutput = $statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')
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.' }
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).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
foreach ($platform in @('windows','linux')) {
foreach ($client in @('codex','claude')) {
$package = Join-Path $root "generated/$client-$platform"
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
$content = Get-Content -LiteralPath (Join-Path $package $file) -Raw
if ($client -eq 'claude') { $settings = $content | ConvertFrom-Json }
if (@(Get-ChildItem "$package/agents" -File).Count -ne 5) { throw "Missing agents in $package" }
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne 22) { throw "Missing skills in $package" }
if (@(Get-ChildItem "$package/agents" -File).Count -ne $sourceAgentCount) { throw "Missing agents in $package" }
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" }
$expected = if ($platform -eq 'windows') { 'powershell .*flashbang.ps1' } else { 'bash .*flashbang.sh' }
if ($content -notmatch $expected -or $content -match '__HOOK_|__WINDOWS_HOOK_') { throw "Incorrect platform command in $package" }
if ($platform -eq 'windows' -and $content -match 'WindowStyle\s+Hidden') { throw "Windows hook hides the terminal in $package" }
+8 -3
View File
@@ -7,14 +7,19 @@ grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps
branch=$(git -C "$root" branch --show-current)
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" ]
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)
rule_skill_count=$(($(wc -l < "$root/adapters/claude/rule-skills.tsv") - 1))
for platform in windows linux; do
for client in codex claude; do
package="$root/generated/$client-$platform"
file=config.toml
[ "$client" != claude ] || file=settings.json
test -f "$package/$file"
test "$(find "$package/agents" -type f | wc -l)" -eq 5
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq 22
test "$(find "$package/agents" -type f | wc -l)" -eq "$source_agent_count"
expected_skill_count=$source_skill_count
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count"
if [ "$platform" = windows ]; then
grep -q 'powershell .*flashbang.ps1' "$package/$file"
! grep -Eq 'WindowStyle[[:space:]]+Hidden' "$package/$file"
@@ -46,7 +51,7 @@ for platform_selection in 1 2; do
[[ "$output" == *'PASS install dry-run'* ]]
platform=windows
[ "$platform_selection" != 2 ] || platform=linux
[[ "$output" == *"-$platform/"* ]]
[[ "$output" == *"-$platform"* ]]
done
done
printf 'PASS four platform packages and six Bash selections\n'
+1 -18
View File
@@ -2,24 +2,7 @@
This instruction set is generated into the client-specific `AGENTS.md` and `CLAUDE.md` files. Do not edit generated copies.
Always load and apply `rules/general.md` before starting any task.
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Load every applicable rule file before editing. Load all matching files when multiple technologies apply.
Load rule files when their subject applies:
- rules/angular.md for Angular work.
- rules/typescript.md for TypeScript work.
- rules/csharp.md for C# or .NET work.
- rules/wpf.md for WPF work.
- rules/ui-ux.md for UI or UX decisions.
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
- rules/git.md for Git operations.
- rules/refactoring.md for refactoring work.
- rules/definition-of-done.md when validating completion.
- rules/decision-rule.md when requirements, behavior, or technical choices need to be evaluated.
__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.
+2
View File
@@ -1,3 +1,5 @@
# Hooks
Hooks are conservative entry points shared by clients. They validate obvious command hazards, check repository-local configuration inputs, and provide an optional post-change verification command. They do not bypass client permission prompts or grant access.
Only the `Stop` hook (`flashbang`) is registered in the generated client configuration. `Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are utility scripts, not active hooks; nothing in this repository invokes them automatically. Wire one up as a client-native `PreToolUse`/equivalent hook yourself if you want it enforced, and do not assume it runs otherwise.
+7 -1
View File
@@ -16,7 +16,6 @@ import re
import shutil
import subprocess
import sys
import tkinter as tk
def notify_fallback() -> None:
@@ -25,6 +24,13 @@ def notify_fallback() -> None:
subprocess.run([notify_send, "AI assistant requires attention"], check=False)
try:
import tkinter as tk
except ImportError:
notify_fallback()
sys.exit(0)
def monitor_bounds(primary_only: bool) -> tuple[int, int, int, int]:
root = tk.Tk()
root.withdraw()