Compare commits
14
Commits
1cba8850f9
...
37d3475b92
No files matched your search
@@ -218,7 +218,7 @@ backed up and retained with a warning. Backups are stored under `backups/<timest
|
||||
Codex skill backups use `.agents/.ai-config-skill-backups/` instead, so Codex does not discover backup copies as skills.
|
||||
Existing `.agents/skills/backups/` content is moved there on the next install, update, or uninstall.
|
||||
Codex `config.toml` synchronization merges setup-owned keys while retaining other root keys, extra fields in managed
|
||||
tables, plugin and marketplace tables, and unowned array tables. Other standalone tables may be dropped; the previous
|
||||
tables, plugin and marketplace tables, other unmanaged tables, and unowned array tables. The previous
|
||||
file is backed up when content changes. Claude `settings.json` synchronization retains unrelated settings and hooks.
|
||||
Dry runs still rebuild `generated/`, but do not modify installation destinations.
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"defaultMode": "auto",
|
||||
"allow": [
|
||||
"Agent"
|
||||
],
|
||||
"allow": __CLAUDE_ALLOW_RULES__,
|
||||
"deny": __CLAUDE_DENY_RULES__
|
||||
},
|
||||
"sandbox": __CLAUDE_SANDBOX__,
|
||||
|
||||
@@ -2,6 +2,9 @@ approval_policy = "on-request"
|
||||
approvals_reviewer = "auto_review"
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
|
||||
[windows]
|
||||
sandbox = "unelevated"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Configuration
|
||||
|
||||
Codex receives a generated global `AGENTS.md`, agent TOML files, skills, rule files, and a `config.toml` adapter. It sets workspace sandboxing, automatic approval review, `agents.enabled = true`, `agents.max_concurrent_threads_per_session = 5`, and `agents.max_depth = 1`. Claude Code receives a generated global `CLAUDE.md`, agent Markdown files, skills, and `settings.json` with `auto` permissions, explicit `Agent` permission, and a five-operation concurrency default. Installation resolves user paths at runtime; the repository contains no machine-specific paths.
|
||||
Codex receives a generated global `AGENTS.md`, agent TOML files, skills, rule files, and a `config.toml` adapter. It sets workspace sandboxing with network access for shell commands (so MCPorter calls such as Context7 work without approval), automatic approval review, `agents.enabled = true`, `agents.max_concurrent_threads_per_session = 5`, and `agents.max_depth = 1`. Claude Code receives a generated global `CLAUDE.md`, agent Markdown files, skills, and `settings.json` with `auto` permissions, explicit `Agent` and `mcporter call context7.*` shell permissions, `mcp.context7.com` as the only allowed sandbox domain where the Claude sandbox is enabled, and a five-operation concurrency default. Installation resolves user paths at runtime; the repository contains no machine-specific paths.
|
||||
|
||||
Both clients embed `shared/rules/general.md` and the rule-loading conditions from `shared/global-instructions.md`. Rules for languages and frameworks live in their own directories, with an `index.md` entry point and focused Markdown rules under `references/`. Security has its own directory with a baseline entry point and focused rules for authentication, web, APIs, data, files, network, cryptography, and supply chains. Git, Microsoft, and cross-cutting rules remain directly under `shared/rules/`. Codex receives the full directory tree; Claude generates a skill for each rule and copies supporting references alongside the index skill.
|
||||
|
||||
@@ -37,4 +37,4 @@ The agent chooses the smallest sufficient checks based on behavior, affected cal
|
||||
|
||||
Delegation and long-running state use the shared orchestration rule, loaded on demand as a Codex rule or Claude skill. The coordinator owns decisions and routes concise evidence-based handoffs. Review and execution verification have separate responsibilities; see [Agents](agents.md).
|
||||
|
||||
Rebuild packages after changes to shared definitions or adapters. Generated defaults reach a user's configuration only through installation or update. Updates merge Codex's setup-owned root keys and table fields while retaining other root keys, extra fields in managed tables, plugin and marketplace tables, and unowned array tables. Other standalone tables may be dropped. Claude settings retain unrelated keys and hooks. See [the README](../README.md) for overwrite and backup behavior.
|
||||
Rebuild packages after changes to shared definitions or adapters. Generated defaults reach a user's configuration only through installation or update. Updates merge Codex's setup-owned root keys and table fields while retaining other root keys, extra fields in managed tables, plugin and marketplace tables, other unmanaged tables such as `mcp_servers` and `model_providers`, and unowned array tables. Claude settings retain unrelated keys and hooks. See [the README](../README.md) for overwrite and backup behavior.
|
||||
+1
-1
@@ -10,7 +10,7 @@ Selecting Context7 with MCPorter registers the server in MCPorter. Selecting Con
|
||||
|
||||
`update.ps1`/`update.sh` lets you revise the saved extension selection. Checking an extension installs it if missing or updates it if owned; existing foreign installations are preserved. Deselection removes only extensions that this setup installed and recorded as owned. Pre-existing or manually installed plugins and skills are retained. Extensions removed from the manifest are also removed when recorded as owned; unrelated installations are retained. Update refuses to run if any selected destination lacks an installation manifest (`Not installed, use the install script.`).
|
||||
|
||||
Codex's local `plugins` and `marketplaces` tables survive configuration synchronization, along with other root keys, extra fields in managed tables, and unowned array tables. Other standalone tables may be dropped; changed files are backed up. Cached plugin files alone are not treated as proof of installation: selection uses the client CLI's installed list. If an older update removed the registrations, inspect the backed-up `config.toml` under `~/.codex/backups/` and reselect the affected plugins on the next update.
|
||||
Codex's local `plugins` and `marketplaces` tables survive configuration synchronization, along with other root keys, extra fields in managed tables, other unmanaged tables, and unowned array tables; changed files are backed up. Cached plugin files alone are not treated as proof of installation: selection uses the client CLI's installed list. If an older update removed the registrations, inspect the backed-up `config.toml` under `~/.codex/backups/` and reselect the affected plugins on the next update.
|
||||
|
||||
Codex receives Caveman through its native plugin adapter. Humanizer, Impeccable, and Anthropic Frontend Design are downloaded as skill payloads from the GitHub sources in the manifest, without running upstream installers. All three appear in the extension selector for Codex. Ownership is recorded in `~/.my-ai-configuration/extensions.json`. Skill files are tracked by hash; modified or foreign files are retained with a warning. Existing installations from before ownership tracking are not automatically adopted.
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ Gitea Actions runs the repository checks from `.gitea/workflows/ci.yml`.
|
||||
| `tests/test-build-validation.ps1`, `tests/test-build-validation.sh` | Validate build failures and generated output. |
|
||||
| `tests/test-capabilities.ps1` | Guard PowerShell capability detection. |
|
||||
| `tests/test-install-guards.ps1`, `tests/test-install-guards.sh` | Validate install and update guard conditions. |
|
||||
| `tests/test-client-smoke.py` | Install a package into a temporary home with foreign settings and ask the installed Claude and Codex CLIs which skills, agents, and hooks they discover, without completing a model request. Skips a client whose CLI is unavailable. |
|
||||
| `tests/test-install-options.py` | Validate interactive option handling and generated manifest changes. |
|
||||
| `tests/test-managed-extensions.py` | Validate plugin, skill, and CLI ownership handling. |
|
||||
| `tests/test-managed-install.ps1`, `tests/test-managed-install.sh` | Validate managed-file synchronization and backups. |
|
||||
|
||||
+5
-4
@@ -4,17 +4,18 @@ Skills are reusable workflows. Rules are persistent constraints and agents are s
|
||||
|
||||
The current structure is organized by purpose:
|
||||
|
||||
- `debugging/`
|
||||
- `planning/`: implementation planning, impact analysis, and work-item definition
|
||||
- `planning/`: impact analysis and work-item definition
|
||||
- `research/`: technical and repository research
|
||||
- `reviews/`: code, security, performance, WPF, UI/UX, business, API, and EF Core reviews
|
||||
- `reviews/`: security, performance, WPF, UI/UX, business, API, and EF Core reviews
|
||||
- `implementation/`: SQL, unit-test, and integration-test writing
|
||||
- `verification/`: facts, work-item, and regression verification
|
||||
- `verification/`: facts and work-item verification
|
||||
- `git/`: pull request reviews
|
||||
- `content-strategy/`, `copy-editing/`, `copywriting/`: planning, editing, and writing text
|
||||
- `image/`, `site-architecture/`: visuals and website structure
|
||||
- `marketing-psychology/`, `offers/`, `product-marketing/`, `cro/`: audience understanding, value framing, and conversion
|
||||
|
||||
General planning, debugging, code review, and completion verification workflows come from Superpowers, so the catalog does not duplicate their triggers. The remaining skills cover focused domains Superpowers does not.
|
||||
|
||||
Each skill defines a focused workflow with the investigation, decision points, and output expected for that task. Keep guidance specific to the skill; shared repository constraints belong in the rules.
|
||||
|
||||
The single catalog in `shared/skills/` includes development skills and focused skills for writing, images, content and site structure, positioning, psychology, and conversion. Builds include every retained skill for both clients. Updating a managed installation backs up and removes unchanged setup-owned skills deleted from the catalog; local changes are backed up and retained.
|
||||
|
||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
|
||||
CLAUDE_TOP_LEVEL = {"permissions", "sandbox", "env", "statusLine", "hooks"}
|
||||
CLAUDE_PERMISSION_KEYS = {"defaultMode", "allow", "deny"}
|
||||
CLAUDE_SANDBOX_KEYS = {"enabled", "allowUnsandboxedCommands", "failIfUnavailable"}
|
||||
CLAUDE_SANDBOX_KEYS = {"enabled", "allowUnsandboxedCommands", "failIfUnavailable", "network"}
|
||||
CLAUDE_STATUS_LINE_KEYS = {"type", "command"}
|
||||
CLAUDE_HOOK_KEYS = {"type", "command", "timeout"}
|
||||
CLAUDE_PERMISSION_MODES = {"acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"}
|
||||
@@ -40,7 +40,8 @@ def validate_claude(path: Path) -> None:
|
||||
if not isinstance(permissions.get(key), list) or not all(isinstance(item, str) for item in permissions[key]):
|
||||
fail(f"permissions.{key} must be an array of strings")
|
||||
sandbox = require_keys(settings.get("sandbox"), CLAUDE_SANDBOX_KEYS, "sandbox")
|
||||
if sandbox != {"enabled": False} and sandbox != {"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True}:
|
||||
if sandbox != {"enabled": False} and sandbox != {"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True,
|
||||
"network": {"allowedDomains": ["mcp.context7.com"]}}:
|
||||
fail("sandbox must be disabled or enabled with fail-closed settings")
|
||||
env = settings.get("env")
|
||||
if not isinstance(env, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in env.items()):
|
||||
|
||||
@@ -9,7 +9,7 @@ if ($sandboxSupported) {
|
||||
$kernel = (& uname -r 2>$null | Out-String).Trim()
|
||||
if ($kernel -match '(?i)microsoft') { $sandboxSupported = $kernel -match '(?i)microsoft-standard-WSL2' }
|
||||
}
|
||||
$claudeSandbox = if ($sandboxSupported) { '{"enabled": true, "allowUnsandboxedCommands": false, "failIfUnavailable": true}' } else { '{"enabled": false}' }
|
||||
$claudeSandbox = if ($sandboxSupported) { '{"enabled": true, "allowUnsandboxedCommands": false, "failIfUnavailable": true, "network": {"allowedDomains": ["mcp.context7.com"]}}' } else { '{"enabled": false}' }
|
||||
$pluginManifest = Join-Path $root 'adapters/plugins.tsv'
|
||||
$capabilityManifest = Join-Path $root 'adapters/claude/capabilities.tsv'
|
||||
|
||||
@@ -185,9 +185,11 @@ foreach ($shell in @('powershell','bash')) {
|
||||
@('Bash(git reset --hard*)','Bash(git clean*)','Bash(rm -rf*)')
|
||||
}
|
||||
$denyRulesJson = ConvertTo-Json -InputObject $denyRules -Compress
|
||||
$shellTool = if ($shell -eq 'powershell') { 'PowerShell' } else { 'Bash' }
|
||||
$allowRulesJson = ConvertTo-Json -InputObject @('Agent', "$shellTool(mcporter call context7.*)") -Compress
|
||||
$compactScript = if ($shell -eq 'powershell') { 'Record-Compact.ps1' } else { 'record-compact.sh' }
|
||||
$pointerScript = if ($shell -eq 'powershell') { 'Show-SessionStatePointer.ps1' } else { 'show-session-state-pointer.sh' }
|
||||
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__POWERSHELL_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__POWERSHELL_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript).Replace('__CLAUDE_SANDBOX__', $claudeSandbox).Replace('__CLAUDE_DENY_RULES__', $denyRulesJson)
|
||||
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__POWERSHELL_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__POWERSHELL_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript).Replace('__CLAUDE_SANDBOX__', $claudeSandbox).Replace('__CLAUDE_DENY_RULES__', $denyRulesJson).Replace('__CLAUDE_ALLOW_RULES__', $allowRulesJson)
|
||||
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ case "$platform" in
|
||||
;;
|
||||
esac
|
||||
sandbox='{"enabled": false}'
|
||||
[ "$sandbox_supported" != true ] || sandbox='{"enabled": true, "allowUnsandboxedCommands": false, "failIfUnavailable": true}'
|
||||
[ "$sandbox_supported" != true ] || sandbox='{"enabled": true, "allowUnsandboxedCommands": false, "failIfUnavailable": true, "network": {"allowedDomains": ["mcp.context7.com"]}}'
|
||||
plugin_manifest="$root/adapters/plugins.tsv"
|
||||
capability_manifest="$root/adapters/claude/capabilities.tsv"
|
||||
|
||||
@@ -174,8 +174,10 @@ for client in codex claude; do
|
||||
content=$(<"$path")
|
||||
if [ "$shell" = powershell ]; then
|
||||
deny_rules='["PowerShell(git reset --hard*)","PowerShell(git clean*)","PowerShell(Remove-Item *-Recurse*)"]'
|
||||
allow_rules='["Agent","PowerShell(mcporter call context7.*)"]'
|
||||
else
|
||||
deny_rules='["Bash(git reset --hard*)","Bash(git clean*)","Bash(rm -rf*)"]'
|
||||
allow_rules='["Agent","Bash(mcporter call context7.*)"]'
|
||||
fi
|
||||
content=${content//__HOOK_COMMAND__/$command}
|
||||
content=${content//__POWERSHELL_HOOK_COMMAND__/$command}
|
||||
@@ -187,6 +189,7 @@ for client in codex claude; do
|
||||
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
|
||||
content=${content//__CLAUDE_SANDBOX__/$sandbox}
|
||||
content=${content//__CLAUDE_DENY_RULES__/$deny_rules}
|
||||
content=${content//__CLAUDE_ALLOW_RULES__/$allow_rules}
|
||||
printf '%s\n' "$content" > "$path"
|
||||
done
|
||||
|
||||
|
||||
@@ -73,18 +73,23 @@ def merge_json(current, managed):
|
||||
groups = value.get(event, [])
|
||||
old_groups = hooks.get(event, [])
|
||||
if isinstance(groups, list) and isinstance(old_groups, list):
|
||||
retained = [group for group in old_groups if not any(
|
||||
re.search(r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)',
|
||||
str(hook.get('command', '')) + str(hook.get('command_windows', '')), re.I)
|
||||
for hook in group.get('hooks', []) if isinstance(hook, dict)
|
||||
)]
|
||||
retained = []
|
||||
for group in old_groups:
|
||||
if not isinstance(group, dict) or not isinstance(group.get('hooks'), list):
|
||||
retained.append(group)
|
||||
continue
|
||||
foreign = [hook for hook in group['hooks'] if not (isinstance(hook, dict) and re.search(
|
||||
r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)',
|
||||
str(hook.get('command', '')) + str(hook.get('command_windows', '')), re.I))]
|
||||
if foreign:
|
||||
retained.append(dict(group, hooks=foreign))
|
||||
merged = groups + [group for group in retained if group not in groups]
|
||||
if merged:
|
||||
hooks[event] = merged
|
||||
else:
|
||||
hooks.pop(event, None)
|
||||
else:
|
||||
hooks[event] = value
|
||||
hooks[event] = groups
|
||||
result[key] = hooks
|
||||
elif isinstance(value, dict) and isinstance(result.get(key), dict):
|
||||
result[key] = merge_json(result[key], value)
|
||||
@@ -129,11 +134,9 @@ def merge_toml(current_text, managed_text, statusline_enabled=True):
|
||||
owned_hook = any(re.search(r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)', line, re.I) for line in block)
|
||||
if plugin_table and header in managed_headers:
|
||||
raise ValueError('Cannot merge conflicting managed plugin tables.')
|
||||
if (plugin_table and header not in managed_headers) or (array_table and not owned_hook):
|
||||
if (header not in managed_headers and not array_table) or (array_table and not owned_hook):
|
||||
extras.extend(block)
|
||||
continue
|
||||
if header not in managed_headers:
|
||||
continue
|
||||
if array_table:
|
||||
continue
|
||||
generated_block = next(lines for name, lines in managed_sections if name == header)
|
||||
@@ -175,18 +178,24 @@ if __name__ == '__main__':
|
||||
parser.add_argument('--current', type=Path)
|
||||
parser.add_argument('--flashbang', choices=('true', 'false'))
|
||||
parser.add_argument('--statusline', choices=('true', 'false'), default='true')
|
||||
parser.add_argument('--managed', type=Path)
|
||||
parser.add_argument('--output', type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
if args.action == 'filter':
|
||||
print(filter_options(args.path, args.flashbang == 'true', args.statusline == 'true'), end='')
|
||||
result = filter_options(args.path, args.flashbang == 'true', args.statusline == 'true')
|
||||
else:
|
||||
managed = args.managed.read_text(encoding='utf-8-sig') if args.managed else sys.stdin.buffer.read().decode('utf-8-sig')
|
||||
current = args.current.read_text(encoding='utf-8-sig')
|
||||
if args.action == 'merge-json':
|
||||
current = json.loads(args.current.read_text(encoding='utf-8-sig'))
|
||||
managed = json.load(sys.stdin)
|
||||
print(json.dumps(merge_json(current, managed), indent=2, ensure_ascii=False) + '\n', end='')
|
||||
result = json.dumps(merge_json(json.loads(current), json.loads(managed)), indent=2, ensure_ascii=False) + '\n'
|
||||
else:
|
||||
print(merge_toml(args.current.read_text(encoding='utf-8-sig'), sys.stdin.read(), args.statusline == 'true'), end='')
|
||||
result = merge_toml(current, managed, args.statusline == 'true')
|
||||
if args.output:
|
||||
args.output.write_text(result, encoding='utf-8', newline='')
|
||||
else:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
print(result, end='')
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
print(f'Install options: {error}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -86,6 +86,26 @@ function Write-ManagedManifest {
|
||||
[System.IO.File]::WriteAllText($ManifestPath, $content, (New-Object System.Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
function Invoke-InstallOptions {
|
||||
param([Parameter(Mandatory=$true)][string[]]$Arguments, [byte[]]$Managed, [Parameter(Mandatory=$true)][string]$ErrorMessage)
|
||||
# Exchange content through UTF-8 files: Windows PowerShell pipes re-encode text by console code page.
|
||||
$outputPath = [System.IO.Path]::GetTempFileName()
|
||||
$managedPath = $null
|
||||
try {
|
||||
if ($null -ne $Managed) {
|
||||
$managedPath = [System.IO.Path]::GetTempFileName()
|
||||
[System.IO.File]::WriteAllBytes($managedPath, $Managed)
|
||||
$Arguments += @('--managed', $managedPath)
|
||||
}
|
||||
& python (Join-Path $PSScriptRoot 'install-options.py') @Arguments --output $outputPath
|
||||
if ($LASTEXITCODE -ne 0) { throw $ErrorMessage }
|
||||
return [System.IO.File]::ReadAllText($outputPath, (New-Object System.Text.UTF8Encoding($false)))
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $outputPath -Force -ErrorAction SilentlyContinue
|
||||
if ($managedPath) { Remove-Item -LiteralPath $managedPath -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
function Sync-ManagedDestination {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -135,8 +155,7 @@ function Sync-ManagedDestination {
|
||||
if ($filterOptions) {
|
||||
$flashbangOption = if ($FlashbangEnabled) { 'true' } else { 'false' }
|
||||
$statusLineOption = if ($StatusLineEnabled) { 'true' } else { 'false' }
|
||||
$rawContent = (& python (Join-Path $PSScriptRoot 'install-options.py') filter --path $_.FullName --flashbang $flashbangOption --statusline $statusLineOption | Out-String)
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not configure install options.' }
|
||||
$rawContent = Invoke-InstallOptions -Arguments @('filter', '--path', $_.FullName, '--flashbang', $flashbangOption, '--statusline', $statusLineOption) -ErrorMessage 'Could not configure install options.'
|
||||
}
|
||||
$needsSubstitution = $filterOptions -or $rawContent.Contains('__AI_CONFIG_ROOT__') -or $rawContent.Contains('__HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_COMMAND__') -or $rawContent.Contains('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__')
|
||||
if ($needsSubstitution) {
|
||||
@@ -147,13 +166,11 @@ function Sync-ManagedDestination {
|
||||
}
|
||||
if ($relative -eq 'config.toml' -and (Test-Path -LiteralPath $target)) {
|
||||
$statusLineFlag = if ($StatusLineEnabled) { 'true' } else { 'false' }
|
||||
$content = [System.Text.Encoding]::UTF8.GetString($newBytes) | & python (Join-Path $PSScriptRoot 'install-options.py') merge-toml --current $target --statusline $statusLineFlag | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not merge managed config.toml keys.' }
|
||||
$content = Invoke-InstallOptions -Arguments @('merge-toml', '--current', $target, '--statusline', $statusLineFlag) -Managed $newBytes -ErrorMessage 'Could not merge managed config.toml keys.'
|
||||
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($content)
|
||||
}
|
||||
if ($relative -eq 'settings.json' -and (Test-Path -LiteralPath $target)) {
|
||||
$finalContent = [System.Text.Encoding]::UTF8.GetString($newBytes) | & python (Join-Path $PSScriptRoot 'install-options.py') merge-json --current $target | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not merge managed settings.json keys.' }
|
||||
$finalContent = Invoke-InstallOptions -Arguments @('merge-json', '--current', $target) -Managed $newBytes -ErrorMessage 'Could not merge managed settings.json keys.'
|
||||
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($finalContent)
|
||||
}
|
||||
$newHash = Get-Sha256HashOfBytes $newBytes
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Behavioral client smoke tests against an isolated installation.
|
||||
|
||||
Each test installs a generated package into a temporary home that already holds
|
||||
foreign settings, then asks the real client CLI what it discovered. Claude runs
|
||||
with an invalid API key and is stopped at its init event; Codex is queried
|
||||
through `codex app-server`. No model request completes.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
SHELL = 'powershell' if os.name == 'nt' else 'bash'
|
||||
MARKER_SCRIPT = "import sys; open(sys.argv[1], 'w').write('ok')"
|
||||
|
||||
|
||||
def frontmatter_names(directory, pattern):
|
||||
names = set()
|
||||
for path in directory.rglob(pattern):
|
||||
match = re.search(r'(?m)^name\s*[:=]\s*"?([^"\r\n]+)"?\s*$', path.read_text(encoding='utf-8-sig'))
|
||||
if match:
|
||||
names.add(match.group(1).strip())
|
||||
return names
|
||||
|
||||
|
||||
def marker_command(marker):
|
||||
return f'"{sys.executable}" -c "{MARKER_SCRIPT}" "{marker}"'
|
||||
|
||||
|
||||
def clean_env(**values):
|
||||
env = {key: value for key, value in os.environ.items()
|
||||
if not key.startswith(('BUN_INSPECT', 'CLAUDE', 'ANTHROPIC', 'CODEX', 'OPENAI'))}
|
||||
env.update(values)
|
||||
return env
|
||||
|
||||
|
||||
def install(client, home):
|
||||
if SHELL == 'powershell':
|
||||
script = ('$ErrorActionPreference = "Stop"; '
|
||||
f'. "{ROOT}/scripts/lib/manifest.ps1"; . "{ROOT}/scripts/lib/install-targets.ps1"; '
|
||||
'$command = "powershell -NoProfile -ExecutionPolicy Bypass -File"; '
|
||||
f'foreach ($item in (Get-InstallTargets -Generated "{ROOT}/generated" -HomePath "{home}" -Shell PowerShell -Client {client})) {{ '
|
||||
'Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp smoke '
|
||||
'-AiConfigRoot $item.Destination.Replace("\\","/") -ShellCommand $command -PowerShellCommand $command -Summary | Out-Null }')
|
||||
args = ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script]
|
||||
else:
|
||||
script = (f'set -e; . "{ROOT}/scripts/lib/manifest.sh"; . "{ROOT}/scripts/lib/install-targets.sh"; '
|
||||
f'get_install_targets "{ROOT}/generated" "{home}" bash {client.lower()} | '
|
||||
'while IFS="|" read -r source destination; do '
|
||||
'sync_managed_destination "$source" "$destination" smoke "$destination" bash "pwsh -NoProfile -File" false true >/dev/null; done')
|
||||
args = ['bash', '-c', script]
|
||||
result = subprocess.run(args, capture_output=True, text=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"install failed: {result.stdout}{result.stderr}")
|
||||
|
||||
|
||||
class ClientSmokeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.base = Path(tempfile.mkdtemp(prefix='ai-config-client-smoke-'))
|
||||
self.home = self.base / 'home'
|
||||
self.work = self.base / 'work'
|
||||
self.work.mkdir(parents=True)
|
||||
subprocess.run(['git', 'init', '-q'], cwd=self.work, check=True)
|
||||
self.marker = self.base / 'foreign-hook.txt'
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.base, ignore_errors=True)
|
||||
|
||||
@unittest.skipUnless(shutil.which('claude'), 'Claude CLI unavailable')
|
||||
def test_claude_discovers_package_and_keeps_foreign_settings(self):
|
||||
package = ROOT / f'generated/claude-{SHELL}'
|
||||
config = self.home / '.claude'
|
||||
config.mkdir(parents=True)
|
||||
foreign_hook = {'hooks': [{'type': 'command', 'command': marker_command(self.marker), 'timeout': 10}]}
|
||||
(config / 'settings.json').write_text(json.dumps({'foreignSetting': 'keep', 'hooks': {'SessionStart': [foreign_hook]}}))
|
||||
install('Claude', self.home)
|
||||
|
||||
process = subprocess.Popen([shutil.which('claude'), '-p', 'smoke', '--output-format', 'stream-json', '--verbose', '--max-turns', '1'],
|
||||
cwd=self.work, env=clean_env(CLAUDE_CONFIG_DIR=str(config), ANTHROPIC_API_KEY='sk-ant-invalid-smoke'),
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, encoding='utf-8')
|
||||
events = []
|
||||
try:
|
||||
for line in process.stdout:
|
||||
events.append(json.loads(line))
|
||||
if events[-1].get('subtype') == 'init':
|
||||
break
|
||||
finally:
|
||||
process.kill()
|
||||
for stream in (process.stdin, process.stdout):
|
||||
if stream:
|
||||
stream.close()
|
||||
process.wait(timeout=30)
|
||||
init = next((event for event in events if event.get('subtype') == 'init'), None)
|
||||
self.assertIsNotNone(init, 'Claude emitted no init event')
|
||||
|
||||
with self.subTest('skills'):
|
||||
self.assertEqual(sorted(frontmatter_names(package / 'skills', 'SKILL.md') - set(init['skills'])), [])
|
||||
with self.subTest('agents'):
|
||||
self.assertEqual(sorted(frontmatter_names(package / 'agents', '*.md') - set(init['agents'])), [])
|
||||
with self.subTest('hooks'):
|
||||
failed = [(event.get('hook_name'), event.get('exit_code'), event.get('stderr', '')[:200])
|
||||
for event in events if event.get('subtype') == 'hook_response' and event.get('exit_code') != 0]
|
||||
self.assertEqual(failed, [])
|
||||
self.assertTrue(self.marker.exists(), 'foreign SessionStart hook did not run')
|
||||
with self.subTest('foreign settings'):
|
||||
self.assertEqual(json.loads((config / 'settings.json').read_text(encoding='utf-8-sig')).get('foreignSetting'), 'keep')
|
||||
|
||||
@unittest.skipUnless(shutil.which('codex'), 'Codex CLI unavailable')
|
||||
def test_codex_discovers_package_and_keeps_foreign_settings(self):
|
||||
package = ROOT / f'generated/codex-{SHELL}'
|
||||
codex_home = self.home / '.codex'
|
||||
codex_home.mkdir(parents=True)
|
||||
command = json.dumps(marker_command(self.marker))
|
||||
(codex_home / 'config.toml').write_text(
|
||||
'[mcp_servers.smoke_demo]\ncommand = "smoke-demo"\nenabled = false\n\n'
|
||||
f'[[hooks.SessionStart]]\n[[hooks.SessionStart.hooks]]\ntype = "command"\ncommand = {command}\ntimeout = 10\n')
|
||||
install('Codex', self.home)
|
||||
|
||||
process = subprocess.Popen([shutil.which('codex'), 'app-server'], cwd=self.work,
|
||||
env=clean_env(CODEX_HOME=str(codex_home), OPENAI_API_KEY='sk-invalid-smoke'),
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, encoding='utf-8')
|
||||
|
||||
def call(request_id, method, params):
|
||||
process.stdin.write(json.dumps({'jsonrpc': '2.0', 'id': request_id, 'method': method, 'params': params}) + '\n')
|
||||
process.stdin.flush()
|
||||
while True:
|
||||
message = json.loads(process.stdout.readline())
|
||||
if message.get('id') == request_id:
|
||||
self.assertNotIn('error', message, method)
|
||||
return message['result']
|
||||
|
||||
try:
|
||||
call(1, 'initialize', {'clientInfo': {'name': 'ai-config-smoke', 'version': '1'}})
|
||||
process.stdin.write(json.dumps({'jsonrpc': '2.0', 'method': 'initialized'}) + '\n')
|
||||
# Codex resolves ~/.agents/skills from the real Windows profile, so register the isolated root explicitly.
|
||||
skills_root = self.home / '.agents/skills'
|
||||
call(2, 'skills/extraRoots/set', {'extraRoots': [str(skills_root)]})
|
||||
skills = call(3, 'skills/list', {'cwds': [str(self.work)], 'forceReload': True})['data'][0]['skills']
|
||||
hooks = call(4, 'hooks/list', {'cwds': [str(self.work)]})['data'][0]['hooks']
|
||||
config = call(5, 'config/read', {})['config']
|
||||
finally:
|
||||
process.kill()
|
||||
for stream in (process.stdin, process.stdout):
|
||||
if stream:
|
||||
stream.close()
|
||||
process.wait(timeout=30)
|
||||
|
||||
isolated = {skill['name'] for skill in skills if Path(skill['path']).resolve().is_relative_to(skills_root.resolve())}
|
||||
self.assertEqual(sorted(frontmatter_names(package / 'skills', 'SKILL.md') - isolated), [], 'skills not discovered')
|
||||
events = {(hook['eventName'], hook['enabled']) for hook in hooks if hook.get('source') == 'user'}
|
||||
self.assertIn(('stop', True), events, 'managed Stop hook not registered')
|
||||
self.assertIn(('sessionStart', True), events, 'foreign SessionStart hook lost')
|
||||
self.assertEqual(config['mcp_servers']['smoke_demo']['command'], 'smoke-demo')
|
||||
self.assertTrue((codex_home / 'agents').is_dir() and frontmatter_names(codex_home / 'agents', '*.toml') == frontmatter_names(package / 'agents', '*.toml'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tomllib
|
||||
import unittest
|
||||
@@ -31,6 +32,47 @@ class InstallOptionsTests(unittest.TestCase):
|
||||
|
||||
self.assertIn(plugin_state, result)
|
||||
|
||||
def test_merge_toml_preserves_unmanaged_tables(self):
|
||||
foreign = ('[mcp_servers.demo]\ncommand = "demo"\n\n[mcp_servers.demo.env]\nTOKEN_NAME = "x"\n\n'
|
||||
'[model_providers.custom]\nname = "Custom"\nbase_url = "https://example.invalid"\n')
|
||||
current = "model = 'old'\n\n[tui]\nstatus_line = ['a']\ntheme = 'dark'\n\n" + foreign
|
||||
managed = "model = 'new'\n\n[tui]\nstatus_line = ['b']\n"
|
||||
|
||||
result = tomllib.loads(OPTIONS.merge_toml(current, managed))
|
||||
|
||||
self.assertEqual(result['model'], 'new')
|
||||
self.assertEqual(result['tui'], {'status_line': ['b'], 'theme': 'dark'})
|
||||
self.assertEqual(result['mcp_servers'], {'demo': {'command': 'demo', 'env': {'TOKEN_NAME': 'x'}}})
|
||||
self.assertEqual(result['model_providers'], {'custom': {'name': 'Custom', 'base_url': 'https://example.invalid'}})
|
||||
|
||||
def test_merge_json_removes_only_owned_hooks_from_mixed_group(self):
|
||||
owned = {'type': 'command', 'command': 'bash "/old/hooks/flashbang.sh"', 'timeout': 8}
|
||||
foreign = {'type': 'command', 'command': 'notify-done', 'timeout': 3}
|
||||
current = {'hooks': {'Stop': [{'matcher': 'custom', 'hooks': [owned, foreign]}],
|
||||
'PreCompact': [{'hooks': [{'type': 'command', 'command': 'bash "/old/record-compact.sh"'}]}]}}
|
||||
managed_stop = {'hooks': [{'type': 'command', 'command': 'bash "/new/hooks/flashbang.sh"', 'timeout': 8}]}
|
||||
managed = {'hooks': {'Stop': [managed_stop], 'PreCompact': []}}
|
||||
|
||||
hooks = OPTIONS.merge_json(current, managed)['hooks']
|
||||
|
||||
self.assertEqual(hooks['Stop'], [managed_stop, {'matcher': 'custom', 'hooks': [foreign]}])
|
||||
self.assertNotIn('PreCompact', hooks)
|
||||
|
||||
def test_merge_cli_accepts_bom_input_and_preserves_non_ascii(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
current = Path(directory) / 'config.toml'
|
||||
current.write_text('# Größe\n[mcp_servers.demo]\ncommand = "ä"\n', encoding='utf-8')
|
||||
output = Path(directory) / 'out.toml'
|
||||
script = str(ROOT / 'scripts/lib/install-options.py')
|
||||
managed = 'model = "new"\n'.encode('utf-8')
|
||||
piped = subprocess.run([sys.executable, script, 'merge-toml', '--current', str(current), '--output', str(output)], input=managed)
|
||||
self.assertEqual(piped.returncode, 0)
|
||||
self.assertEqual(tomllib.loads(output.read_text(encoding='utf-8'))['mcp_servers']['demo']['command'], 'ä')
|
||||
managed_path = Path(directory) / 'managed.toml'
|
||||
managed_path.write_bytes(managed)
|
||||
subprocess.run([sys.executable, script, 'merge-toml', '--current', str(current), '--managed', str(managed_path), '--output', str(output)], check=True)
|
||||
self.assertIn('# Größe', output.read_text(encoding='utf-8'))
|
||||
|
||||
def test_filter_preserves_other_claude_hooks_and_status(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / 'settings.json'
|
||||
|
||||
@@ -63,7 +63,7 @@ try {
|
||||
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins'
|
||||
$mergedConfig = [System.IO.File]::ReadAllText($configPath)
|
||||
if (-not ($mergedConfig -replace "`r`n", "`n").Contains(($pluginState -replace "`r`n", "`n"))) { throw 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.' }
|
||||
if (-not $mergedConfig.Contains("model = 'old'") -or -not $mergedConfig.Contains("model_reasoning_effort = 'high'") -or -not $mergedConfig.Contains("approvals_reviewer = 'auto_review'") -or $mergedConfig.Contains("approvals_reviewer = 'user'") -or $mergedConfig.Contains('[other]')) { throw 'Updating config.toml must preserve local model settings and enforce automatic review.' }
|
||||
if (-not $mergedConfig.Contains("model = 'old'") -or -not $mergedConfig.Contains("model_reasoning_effort = 'high'") -or -not $mergedConfig.Contains("approvals_reviewer = 'auto_review'") -or $mergedConfig.Contains("approvals_reviewer = 'user'") -or -not $mergedConfig.Contains('[other]')) { throw 'Updating config.toml must preserve local settings and tables and enforce automatic review.' }
|
||||
if ($mergedConfig.IndexOf("model = 'old'") -gt $mergedConfig.IndexOf('[tui]') -or $mergedConfig.IndexOf("model_reasoning_effort = 'high'") -gt $mergedConfig.IndexOf('[tui]')) { throw 'Model settings must remain in the TOML root table.' }
|
||||
if ([System.IO.File]::ReadAllText((Join-Path $destination 'backups/plugins/config.toml')) -ne $originalConfig) { throw 'Original plugin configuration must be backed up.' }
|
||||
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-repeat'
|
||||
|
||||
@@ -74,7 +74,7 @@ merged_config=$(cat "$destination/config.toml")
|
||||
merged_config_normalized=${merged_config//$'\r'/}
|
||||
plugin_state_normalized=${plugin_state//$'\r'/}
|
||||
[[ "$merged_config_normalized" == *"$plugin_state_normalized"* ]] || { printf 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.\n' >&2; exit 1; }
|
||||
[[ "$merged_config" == *"model = 'old'"* && "$merged_config" == *"model_reasoning_effort = 'high'"* && "$merged_config" == *"approvals_reviewer = 'auto_review'"* && "$merged_config" != *"approvals_reviewer = 'user'"* && "$merged_config" != *'[other]'* ]] || { printf 'Updating config.toml must preserve local model settings and enforce automatic review.\n' >&2; exit 1; }
|
||||
[[ "$merged_config" == *"model = 'old'"* && "$merged_config" == *"model_reasoning_effort = 'high'"* && "$merged_config" == *"approvals_reviewer = 'auto_review'"* && "$merged_config" != *"approvals_reviewer = 'user'"* && "$merged_config" == *'[other]'* ]] || { printf 'Updating config.toml must preserve local model settings and enforce automatic review.\n' >&2; exit 1; }
|
||||
[ "$(printf '%s\n' "$merged_config" | grep -n "model = 'old'" | cut -d: -f1)" -lt "$(printf '%s\n' "$merged_config" | grep -n '^\[tui\]$' | cut -d: -f1)" ] || { printf 'Model must remain in TOML root table.\n' >&2; exit 1; }
|
||||
[ "$(printf '%s\n' "$merged_config" | grep -n "model_reasoning_effort = 'high'" | cut -d: -f1)" -lt "$(printf '%s\n' "$merged_config" | grep -n '^\[tui\]$' | cut -d: -f1)" ] || { printf 'Model effort must remain in TOML root table.\n' >&2; exit 1; }
|
||||
[ "$(cat "$destination/backups/plugins/config.toml")" = "$original_config" ] || { printf 'Original plugin configuration must be backed up.\n' >&2; exit 1; }
|
||||
|
||||
@@ -48,7 +48,7 @@ foreach ($shell in @('powershell','bash')) {
|
||||
if ($client -eq 'claude' -and (Get-Content (Join-Path $package 'agents/reviewer.md') -Raw) -notmatch "(?m)^tools: Read,Grep,Glob,$expectedReviewerShellTool\r?`$") { throw "Reviewer capability profile is missing in $package" }
|
||||
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
|
||||
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" }
|
||||
if (-not (Test-Path "$package/skills/debugging/SKILL.md") -or -not (Test-Path "$package/skills/copywriting/SKILL.md") -or -not (Test-Path "$package/skills/marketing-psychology/SKILL.md") -or -not (Test-Path "$package/skills/image/SKILL.md") -or (Test-Path "$package/skills/marketing-plan/SKILL.md") -or (Test-Path "$package/skills/social/SKILL.md")) { throw "Incorrect skill catalog in $package" }
|
||||
if ((Test-Path "$package/skills/debugging/SKILL.md") -or -not (Test-Path "$package/skills/reviews/security-review/SKILL.md") -or -not (Test-Path "$package/skills/copywriting/SKILL.md") -or -not (Test-Path "$package/skills/marketing-psychology/SKILL.md") -or -not (Test-Path "$package/skills/image/SKILL.md") -or (Test-Path "$package/skills/marketing-plan/SKILL.md") -or (Test-Path "$package/skills/social/SKILL.md")) { throw "Incorrect skill catalog in $package" }
|
||||
$expected = if ($shell -eq 'powershell') { '__POWERSHELL_COMMAND__ .*flashbang.ps1' } else { 'bash .*flashbang.sh' }
|
||||
if ($content -notmatch $expected -or $content -match '__HOOK_|__POWERSHELL_HOOK_') { throw "Incorrect shell command in $package" }
|
||||
if ($shell -eq 'powershell' -and $content -match 'WindowStyle\s+Hidden') { throw "PowerShell hook hides the terminal in $package" }
|
||||
@@ -62,7 +62,7 @@ foreach ($shell in @('powershell','bash')) {
|
||||
throw "Claude finish hook is incorrect in $package"
|
||||
} elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') {
|
||||
throw "Claude auto permission mode is missing in $package"
|
||||
} elseif ($sandboxSupported -and ($settings.sandbox.enabled -ne $true -or $settings.sandbox.allowUnsandboxedCommands -ne $false -or $settings.sandbox.failIfUnavailable -ne $true -or @($settings.sandbox.PSObject.Properties).Count -ne 3)) {
|
||||
} elseif ($sandboxSupported -and ($settings.sandbox.enabled -ne $true -or $settings.sandbox.allowUnsandboxedCommands -ne $false -or $settings.sandbox.failIfUnavailable -ne $true -or (@($settings.sandbox.network.allowedDomains) -join ',') -ne 'mcp.context7.com' -or @($settings.sandbox.PSObject.Properties).Count -ne 4)) {
|
||||
throw "Claude strict sandbox is missing in $package"
|
||||
} elseif (-not $sandboxSupported -and ($settings.sandbox.enabled -ne $false -or @($settings.sandbox.PSObject.Properties).Count -ne 1)) {
|
||||
throw "Claude sandbox must be disabled in $package"
|
||||
|
||||
@@ -55,7 +55,8 @@ for shell in powershell bash; do
|
||||
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"
|
||||
test -f "$package/skills/debugging/SKILL.md"
|
||||
test ! -e "$package/skills/debugging/SKILL.md"
|
||||
test -f "$package/skills/reviews/security-review/SKILL.md"
|
||||
test -f "$package/skills/copywriting/SKILL.md"
|
||||
test -f "$package/skills/marketing-psychology/SKILL.md"
|
||||
test -f "$package/skills/image/SKILL.md"
|
||||
@@ -79,7 +80,7 @@ for shell in powershell bash; do
|
||||
! grep -q 'flashbang-if-input' "$package/$file"
|
||||
! grep -q '"async"[[:space:]]*:[[:space:]]*true' "$package/$file"
|
||||
grep -q '"defaultMode"[[:space:]]*:[[:space:]]*"auto"' "$package/$file"
|
||||
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); expected={"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True} if sys.argv[2] == "true" else {"enabled": False}; assert settings["sandbox"] == expected' "$package/$file" "$sandbox_supported"
|
||||
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); expected={"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True, "network": {"allowedDomains": ["mcp.context7.com"]}} if sys.argv[2] == "true" else {"enabled": False}; assert settings["sandbox"] == expected' "$package/$file" "$sandbox_supported"
|
||||
statusline_extension='sh'
|
||||
[ "$shell" != powershell ] || statusline_extension=ps1
|
||||
test -f "$package/statusline/statusline.$statusline_extension"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Implementer
|
||||
|
||||
Understand requirements and existing code, implement the smallest complete solution, follow applicable rules, and choose proportionate verification. Inspect your changes to the degree needed for their risk; a separate or full review is not automatic. Preserve unrelated user work. Do not add comments or documentation without explicit user approval.
|
||||
Understand requirements and existing code, implement the smallest complete solution, follow applicable rules, and choose proportionate verification. Inspect your changes to the degree needed for their risk; a separate or full review is not automatic. Preserve unrelated user work.
|
||||
|
||||
Stay within assigned ownership, start at supplied references, and inspect additional context only when needed. Return changes, evidence, and unresolved risks using the shared orchestration handoff protocol; the parent owns shared task state.
|
||||
@@ -38,7 +38,7 @@
|
||||
* Remove dead code.
|
||||
* Remove unused imports and dependencies.
|
||||
* Remove commented-out code instead of keeping it in source files.
|
||||
* Do not add comments unless the user explicitly requests them.
|
||||
* Add comments only for non-obvious decisions or public contracts.
|
||||
* Treat Angular, TypeScript and lint warnings seriously.
|
||||
* Do not suppress diagnostics without a concrete reason.
|
||||
* Prefer correctness, security and maintainability over cleverness.
|
||||
|
||||
@@ -15,7 +15,7 @@ A task is only considered done when all applicable requirements below are fulfil
|
||||
* No temporary code, debug output, commented-out code, placeholders, or TODOs remain unless explicitly intended.
|
||||
* Error handling and edge cases are handled appropriately.
|
||||
* The implementation is readable, maintainable, and production-ready.
|
||||
* Documentation and comments follow `general.md`'s Documentation and Comments rules (not changed automatically, none created without explicit instruction).
|
||||
* Directly affected documentation and comments are updated; other documentation or comments are added only for a concrete need.
|
||||
* Any delegated research, review, or verification is complete and its relevant findings are resolved or reported.
|
||||
|
||||
## Work Items
|
||||
|
||||
@@ -4,10 +4,10 @@ Apply instructions in this order: direct user instructions; security and data-lo
|
||||
|
||||
Preserve security, data, compatibility, and unrelated user work. Ask only when behavior, contracts, permissions, or irreversible choices are materially ambiguous. Otherwise choose the smallest reversible change.
|
||||
|
||||
Use one applicable workflow per task. Treat skills and plugins as guidance subordinate to these rules. Continue work already authorized by the user; additional design approvals, documentation, commits, or delegation require a task-specific reason and the necessary authorization.
|
||||
Use one applicable workflow per task. Treat skills and plugins as guidance subordinate to these rules. Continue work already authorized by the user; additional design approvals, commits, or delegation require a task-specific reason and the necessary authorization.
|
||||
|
||||
Start repository discovery with direct reads and `rg` for known paths, symbols, exact strings, localized changes, configuration edits, and routine checks. Use the installed semantic retrieval CLI only when terminology is unknown, the question spans multiple components, or direct search did not locate the relevant flow: `python ~/.my-ai-configuration/semantic-retrieval/server.py search --root . --data-dir ~/.my-ai-configuration/semantic-retrieval/data --model-cache ~/.my-ai-configuration/semantic-retrieval/model-cache --query "..." --top-k 5`. Each uncached search uses Qwen3-Embedding-0.6B for candidate retrieval and Qwen3-Reranker-0.6B for final ranking; reuse the cache for repeated unchanged queries and verify results against current files. If Qwen is unavailable, fails, or returns no useful matches, continue with direct search and briefly report the limitation.
|
||||
|
||||
Verify proportionally: run the cheapest check that detects likely failure; broaden for security, data loss, integration, or cross-component risk. Never claim unrun checks passed. Report failures and material gaps.
|
||||
|
||||
Do not create documentation or explanatory comments without a clear need. Update existing documentation or comments only when directly affected by an API, configuration, or behavior change and necessary to keep them accurate. Write repository content and user-facing output in English.
|
||||
Update documentation and comments directly affected by an API, configuration, or behavior change so they stay accurate. Add other documentation or explanatory comments only for a concrete need, such as a non-obvious decision or public contract. Write repository content and user-facing output in English.
|
||||
@@ -9,4 +9,4 @@ Apply these rules when changing TypeScript syntax, control flow, or everyday cod
|
||||
* Use strict equality. Handle `null` and `undefined` intentionally and do not use `||` when `0`, `false`, or an empty string is a valid value.
|
||||
* Use optional chaining and nullish coalescing where they clarify behavior; avoid mixing nullish conventions without a project reason.
|
||||
* Keep naming descriptive and consistent. Avoid vague names and abbreviations unless they are established domain terms.
|
||||
* Remove dead code and unused imports. Do not add comments unless the user asks or a project rule requires them.
|
||||
* Remove dead code and unused imports. Add comments only for non-obvious decisions or public contracts.
|
||||
@@ -44,7 +44,7 @@
|
||||
* Use descriptive names.
|
||||
* Remove dead code.
|
||||
* Remove commented-out code.
|
||||
* Do not add comments unless the user explicitly requests them.
|
||||
* Add comments only for non-obvious decisions or public contracts.
|
||||
* Treat compiler, analyzer and binding warnings seriously.
|
||||
* Do not suppress warnings without a concrete reason.
|
||||
* Prefer correctness, responsiveness, security and maintainability over cleverness.
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: debugging
|
||||
description: Use when code, tests, commands, integrations, or user-visible behavior fail unexpectedly; reproduce the failure, trace the cause, and verify a focused fix.
|
||||
---
|
||||
|
||||
# Debugging
|
||||
|
||||
Use when a command, test, integration, or user-visible behavior fails or behaves unexpectedly.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. State expected and observed behavior. Capture the exact error, input, environment, and reproduction steps that matter.
|
||||
2. Reproduce the failure with the smallest relevant command or scenario. If it cannot be reproduced, inspect logs, recent changes, callers, configuration, and environment differences before changing code.
|
||||
3. Trace the failing path through its callers and dependencies. Compare a failing case with a successful one and identify the earliest point where behavior diverges.
|
||||
4. Form a root-cause explanation supported by evidence. If several causes remain plausible, gather the cheapest evidence that distinguishes them.
|
||||
5. Fix the cause at the narrowest shared boundary that covers affected callers. Preserve unrelated behavior and keep error handling intact.
|
||||
6. Run the smallest check that would fail if the defect returned, then add broader regression checks when the change affects connected paths or data.
|
||||
|
||||
Do not hide the failure with retries, broad exception handling, suppressed diagnostics, or caller-specific guards unless evidence shows that is the actual cause.
|
||||
|
||||
## Report
|
||||
|
||||
Summarize the cause, evidence, change, checks run, and any remaining uncertainty. Separate environment failures from product failures. Never claim a check passed if it did not run.
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: implementation-planner
|
||||
description: Use when requirements are understood but implementation needs multiple dependent steps, affected areas, risk handling, or coordinated verification. Produce a plan without making the changes.
|
||||
---
|
||||
|
||||
# Implementation Planner
|
||||
|
||||
Use when an understood change has multiple dependent steps or meaningful coordination risk.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Restate the intended outcome and acceptance conditions without expanding the request.
|
||||
2. Identify the files, components, data contracts, or workflows likely to change. Confirm the repository's existing patterns before proposing new structure.
|
||||
3. Order work by dependency. Keep steps small enough to review and verify independently where possible.
|
||||
4. Call out decisions that affect compatibility, data, permissions, or external systems. Resolve routine reversible choices using repository conventions.
|
||||
5. Pair each substantial step with the cheapest useful verification.
|
||||
6. Keep the plan concise and omit speculative scaffolding, unrelated cleanup, and work that can safely wait.
|
||||
|
||||
Return an actionable sequence with affected areas, risks, and checks. This skill plans only; do not implement the change.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Use for a general review of a code diff when looking for concrete defects, regressions, compatibility issues, and maintainability risks. For focused API, security, performance, UI, WPF, or EF Core reviews, use the matching review skill.
|
||||
---
|
||||
|
||||
# Code Review
|
||||
|
||||
Review the requested diff independently for defects and material regression risk.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the diff and the surrounding code, then identify the behavior and contracts the change intends to affect.
|
||||
2. Trace callers and data flow through relevant shared helpers. Check normal, boundary, and failure paths rather than reviewing changed lines in isolation.
|
||||
3. Look for incorrect behavior, compatibility breaks, unsafe input or data handling, resource leaks, concurrency problems, and avoidable complexity where relevant.
|
||||
4. Check whether the change has focused verification and whether stated results match the evidence. Do not run broad checks by default.
|
||||
5. Ignore style preferences unless they make the code error-prone or conflict with a repository rule.
|
||||
|
||||
## Findings
|
||||
|
||||
List only actionable issues, ordered by severity. Include file and line, condition, concrete impact, and the smallest practical fix. Do not modify code unless explicitly asked. If no defect is found, say so and note important unverified risks.
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: regression-verification
|
||||
description: Use after a code or configuration change when adjacent existing behavior may have regressed; select and run focused checks, then report results and gaps.
|
||||
---
|
||||
|
||||
# Regression Verification
|
||||
|
||||
Use after a change when existing behavior may have been affected.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. List the user-visible behaviors and contracts adjacent to the change, including sibling callers that share modified logic.
|
||||
2. Select existing tests or short scenarios that exercise those behaviors and their important boundaries.
|
||||
3. Run checks at the narrowest useful level. Expand only when shared components, persisted data, security boundaries, or integration paths justify it.
|
||||
4. Compare failures with the baseline and distinguish regressions from pre-existing or environment-specific failures.
|
||||
5. Check generated output or compatibility artifacts when the source change feeds them.
|
||||
|
||||
Report behaviors checked, exact checks run, results, and any gaps. Never imply that unrun tests passed.
|
||||
Reference in new issue
Block a user