Compare commits

...
5 Commits
Author SHA1 Message Date
fraujulian e712a75e22 docs(agents): scope task state to long work
CI / Capabilities and shell quality (push) Successful in 12s
CI / Python tests and compilation (push) Successful in 13s
CI / PowerShell tests on Ubuntu (push) Successful in 42s
CI / CI configuration security checks (push) Successful in 4s
CI / Build and inspect generated packages (push) Successful in 9s
CI / Qwen retrieval integration (push) Successful in 14m24s
CI / Bash integration and installation guards (push) Successful in 45s
CI / Installed client configuration smoke tests (push) Successful in 14s
2026-09-25 19:11:51 +02:00
fraujulian 67b5ba5b59 docs(prompt-budget): clarify measured context 2026-09-25 19:11:44 +02:00
fraujulian 3e043da9aa ci: build packages for integration checks 2026-09-25 19:11:36 +02:00
fraujulian 32154e6a89 test(prompt-budget): cover token thresholds 2026-09-25 19:11:28 +02:00
fraujulian 8fc141860f feat(prompt-budget): track context KPIs 2026-09-25 19:11:21 +02:00
11 changed files with 260 additions and 56 deletions

No files matched your search

+4
View File
@@ -115,6 +115,10 @@ jobs:
needs: [build] needs: [build]
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build packages for integration checks
env:
REQUIRE_CODEX_SCHEMA: 'false'
run: bash scripts/commands/build.sh --summary
- name: Run integration tests - name: Run integration tests
run: | run: |
set -euo pipefail set -euo pipefail
+23 -1
View File
@@ -1 +1,23 @@
{"Claude permanent context":4000,"Codex permanent context":4000,"Claude permanent context absolute limit":6000,"Codex permanent context absolute limit":6000} {
"regression_percent": 10,
"permanent_context_soft_target_tokens": 4000,
"permanent_context_hard_limit_tokens": 6000,
"metrics": {
"permanent_context_tokens": {
"claude": 1481,
"codex": 1481
},
"skill_metadata_tokens": {
"claude": 1124,
"codex": 1124
},
"rule_catalog_tokens": {
"claude": 28036,
"codex": 8058
},
"agent_metadata_tokens": {
"claude": 126,
"codex": 52
}
}
}
+9 -2
View File
@@ -1,2 +1,9 @@
Claude 4000 6000 metric client baseline_tokens
Codex 4000 6000 permanent_context_tokens claude 1481
permanent_context_tokens codex 1481
skill_metadata_tokens claude 1124
skill_metadata_tokens codex 1124
rule_catalog_tokens claude 28036
rule_catalog_tokens codex 8058
agent_metadata_tokens claude 126
agent_metadata_tokens codex 52
1 Claude metric 4000 client 6000 baseline_tokens
2 Codex permanent_context_tokens 4000 claude 6000 1481
3 permanent_context_tokens codex 1481
4 skill_metadata_tokens claude 1124
5 skill_metadata_tokens codex 1124
6 rule_catalog_tokens claude 28036
7 rule_catalog_tokens codex 8058
8 agent_metadata_tokens claude 126
9 agent_metadata_tokens codex 52
+1 -1
View File
@@ -20,7 +20,7 @@ The shared [orchestration rule](../shared/rules/orchestration.md) defines the `R
## Task state ## Task state
For long-running work, the main agent maintains one compact state containing `goal`, `decisions`, `changed`, `verified`, `open`, and `risks`. An existing task-state facility is preferred; the file fallback is `.ai-session/state.json` in the current workspace. Subagents may read it when needed, but send updates to the parent. Keep unrelated concurrent tasks in separate checkouts, and keep runtime state out of commits. See the [canonical task-state policy](../shared/rules/orchestration.md#canonical-task-state) for when to use state and what it may contain. The optional `.ai-session/state.json` file is local handoff state for a long-running active goal, not persistent project memory.
The optional `scripts/commands/session-state.ps1` and `scripts/commands/session-state.sh` helpers update this workspace file. Omitted fields are preserved. Their `-Workspace`/`--workspace` option selects the workspace explicitly; the default is the current working directory. Both accept goal, decisions, changed files, completed verification, open items, and risks. `-Pointer`/`--pointer` only prints the existing file's location. The optional `scripts/commands/session-state.ps1` and `scripts/commands/session-state.sh` helpers update this workspace file. Omitted fields are preserved. Their `-Workspace`/`--workspace` option selects the workspace explicitly; the default is the current working directory. Both accept goal, decisions, changed files, completed verification, open items, and risks. `-Pointer`/`--pointer` only prints the existing file's location.
+4 -1
View File
@@ -21,13 +21,15 @@ Run scripts from the repository root. PowerShell and Bash commands have equivale
| `commands/update.ps1`, `commands/update.sh` | Update an existing installation; Quick reuses saved choices. | | `commands/update.ps1`, `commands/update.sh` | Update an existing installation; Quick reuses saved choices. |
| `commands/uninstall.ps1`, `commands/uninstall.sh` | Remove setup-owned configuration and extensions. | | `commands/uninstall.ps1`, `commands/uninstall.sh` | Remove setup-owned configuration and extensions. |
| `commands/doctor.ps1`, `commands/doctor.sh` | Check generated output and installed configuration. | | `commands/doctor.ps1`, `commands/doctor.sh` | Check generated output and installed configuration. |
| `commands/prompt-budget.ps1`, `commands/prompt-budget.sh` | Warn above 115% of the 4,000-byte soft target; fail above the 6,000-byte hard limit for generated permanent instructions. | | `commands/prompt-budget.ps1`, `commands/prompt-budget.sh` | Track permanent context, skill metadata, the rule catalog, and agent metadata; warn above 4,000 permanent-context tokens, fail when any token estimate exceeds its baseline by 10%, or permanent context exceeds 6,000 tokens. |
| `commands/repository-context.ps1`, `commands/repository-context.sh` | Refresh the local repository-context cache. | | `commands/repository-context.ps1`, `commands/repository-context.sh` | Refresh the local repository-context cache. |
| `commands/session-state.ps1`, `commands/session-state.sh` | Read or update the local agent session state. | | `commands/session-state.ps1`, `commands/session-state.sh` | Read or update the local agent session state. |
| `commands/telemetry.ps1`, `commands/telemetry.sh` | Append a local telemetry event. | | `commands/telemetry.ps1`, `commands/telemetry.sh` | Append a local telemetry event. |
| `commands/add-instruction.ps1`, `commands/add-instruction.sh` | Append a client-specific user instruction outside managed configuration. | | `commands/add-instruction.ps1`, `commands/add-instruction.sh` | Append a client-specific user instruction outside managed configuration. |
| `commands/add-credentials.ps1`, `commands/add-credentials.sh` | Store a client-scoped tool credential in the operating-system credential store. | | `commands/add-credentials.ps1`, `commands/add-credentials.sh` | Store a client-scoped tool credential in the operating-system credential store. |
Prompt KPIs estimate tokens as UTF-8 bytes divided by four, using the larger shell package per client. Skill metadata counts `SKILL.md` frontmatter outside rule skills; the rule catalog counts all generated rule entry files but excludes references, so it measures the available catalog size rather than rules loaded in a typical session; agent metadata counts frontmatter for Claude and `name`/`description` for Codex. All four metrics fail CI above baseline +10%; permanent context also warns above 4,000 tokens and fails above 6,000 tokens.
Custom instructions are read before generated rules, skills, and plugins. They remain under `~/.my-ai-configuration/instructions/`, so installation and update scripts never manage or reset them. Custom instructions are read before generated rules, skills, and plugins. They remain under `~/.my-ai-configuration/instructions/`, so installation and update scripts never manage or reset them.
On Windows, credentials are stored in Windows Credential Manager. Use `~/.my-ai-configuration/bin/with-credential.ps1` or `.sh` to run a tool with a stored key available only in that child process. The key must be a valid environment-variable name; its value is never written to configuration. On Windows, credentials are stored in Windows Credential Manager. Use `~/.my-ai-configuration/bin/with-credential.ps1` or `.sh` to run a tool with a stored key available only in that child process. The key must be a valid environment-variable name; its value is never written to configuration.
@@ -47,6 +49,7 @@ Gitea Actions runs the repository checks from `.gitea/workflows/ci.yml`.
| `tests/test-managed-extensions.py` | Validate plugin, skill, and CLI ownership handling. | | `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. | | `tests/test-managed-install.ps1`, `tests/test-managed-install.sh` | Validate managed-file synchronization and backups. |
| `tests/test-plugin-selection.ps1`, `tests/test-plugin-selection.sh` | Validate plugin selection. | | `tests/test-plugin-selection.ps1`, `tests/test-plugin-selection.sh` | Validate plugin selection. |
| `tests/test-prompt-budget.py` | Validate prompt budget threshold evaluation. |
| `tests/test-prompt-inventory.py` | Validate prompt inventory collection. | | `tests/test-prompt-inventory.py` | Validate prompt inventory collection. |
| `tests/test-quick-update.py` | Validate saved Quickupdate choices. | | `tests/test-quick-update.py` | Validate saved Quickupdate choices. |
| `tests/test-quick-update.ps1`, `tests/test-quick-update.sh` | Run the Quickupdate test through the relevant shell. | | `tests/test-quick-update.ps1`, `tests/test-quick-update.sh` | Run the Quickupdate test through the relevant shell. |
+6 -29
View File
@@ -2,34 +2,11 @@
param([switch]$Summary) param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$generated = Join-Path $root 'generated' $budgetScript = Join-Path $root 'scripts/lib/prompt-budget.py'
if (-not (Test-Path (Join-Path $generated 'claude-bash/CLAUDE.md'))) { throw 'Generated output is missing; run build first.' } $budgetArgs = @()
function Measure-Tree([string]$path) { if ($Summary) { $budgetArgs += '--summary' }
if (-not (Test-Path $path)) { return 0 } & python $budgetScript @budgetArgs
return [int64]((Get-ChildItem $path -File -Recurse | ForEach-Object { $_.Length } | Measure-Object -Sum).Sum) $budgetExit = $LASTEXITCODE
}
function Tokens([int64]$bytes) { return [int][Math]::Ceiling($bytes / 4.0) }
$claude = (Get-Item (Join-Path $generated 'claude-bash/CLAUDE.md')).Length
$codex = (Get-Item (Join-Path $generated 'codex-bash/AGENTS.md')).Length
$skills = Measure-Tree (Join-Path $generated 'claude-bash/skills')
$lazyItem = Get-ChildItem (Join-Path $root 'shared/rules') -File -Recurse | Sort-Object Length -Descending | Select-Object -First 1
$lazy = if ($lazyItem) { [int64]$lazyItem.Length } else { 0 }
$baseline = Join-Path $root 'adapters/prompt-budget-baseline.json'
$old = if (Test-Path $baseline) { Get-Content $baseline -Raw | ConvertFrom-Json } else { $null }
$rows = @([pscustomobject]@{Name='Claude permanent context';Bytes=$claude;Tokens=(Tokens $claude)},[pscustomobject]@{Name='Codex permanent context';Bytes=$codex;Tokens=(Tokens $codex)},[pscustomobject]@{Name='Claude skill files (lazy)';Bytes=$skills;Tokens=(Tokens $skills)},[pscustomobject]@{Name='Largest lazy rule';Bytes=$lazy;Tokens=(Tokens $lazy)})
$fail = $false
$baselineValues = @{}
if ($old) {
foreach ($row in $rows | Select-Object -First 2) {
$previous = [double]$old.($row.Name)
$absolute = [double]$old.(($row.Name + ' absolute limit'))
$baselineValues[$row.Name] = @($previous, $absolute)
if ($previous -gt 0 -and $row.Bytes -gt $previous * 1.15) { Write-Warning "Prompt budget soft target exceeded: $($row.Name) $($row.Bytes) bytes (target $previous, 15% tolerance)." }
if ($absolute -gt 0 -and $row.Bytes -gt $absolute) { $fail = $true; Write-Error "Prompt budget hard limit failed: $($row.Name) $($row.Bytes) bytes (hard limit $absolute)." }
}
}
if ($Summary) { Write-Output 'Prompt Budget'; foreach ($row in $rows | Select-Object -First 2) { $limits = $baselineValues[$row.Name]; $base = if($limits){$limits[0]}else{'-'}; $absolute = if($limits){$limits[1]}else{'-'}; Write-Output ('{0,-28} {1,8} bytes soft target {2} hard limit {3}' -f $row.Name,$row.Bytes,$base,$absolute) }; $rows | Select-Object -Skip 2 | ForEach-Object { Write-Output ('{0,-28} {1,8} bytes {2,6} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } else { $rows | ForEach-Object { Write-Output ('{0}: {1} bytes / {2} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } }
& python (Join-Path $root 'scripts/lib/prompt-inventory.py') --home ([Environment]::GetFolderPath('UserProfile')) & python (Join-Path $root 'scripts/lib/prompt-inventory.py') --home ([Environment]::GetFolderPath('UserProfile'))
if ($LASTEXITCODE -ne 0) { throw 'Prompt inventory failed.' } if ($LASTEXITCODE -ne 0) { throw 'Prompt inventory failed.' }
if ($fail) { exit 1 } exit $budgetExit
exit 0
+5 -20
View File
@@ -3,24 +3,9 @@ set -euo pipefail
summary=false summary=false
for arg in "$@"; do case "$arg" in --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done for arg in "$@"; do case "$arg" in --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
generated="$root/generated" budget_args=()
[ -f "$generated/claude-bash/CLAUDE.md" ] || { printf 'Generated output is missing; run build first.\n' >&2; exit 1; } [ "$summary" = false ] || budget_args+=(--summary)
bytes() { wc -c < "$1" | tr -d ' '; } budget_status=0
tree_bytes() { find "$1" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {print s+0}'; } python3 "$root/scripts/lib/prompt-budget.py" "${budget_args[@]}" || budget_status=$?
tokens() { awk -v b="$1" 'BEGIN { printf "%d", (b + 3) / 4 }'; }
claude=$(bytes "$generated/claude-bash/CLAUDE.md"); codex=$(bytes "$generated/codex-bash/AGENTS.md"); skills=$(tree_bytes "$generated/claude-bash/skills"); lazy=$(find "$root/shared/rules" -type f -printf '%s %p\n' | sort -nr | awk 'NR==1 {print $1+0}'); fail=false
claude_baseline=0; codex_baseline=0; claude_absolute=0; codex_absolute=0
if [ -f "$root/adapters/prompt-budget-baseline.tsv" ]; then
while IFS=$'\t' read -r name old absolute; do
[ "$name" = Claude ] || [ "$name" = Codex ] || continue
current=$([ "$name" = Claude ] && printf '%s' "$claude" || printf '%s' "$codex")
[ "$name" = Claude ] && claude_baseline=$old && claude_absolute=$absolute
[ "$name" = Codex ] && codex_baseline=$old && codex_absolute=$absolute
if awk -v n="$current" -v o="$old" 'BEGIN { exit !(o > 0 && n > o * 1.15) }'; then printf 'Prompt budget soft target exceeded: %s %s bytes (target %s, 15%% tolerance).\n' "$name" "$current" "$old" >&2; fi
if awk -v n="$current" -v a="$absolute" 'BEGIN { exit !(a > 0 && n > a) }'; then printf 'Prompt budget hard limit failed: %s %s bytes (hard limit %s).\n' "$name" "$current" "$absolute" >&2; fail=true; fi
done < "$root/adapters/prompt-budget-baseline.tsv"
fi
if [ "$summary" = true ]; then printf 'Prompt Budget\nClaude permanent context %8s bytes soft target %s hard limit %s\nCodex permanent context %8s bytes soft target %s hard limit %s\nClaude skill files (lazy) %8s bytes %6s tokens\nLargest lazy rule %8s bytes %6s tokens\n' "$claude" "$claude_baseline" "$claude_absolute" "$codex" "$codex_baseline" "$codex_absolute" "$skills" "$(tokens "$skills")" "$lazy" "$(tokens "$lazy")"; else printf 'Prompt budget: Claude %s bytes, Codex %s bytes, skills %s bytes, largest lazy rule %s bytes\n' "$claude" "$codex" "$skills" "$lazy"; fi
python3 "$root/scripts/lib/prompt-inventory.py" --home "${HOME:?HOME is required}" python3 "$root/scripts/lib/prompt-inventory.py" --home "${HOME:?HOME is required}"
$fail && { printf 'Prompt budget: permanent context exceeded hard limit.\n' >&2; exit 1; } exit "$budget_status"
exit 0
+154
View File
@@ -0,0 +1,154 @@
import argparse
import csv
import json
import math
import re
import tomllib
from pathlib import Path
METRICS = (
'permanent_context_tokens',
'skill_metadata_tokens',
'rule_catalog_tokens',
'agent_metadata_tokens',
)
CLIENTS = ('claude', 'codex')
TOKEN_BYTES = 4
def read_frontmatter(path):
content = path.read_text(encoding='utf-8-sig')
match = re.match(r'\A---\r?\n(.*?)^---\s*$', content, re.MULTILINE | re.DOTALL)
return match.group(1).encode('utf-8') if match else b''
def tokens(byte_count):
return math.ceil(byte_count / TOKEN_BYTES)
def measure_package(package, client):
instruction_name = 'CLAUDE.md' if client == 'claude' else 'AGENTS.md'
instruction_bytes = (package / instruction_name).stat().st_size
skill_root = package / 'skills'
skill_metadata_bytes = sum(
len(read_frontmatter(path))
for path in skill_root.rglob('SKILL.md')
if path.relative_to(skill_root).parts[0] != 'rules'
)
if client == 'claude':
rule_files = (package / 'skills' / 'rules').glob('rules-*/SKILL.md')
else:
rule_root = package / 'rules'
rule_files = (
path for path in rule_root.rglob('*.md')
if 'references' not in path.relative_to(rule_root).parts
)
rule_catalog_bytes = sum(path.stat().st_size for path in rule_files)
agent_root = package / 'agents'
if client == 'claude':
agent_metadata_bytes = sum(len(read_frontmatter(path)) for path in agent_root.glob('*.md'))
else:
agent_metadata_bytes = 0
for path in agent_root.glob('*.toml'):
agent = tomllib.loads(path.read_text(encoding='utf-8-sig'))
agent_metadata_bytes += sum(len(agent.get(key, '').encode('utf-8')) for key in ('name', 'description'))
return {
'permanent_context_tokens': tokens(instruction_bytes),
'skill_metadata_tokens': tokens(skill_metadata_bytes),
'rule_catalog_tokens': tokens(rule_catalog_bytes),
'agent_metadata_tokens': tokens(agent_metadata_bytes),
}
def measure(root):
result = {}
for client in CLIENTS:
packages = [root / 'generated' / f'{client}-{shell}' for shell in ('bash', 'powershell')]
for package in packages:
if not package.is_dir():
raise FileNotFoundError(f'Generated package missing: {package}')
measurements = [measure_package(package, client) for package in packages]
result[client] = {
metric: max(item[metric] for item in measurements)
for metric in METRICS
}
return result
def load_baseline(root):
path = root / 'adapters' / 'prompt-budget-baseline.json'
baseline = json.loads(path.read_text(encoding='utf-8'))
if baseline.get('regression_percent') != 10:
raise ValueError('Prompt budget regression threshold must be 10%.')
metric_values = baseline.get('metrics', {})
expected = {(metric, client): metric_values.get(metric, {}).get(client) for metric in METRICS for client in CLIENTS}
if any(not isinstance(value, int) or value <= 0 for value in expected.values()):
raise ValueError('Prompt budget baseline is missing a positive integer metric.')
for key in ('permanent_context_soft_target_tokens', 'permanent_context_hard_limit_tokens'):
if not isinstance(baseline.get(key), int) or baseline[key] <= 0:
raise ValueError(f'Prompt budget baseline is missing a positive {key}.')
tsv_path = root / 'adapters' / 'prompt-budget-baseline.tsv'
with tsv_path.open(encoding='utf-8', newline='') as stream:
rows = list(csv.DictReader(stream, delimiter='\t'))
actual = {(row['metric'], row['client']): int(row['baseline_tokens']) for row in rows}
if actual != expected:
raise ValueError('Prompt budget JSON and TSV baselines do not match.')
return baseline
def evaluate(current, baseline):
failures = []
warnings = []
limit_percent = 100 + baseline['regression_percent']
for metric in METRICS:
for client in CLIENTS:
value = current[client][metric]
previous = baseline['metrics'][metric][client]
if value * 100 > previous * limit_percent:
failures.append(f'{metric} {client}: {value} tokens (baseline {previous}, limit +{baseline["regression_percent"]}%).')
hard_limit = baseline['permanent_context_hard_limit_tokens']
soft_target = baseline['permanent_context_soft_target_tokens']
for client in CLIENTS:
value = current[client]['permanent_context_tokens']
if value > soft_target:
warnings.append(f'permanent_context_tokens {client}: {value} tokens exceeds the {soft_target}-token soft target.')
if value > hard_limit:
failures.append(f'permanent_context_tokens {client}: {value} tokens exceeds the {hard_limit}-token hard limit.')
return failures, warnings
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--summary', action='store_true')
args = parser.parse_args()
root = Path(__file__).resolve().parents[2]
baseline = load_baseline(root)
current = measure(root)
failures, warnings = evaluate(current, baseline)
print('Prompt Budget')
if args.summary:
for metric in METRICS:
for client in CLIENTS:
value = current[client][metric]
previous = baseline['metrics'][metric][client]
print(f'{metric} {client}: {value} tokens (baseline {previous}, fail above +{baseline["regression_percent"]}%)')
else:
for metric in METRICS:
print(f'{metric}: Claude {current["claude"][metric]}, Codex {current["codex"][metric]} tokens')
for warning in warnings:
print(f'WARN {warning}')
for failure in failures:
print(f'FAIL {failure}')
return 1 if failures else 0
if __name__ == '__main__':
raise SystemExit(main())
+1 -1
View File
@@ -40,7 +40,7 @@ def main():
directories.append(args.home / '.agents' / 'skills') directories.append(args.home / '.agents' / 'skills')
report(f'Installed {client} user catalog', directories) report(f'Installed {client} user catalog', directories)
report(f'Cached {client} plugins (all versions)', [args.home / f'.{client}' / 'plugins' / 'cache'], warn_duplicates=False) report(f'Cached {client} plugins (all versions)', [args.home / f'.{client}' / 'plugins' / 'cache'], warn_duplicates=False)
print('Coverage: baseline covers generated instructions only. Inventory is not measured session context; plugin caches include inactive versions. Client prompts, tool schemas and dynamic hook output are excluded. Token counts use bytes/4 estimates.') print('Coverage: prompt KPIs cover generated permanent instructions, skill metadata, the full rule catalog, and agent metadata. Rule catalog size does not represent rules loaded in a typical session. Counts are bytes/4 estimates, not runtime token telemetry. Installed prompts, tool schemas, dynamic hook output, and inactive plugin versions are excluded.')
if __name__ == '__main__': if __name__ == '__main__':
+50
View File
@@ -0,0 +1,50 @@
import runpy
import unittest
from pathlib import Path
MODULE = runpy.run_path(str(Path(__file__).resolve().parents[1] / 'lib' / 'prompt-budget.py'))
class PromptBudgetTests(unittest.TestCase):
def setUp(self):
self.baseline = {
'regression_percent': 10,
'permanent_context_soft_target_tokens': 4000,
'permanent_context_hard_limit_tokens': 6000,
'metrics': {
metric: {'claude': 100, 'codex': 100}
for metric in MODULE['METRICS']
},
}
self.current = {
client: {metric: 110 for metric in MODULE['METRICS']}
for client in MODULE['CLIENTS']
}
def test_ten_percent_growth_passes(self):
failures, warnings = MODULE['evaluate'](self.current, self.baseline)
self.assertEqual(failures, [])
self.assertEqual(warnings, [])
def test_growth_above_ten_percent_fails(self):
self.current['claude']['permanent_context_tokens'] = 111
failures, _ = MODULE['evaluate'](self.current, self.baseline)
self.assertEqual(len(failures), 1)
self.assertIn('permanent_context_tokens claude', failures[0])
def test_permanent_context_hard_limit_fails(self):
self.current['codex']['permanent_context_tokens'] = 6001
failures, _ = MODULE['evaluate'](self.current, self.baseline)
self.assertTrue(any('permanent_context_tokens codex' in failure for failure in failures))
def test_soft_target_warns_in_tokens(self):
self.baseline['metrics']['permanent_context_tokens']['claude'] = 4000
self.current['claude']['permanent_context_tokens'] = 4001
failures, warnings = MODULE['evaluate'](self.current, self.baseline)
self.assertEqual(failures, [])
self.assertTrue(any('permanent_context_tokens claude' in warning for warning in warnings))
if __name__ == '__main__':
unittest.main()
+3 -1
View File
@@ -37,7 +37,9 @@ Verifier: execution evidence against acceptance criteria. Choose relevant tests,
## Canonical task state ## Canonical task state
For long-running work, the main agent maintains one compact state with `goal`, `decisions`, `changed`, `verified`, `open`, and `risks`. Store conclusions and file/symbol references, never transcripts, copied source, secrets, or narration. Update after material decisions, changes, and verification, before compaction when possible. Mark stale evidence after relevant edits. Use task state only for work expected to span multiple hours, compaction, or an interrupted session. For short, bounded work, rely on the conversation and current Git diff/working tree; do not create state just to record routine progress. Task state is a handoff for the active goal, not persistent project memory.
When needed, the main agent maintains one compact state with `goal`, `decisions`, `changed`, `verified`, `open`, and `risks`. Store conclusions and file/symbol references, never transcripts, copied source, secrets, unrelated project knowledge, or narration. Update after material decisions, changes, and verification, before compaction when possible. Mark stale evidence after relevant edits and reset state when starting a different goal.
Use an existing task-state facility when available; otherwise use the workspace's `.ai-session/state.json`. Keep one active owner per workspace; use separate checkouts for concurrent unrelated tasks. Runtime state is local, not committed. Explicitly share its location when needed; subagents may read it but return updates to the parent instead of maintaining copies. Do not paste the full state into every assignment. Use an existing task-state facility when available; otherwise use the workspace's `.ai-session/state.json`. Keep one active owner per workspace; use separate checkouts for concurrent unrelated tasks. Runtime state is local, not committed. Explicitly share its location when needed; subagents may read it but return updates to the parent instead of maintaining copies. Do not paste the full state into every assignment.