cli-router 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli_router-0.3.2/.github/actionlint.yaml +3 -0
- cli_router-0.3.2/.github/dependabot.yml +14 -0
- cli_router-0.3.2/.github/workflows/agent-drift-watchdog.yml +255 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.github/workflows/publish.yml +10 -9
- cli_router-0.3.2/.github/workflows/test.yml +58 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/AGENTS.md +50 -4
- {cli_router-0.3.0 → cli_router-0.3.2}/CHANGELOG.md +19 -0
- cli_router-0.3.2/MEMORY.md +60 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/PKG-INFO +105 -6
- {cli_router-0.3.0 → cli_router-0.3.2}/README.md +95 -5
- cli_router-0.3.2/STATE.md +55 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/__init__.py +1 -1
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/cli.py +82 -12
- cli_router-0.3.2/cli_router/config.py +310 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/doctor.py +76 -13
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/failures.py +27 -1
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/logs.py +8 -6
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/models.py +2 -2
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/presets/generic.yaml +2 -1
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/presets/hermes.yaml +1 -1
- cli_router-0.3.2/cli_router/receipts.py +185 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/runner.py +162 -21
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/streamfmt.py +3 -3
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/tools.py +18 -3
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/tui.py +27 -20
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/workflows.py +106 -27
- cli_router-0.3.2/docs/agent-loops/self-hosted-runner.md +131 -0
- cli_router-0.3.2/docs/releasing.md +182 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/examples/claude_codex.yaml +9 -2
- {cli_router-0.3.0 → cli_router-0.3.2}/examples/cli-router.yaml +2 -1
- {cli_router-0.3.0 → cli_router-0.3.2}/examples/local_models.yaml +2 -1
- {cli_router-0.3.0 → cli_router-0.3.2}/pyproject.toml +37 -1
- cli_router-0.3.2/scripts/agent/apply_drift_fix.sh +28 -0
- cli_router-0.3.2/scripts/agent/build_prompt.py +52 -0
- cli_router-0.3.2/scripts/agent/detect_drift.py +78 -0
- cli_router-0.3.2/scripts/agent/preflight.py +95 -0
- cli_router-0.3.2/scripts/agent/validate_patch.py +60 -0
- cli_router-0.3.2/scripts/check_release.py +40 -0
- cli_router-0.3.2/tests/test_agent_scripts.py +96 -0
- cli_router-0.3.2/tests/test_cli.py +471 -0
- cli_router-0.3.2/tests/test_config.py +364 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_doctor.py +62 -2
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_failures.py +12 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_models.py +8 -2
- cli_router-0.3.2/tests/test_release.py +19 -0
- cli_router-0.3.2/tests/test_runner.py +337 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_tui.py +10 -8
- cli_router-0.3.2/tests/test_version.py +7 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_workflows.py +456 -1
- cli_router-0.3.0/.github/workflows/test.yml +0 -24
- cli_router-0.3.0/cli_router/config.py +0 -172
- cli_router-0.3.0/tests/test_cli.py +0 -218
- cli_router-0.3.0/tests/test_config.py +0 -144
- cli_router-0.3.0/tests/test_runner.py +0 -175
- {cli_router-0.3.0 → cli_router-0.3.2}/.claude/skills/tui-design/SKILL.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.claude/skills/tui-design/references/cli-basics.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.claude/skills/tui-design/references/ecosystem-go.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.claude/skills/tui-design/references/ecosystem-python.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.claude/skills/tui-design/references/ecosystem-rust.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.claude/skills/tui-design/references/ecosystem-typescript.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.claude/skills/tui-design/references/exemplar-apps.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.claude/skills/tui-design/references/interaction-patterns.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.claude/skills/tui-design/references/visual-patterns.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/.gitignore +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/LICENSE +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/PRD.md +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/artifacts.py +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/extractors.py +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/modelcache.py +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/presets/__init__.py +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/presets/claude.yaml +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/presets/codex.yaml +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/presets/grok.yaml +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/cli_router/runs.py +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_extractors.py +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_logs.py +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_modelcache.py +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_runs.py +0 -0
- {cli_router-0.3.0 → cli_router-0.3.2}/tests/test_streamfmt.py +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
name: Agent drift watchdog
|
|
2
|
+
|
|
3
|
+
# Scheduled loop: inspect live model catalogs on the self-hosted runner. A
|
|
4
|
+
# single-runner removal opens an advisory issue; only a maintainer-confirmed
|
|
5
|
+
# manual run may invoke the local patch agent and open a review-required PR.
|
|
6
|
+
#
|
|
7
|
+
# SECURITY: this runs on a self-hosted runner that holds your subscription
|
|
8
|
+
# credentials. It is triggered ONLY by the schedule or a maintainer clicking
|
|
9
|
+
# "Run workflow" — never by pull_request, and never by fork code. Do not add
|
|
10
|
+
# untrusted triggers to this file.
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
schedule:
|
|
14
|
+
- cron: "0 14 * * 1" # Mondays 14:00 UTC
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
inputs:
|
|
17
|
+
dry_run:
|
|
18
|
+
description: "Detect and report drift only; do not patch or open a PR"
|
|
19
|
+
type: boolean
|
|
20
|
+
default: true
|
|
21
|
+
confirm_removals:
|
|
22
|
+
description: "Confirm this runner's missing models should be patched"
|
|
23
|
+
type: boolean
|
|
24
|
+
default: false
|
|
25
|
+
|
|
26
|
+
permissions: {}
|
|
27
|
+
|
|
28
|
+
concurrency:
|
|
29
|
+
group: agent-drift-watchdog
|
|
30
|
+
cancel-in-progress: false
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
preflight:
|
|
34
|
+
runs-on: [self-hosted, mothership]
|
|
35
|
+
timeout-minutes: 5
|
|
36
|
+
permissions:
|
|
37
|
+
contents: read
|
|
38
|
+
issues: write
|
|
39
|
+
outputs:
|
|
40
|
+
proceed: ${{ steps.gate.outputs.proceed }}
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
43
|
+
with:
|
|
44
|
+
persist-credentials: false
|
|
45
|
+
- id: gate
|
|
46
|
+
name: Circuit breaker + kill switch
|
|
47
|
+
env:
|
|
48
|
+
GH_TOKEN: ${{ github.token }}
|
|
49
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
50
|
+
DAILY_CAP: ${{ vars.AGENT_DAILY_CAP || '10' }}
|
|
51
|
+
run: python3 scripts/agent/preflight.py
|
|
52
|
+
|
|
53
|
+
drift:
|
|
54
|
+
needs: preflight
|
|
55
|
+
if: needs.preflight.outputs.proceed == 'true'
|
|
56
|
+
runs-on: [self-hosted, mothership]
|
|
57
|
+
timeout-minutes: 30
|
|
58
|
+
permissions:
|
|
59
|
+
contents: read
|
|
60
|
+
issues: write
|
|
61
|
+
outputs:
|
|
62
|
+
patch_ready: ${{ steps.package.outputs.patch_ready }}
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
65
|
+
with:
|
|
66
|
+
persist-credentials: false
|
|
67
|
+
|
|
68
|
+
- name: Install cli-router in an isolated environment
|
|
69
|
+
run: |
|
|
70
|
+
python3 -m venv "$RUNNER_TEMP/cli-router-venv"
|
|
71
|
+
"$RUNNER_TEMP/cli-router-venv/bin/python" -m pip install -e .
|
|
72
|
+
echo "$RUNNER_TEMP/cli-router-venv/bin" >> "$GITHUB_PATH"
|
|
73
|
+
|
|
74
|
+
- name: Detect drift
|
|
75
|
+
id: detect
|
|
76
|
+
env:
|
|
77
|
+
CONFIRM_REMOVALS: ${{ inputs.confirm_removals || 'false' }}
|
|
78
|
+
run: |
|
|
79
|
+
confirm_arg=""
|
|
80
|
+
if [ "$CONFIRM_REMOVALS" = "true" ]; then
|
|
81
|
+
confirm_arg="--confirm-removals"
|
|
82
|
+
fi
|
|
83
|
+
python3 scripts/agent/detect_drift.py --timeout 15 $confirm_arg | tee drift.json
|
|
84
|
+
drift=$(python3 -c "import json; print(str(json.load(open('drift.json'))['drift']).lower())")
|
|
85
|
+
candidate=$(python3 -c "import json; print(str(json.load(open('drift.json'))['candidate_drift']).lower())")
|
|
86
|
+
echo "drift=$drift" >> "$GITHUB_OUTPUT"
|
|
87
|
+
echo "candidate=$candidate" >> "$GITHUB_OUTPUT"
|
|
88
|
+
|
|
89
|
+
- name: No candidate drift
|
|
90
|
+
if: steps.detect.outputs.candidate != 'true'
|
|
91
|
+
run: echo "No retired shipped models. Nothing to do."
|
|
92
|
+
|
|
93
|
+
- name: Report candidate drift for maintainer confirmation
|
|
94
|
+
if: steps.detect.outputs.candidate == 'true' && steps.detect.outputs.drift != 'true'
|
|
95
|
+
env:
|
|
96
|
+
GH_TOKEN: ${{ github.token }}
|
|
97
|
+
run: |
|
|
98
|
+
gh label create agent-drift --color 1d76db --description "Model-list drift candidate" --force
|
|
99
|
+
existing=$(gh issue list --label agent-drift --state open --json number --jq '.[0].number')
|
|
100
|
+
body="$(cat drift.json)"
|
|
101
|
+
if [ -n "$existing" ]; then
|
|
102
|
+
gh issue comment "$existing" --body "Fresh single-runner observation; confirmation is still required:
|
|
103
|
+
|
|
104
|
+
\`\`\`json
|
|
105
|
+
$body
|
|
106
|
+
\`\`\`"
|
|
107
|
+
else
|
|
108
|
+
gh issue create --label agent-created --label agent-drift \
|
|
109
|
+
--title "Provider model-list drift requires confirmation" \
|
|
110
|
+
--body "One runner observed a shipped fallback model missing. This can vary by account or CLI version, so no patch was applied.
|
|
111
|
+
|
|
112
|
+
Re-run this workflow manually with \`confirm_removals=true\` after verification.
|
|
113
|
+
|
|
114
|
+
\`\`\`json
|
|
115
|
+
$body
|
|
116
|
+
\`\`\`"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
- name: Dry run — report only
|
|
120
|
+
if: steps.detect.outputs.candidate == 'true' && inputs.dry_run == true
|
|
121
|
+
run: |
|
|
122
|
+
echo "Drift found; dry_run=true, not patching:"
|
|
123
|
+
cat drift.json
|
|
124
|
+
|
|
125
|
+
# --- The following steps run only when there is real drift and it's not a dry run ---
|
|
126
|
+
|
|
127
|
+
- name: Build agent prompt
|
|
128
|
+
if: steps.detect.outputs.drift == 'true' && inputs.dry_run != true
|
|
129
|
+
run: python3 scripts/agent/build_prompt.py drift.json | tee patch_prompt.txt
|
|
130
|
+
|
|
131
|
+
- name: Create isolated patch branch
|
|
132
|
+
if: steps.detect.outputs.drift == 'true' && inputs.dry_run != true
|
|
133
|
+
run: |
|
|
134
|
+
git switch -c "agent/drift-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
|
135
|
+
git config --local credential.helper ""
|
|
136
|
+
|
|
137
|
+
- name: Reject persistent runner GitHub authentication
|
|
138
|
+
if: steps.detect.outputs.drift == 'true' && inputs.dry_run != true
|
|
139
|
+
run: |
|
|
140
|
+
if env -u GH_TOKEN -u GITHUB_TOKEN -u GH_CONFIG_DIR gh auth status >/dev/null 2>&1; then
|
|
141
|
+
echo "Persistent gh authentication is available to the runner user; refusing to launch the patch agent." >&2
|
|
142
|
+
exit 1
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
- name: Apply fix with local agent
|
|
146
|
+
if: steps.detect.outputs.drift == 'true' && inputs.dry_run != true
|
|
147
|
+
env:
|
|
148
|
+
GH_CONFIG_DIR: ${{ runner.temp }}/cli-router-empty-gh
|
|
149
|
+
run: |
|
|
150
|
+
mkdir -p "$GH_CONFIG_DIR"
|
|
151
|
+
env -u GH_TOKEN -u GITHUB_TOKEN -u ACTIONS_RUNTIME_TOKEN \
|
|
152
|
+
-u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL \
|
|
153
|
+
bash scripts/agent/apply_drift_fix.sh patch_prompt.txt
|
|
154
|
+
|
|
155
|
+
- name: Validate agent patch scope
|
|
156
|
+
if: steps.detect.outputs.drift == 'true' && inputs.dry_run != true
|
|
157
|
+
run: python3 scripts/agent/validate_patch.py
|
|
158
|
+
|
|
159
|
+
- id: package
|
|
160
|
+
name: Package the validated patch
|
|
161
|
+
if: steps.detect.outputs.drift == 'true' && inputs.dry_run != true
|
|
162
|
+
run: |
|
|
163
|
+
set -euo pipefail
|
|
164
|
+
git diff --binary HEAD -- CHANGELOG.md cli_router/models.py tests/test_models.py > "$RUNNER_TEMP/drift.patch"
|
|
165
|
+
test -s "$RUNNER_TEMP/drift.patch"
|
|
166
|
+
echo "patch_ready=true" >> "$GITHUB_OUTPUT"
|
|
167
|
+
|
|
168
|
+
- name: Upload the validated patch
|
|
169
|
+
if: steps.package.outputs.patch_ready == 'true'
|
|
170
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
171
|
+
with:
|
|
172
|
+
name: validated-drift-patch
|
|
173
|
+
path: ${{ runner.temp }}/drift.patch
|
|
174
|
+
if-no-files-found: error
|
|
175
|
+
retention-days: 1
|
|
176
|
+
|
|
177
|
+
verify_patch:
|
|
178
|
+
needs: drift
|
|
179
|
+
if: needs.drift.outputs.patch_ready == 'true'
|
|
180
|
+
runs-on: ubuntu-latest
|
|
181
|
+
timeout-minutes: 15
|
|
182
|
+
permissions:
|
|
183
|
+
contents: read
|
|
184
|
+
steps:
|
|
185
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
186
|
+
with:
|
|
187
|
+
persist-credentials: false
|
|
188
|
+
|
|
189
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
190
|
+
with:
|
|
191
|
+
python-version: "3.12"
|
|
192
|
+
|
|
193
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
194
|
+
with:
|
|
195
|
+
name: validated-drift-patch
|
|
196
|
+
path: ${{ runner.temp }}
|
|
197
|
+
|
|
198
|
+
- name: Apply and validate the isolated patch
|
|
199
|
+
run: |
|
|
200
|
+
git apply "$RUNNER_TEMP/drift.patch"
|
|
201
|
+
python3 scripts/agent/validate_patch.py
|
|
202
|
+
|
|
203
|
+
- name: Verify in an ephemeral read-only job
|
|
204
|
+
run: |
|
|
205
|
+
python -m pip install -e .
|
|
206
|
+
python -m pytest -q
|
|
207
|
+
|
|
208
|
+
publish:
|
|
209
|
+
needs: [drift, verify_patch]
|
|
210
|
+
if: needs.drift.outputs.patch_ready == 'true'
|
|
211
|
+
runs-on: ubuntu-latest
|
|
212
|
+
timeout-minutes: 10
|
|
213
|
+
permissions:
|
|
214
|
+
contents: write
|
|
215
|
+
pull-requests: write
|
|
216
|
+
issues: write
|
|
217
|
+
steps:
|
|
218
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
219
|
+
with:
|
|
220
|
+
persist-credentials: false
|
|
221
|
+
|
|
222
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
223
|
+
with:
|
|
224
|
+
name: validated-drift-patch
|
|
225
|
+
path: ${{ runner.temp }}
|
|
226
|
+
|
|
227
|
+
- name: Apply the verified patch
|
|
228
|
+
run: |
|
|
229
|
+
git apply "$RUNNER_TEMP/drift.patch"
|
|
230
|
+
python3 scripts/agent/validate_patch.py
|
|
231
|
+
|
|
232
|
+
- name: Open review-required PR
|
|
233
|
+
env:
|
|
234
|
+
GH_TOKEN: ${{ github.token }}
|
|
235
|
+
run: |
|
|
236
|
+
set -euo pipefail
|
|
237
|
+
gh auth setup-git
|
|
238
|
+
git config user.name "cli-router-agent[bot]"
|
|
239
|
+
git config user.email "cli-router-agent@users.noreply.github.com"
|
|
240
|
+
branch="agent/drift-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
|
241
|
+
git switch -c "$branch"
|
|
242
|
+
git add -- cli_router/models.py tests/test_models.py CHANGELOG.md
|
|
243
|
+
git -c core.hooksPath=/dev/null commit -m "Fix provider model-list drift (automated)"
|
|
244
|
+
git push -u origin "$branch"
|
|
245
|
+
|
|
246
|
+
# Ensure labels exist (ignore errors if already created).
|
|
247
|
+
gh label create agent-created --color 5319e7 --description "Opened by an agent loop" 2>/dev/null || true
|
|
248
|
+
gh label create agent-drift --color 1d76db --description "Model-list drift fix" 2>/dev/null || true
|
|
249
|
+
|
|
250
|
+
url=$(gh pr create \
|
|
251
|
+
--title "Fix provider model-list drift" \
|
|
252
|
+
--body "Automated by the drift watchdog. \`cli-router doctor\` found a retired shipped model; this updates \`DEFAULT_MODELS\`, the test, and the changelog. The full test suite passed on the runner.
|
|
253
|
+
The self-hosted runner supplied only a validated three-file patch; publication ran in this isolated GitHub-hosted job." \
|
|
254
|
+
--label agent-created --label agent-drift)
|
|
255
|
+
echo "Opened $url"
|
|
@@ -3,7 +3,6 @@ name: Publish to PyPI
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
|
-
workflow_dispatch:
|
|
7
6
|
|
|
8
7
|
permissions:
|
|
9
8
|
contents: read
|
|
@@ -14,10 +13,10 @@ jobs:
|
|
|
14
13
|
runs-on: ubuntu-latest
|
|
15
14
|
strategy:
|
|
16
15
|
matrix:
|
|
17
|
-
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
18
17
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
20
|
-
- uses: actions/setup-python@v6
|
|
18
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
19
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
21
20
|
with:
|
|
22
21
|
python-version: ${{ matrix.python-version }}
|
|
23
22
|
- name: Install test dependencies
|
|
@@ -26,14 +25,16 @@ jobs:
|
|
|
26
25
|
run: python -m pip install -e .
|
|
27
26
|
- name: Run tests
|
|
28
27
|
run: python -m pytest tests -q
|
|
28
|
+
- name: Validate release identity
|
|
29
|
+
run: python scripts/check_release.py
|
|
29
30
|
|
|
30
31
|
build:
|
|
31
32
|
name: Build distributions
|
|
32
33
|
runs-on: ubuntu-latest
|
|
33
34
|
needs: test
|
|
34
35
|
steps:
|
|
35
|
-
- uses: actions/checkout@
|
|
36
|
-
- uses: actions/setup-python@v6
|
|
36
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
37
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
37
38
|
with:
|
|
38
39
|
python-version: "3.12"
|
|
39
40
|
- name: Install build tools
|
|
@@ -43,7 +44,7 @@ jobs:
|
|
|
43
44
|
- name: Check distributions
|
|
44
45
|
run: python -m twine check dist/*
|
|
45
46
|
- name: Upload distributions
|
|
46
|
-
uses: actions/upload-artifact@
|
|
47
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
47
48
|
with:
|
|
48
49
|
name: dist
|
|
49
50
|
path: dist/
|
|
@@ -60,9 +61,9 @@ jobs:
|
|
|
60
61
|
|
|
61
62
|
steps:
|
|
62
63
|
- name: Download distributions
|
|
63
|
-
uses: actions/download-artifact@
|
|
64
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
64
65
|
with:
|
|
65
66
|
name: dist
|
|
66
67
|
path: dist/
|
|
67
68
|
- name: Publish package
|
|
68
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
20
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
- name: Install test dependencies
|
|
24
|
+
run: python -m pip install --upgrade pip pytest
|
|
25
|
+
- name: Install package
|
|
26
|
+
run: python -m pip install -e .
|
|
27
|
+
- name: Test
|
|
28
|
+
run: python -m pytest tests -q
|
|
29
|
+
|
|
30
|
+
quality:
|
|
31
|
+
name: Quality and package
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
35
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
36
|
+
with:
|
|
37
|
+
python-version: "3.12"
|
|
38
|
+
- name: Install package and development tools
|
|
39
|
+
run: python -m pip install --upgrade pip -e ".[dev]"
|
|
40
|
+
- name: Lint
|
|
41
|
+
run: ruff check cli_router scripts tests
|
|
42
|
+
- name: Type check
|
|
43
|
+
run: mypy cli_router scripts
|
|
44
|
+
- name: Lint GitHub Actions workflows
|
|
45
|
+
run: |
|
|
46
|
+
curl --fail --silent --show-error --location \
|
|
47
|
+
--output /tmp/actionlint.tar.gz \
|
|
48
|
+
https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz
|
|
49
|
+
echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 /tmp/actionlint.tar.gz" \
|
|
50
|
+
| sha256sum --check --strict
|
|
51
|
+
tar -xzf /tmp/actionlint.tar.gz -C /tmp actionlint
|
|
52
|
+
/tmp/actionlint -color
|
|
53
|
+
- name: Test with coverage
|
|
54
|
+
run: coverage run -m pytest tests -q && coverage report
|
|
55
|
+
- name: Build package
|
|
56
|
+
run: python -m build
|
|
57
|
+
- name: Check distributions
|
|
58
|
+
run: python -m twine check dist/*
|
|
@@ -12,6 +12,23 @@ CLI-Router is a Python command-line orchestrator for external AI coding CLIs. It
|
|
|
12
12
|
|
|
13
13
|
Keep that boundary intact. New behavior should make orchestration more reliable or configurable, not turn this package into an agent.
|
|
14
14
|
|
|
15
|
+
## Agent Orientation and Continuity
|
|
16
|
+
|
|
17
|
+
This repository keeps agent-neutral continuity files in the repository root. Every coding agent should use them, regardless of vendor or runtime:
|
|
18
|
+
|
|
19
|
+
1. Read `AGENTS.md` for normative repository rules.
|
|
20
|
+
2. Read `STATE.md` for the last verified project, release, CI, and operational state. Confirm time-sensitive claims against the repository or service before acting.
|
|
21
|
+
3. Read `MEMORY.md` for durable lessons and known operational traps. Treat it as accumulated guidance, not a substitute for direct verification.
|
|
22
|
+
4. Follow task-specific documentation under `docs/`, especially `docs/releasing.md` for releases.
|
|
23
|
+
|
|
24
|
+
Keep the files distinct:
|
|
25
|
+
|
|
26
|
+
- Update `AGENTS.md` when repository-wide working rules or invariants change.
|
|
27
|
+
- Update `STATE.md` after releases or when the current branch, version, CI, publishing, or known-blocker state materially changes.
|
|
28
|
+
- Update `MEMORY.md` when a reusable lesson is learned about this repository or its development environment.
|
|
29
|
+
- Never record tokens, passwords, private keys, one-time device codes, credential contents, or other secrets in any repository file. Paths to credential stores and safe verification commands are acceptable.
|
|
30
|
+
- If continuity documentation conflicts with executable evidence, prefer the evidence and correct the stale documentation in the same change when practical.
|
|
31
|
+
|
|
15
32
|
## Source Layout
|
|
16
33
|
|
|
17
34
|
- `cli_router/cli.py`: argparse CLI entry point and command dispatch.
|
|
@@ -23,6 +40,7 @@ Keep that boundary intact. New behavior should make orchestration more reliable
|
|
|
23
40
|
- `cli_router/failures.py`: external-tool failure classification and user-facing messages.
|
|
24
41
|
- `cli_router/artifacts.py`: run directory and artifact persistence.
|
|
25
42
|
- `cli_router/tools.py`: `tools list` and `tools test`.
|
|
43
|
+
- `cli_router/receipts.py`: schema-versioned JSON receipts for automation-facing CLI output.
|
|
26
44
|
- `cli_router/presets/`: packaged YAML defaults/presets.
|
|
27
45
|
- `examples/`: user-facing example configs.
|
|
28
46
|
- `tests/`: pytest coverage using fake subprocess CLIs, not real Claude/Codex calls.
|
|
@@ -84,13 +102,16 @@ Config lookup order is:
|
|
|
84
102
|
4. `~/.config/cli-router/config.yaml`
|
|
85
103
|
5. Built-in defaults from `cli_router/presets/generic.yaml`
|
|
86
104
|
|
|
87
|
-
Config version is currently `version: 1
|
|
105
|
+
Config version is currently `version: 2`; legacy `version: 1` remains supported. Version 2 requires `requires_cli_router`, which gives older routers a hard failure on the schema version rather than letting them ignore an unknown compatibility key.
|
|
88
106
|
|
|
89
107
|
Supported placeholders are literal token replacements:
|
|
90
108
|
|
|
91
109
|
- `{prompt}`
|
|
92
110
|
- `{user_prompt}`
|
|
93
111
|
- `{plan_path}`
|
|
112
|
+
- `{previous_output}`
|
|
113
|
+
- `{all_stage_outputs}`
|
|
114
|
+
- `{target_root}`
|
|
94
115
|
|
|
95
116
|
Do not switch to Python `str.format` semantics without a strong reason. Current literal replacement intentionally allows command snippets and prompts to contain unrelated braces.
|
|
96
117
|
|
|
@@ -98,7 +119,8 @@ Workflow stages use:
|
|
|
98
119
|
|
|
99
120
|
- `id`: unique stage id within the workflow. The list order is the default execution order.
|
|
100
121
|
- `tool`: primary tool name.
|
|
101
|
-
- `fallback_tools`: optional ordered list of tool names
|
|
122
|
+
- `fallback_tools`: optional ordered list of tool names or `{tool, on}` policy mappings. Fallback is fail-closed: only `auth_required`, `usage_limit`, `timeout`, and `transport_failure` may be named, and legacy string entries use that same safe operational set.
|
|
123
|
+
- `max_fallback_attempts`: optional nonnegative cap on fallback subprocesses actually started. Nonmatching policies are skipped without consuming the cap; a failed fallback's failure kind becomes the trigger for later policies. Fallback must never run after semantic/generic command failures, `unsupported_model`, `extraction_failed`, or configuration errors.
|
|
102
124
|
- `enabled`: optional boolean; `false` skips the stage in default `run` and `implement`, while explicit `--stages` can still select it.
|
|
103
125
|
- `input_template`: rendered prompt sent as `{prompt}` to the tool command.
|
|
104
126
|
- `output_file`: planner output path, usually `PLAN.md`.
|
|
@@ -124,6 +146,16 @@ Tool config supports:
|
|
|
124
146
|
- `output.extract`: top-level or dotted JSON field path.
|
|
125
147
|
- `timeout_seconds`: optional positive number; timeout returns exit code `124`.
|
|
126
148
|
- `provider`, `model`, and `effort`: optional TUI metadata for Model Config display/editing. These do not replace `command`.
|
|
149
|
+
- `cwd`: optional working directory with literal placeholder replacement; workflows expose `{target_root}` as the router invocation directory.
|
|
150
|
+
- `environment_mode`: `inherit` (default) or `allowlist`; allowlist mode starts with only names in `environment_allowlist`.
|
|
151
|
+
- `environment`: string mapping applied after the inherited/allowlisted base; values support literal placeholders.
|
|
152
|
+
- `environment_unset`: environment names removed after overrides.
|
|
153
|
+
- `stdin`: `inherit` (default) or `closed`.
|
|
154
|
+
- `redact_environment_values`: environment names whose nonempty values are replaced in captured/streamed output and recorded commands before artifact persistence. Without this explicit setting, preserve raw stdout/stderr.
|
|
155
|
+
|
|
156
|
+
Top-level `requires_cli_router` accepts a PEP 440 specifier such as `">=0.3.2,<0.4.0"`. It is mandatory for config version 2. All config-loading commands must fail before execution when the running version is incompatible.
|
|
157
|
+
|
|
158
|
+
Stable machine-readable output uses a single JSON object with `schema_version: 1`. `--json` is supported for `--version`, `check`, `plan`, `run`, `implement`, and `tools test`; preserve existing field names and semantics when extending the receipt schema.
|
|
127
159
|
|
|
128
160
|
Diagnostic defaults support:
|
|
129
161
|
|
|
@@ -176,7 +208,7 @@ Fallback attempts include the tool name:
|
|
|
176
208
|
- `planner.codex-planner.stderr`
|
|
177
209
|
- `planner.codex-planner.extracted.md`
|
|
178
210
|
|
|
179
|
-
Every run writes `run.yaml` containing stage summaries, commands, return codes, extracted output, `failure_kind`, workflow start/finish timestamps, total duration, and per-stage duration metrics.
|
|
211
|
+
Every run writes `run.yaml` containing stage summaries, commands, return codes, extracted output, `failure_kind`, workflow start/finish timestamps, total duration, and per-stage duration metrics. Fallback attempt records also contain original-primary provenance (`primary_tool`, `primary_failure_kind`), immediate-trigger provenance (`trigger_tool`, `trigger_failure_kind`), `fallback_tool`, `fallback_reason`, and `fallback_attempt`.
|
|
180
212
|
|
|
181
213
|
Persistent diagnostics write under `~/.cli-router/logs/` by default:
|
|
182
214
|
|
|
@@ -198,6 +230,8 @@ Current `failure_kind` values include:
|
|
|
198
230
|
- `command_not_found`
|
|
199
231
|
- `command_failed`
|
|
200
232
|
- `extraction_failed`
|
|
233
|
+
- `transport_failure`
|
|
234
|
+
- `configuration_error`
|
|
201
235
|
|
|
202
236
|
Known real-world messages are covered by tests, including:
|
|
203
237
|
|
|
@@ -262,7 +296,7 @@ These observations are environment-specific. Do not hardcode them into package d
|
|
|
262
296
|
## Design Constraints
|
|
263
297
|
|
|
264
298
|
- Keep the CLI stable: `cli-router run`, `plan`, `implement`, `check`, `config show`, `tools list`, `tools test <name>`.
|
|
265
|
-
- Keep dependencies minimal. Current runtime dependencies are `pyyaml
|
|
299
|
+
- Keep dependencies minimal. Current runtime dependencies are `packaging`, `pyyaml`, and `rich`; the CLI uses `argparse`. This is why the TUI remains Rich-based instead of adopting Textual without an explicit product decision.
|
|
266
300
|
- Prefer list-form subprocess commands in examples and tests.
|
|
267
301
|
- Preserve stdout/stderr exactly in artifacts. Do not redact or transform raw logs unless a user explicitly requests a privacy feature.
|
|
268
302
|
- Avoid hidden behavior. If CLI-Router retries or falls back, make that visible through summaries and `run.yaml`.
|
|
@@ -285,3 +319,15 @@ Generated files that should normally remain untracked:
|
|
|
285
319
|
- `.pytest_cache/`
|
|
286
320
|
|
|
287
321
|
Before finalizing substantive changes, run `git status --short` and report any verification commands run.
|
|
322
|
+
|
|
323
|
+
## Protected Branch and Release Workflow
|
|
324
|
+
|
|
325
|
+
`main` is protected. Direct pushes are expected to fail with `GH006`; release metadata and all other changes must go through a pull request and the required checks. Do not bypass protection merely to complete a release.
|
|
326
|
+
|
|
327
|
+
Use `docs/releasing.md` as the canonical release runbook. Important invariants:
|
|
328
|
+
|
|
329
|
+
- Create the release commit on a branch, merge it through a green pull request, synchronize local `main`, and only then create the annotated release tag. The tag must point to the actual merged `main` commit, not a pre-merge or squash-source commit.
|
|
330
|
+
- Pushing a tag does not publish the package. `.github/workflows/publish.yml` runs when a GitHub Release is published.
|
|
331
|
+
- PyPI publishing uses GitHub Actions trusted publishing. Do not collect or add a local PyPI password/token for the normal release path.
|
|
332
|
+
- Git transport and GitHub API authentication are separate. This repository's `origin` uses SSH, while `gh` stores its API credential persistently in the user's GitHub CLI configuration. A successful SSH test does not prove that `gh` is authenticated.
|
|
333
|
+
- Before GitHub API work, run `gh auth status`. If the stored credential is invalid, reauthenticate once with `gh auth login`, verify the result, and reuse the persistent credential; do not start repeated login flows without checking status.
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.2 - 2026-07-19
|
|
6
|
+
|
|
7
|
+
- Added fail-closed conditional fallback policies with per-tool `on` failure allowlists, `max_fallback_attempts`, transport-failure classification, and fallback provenance in run manifests. Legacy string fallbacks now use only the safe operational failure set and never retry semantic, extraction, unsupported-model, or configuration failures.
|
|
8
|
+
- Fixed conditional fallback selection to scan past nonmatching policies, count only started fallback subprocesses against `max_fallback_attempts`, carry failed fallback classifications into later policy checks, and record both original-primary and immediate-trigger provenance.
|
|
9
|
+
- Added schema-versioned JSON receipts for `--version`, `check`, `plan`, `run`, `implement`, and `tools test`, including config checksums, run/stage/model details, failure and fallback data, durations, outcomes, and artifact paths.
|
|
10
|
+
- Captured config source and effective merged-config checksums immutably at load time so receipts remain bound to the configuration actually executed, and added provider reasoning effort to workflow and tool-test stage receipts.
|
|
11
|
+
- Added tool-level cwd, environment allowlisting/overrides/unsets, closed stdin, and configured environment-value redaction for commands and captured/streamed output.
|
|
12
|
+
- Added config schema v2 with mandatory `requires_cli_router` PEP 440 compatibility declarations, while retaining v1 support. Older routers reject the v2 schema instead of silently ignoring its safety requirement.
|
|
13
|
+
|
|
14
|
+
## 0.3.1 - 2026-07-19
|
|
15
|
+
|
|
16
|
+
- Made workflow outcomes fail closed: `run` and `implement` now reject empty resolved stage lists, and `stop_on_failure: false` preserves the first failed stage as the overall result even when later stages succeed.
|
|
17
|
+
- Terminate the full subprocess process group on command timeout so provider helper processes do not survive an exit `124`.
|
|
18
|
+
- Restored home-config-only TUI persistence; project-local and explicit configuration files are no longer silently rewritten.
|
|
19
|
+
- Updated generated Hermes commands to the current non-interactive `hermes --oneshot` interface and corrected Grok `--single` argument ordering.
|
|
20
|
+
- Hardened Doctor repair backends to run tool-free/read-only from an isolated temporary directory without GitHub repository tokens; providers without a safe tool-free mode are excluded.
|
|
21
|
+
- Changed model drift removals to require explicit maintainer confirmation, isolated the patch agent from GitHub credentials, added a strict patch-file allowlist, and removed auto-merge from the watchdog.
|
|
22
|
+
- Added release identity validation, Python 3.14 CI, Ruff, mypy, actionlint, branch-aware coverage, package checks, script tests, and Dependabot configuration.
|
|
23
|
+
|
|
5
24
|
## 0.3.0 - 2026-07-13
|
|
6
25
|
|
|
7
26
|
- Added a `doctor` feature (`cli-router doctor`, `--repair`) to catch agent-CLI drift over time. `doctor` reports each provider's discovery health (CLI present? live model list parses?). With `--repair`, when a provider's CLI is installed and responding but its model list no longer parses, Doctor uses a working agent as an LLM parser — it runs the sick provider's own list command itself, hands only the raw text to the agent, and expects back a JSON array of model ids (LLM-as-parser, never LLM-as-operator). Recovered lists are written to `~/.cli-router/model-cache.yaml` (`cli_router.modelcache.ModelCache`), which layers between live discovery and the static `DEFAULT_MODELS` fallback, so the fix survives across runs without editing source. Model discovery and the TUI model pickers now consult that cache.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Repository Memory
|
|
2
|
+
|
|
3
|
+
This file preserves durable, high-signal lessons for maintainers and coding agents. It must never contain credential values, private keys, one-time device codes, or other secrets. Current facts belong in `STATE.md`; normative rules belong in `AGENTS.md`.
|
|
4
|
+
|
|
5
|
+
## GitHub Authentication Is Split
|
|
6
|
+
|
|
7
|
+
- Git transport and GitHub API authentication are independent. The repository remote uses SSH, so `ssh -T git@github.com` can succeed while `gh auth status` fails.
|
|
8
|
+
- `gh` API authentication should persist across sessions in the user's GitHub CLI configuration. In this WSL environment the credential store is `/home/razor/.config/gh/hosts.yml`.
|
|
9
|
+
- Never read, print, commit, or relay the token. Verify it safely with `gh auth status`; `gh auth token` may be used only with output suppressed when a script must test readability.
|
|
10
|
+
- An SSH success message exits nonzero because GitHub does not provide shell access. Judge `ssh -T git@github.com` by its message, not only its exit code.
|
|
11
|
+
- `gh auth setup-git` configures HTTPS Git credentials; it does not repair GitHub API authentication and is unnecessary for this repository's SSH remote.
|
|
12
|
+
|
|
13
|
+
### 2026-07-19 Authentication Incident
|
|
14
|
+
|
|
15
|
+
- The saved `gh` credential existed but GitHub rejected it as invalid. SSH push authentication remained healthy.
|
|
16
|
+
- A single browser/device flow with `gh auth login --hostname github.com --git-protocol ssh --web` replaced the invalid API credential. After the user authorized it, `gh auth status` and suppressed token-readability checks succeeded without another login.
|
|
17
|
+
- The correct response to a future failure is: check `gh auth status`, distinguish API auth from SSH auth and network failures, then reauthenticate only if GitHub reports the stored API credential invalid.
|
|
18
|
+
- Device authorization codes are transient secrets. Show them only to the user during the active flow; never save them in repository documentation, logs, commits, or chat summaries intended for reuse.
|
|
19
|
+
|
|
20
|
+
## Protected Main Changes the Release Order
|
|
21
|
+
|
|
22
|
+
- Direct pushes to `main` fail with `GH006`; this is expected protection, not an authentication failure.
|
|
23
|
+
- Start release metadata work on a release branch, push it, open a pull request, and wait for all required jobs.
|
|
24
|
+
- The repository commonly squash-merges pull requests. A tag created on the source commit before the squash would not point to the final `main` commit.
|
|
25
|
+
- Create the annotated version tag only after fetching the merged `main`. Verify the tag's peeled commit matches `origin/main` before pushing it.
|
|
26
|
+
- If an unpushed local tag points to the source commit, it is safe to recreate it at the identical merged tree. If a tag has already been pushed or a release may have published, stop and assess before rewriting it; do not force-update a public release tag casually.
|
|
27
|
+
|
|
28
|
+
## Publishing Mechanics
|
|
29
|
+
|
|
30
|
+
- A tag push alone does not publish CLI-Router. Publishing starts when a non-draft GitHub Release is published for the tag.
|
|
31
|
+
- `.github/workflows/publish.yml` performs tests, release-identity validation, build, Twine checks, artifact handoff, and PyPI trusted publishing.
|
|
32
|
+
- Trusted publishing uses GitHub OIDC. A normal release should not need `TWINE_PASSWORD`, `PYPI_API_TOKEN`, or a manual upload from a maintainer machine.
|
|
33
|
+
- Monitor the release workflow through the final `Publish CLI-Router to PyPI` job. A successful build job alone does not prove publication.
|
|
34
|
+
- Independently verify publication from a fresh virtual environment using the public PyPI index and the exact version. Check both `importlib.metadata.version("cli-router")` and `cli_router.__version__`, then run `cli-router check`.
|
|
35
|
+
|
|
36
|
+
## Local Verification Environment
|
|
37
|
+
|
|
38
|
+
- System Python may be protected by PEP 668 or lack package metadata and development tools. An uninstalled checkout can make `tests/test_version.py` fail even when the source versions agree.
|
|
39
|
+
- Use a temporary virtual environment, install `-e ".[dev]"`, and run tests, Ruff, mypy, coverage, CLI smoke checks, release identity, build, and Twine there.
|
|
40
|
+
- Build release artifacts in a fresh temporary output directory to avoid confusing stale `dist/` files with the current release.
|
|
41
|
+
- Network isolation can prevent build dependencies from resolving. Treat dependency-download failures separately from code failures and rerun with the environment's approved network mechanism.
|
|
42
|
+
|
|
43
|
+
## PyYAML Treats `on` as a YAML 1.1 Boolean
|
|
44
|
+
|
|
45
|
+
- PyYAML's safe loader parses an unquoted `on:` mapping key as boolean `true`, even though users naturally write conditional fallback policies with `on:`.
|
|
46
|
+
- CLI-Router normalizes boolean `true` back to the documented `on` key inside fallback policy mappings before validation. Preserve this normalization unless the entire loader is deliberately migrated to YAML 1.2 semantics.
|
|
47
|
+
|
|
48
|
+
## Compatibility Keys Need a Schema Bootstrap
|
|
49
|
+
|
|
50
|
+
- A router released before `requires_cli_router` existed can ignore that unknown top-level key, so the key alone cannot protect a safety-sensitive config from old binaries.
|
|
51
|
+
- Config version 2 is the bootstrap boundary: old routers that only accept version 1 fail immediately, while new routers require and evaluate `requires_cli_router`. Keep legacy version 1 support, including treating configs with no explicit version as v1.
|
|
52
|
+
|
|
53
|
+
## Fallback Policies Filter; Attempt Caps Count Processes
|
|
54
|
+
|
|
55
|
+
- A nonmatching conditional fallback policy is skipped, not treated as a failed attempt and not charged against `max_fallback_attempts`.
|
|
56
|
+
- After a fallback subprocess fails, its classified failure becomes the immediate trigger for later policies. Keep original-primary provenance separate from this immediate-trigger provenance so multi-hop chains remain auditable.
|
|
57
|
+
|
|
58
|
+
## Config Receipt Identity Is a Load-Time Snapshot
|
|
59
|
+
|
|
60
|
+
- Receipt identity must hash the exact source bytes read and the canonical effective merged config while loading. Never reread the source path when emitting a receipt: another process can replace the file between execution and receipt emission.
|