Compare commits
25
Commits
No files matched your search
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
- One shared source of truth for Codex and Claude Code.
|
||||||
- Separate generated packages for Windows and Linux.
|
- Separate generated packages for Windows and Linux.
|
||||||
- Native PowerShell 5.1 and Bash scripts.
|
- Native PowerShell 5.1 and Bash scripts.
|
||||||
- Interactive installation for Codex, Claude Code, or both.
|
- Interactive or non-interactive (`-Client`/`-Platform`) installation for Codex, Claude Code, or both.
|
||||||
- Dry-run support and timestamped backups of replaced managed files.
|
- 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.
|
- 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
|
## Architecture
|
||||||
|
|
||||||
@@ -74,10 +81,11 @@ Every repository change must pass the build before completion. Building only wri
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
The installer runs a fresh build and then asks for the target platform and client. Selecting Linux or Windows chooses
|
The installer runs a fresh build and then asks for the target platform and client, unless they are passed as
|
||||||
the generated package format; installation always targets the current user's home directory.
|
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: (Windows)
|
||||||
```powershell
|
```powershell
|
||||||
@@ -89,7 +97,7 @@ Bash: (Linux)
|
|||||||
./scripts/install.sh --dry-run
|
./scripts/install.sh --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
Install the selected configuration:
|
Install the selected configuration interactively:
|
||||||
|
|
||||||
Powershell: (Windows)
|
Powershell: (Windows)
|
||||||
```powershell
|
```powershell
|
||||||
@@ -101,9 +109,29 @@ Bash: (Linux)
|
|||||||
./scripts/install.sh
|
./scripts/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Existing managed files receive timestamped backups before replacement. Unrelated files are left untouched. Selecting
|
Install non-interactively, for scripting or CI:
|
||||||
Codex also installs shared skills to `.agents/skills`. A normal installation ensures configured plugins are present and
|
|
||||||
updates plugins that are already installed.
|
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
|
Run the same installation command whenever the repository configuration should be updated. There is no separate update
|
||||||
script.
|
script.
|
||||||
@@ -127,10 +155,14 @@ The doctor reports concise `PASS`, `WARN`, and `FAIL` results without reading or
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Codex receives global instructions, rules, skills, agent TOML files, hooks, and `config.toml`. Its defaults keep writes
|
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
|
Claude Code receives the equivalent global instructions, agents, hooks, and `settings.json`. Only `general.md` ships as
|
||||||
settings use the supported automatic permission mode where available.
|
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:
|
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
|
### Add a rule
|
||||||
|
|
||||||
Create a focused Markdown file in `shared/rules/`. Add its loading condition to `shared/global-instructions.md` when the
|
Create a focused Markdown file in `shared/rules/`. For a rule that applies only to a language, framework, tool, or
|
||||||
rule applies only to a language, framework, tool, or change area.
|
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
|
### Add a skill
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__\"",
|
"command": "__HOOK_COMMAND__ \"__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__\"",
|
||||||
"timeout": 20
|
"timeout": 8
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
@@ -7,7 +7,7 @@ status_line = ["model", "reasoning", "project-name", "git-branch", "context-wind
|
|||||||
|
|
||||||
[agents]
|
[agents]
|
||||||
enabled = true
|
enabled = true
|
||||||
max_threads = 5
|
max_concurrent_threads_per_session = 5
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
hooks = true
|
hooks = true
|
||||||
@@ -17,4 +17,4 @@ hooks = true
|
|||||||
type = "command"
|
type = "command"
|
||||||
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__"'
|
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__"'
|
||||||
command_windows = '__WINDOWS_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__WINDOWS_HOOK_SCRIPT__"'
|
command_windows = '__WINDOWS_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__WINDOWS_HOOK_SCRIPT__"'
|
||||||
timeout = 20
|
timeout = 8
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# Configuration
|
# 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.
|
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
@@ -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 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,8 @@ The current structure is organized by purpose:
|
|||||||
- `git/`: pull request responses and reviews
|
- `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.
|
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`.
|
||||||
+122
-25
@@ -1,20 +1,40 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param()
|
param()
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
$shared = Join-Path $root 'shared'
|
$shared = Join-Path $root 'shared'
|
||||||
$output = Join-Path $root 'generated'
|
$output = Join-Path $root 'generated'
|
||||||
$pluginManifest = Join-Path $root 'adapters/plugins.tsv'
|
$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" }
|
if (-not (Test-Path -LiteralPath $pluginManifest)) { throw "Missing plugin manifest: $pluginManifest" }
|
||||||
$pluginEntries = @(Import-Csv -LiteralPath $pluginManifest -Delimiter ([char]9))
|
$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 ($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 ([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
|
Remove-Item $output -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
New-Item $output -ItemType Directory -Force | Out-Null
|
New-Item $output -ItemType Directory -Force | Out-Null
|
||||||
|
|
||||||
function Copy-Directory($source, $destination) {
|
function Copy-Directory($source, $destination) {
|
||||||
New-Item $destination -ItemType Directory -Force | Out-Null
|
New-Item $destination -ItemType Directory -Force | Out-Null
|
||||||
Get-ChildItem $source -File -Recurse | ForEach-Object {
|
Get-ChildItem $source -File -Recurse | ForEach-Object {
|
||||||
@@ -32,38 +52,97 @@ function Read-Field($path, $name) {
|
|||||||
function Quote-Toml($value) {
|
function Quote-Toml($value) {
|
||||||
return ('"' + $value.Replace('\','\\').Replace('"','\"').Replace("`r",'').Replace("`n",'\n') + '"')
|
return ('"' + $value.Replace('\','\\').Replace('"','\"').Replace("`r",'').Replace("`n",'\n') + '"')
|
||||||
}
|
}
|
||||||
|
|
||||||
& (Join-Path $root 'shared/hooks/scripts/Test-SessionConfig.ps1') -RepositoryRoot $root
|
& (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')) {
|
foreach ($platform in @('windows','linux')) {
|
||||||
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "codex-$platform/skills")
|
New-Item (Join-Path $output "codex-$platform") -ItemType Directory -Force | Out-Null
|
||||||
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "claude-$platform/skills")
|
New-Item (Join-Path $output "claude-$platform") -ItemType Directory -Force | Out-Null
|
||||||
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "codex-$platform/rules")
|
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "codex-$platform/skills")
|
||||||
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "claude-$platform/rules")
|
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "claude-$platform/skills")
|
||||||
New-Item (Join-Path $output "codex-$platform") -ItemType Directory -Force | Out-Null
|
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "codex-$platform/rules")
|
||||||
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 "codex-$platform/hooks")
|
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$platform/hooks")
|
||||||
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$platform/hooks")
|
Copy-Directory (Join-Path $shared 'statusline') (Join-Path $output "claude-$platform/statusline")
|
||||||
Copy-Directory (Join-Path $shared 'statusline') (Join-Path $output "claude-$platform/statusline")
|
|
||||||
Copy-Item (Join-Path $shared 'global-instructions.md') (Join-Path $output "codex-$platform/AGENTS.md") -Force
|
# Claude: general.md stays a plain, always-applied rule file. Every technology- or
|
||||||
Copy-Item (Join-Path $shared 'global-instructions.md') (Join-Path $output "claude-$platform/CLAUDE.md") -Force
|
# situation-specific rule becomes a skill instead, so only its name and description
|
||||||
Copy-Item (Join-Path $root 'adapters/codex/config/config.toml') (Join-Path $output "codex-$platform/config.toml") -Force
|
# sit permanently in context; the full text loads only when the skill is invoked.
|
||||||
Copy-Item (Join-Path $root 'adapters/claude/config/settings.json') (Join-Path $output "claude-$platform/settings.json") -Force
|
$claudeRulesDir = Join-Path $output "claude-$platform/rules"
|
||||||
foreach ($client in @('codex','claude')) {
|
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"
|
$agentsOutput = Join-Path $output "$client-$platform/agents"
|
||||||
New-Item $agentsOutput -ItemType Directory -Force | Out-Null
|
New-Item $agentsOutput -ItemType Directory -Force | Out-Null
|
||||||
Get-ChildItem (Join-Path $shared 'agents') -Directory | Sort-Object Name | ForEach-Object {
|
foreach ($agentDir in $sourceAgentDirs) {
|
||||||
$metadata = Join-Path $_.FullName 'agent.yml'
|
$metadata = Join-Path $agentDir.FullName 'agent.yml'
|
||||||
$name = Read-Field $metadata 'name'
|
$name = Read-Field $metadata 'name'
|
||||||
$description = Read-Field $metadata 'description'
|
$description = Read-Field $metadata 'description'
|
||||||
$instructions = Get-Content (Join-Path $_.FullName 'instructions.md') -Raw
|
$instructions = Get-Content (Join-Path $agentDir.FullName 'instructions.md') -Raw
|
||||||
if ($client -eq 'codex') {
|
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
|
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 {
|
} else {
|
||||||
Set-Content (Join-Path $agentsOutput "$name.md") "---`r`nname: $name`r`ndescription: $description`r`n---`r`n`r`n$instructions" -Encoding UTF8
|
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')) {
|
foreach ($client in @('codex','claude')) {
|
||||||
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
|
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
|
||||||
$path = Join-Path $output "$client-$platform/$file"
|
$path = Join-Path $output "$client-$platform/$file"
|
||||||
$command = if ($platform -eq 'windows') { 'powershell -NoProfile -ExecutionPolicy Bypass -File' } else { 'bash' }
|
$command = if ($platform -eq 'windows') { 'powershell -NoProfile -ExecutionPolicy Bypass -File' } else { 'bash' }
|
||||||
@@ -72,7 +151,25 @@ foreach ($client in @('codex','claude')) {
|
|||||||
$content = Get-Content -LiteralPath $path -Raw
|
$content = Get-Content -LiteralPath $path -Raw
|
||||||
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__WINDOWS_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__WINDOWS_HOOK_SCRIPT__', $script).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript)
|
$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
|
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Write-Output 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux'
|
|
||||||
|
|
||||||
|
$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" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$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
@@ -5,8 +5,40 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|||||||
shared="$root/shared"
|
shared="$root/shared"
|
||||||
output="$root/generated"
|
output="$root/generated"
|
||||||
plugin_manifest="$root/adapters/plugins.tsv"
|
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; }
|
[ -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() {
|
copy_directory() {
|
||||||
local source=$1 destination=$2
|
local source=$1 destination=$2
|
||||||
@@ -32,29 +64,91 @@ quote_toml() {
|
|||||||
|
|
||||||
bash "$shared/hooks/scripts/test-session-config.sh" "$root"
|
bash "$shared/hooks/scripts/test-session-config.sh" "$root"
|
||||||
rm -rf -- "$output"
|
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
|
for platform in windows linux; do
|
||||||
mkdir -p "$output/codex-$platform" "$output/claude-$platform"
|
mkdir -p "$output/codex-$platform" "$output/claude-$platform"
|
||||||
copy_directory "$shared/skills" "$output/codex-$platform/skills"
|
copy_directory "$shared/skills" "$output/codex-$platform/skills"
|
||||||
copy_directory "$shared/skills" "$output/claude-$platform/skills"
|
copy_directory "$shared/skills" "$output/claude-$platform/skills"
|
||||||
copy_directory "$shared/rules" "$output/codex-$platform/rules"
|
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/codex-$platform/hooks"
|
||||||
copy_directory "$shared/hooks" "$output/claude-$platform/hooks"
|
copy_directory "$shared/hooks" "$output/claude-$platform/hooks"
|
||||||
copy_directory "$shared/statusline" "$output/claude-$platform/statusline"
|
copy_directory "$shared/statusline" "$output/claude-$platform/statusline"
|
||||||
|
|
||||||
cp "$shared/global-instructions.md" "$output/codex-$platform/AGENTS.md"
|
# Claude: general.md stays a plain, always-applied rule file. Every technology- or
|
||||||
cp "$shared/global-instructions.md" "$output/claude-$platform/CLAUDE.md"
|
# 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/codex/config/config.toml" "$output/codex-$platform/config.toml"
|
||||||
cp "$root/adapters/claude/config/settings.json" "$output/claude-$platform/settings.json"
|
cp "$root/adapters/claude/config/settings.json" "$output/claude-$platform/settings.json"
|
||||||
|
|
||||||
for client in codex claude; do
|
for client in codex claude; do
|
||||||
agents_output="$output/$client-$platform/agents"
|
agents_output="$output/$client-$platform/agents"
|
||||||
mkdir -p "$agents_output"
|
mkdir -p "$agents_output"
|
||||||
for agent in "$shared"/agents/*; do
|
for agent_dir in "${agent_dirs[@]}"; do
|
||||||
metadata="$agent/agent.yml"
|
metadata="${agent_dir}agent.yml"
|
||||||
name=$(read_field "$metadata" name)
|
name=$(read_field "$metadata" name)
|
||||||
description=$(read_field "$metadata" description)
|
description=$(read_field "$metadata" description)
|
||||||
instructions=$(<"$agent/instructions.md")
|
instructions=$(<"${agent_dir}instructions.md")
|
||||||
if [ "$client" = codex ]; then
|
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"
|
printf 'name = %s\ndescription = %s\ndeveloper_instructions = %s\n' "$(quote_toml "$name")" "$(quote_toml "$description")" "$(quote_toml "$instructions")" > "$agents_output/$name.toml"
|
||||||
else
|
else
|
||||||
@@ -84,6 +178,32 @@ for client in codex claude; do
|
|||||||
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
|
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
|
||||||
printf '%s\n' "$content" > "$path"
|
printf '%s\n' "$content" > "$path"
|
||||||
done
|
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
@@ -2,16 +2,67 @@
|
|||||||
param()
|
param()
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
||||||
$homePath = [Environment]::GetFolderPath('UserProfile')
|
$homePath = [Environment]::GetFolderPath('UserProfile')
|
||||||
$fail = $false
|
$fail = $false
|
||||||
function Result($state, $message) { Write-Output ("$state $message"); if ($state -eq 'FAIL') { $script:fail = $true } }
|
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" } }
|
foreach ($tool in @('codex','claude')) { if (Get-Command $tool -ErrorAction SilentlyContinue) { $version = & $tool --version 2>&1 | Select-Object -First 1; Result 'PASS' "$tool available ($version)" } else { Result 'WARN' "$tool unavailable" } }
|
||||||
if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude status line' } else { Result 'WARN' 'jq unavailable; Claude status line is disabled' } }
|
if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude status line' } else { Result 'WARN' 'jq unavailable; Claude status line is disabled' } }
|
||||||
|
|
||||||
|
$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')) {
|
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/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' } else { Result 'FAIL' 'generated hooks 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 ($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" } }
|
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 }
|
if ($fail) { exit 1 }
|
||||||
Write-Output 'PASS doctor'
|
Write-Output 'PASS doctor'
|
||||||
+63
-2
@@ -2,6 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
. "$root/scripts/lib/manifest.sh"
|
||||||
home_path=${HOME:?HOME is required}
|
home_path=${HOME:?HOME is required}
|
||||||
failed=false
|
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
|
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
|
done
|
||||||
command -v jq >/dev/null && result PASS 'jq available for Claude status line' || result WARN 'jq unavailable; Claude status line is disabled'
|
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
|
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/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' || result FAIL 'generated hooks 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 ($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
|
done
|
||||||
|
|
||||||
for path in .codex/AGENTS.md .codex/config.toml .claude/CLAUDE.md .claude/settings.json; do
|
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"
|
[ -f "$home_path/$path" ] && result PASS "installed $path" || result WARN "not installed $path"
|
||||||
done
|
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
|
for path in shared/rules shared/skills shared/agents shared/hooks/scripts; do
|
||||||
[ -e "$root/$path" ] && result PASS "source $path" || result FAIL "missing $path"
|
[ -e "$root/$path" ] && result PASS "source $path" || result FAIL "missing $path"
|
||||||
done
|
done
|
||||||
|
|
||||||
"$failed" && exit 1
|
"$failed" && exit 1
|
||||||
printf 'PASS doctor\n'
|
printf 'PASS doctor\n'
|
||||||
+31
-37
@@ -1,24 +1,38 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param([switch]$DryRun)
|
param(
|
||||||
|
[switch]$DryRun,
|
||||||
|
[ValidateSet('Codex','Claude','Both')][string]$Client,
|
||||||
|
[ValidateSet('Windows','Linux')][string]$Platform,
|
||||||
|
[switch]$UpdatePlugins
|
||||||
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$root = Split-Path $PSScriptRoot -Parent
|
$root = Split-Path $PSScriptRoot -Parent
|
||||||
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
|
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
||||||
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
|
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
|
||||||
& $buildScript
|
& $buildScript
|
||||||
if (-not $?) { throw 'Build failed. Installation was not started.' }
|
if (-not $?) { throw 'Build failed. Installation was not started.' }
|
||||||
$generated = Join-Path $root 'generated'
|
$generated = Join-Path $root 'generated'
|
||||||
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
|
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
|
||||||
Write-Output 'Select target platform:'
|
|
||||||
Write-Output '1) Windows'
|
if (-not $Platform) {
|
||||||
Write-Output '2) Linux'
|
Write-Output 'Select target platform:'
|
||||||
do { $platformSelection = Read-Host 'Selection [1-2]' } while ($platformSelection -notin @('1','2'))
|
Write-Output '1) Windows'
|
||||||
$platform = @{'1'='windows'; '2'='linux'}[$platformSelection]
|
Write-Output '2) Linux'
|
||||||
Write-Output 'Select installation target:'
|
do { $platformSelection = Read-Host 'Selection [1-2]' } while ($platformSelection -notin @('1','2'))
|
||||||
Write-Output '1) Codex'
|
$Platform = @{'1'='Windows'; '2'='Linux'}[$platformSelection]
|
||||||
Write-Output '2) Claude'
|
}
|
||||||
Write-Output '3) Both'
|
$platform = $Platform.ToLowerInvariant()
|
||||||
do { $selection = Read-Host 'Selection [1-3]' } while ($selection -notin @('1','2','3'))
|
|
||||||
$Client = @{'1'='Codex'; '2'='Claude'; '3'='Both'}[$selection]
|
if (-not $Client) {
|
||||||
|
Write-Output 'Select installation target:'
|
||||||
|
Write-Output '1) Codex'
|
||||||
|
Write-Output '2) Claude'
|
||||||
|
Write-Output '3) Both'
|
||||||
|
do { $selection = Read-Host 'Selection [1-3]' } while ($selection -notin @('1','2','3'))
|
||||||
|
$Client = @{'1'='Codex'; '2'='Claude'; '3'='Both'}[$selection]
|
||||||
|
}
|
||||||
|
|
||||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||||
$homePath = [Environment]::GetFolderPath('UserProfile')
|
$homePath = [Environment]::GetFolderPath('UserProfile')
|
||||||
$shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File'
|
$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"); 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 @('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') } }
|
if ($Client -in @('Claude','Both')) { $targets += @{ Source=(Join-Path $generated "claude-$platform"); Destination=(Join-Path $homePath '.claude') } }
|
||||||
|
|
||||||
foreach ($item in $targets) {
|
foreach ($item in $targets) {
|
||||||
$backupRoot = Join-Path $item.Destination 'backups'
|
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
|
||||||
Get-ChildItem $item.Source -File -Recurse | ForEach-Object {
|
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun
|
||||||
$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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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' }))
|
Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' }))
|
||||||
+49
-44
@@ -2,71 +2,76 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
dry_run=false
|
dry_run=false
|
||||||
for argument in "$@"; do
|
client=""
|
||||||
[ "$argument" != "--dry-run" ] || dry_run=true
|
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
|
done
|
||||||
|
|
||||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
. "$root/scripts/lib/plugins.sh"
|
. "$root/scripts/lib/plugins.sh"
|
||||||
|
. "$root/scripts/lib/manifest.sh"
|
||||||
bash "$root/scripts/build.sh"
|
bash "$root/scripts/build.sh"
|
||||||
generated="$root/generated"
|
generated="$root/generated"
|
||||||
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
|
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
|
||||||
|
|
||||||
printf 'Select target platform:\n1) Windows\n2) Linux\n'
|
if [ -z "$platform" ]; then
|
||||||
while :; do
|
printf 'Select target platform:\n1) Windows\n2) Linux\n'
|
||||||
|
while :; do
|
||||||
read -r -p 'Selection [1-2] ' platform_selection
|
read -r -p 'Selection [1-2] ' platform_selection
|
||||||
case "$platform_selection" in 1) platform=windows; break;; 2) platform=linux; break;; esac
|
case "$platform_selection" in 1) platform=windows; break;; 2) platform=linux; break;; esac
|
||||||
done
|
done
|
||||||
printf 'Select installation target:\n1) Codex\n2) Claude\n3) Both\n'
|
else
|
||||||
while :; do
|
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
|
read -r -p 'Selection [1-3] ' selection
|
||||||
case "$selection" in 1|2|3) break;; esac
|
case "$selection" in 1) client=codex; break;; 2) client=claude; break;; 3) client=both; break;; esac
|
||||||
done
|
done
|
||||||
|
else
|
||||||
|
case "$client" in
|
||||||
|
codex|Codex) client=codex ;;
|
||||||
|
claude|Claude) client=claude ;;
|
||||||
|
both|Both) client=both ;;
|
||||||
|
*) printf 'Unknown --client value: %s (expected codex, claude, or both)\n' "$client" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
stamp=$(date +%Y%m%d-%H%M%S)
|
stamp=$(date +%Y%m%d-%H%M%S)
|
||||||
home_path=${HOME:?HOME is required}
|
home_path=${HOME:?HOME is required}
|
||||||
|
shell_command=bash
|
||||||
|
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
|
|
||||||
targets=()
|
targets=()
|
||||||
case "$selection" in
|
case "$client" in
|
||||||
1|3) targets+=("$generated/codex-$platform|$home_path/.codex" "$generated/codex-$platform/skills|$home_path/.agents/skills");;
|
codex|both) targets+=("$generated/codex-$platform|$home_path/.codex" "$generated/codex-$platform/skills|$home_path/.agents/skills");;
|
||||||
esac
|
esac
|
||||||
case "$selection" in
|
case "$client" in
|
||||||
2|3) targets+=("$generated/claude-$platform|$home_path/.claude");;
|
claude|both) targets+=("$generated/claude-$platform|$home_path/.claude");;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
for target_pair in "${targets[@]}"; do
|
for target_pair in "${targets[@]}"; do
|
||||||
source=${target_pair%%|*}
|
source=${target_pair%%|*}
|
||||||
destination=${target_pair#*|}
|
destination=${target_pair#*|}
|
||||||
while IFS= read -r -d '' source_file; do
|
ai_config_root=$destination
|
||||||
relative=${source_file#"$source/"}
|
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
|
||||||
target="$destination/$relative"
|
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$windows_shell_command" "$dry_run"
|
||||||
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)
|
|
||||||
done
|
done
|
||||||
|
|
||||||
client_selection=both
|
install_configured_plugins "$root" "$client" "$dry_run" "$update_plugins"
|
||||||
case "$selection" in
|
|
||||||
1) client_selection=codex;;
|
|
||||||
2) client_selection=claude;;
|
|
||||||
esac
|
|
||||||
install_configured_plugins "$root" "$client_selection" "$dry_run" true
|
|
||||||
|
|
||||||
if "$dry_run"; then printf 'PASS install dry-run\n'; else printf 'PASS install\n'; fi
|
if "$dry_run"; then printf 'PASS install dry-run\n'; else printf 'PASS install\n'; fi
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
@@ -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'
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
@@ -7,14 +7,18 @@ $statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"}
|
|||||||
$branch = & git -C $root branch --show-current
|
$branch = & git -C $root branch --show-current
|
||||||
$statusOutput = $statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')
|
$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.' }
|
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 ($platform in @('windows','linux')) {
|
||||||
foreach ($client in @('codex','claude')) {
|
foreach ($client in @('codex','claude')) {
|
||||||
$package = Join-Path $root "generated/$client-$platform"
|
$package = Join-Path $root "generated/$client-$platform"
|
||||||
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
|
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
|
||||||
$content = Get-Content -LiteralPath (Join-Path $package $file) -Raw
|
$content = Get-Content -LiteralPath (Join-Path $package $file) -Raw
|
||||||
if ($client -eq 'claude') { $settings = $content | ConvertFrom-Json }
|
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/agents" -File).Count -ne $sourceAgentCount) { throw "Missing agents in $package" }
|
||||||
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne 22) { throw "Missing skills 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' }
|
$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 ($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" }
|
if ($platform -eq 'windows' -and $content -match 'WindowStyle\s+Hidden') { throw "Windows hook hides the terminal in $package" }
|
||||||
|
|||||||
@@ -7,14 +7,19 @@ grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps
|
|||||||
branch=$(git -C "$root" branch --show-current)
|
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=$(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" ]
|
[ "$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 platform in windows linux; do
|
||||||
for client in codex claude; do
|
for client in codex claude; do
|
||||||
package="$root/generated/$client-$platform"
|
package="$root/generated/$client-$platform"
|
||||||
file=config.toml
|
file=config.toml
|
||||||
[ "$client" != claude ] || file=settings.json
|
[ "$client" != claude ] || file=settings.json
|
||||||
test -f "$package/$file"
|
test -f "$package/$file"
|
||||||
test "$(find "$package/agents" -type f | wc -l)" -eq 5
|
test "$(find "$package/agents" -type f | wc -l)" -eq "$source_agent_count"
|
||||||
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq 22
|
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
|
if [ "$platform" = windows ]; then
|
||||||
grep -q 'powershell .*flashbang.ps1' "$package/$file"
|
grep -q 'powershell .*flashbang.ps1' "$package/$file"
|
||||||
! grep -Eq 'WindowStyle[[:space:]]+Hidden' "$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'* ]]
|
[[ "$output" == *'PASS install dry-run'* ]]
|
||||||
platform=windows
|
platform=windows
|
||||||
[ "$platform_selection" != 2 ] || platform=linux
|
[ "$platform_selection" != 2 ] || platform=linux
|
||||||
[[ "$output" == *"-$platform/"* ]]
|
[[ "$output" == *"-$platform"* ]]
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
printf 'PASS four platform packages and six Bash selections\n'
|
printf 'PASS four platform packages and six Bash selections\n'
|
||||||
|
|||||||
@@ -2,24 +2,7 @@
|
|||||||
|
|
||||||
This instruction set is generated into the client-specific `AGENTS.md` and `CLAUDE.md` files. Do not edit generated copies.
|
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.
|
__RULE_LOADING__
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# Hooks
|
# Hooks
|
||||||
|
|
||||||
Hooks are conservative entry points shared by clients. They validate obvious command hazards, check repository-local configuration inputs, and provide an optional post-change verification command. They do not bypass client permission prompts or grant access.
|
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.
|
||||||
@@ -16,7 +16,6 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tkinter as tk
|
|
||||||
|
|
||||||
|
|
||||||
def notify_fallback() -> None:
|
def notify_fallback() -> None:
|
||||||
@@ -25,6 +24,13 @@ def notify_fallback() -> None:
|
|||||||
subprocess.run([notify_send, "AI assistant requires attention"], check=False)
|
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]:
|
def monitor_bounds(primary_only: bool) -> tuple[int, int, int, int]:
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
|
|||||||
Reference in new issue
Block a user