claude-dev-env 1.92.0 → 1.93.0
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.
- package/CLAUDE.md +5 -39
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +340 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +165 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +7 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/rules/testing.md +0 -2
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +184 -0
- package/skills/orchestrator-refresh/SKILL.md +37 -0
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +32 -164
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- package/skills/verified-build/SKILL.md +4 -9
- package/skills/team-advisor-refresh/SKILL.md +0 -25
package/CLAUDE.md
CHANGED
|
@@ -6,15 +6,11 @@ The user delegates execution to you and expects zero manual steps unless strictl
|
|
|
6
6
|
|
|
7
7
|
## Timeless Documentation (all `.md` files)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Docs describe the current state only. Full rule set and enforcement: `~/.claude/rules/no-historical-clutter.md` (hook `state-description-blocker`) and `~/.claude/rules/self-contained-docs.md`.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- No references to the conversation that produced the doc (`as discussed`, `Option A`, `after Round 3`).
|
|
11
|
+
## Private values stay out of the committed tree
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## GOTCHAS
|
|
17
|
-
ALWAYS start each session with a /loop 15m populate or update the task list based on remaining todos.
|
|
13
|
+
A value the code needs at run time that is itself private — a host, ssh user or port, owner scope, or id — lives in git-ignored local config with a committed placeholder, not in the source.
|
|
18
14
|
|
|
19
15
|
## Choosing Edit vs Write
|
|
20
16
|
|
|
@@ -45,7 +41,7 @@ Reserve `Read`/`Grep`/`Glob` for files you will actually touch this turn. Compos
|
|
|
45
41
|
Run every multi-step code task in two phases:
|
|
46
42
|
|
|
47
43
|
1. **Coders** — one coder agent per scoped assignment writes the code. A coder that hits a decision it can't reasonably solve consults the advisor (see beginning of this file).
|
|
48
|
-
2. **Verification** — when the coders finish, the main session spawns the `code-verifier` agent in a fresh context, but you must first verify that their work is based on upstream's origin main (aka: the commit live on github). It derives and runs the checks itself rather than trusting coder reports: the task's named gates, tests against baselines recorded before the coders ran, and a two-way diff-vs-assignment reading (every task item maps to a hunk, every hunk maps to a task item, nothing missing). A finding must cite a failing command or a named task item. Source: the fresh-context review step in Claude Code best practices (https://code.claude.com/docs/en/best-practices) — the agent doing the work isn't the one grading it.
|
|
44
|
+
2. **Verification** — when the coders finish, the main session spawns the `code-verifier` agent in a fresh context, but you must first verify that their work is based on upstream's origin main (aka: the commit live on github). It derives and runs the checks itself rather than trusting coder reports: the task's named gates, tests against baselines recorded before the coders ran, and a two-way diff-vs-assignment reading (every task item maps to a hunk, every hunk maps to a task item, nothing missing). A finding must cite a failing command or a named task item. Before it emits the verdict, it puts the draft through one strongest-tier validation subagent — selected per the advisor protocol's host detection and tier ladder — that tries to refute it, and it re-checks and corrects any part the validator overturns. Source: the fresh-context review step in Claude Code best practices (https://code.claude.com/docs/en/best-practices) — the agent doing the work isn't the one grading it.
|
|
49
45
|
|
|
50
46
|
Repair agents run only on reported findings; the verifier re-checks after each repair. Work lands (commit, push, draft PR) only on a clean verdict — enforced by the `verified_commit_gate` hook, which blocks `git commit`/`git push` unless a hook-minted verdict covers the current branch diff. The one exemption is mechanical, not discretionary: a diff whose every changed file is non-code or has an unchanged Python AST once docstrings are stripped (docs, docstrings, comments).
|
|
51
47
|
|
|
@@ -57,14 +53,12 @@ After any sub-agent returns a PR description, file list, or counts, verify each
|
|
|
57
53
|
|
|
58
54
|
Track every task with the task tool, always — for all sessions and all tasks. Capture each task with `TaskCreate` as it arrives, mark it `in_progress` with `TaskUpdate` when you start, and `completed` when it is done. Run `/task-build` to gather any open tasks and add them to the list in one pass.
|
|
59
55
|
|
|
60
|
-
## Working in the claude-
|
|
56
|
+
## Working in the claude-dev-env Repo
|
|
61
57
|
|
|
62
58
|
When changing how skills, rules, or hooks install or sync in this repo (for example adding a skill), read `docs/references/skill-install-system.md` — it maps the install pipeline in `packages/claude-dev-env/bin/install.mjs`.
|
|
63
59
|
|
|
64
60
|
## Additional Non-overlapping Rules
|
|
65
61
|
|
|
66
|
-
- **task_scope:** Match every action to what was explicitly requested. When intent is ambiguous, research official docs and present options via AskUserQuestion before making any changes. Proceed with edits only on explicit instruction.
|
|
67
|
-
- **confirm_implementation_forks:** When two or more viable paths would satisfy the goal and the choice changes the deliverable — its scope, completeness, deferred work, dependencies, or a hard-to-reverse contract — stop and ask which path via AskUserQuestion before implementing. A path that defers work or leaves a placeholder creating a follow-up task is itself a fork to surface, not a default to take silently. Phrase the question in plain language with only the detail needed to decide. See [`confirm-implementation-forks`](rules/confirm-implementation-forks.md).
|
|
68
62
|
- **disambiguate_overloaded_terms:** When a word in the request has two different technical meanings — "conflict" (git-merge versus functional/behavioral), "sync" (fast-forward versus commit), and the like — confirm which one is meant via AskUserQuestion before analyzing or acting.
|
|
69
63
|
|
|
70
64
|
## Serena (Code Intelligence MCP)
|
|
@@ -87,31 +81,3 @@ Before any coding task, call the `initial_instructions` tool to load the Serena
|
|
|
87
81
|
1. **Serena** — symbol-level navigation (declarations, references, implementations, rename)
|
|
88
82
|
2. **es.exe** — file-system search by name/path/extension/size/date (Everything CLI)
|
|
89
83
|
3. **Grep/Glob** — content and pattern matching
|
|
90
|
-
|
|
91
|
-
## Everything Search (es.exe CLI)
|
|
92
|
-
|
|
93
|
-
Search files by name, path, extension, size, or date with the Everything command-line tool `es.exe`. It reads the same live index the desktop app keeps, so results return instantly.
|
|
94
|
-
|
|
95
|
-
### Invocation and scope tokens
|
|
96
|
-
Run `es.exe` with a scoped query — a project path, an `ext:`/`dm:`/`size:` filter, or a name pattern. The `es_exe_path_rewriter` hook resolves a `{project-name}` placeholder or a bare registry key from `~/.claude/project-paths.json` into its quoted absolute path before the command runs (it allows and rewrites, never blocks).
|
|
97
|
-
|
|
98
|
-
### Hard limits
|
|
99
|
-
- Scope every search. A bare whole-drive scan or a network-share sweep is out of bounds.
|
|
100
|
-
|
|
101
|
-
### Fallback order
|
|
102
|
-
1. **es.exe** — file-system search by name/path/extension/size/date
|
|
103
|
-
2. **Debug** — try to find out why es.exe isn't working, and then prompt user for next-steps if you can't self-heal.
|
|
104
|
-
3. **Grep** — file-content search (Grep owns content)
|
|
105
|
-
4. **Glob** — relative-path pattern matching within the current project
|
|
106
|
-
|
|
107
|
-
### Search syntax quick reference
|
|
108
|
-
- `ext:py` — find by extension (multiple: `ext:ts | ext:js`)
|
|
109
|
-
- `path:src\components` — match against full path
|
|
110
|
-
- `*.config.*` — wildcards
|
|
111
|
-
- `size:>10mb` — size filter
|
|
112
|
-
- `dm:today` / `dm:thisweek` — date modified filter
|
|
113
|
-
- `-n 50` — limit results; `-sort dm` — sort by date modified
|
|
114
|
-
- `foo bar` — AND, `foo | bar` — OR, `!foo` — NOT
|
|
115
|
-
- `"exact phrase"` — literal match
|
|
116
|
-
|
|
117
|
-
Full operator reference: `skills/everything-search/SKILL.md`.
|
package/_shared/CLAUDE.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# _shared
|
|
2
2
|
|
|
3
|
-
Cross-cutting runtime assets shared by multiple PR-loop skills (`bugteam`, `pr-converge`, `findbugs`, `fixbugs`). Files here are installed into `~/.claude/_shared/` by `bin/install.mjs` alongside the skill directories that import them.
|
|
3
|
+
Cross-cutting runtime assets shared by multiple skills. The PR-loop skills (`bugteam`, `pr-converge`, `findbugs`, `fixbugs`) are one such cluster; other clusters live here too. Files here are installed into `~/.claude/_shared/` by `bin/install.mjs` alongside the skill directories that import them.
|
|
4
4
|
|
|
5
5
|
## Contents
|
|
6
6
|
|
|
7
7
|
| Entry | Description |
|
|
8
8
|
|---|---|
|
|
9
9
|
| `pr-loop/` | Docs, scripts, and constants for the PR-loop workflow suite |
|
|
10
|
+
| `advisor/` | Warm-advisor spawn-and-consult protocol for `team-advisor` and `orchestrator` |
|
|
10
11
|
|
|
11
12
|
## Install path
|
|
12
13
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# _shared/advisor
|
|
2
|
+
|
|
3
|
+
Warm-advisor bind-and-consult protocol shared by `team-advisor`, `orchestrator`, `orchestrator-refresh`, and every executor subagent `orchestrator` routes work to. Changes here affect all of these simultaneously — treat this as a breaking-change surface.
|
|
4
|
+
|
|
5
|
+
Host profile (Claude vs Grok) is detected first; Claude walks the multi-tier Agent spawn ladder, Grok self-binds as the advisor with a separate executor paste block.
|
|
6
|
+
|
|
7
|
+
## Key documents
|
|
8
|
+
|
|
9
|
+
| File | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `advisor-protocol.md` | Host profiles first, Claude-only model floor, warm-up spawn procedure and charter, consult format and cadence, lifecycle ownership (spawn on Claude / re-charter on Grok), host-matched Advisor blocks for executor spawns, and the Claude CLI fallback chain |
|
|
12
|
+
|
|
13
|
+
## Subdirectory
|
|
14
|
+
|
|
15
|
+
| Entry | Description |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `scripts/` | `model_tier_run_validator.py` (spawn-walk log checks), `tier_model_ids.py` (`resolve_cli_model_id` / short CLI/Agent aliases / `detect_host_profile`), and `advisor_scripts_constants` under `scripts/config/` (ladder, short-alias map, host profiles, SendMessage wait bound) |
|
|
18
|
+
|
|
19
|
+
## Breaking-change rule
|
|
20
|
+
|
|
21
|
+
A change to host detection, the model-floor rule, the charter template, either host's Advisor block, or lifecycle ownership in `advisor-protocol.md` requires updating every consuming skill (`team-advisor`, `orchestrator`, `orchestrator-refresh`) in the same commit.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Advisor Protocol
|
|
2
|
+
|
|
3
|
+
Shared spawn-once, consult-by-message protocol for a warm advisor. Two skills depend on this: `team-advisor` (binds the advisor for its own consulting session) and `orchestrator` (binds the same advisor and lets its own routed executor subagents consult it too). Executor spawn prompts are a third consumer, via the host-matched Advisor block below.
|
|
4
|
+
|
|
5
|
+
**First step of every bind:** detect the host profile (next section). Do not walk the model-floor ladder, spawn `session-advisor`, or open the CLI fallback until the host is known. On Grok, skip straight to **Host profiles → Grok host**. On Claude, continue with **Model floor** and the rest of this document.
|
|
6
|
+
|
|
7
|
+
## Host profiles
|
|
8
|
+
|
|
9
|
+
Detect the host profile **before** any model-floor walk. Source of truth for names and detection: `HOST_PROFILE_CLAUDE`, `HOST_PROFILE_GROK`, `ALL_HOST_PROFILES`, and `detect_host_profile(...)` in `$HOME/.claude/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py` and `tier_model_ids.py`.
|
|
10
|
+
|
|
11
|
+
Detection order:
|
|
12
|
+
|
|
13
|
+
1. `ADVISOR_HOST_PROFILE=Grok` or `=Claude` (explicit override; any letter case).
|
|
14
|
+
2. `GROK_BUILD=1` (or `true` / `yes` / `on`) — Grok Build / xAI harness.
|
|
15
|
+
3. Default: Claude.
|
|
16
|
+
|
|
17
|
+
### Claude host
|
|
18
|
+
|
|
19
|
+
Use the **Model floor** ladder below (Fable → Opus → Sonnet → Haiku). Warm-up spawns `subagent_type: session-advisor` via the Agent tool; consults go through `SendMessage` to that warm agent. When every candidate down to the floor fails, take the CLI Claude-chain fallback. Paste the **Claude host** Advisor block into every executor spawn prompt.
|
|
20
|
+
|
|
21
|
+
### Grok host
|
|
22
|
+
|
|
23
|
+
Grok Build / xAI has exactly one model tier. Do **not** walk the Claude ladder and do **not** require a `claude` binary.
|
|
24
|
+
|
|
25
|
+
1. Detect host profile first (this section).
|
|
26
|
+
2. Set `own_tier = Grok`, `candidate_tiers = ["Grok"]`, one attempt only, `selected_tier = Grok`.
|
|
27
|
+
3. **Self-as-advisor:** skip the Agent-tool spawn of `session-advisor`. The orchestrating Grok session *is* the advisor — the same process answers ENDORSE / CORRECTION / PLAN / STOP. Executors (when any) report blockers to the orchestrating session; that session answers with the four signals inline (or via its own same-session consult loop). There is no SendMessage-to-another-Claude-agent path on a pure Grok host.
|
|
28
|
+
4. Record the single attempt as `{tier: "Grok", result: "self"}` — the `self` token marks a self-bind so logs stay honest (not a separate spawn).
|
|
29
|
+
5. CLI Claude-chain fallback does **not** apply on a pure Grok host.
|
|
30
|
+
6. Paste the **Grok host** Advisor block into every executor spawn prompt — never the Claude SendMessage block.
|
|
31
|
+
|
|
32
|
+
Resolve the Grok alias with `resolve_cli_model_id("Grok")` → `grok` when a model field is required.
|
|
33
|
+
|
|
34
|
+
## Model floor
|
|
35
|
+
|
|
36
|
+
**Claude host only.** On a Grok host, skip this section entirely — follow **Host profiles → Grok host**.
|
|
37
|
+
|
|
38
|
+
The advisor's model tier must be at or above the highest tier of any consumer that will reach it. Each consuming skill supplies its own consumer set when computing the floor:
|
|
39
|
+
- `team-advisor`: the sole consumer is the calling session itself, so the floor is just that session's own tier.
|
|
40
|
+
- `orchestrator`: the consumer set is the orchestrating session plus every tier named in its routing table, so the floor is the max of those.
|
|
41
|
+
|
|
42
|
+
Ladder, strongest first (canonical Title Case names: `Fable`, `Opus`, `Sonnet`, `Haiku`; the validator accepts any letter case and normalizes to Title Case): Fable, Opus, Sonnet, Haiku. Read the floor tier — the lower bound only — then try the warm-agent spawn top-down from Fable, stopping at the floor tier — never spawn below it. Each walk attempt sets the Agent tool `model:` field to the short alias for that attempt's candidate tier (`resolve_cli_model_id(candidate_tier)` — for example `opus`, not Title Case `Opus`). The warm agent is created at `selected_tier` (the first ladder tier that actually spawned), which may sit above the floor. If even the floor tier's spawn fails, move to the CLI fallback below rather than spawning below the floor.
|
|
43
|
+
|
|
44
|
+
Emit a structured spawn-walk log so it can be checked mechanically rather than inferred from a transcript. Record: `own_tier` (the floor tier read at the top of this section), `candidate_tiers` (the ladder slice down to that floor), `attempts` (one `{tier, result}` entry appended as each spawn try happens, `result` one of `spawned` for a Claude Agent spawn, `self` for a Grok host self-bind, or a failure reason such as `unavailable`), and `selected_tier` (the tier of the first successful bind — first `spawned` or `self` entry — or `null` paired with a `fallback_reason` string when none bound and the CLI fallback took over). Write the log as JSON with those field names to a path the session controls — typically `<job-temp-dir>/model-tier-run.json` (or the OS temp directory when no job directory exists). Check it with:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
python "$HOME/.claude/_shared/advisor/scripts/model_tier_run_validator.py" <path-to-model-tier-run.json>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Exit code `0` means every invariant holds; `1` means a ladder invariant failed; `2` means the path or JSON was unusable. The same checks are available in-process via `validate_model_tier_run(run)`.
|
|
51
|
+
|
|
52
|
+
The validator checks ladder shape only (candidate slice, attempt order, success-token rules per tier). On a pure Grok host the protocol forbids CLI fallback after a failed self-bind; a log with `own_tier=Grok`, exhausted attempts, and `selected_tier=null` may still pass the structural validator — treat that as a host-policy violation outside the ladder checker.
|
|
53
|
+
|
|
54
|
+
## Warm-up (once per session)
|
|
55
|
+
|
|
56
|
+
On a Grok host, skip this section — follow **Host profiles → Grok host** (self-as-advisor; no Agent spawn walk).
|
|
57
|
+
|
|
58
|
+
On a Claude host, the consuming skill's session walks the candidate tiers top-down. For each attempt, spawn with:
|
|
59
|
+
- `subagent_type: session-advisor` (see [`agents/session-advisor.md`](../../agents/session-advisor.md) for the full signal contract).
|
|
60
|
+
- `model`: the short alias for that attempt's candidate tier via `resolve_cli_model_id` (or the alias table under Fallback) — for example `opus`, not Title Case `Opus`. The floor is only the lower bound of the walk; the walk still tries stronger tiers first.
|
|
61
|
+
- `name`: a name the session and every consumer will use to reach it (e.g. `team-advisor-agent`).
|
|
62
|
+
- `run_in_background: true`.
|
|
63
|
+
|
|
64
|
+
Stop at the first successful spawn. That attempt's tier is `selected_tier`; the warm agent lives at that tier for the rest of the session. If every candidate down to the floor fails, take the CLI fallback below.
|
|
65
|
+
|
|
66
|
+
Charter (the spawn prompt): the agent's role — standing reviewer, never edits files or runs commands, only answers via SendMessage — the repo path, and the session's current goal in two or three sentences. State plainly:
|
|
67
|
+
- Every consult carries: who is asking (name and assignment), what changed since their last consult, the live decision or question, and any load-bearing paths or excerpts.
|
|
68
|
+
- Reply via SendMessage to whoever sent the consult, by name — never route a reply through the spawning session or "main." Many different consumers may reach this one agent; each reply goes back to its own sender.
|
|
69
|
+
- Treat each consult on its own terms, keyed to the sender's stated assignment. Different consumers' consults will interleave in this one transcript — don't blend context across consumers unless a consult explicitly asks for that.
|
|
70
|
+
- If a consult re-raises a question already answered, with nothing new attached, reply by restating the prior answer and naming it as a restatement.
|
|
71
|
+
|
|
72
|
+
The agent finishes its first turn standing by. `SendMessage` alone is what resumes it — no polling loop, no `ScheduleWakeup` keep-alive.
|
|
73
|
+
|
|
74
|
+
## Consulting the warm agent
|
|
75
|
+
|
|
76
|
+
Send a consult whenever one of these holds:
|
|
77
|
+
- A nontrivial plan is about to be locked in and acted on.
|
|
78
|
+
- The consumer believes its assigned work is finished.
|
|
79
|
+
- A commit, push, or other hard-to-reverse action is about to run.
|
|
80
|
+
- The same failure has come back more than once, or progress has stalled.
|
|
81
|
+
- The chosen approach is being reconsidered.
|
|
82
|
+
|
|
83
|
+
Each consult carries, in order: who you are and your assignment (only needed on a shared advisor with multiple consumers — skip this for a single-consumer team-advisor session), the delta since your last consult (what was done, in order, with real output where it matters — never a full recap), the live decision or blocker, and any paths or excerpts needed to answer well.
|
|
84
|
+
|
|
85
|
+
**New-evidence rule.** Re-raise a question the advisor already answered only when you have something new to attach — the result of attempting the advised step, fresh tool output, or a changed constraint. Without new evidence, act on the standing answer.
|
|
86
|
+
|
|
87
|
+
**Report-back rule.** After a CORRECTION or PLAN, your next consult on that topic opens with what happened when you followed it.
|
|
88
|
+
|
|
89
|
+
Treat the reply as a serious second opinion: a CORRECTION — whether it names a wrong step or a risk worth closing — is something to address before treating the plan or the work as done. A STOP, or a consult that finds the advisor unreachable, is reported up rather than retried — team-advisor's sole consumer is the session itself, so it reports to the user; orchestrator's executors report to the orchestrating session, which decides. When the advisor becomes unreachable, report that to the session that owns its lifecycle (see below); that session alone decides whether to respawn (Claude) or re-charter itself (Grok).
|
|
90
|
+
|
|
91
|
+
## Advisor block — paste the host-matched block into every executor spawn prompt
|
|
92
|
+
|
|
93
|
+
Each paragraph is self-contained — the executor receives only this text, not the rest of this document, so it carries everything it needs on its own. Paste **exactly one** block, chosen by host profile.
|
|
94
|
+
|
|
95
|
+
### Claude host (SendMessage to warm advisor)
|
|
96
|
+
|
|
97
|
+
> A shared session advisor named `<name>` is reachable via SendMessage. Consult it before locking in a nontrivial approach, once you believe your assignment is done, before any hard-to-reverse action, when the same failure repeats or progress has stalled, and when the chosen approach is being reconsidered. Open each consult with who you are and your assignment, then: what you tried, the exact decision or blocker, and relevant paths or excerpts. Re-raise something it already answered only when you have new evidence to attach — the result of trying its advice, fresh output, or a changed constraint; otherwise act on its standing answer. After a CORRECTION or PLAN, your next consult on that topic opens with what happened when you followed it. Its replies open with one of ENDORSE, CORRECTION, PLAN, or STOP — treat CORRECTION and PLAN as actions to take. On STOP, or if the advisor is unreachable, report that back to whoever assigned you and leave lifecycle decisions to the session that owns the advisor.
|
|
98
|
+
|
|
99
|
+
### Grok host (self-as-advisor; report to orchestrating session)
|
|
100
|
+
|
|
101
|
+
> The orchestrating session is the shared advisor for this run — there is no separate `session-advisor` agent and no SendMessage path to one. Report blockers and hard decisions to the **orchestrating session** (the session that assigned you) before locking in a nontrivial approach, once you believe your assignment is done, before any hard-to-reverse action, when the same failure repeats or progress has stalled, and when the chosen approach is being reconsidered. Open each report with who you are and your assignment, then: what you tried, the exact decision or blocker, and relevant paths or excerpts. Re-raise something already answered only when you have new evidence to attach — the result of trying prior advice, fresh output, or a changed constraint; otherwise act on the standing answer. After a CORRECTION or PLAN, your next report on that topic opens with what happened when you followed it. The orchestrating session answers with one of ENDORSE, CORRECTION, PLAN, or STOP — treat CORRECTION and PLAN as actions to take. On STOP, or if the orchestrating session is unreachable, stop work and surface that upward; do not spawn a `session-advisor` agent yourself.
|
|
102
|
+
|
|
103
|
+
## Lifecycle ownership
|
|
104
|
+
|
|
105
|
+
### Claude host
|
|
106
|
+
|
|
107
|
+
The session that spawns the shared advisor owns its whole lifecycle — spawn, drift-respawn, and shutdown. Every other consumer (executors, or any other consulting session) only ever sends it messages; none of them spawn, respawn, or shut it down themselves. One shared advisor exists per orchestrated session, owned by the session that spawned it.
|
|
108
|
+
|
|
109
|
+
**Re-spawn on drift.** If a reply shows the agent working from a stale picture, or the session pivots to an unrelated task, the owning session ends that agent and spawns a fresh one with a new charter, rather than forcing the old context to stretch across two different jobs.
|
|
110
|
+
|
|
111
|
+
### Grok host
|
|
112
|
+
|
|
113
|
+
The orchestrating session owns the self-as-advisor role for the whole run. There is no Agent spawn to end or respawn.
|
|
114
|
+
|
|
115
|
+
**Re-charter on drift.** If the session's own answers show a stale picture, or the task pivots, re-state the self-as-advisor charter in-session (goal, four-signal contract, executor report path) and log another `{tier: "Grok", result: "self"}` bind only when a fresh structural log is needed. Executors keep reporting to the orchestrating session; they never spawn a replacement advisor.
|
|
116
|
+
|
|
117
|
+
## Fallback: the CLI chain
|
|
118
|
+
|
|
119
|
+
This section applies on a **Claude host** only. On a pure Grok host, skip it — there is no Claude binary chain and the session is already the advisor (see **Host profiles**).
|
|
120
|
+
|
|
121
|
+
Fall back to the CLI when any of these holds, rather than on judgment call:
|
|
122
|
+
- The Agent-tool spawn errors at every candidate tier down to the floor — the tool itself, not just the top tier, is unavailable.
|
|
123
|
+
- `SendMessage` to the shared advisor errors, or draws no reply within the bound in `ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS` (120) in `$HOME/.claude/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py`, and a re-spawn also fails.
|
|
124
|
+
- The running session is itself a subagent barred from spawning further agents.
|
|
125
|
+
|
|
126
|
+
Map `selected_tier` when one exists (the warm agent already bound above the floor, or at it); map the floor tier only when the walk exhausted with `selected_tier=null`. Resolve that tier to its CLI / Agent model alias before the first call — the CLI's `--model` flag and the Agent tool's `model:` field take the short aliases below, not free-form ladder prose. Source of truth: `ALL_CLI_MODEL_ID_BY_TIER` and `resolve_cli_model_id(tier)` in the same constants package / `tier_model_ids.py` helper:
|
|
127
|
+
|
|
128
|
+
| Ladder tier (Title Case) | CLI / Agent `model` alias |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Fable | `fable` |
|
|
131
|
+
| Opus | `opus` |
|
|
132
|
+
| Sonnet | `sonnet` |
|
|
133
|
+
| Haiku | `haiku` |
|
|
134
|
+
| Grok (Grok host only; not part of the Claude walk) | `grok` |
|
|
135
|
+
|
|
136
|
+
Resolve in code with `python -c "from tier_model_ids import resolve_cli_model_id; print(resolve_cli_model_id('Opus'))"` from `$HOME/.claude/_shared/advisor/scripts/` (any letter case accepted; unknown tiers raise `ValueError`). Use `python "$HOME/.claude/scripts/claude_chain_runner.py" -- -p --model <model alias> --output-format json` in place of the Agent-tool spawn. The chain runner walks the fallback chain configured at `~/.claude/claude-chain.json` (typically `claude` then `claude-ev`), so a usage-limited primary account still gets served. Write the charter or the consult brief to a temporary file under the job's own temporary directory (or the OS temp directory when no job directory exists) and pipe it in, rather than passing either as an inline argument, and drop that file once the consult completes.
|
|
137
|
+
|
|
138
|
+
Read the `session_id` out of the first call's JSON response and pass it to `-p --resume <session_id> --output-format json` on every later consult — `-p` stays on the resume call too, since it is still a non-interactive invocation. A usage-limit failover to the next binary in the chain does not carry the `session_id` forward: a session store belongs to the binary and account that minted it, so a `--resume` against the new binary can fail. Treat that failure as starting over, not as an error to retry — resend the charter plus a compact recap of the consults since the last one, capture the new `session_id` the fresh call returns, and continue the fallback path from there.
|
|
139
|
+
|
|
140
|
+
## Mechanism, not a measured saving
|
|
141
|
+
|
|
142
|
+
One warm agent, addressed by name, whose transcript accumulates across consults — each consult sends only the delta since the last one. Whether this yields a measured token or cost saving over repeated cold spawns has not been directly verified from inside a session; state the mechanism, not a caching claim, in any consuming skill's own text.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Named constants for the advisor scripts."""
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Constants for the model-tier-run validator and CLI / Agent alias map.
|
|
2
|
+
|
|
3
|
+
They name the parts of a spawn-walk log: the tier ladder (strongest
|
|
4
|
+
first), the two host profiles, the spawn-log dict keys, and the tokens
|
|
5
|
+
that mark a bind.
|
|
6
|
+
|
|
7
|
+
::
|
|
8
|
+
|
|
9
|
+
attempt = {"tier": "Opus", "result": "spawned"}
|
|
10
|
+
^^^^^^ ^^^^^^^^^
|
|
11
|
+
a ladder tier SPAWN_SUCCESS_TOKEN
|
|
12
|
+
ok: "self" marks a Grok host self-bind (SELF_BIND_SUCCESS_TOKEN)
|
|
13
|
+
flag: any other result token counts as no bind
|
|
14
|
+
|
|
15
|
+
The alias map turns each tier into its short CLI / Agent name (``opus``,
|
|
16
|
+
``grok``), never a dated full model ID.
|
|
17
|
+
|
|
18
|
+
Host-profile detection (see ``detect_host_profile``):
|
|
19
|
+
|
|
20
|
+
- ``ADVISOR_HOST_PROFILE=Grok`` or ``=Claude`` — explicit override
|
|
21
|
+
- ``GROK_BUILD=1`` (or ``true`` / ``yes``) — Grok Build / xAI harness
|
|
22
|
+
- default when neither is set: Claude
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
HOST_PROFILE_CLAUDE: str = "Claude"
|
|
28
|
+
HOST_PROFILE_GROK: str = "Grok"
|
|
29
|
+
ALL_HOST_PROFILES: tuple[str, ...] = (HOST_PROFILE_CLAUDE, HOST_PROFILE_GROK)
|
|
30
|
+
|
|
31
|
+
ALL_MODEL_TIERS: tuple[str, ...] = ("Fable", "Opus", "Sonnet", "Haiku")
|
|
32
|
+
GROK_MODEL_TIER: str = "Grok"
|
|
33
|
+
ALL_KNOWN_TIER_NAMES: tuple[str, ...] = (*ALL_MODEL_TIERS, GROK_MODEL_TIER)
|
|
34
|
+
|
|
35
|
+
ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS: int = 120
|
|
36
|
+
|
|
37
|
+
ALL_CLI_MODEL_ID_BY_TIER: dict[str, str] = {
|
|
38
|
+
"Fable": "fable",
|
|
39
|
+
"Opus": "opus",
|
|
40
|
+
"Sonnet": "sonnet",
|
|
41
|
+
"Haiku": "haiku",
|
|
42
|
+
GROK_MODEL_TIER: "grok",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
HOST_PROFILE_ENV_VAR: str = "ADVISOR_HOST_PROFILE"
|
|
46
|
+
GROK_BUILD_ENV_VAR: str = "GROK_BUILD"
|
|
47
|
+
ALL_GROK_BUILD_TRUTHY_VALUES: frozenset[str] = frozenset(
|
|
48
|
+
{"1", "true", "yes", "on"}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
TIER_KEY: str = "tier"
|
|
52
|
+
SPAWN_OUTCOME_KEY: str = "result"
|
|
53
|
+
SPAWN_SUCCESS_TOKEN: str = "spawned"
|
|
54
|
+
SELF_BIND_SUCCESS_TOKEN: str = "self"
|
|
55
|
+
|
|
56
|
+
UNKNOWN_OWN_TIER_MESSAGE: str = "own_tier is not a known model tier"
|
|
57
|
+
UNKNOWN_LADDER_NAME_ERROR: str = "ladder name is not a known model tier: {!r}"
|
|
58
|
+
UNKNOWN_HOST_PROFILE_ERROR: str = "host profile is not a known profile: {!r}"
|
|
59
|
+
CANDIDATE_TIERS_MISMATCH_MESSAGE: str = (
|
|
60
|
+
"candidate_tiers does not match the ladder slice down to own_tier"
|
|
61
|
+
)
|
|
62
|
+
ATTEMPT_TIER_OUT_OF_SLICE_MESSAGE: str = (
|
|
63
|
+
"a spawn try names a tier outside the candidate slice"
|
|
64
|
+
)
|
|
65
|
+
ATTEMPT_ORDER_MISMATCH_MESSAGE: str = (
|
|
66
|
+
"spawn tries do not walk the candidate tiers in ladder order"
|
|
67
|
+
)
|
|
68
|
+
SELECTED_TIER_MISMATCH_MESSAGE: str = (
|
|
69
|
+
"selected_tier does not match the first successful bind (spawned or self)"
|
|
70
|
+
)
|
|
71
|
+
SELECTED_TIER_NOT_NULL_MESSAGE: str = (
|
|
72
|
+
"selected_tier must be null when no spawn try succeeded"
|
|
73
|
+
)
|
|
74
|
+
MISSING_FALLBACK_REASON_MESSAGE: str = (
|
|
75
|
+
"fallback_reason is required when no spawn try succeeded"
|
|
76
|
+
)
|
|
77
|
+
INCOMPLETE_FALLBACK_WALK_MESSAGE: str = (
|
|
78
|
+
"selected_tier is null but attempts did not exhaust every candidate tier"
|
|
79
|
+
)
|
|
80
|
+
CLI_USAGE_MESSAGE: str = (
|
|
81
|
+
"usage: model_tier_run_validator.py <spawn-walk-log.json>"
|
|
82
|
+
)
|
|
83
|
+
CLI_MISSING_PATH_EXIT_CODE: int = 2
|
|
84
|
+
CLI_INVALID_JSON_EXIT_CODE: int = 2
|
|
85
|
+
CLI_VALIDATION_FAILURE_EXIT_CODE: int = 1
|
|
86
|
+
CLI_SUCCESS_EXIT_CODE: int = 0
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Mechanically validate a model-tier spawn-walk log.
|
|
2
|
+
|
|
3
|
+
The advisor protocol's Model floor section emits a structured spawn-walk log.
|
|
4
|
+
This validator reads that log back and checks its invariants. A run is judged
|
|
5
|
+
from data, not inferred from a transcript.
|
|
6
|
+
|
|
7
|
+
::
|
|
8
|
+
|
|
9
|
+
ladder_walk = ModelTierRun(
|
|
10
|
+
own_tier="Opus",
|
|
11
|
+
candidate_tiers=["Fable", "Opus"],
|
|
12
|
+
attempts=[
|
|
13
|
+
{"tier": "Fable", "result": "unavailable"},
|
|
14
|
+
{"tier": "Opus", "result": "spawned"},
|
|
15
|
+
],
|
|
16
|
+
selected_tier="Opus",
|
|
17
|
+
)
|
|
18
|
+
validate_model_tier_run(ladder_walk) # ok: returns None, raises nothing
|
|
19
|
+
|
|
20
|
+
self_bind = ModelTierRun(
|
|
21
|
+
own_tier="Grok",
|
|
22
|
+
candidate_tiers=["Grok"],
|
|
23
|
+
attempts=[{"tier": "Grok", "result": "self"}],
|
|
24
|
+
selected_tier="Grok",
|
|
25
|
+
)
|
|
26
|
+
validate_model_tier_run(self_bind) # ok: host self-as-advisor bind
|
|
27
|
+
|
|
28
|
+
A run whose selected_tier is not the first successful bind fails.
|
|
29
|
+
On any broken invariant, validate_model_tier_run raises ModelTierRunError.
|
|
30
|
+
|
|
31
|
+
CLI::
|
|
32
|
+
|
|
33
|
+
python model_tier_run_validator.py path/to/spawn-walk-log.json
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import sys
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
_scripts_directory = str(Path(__file__).resolve().parent)
|
|
44
|
+
_config_directory = str(Path(__file__).resolve().parent / "config")
|
|
45
|
+
if _config_directory not in sys.path:
|
|
46
|
+
sys.path.insert(0, _config_directory)
|
|
47
|
+
if _scripts_directory not in sys.path:
|
|
48
|
+
sys.path.insert(0, _scripts_directory)
|
|
49
|
+
|
|
50
|
+
from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
|
|
51
|
+
ALL_MODEL_TIERS,
|
|
52
|
+
ATTEMPT_ORDER_MISMATCH_MESSAGE,
|
|
53
|
+
ATTEMPT_TIER_OUT_OF_SLICE_MESSAGE,
|
|
54
|
+
CANDIDATE_TIERS_MISMATCH_MESSAGE,
|
|
55
|
+
CLI_INVALID_JSON_EXIT_CODE,
|
|
56
|
+
CLI_MISSING_PATH_EXIT_CODE,
|
|
57
|
+
CLI_SUCCESS_EXIT_CODE,
|
|
58
|
+
CLI_USAGE_MESSAGE,
|
|
59
|
+
CLI_VALIDATION_FAILURE_EXIT_CODE,
|
|
60
|
+
GROK_MODEL_TIER,
|
|
61
|
+
INCOMPLETE_FALLBACK_WALK_MESSAGE,
|
|
62
|
+
MISSING_FALLBACK_REASON_MESSAGE,
|
|
63
|
+
SELECTED_TIER_MISMATCH_MESSAGE,
|
|
64
|
+
SELECTED_TIER_NOT_NULL_MESSAGE,
|
|
65
|
+
SELF_BIND_SUCCESS_TOKEN,
|
|
66
|
+
SPAWN_OUTCOME_KEY,
|
|
67
|
+
SPAWN_SUCCESS_TOKEN,
|
|
68
|
+
TIER_KEY,
|
|
69
|
+
UNKNOWN_OWN_TIER_MESSAGE,
|
|
70
|
+
)
|
|
71
|
+
from tier_model_ids import canonical_tier_name # noqa: E402
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class ModelTierRun:
|
|
76
|
+
own_tier: str
|
|
77
|
+
candidate_tiers: list[str]
|
|
78
|
+
attempts: list[dict[str, str]]
|
|
79
|
+
selected_tier: str | None
|
|
80
|
+
fallback_reason: str | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ModelTierRunError(ValueError):
|
|
84
|
+
"""Raised when a model-tier spawn-walk log violates an invariant."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _canonical_tier_list(all_tier_names: list[str]) -> list[str] | None:
|
|
88
|
+
all_canonical_tiers: list[str] = []
|
|
89
|
+
for each_tier_name in all_tier_names:
|
|
90
|
+
maybe_canonical_tier = canonical_tier_name(each_tier_name)
|
|
91
|
+
if maybe_canonical_tier is None:
|
|
92
|
+
return None
|
|
93
|
+
all_canonical_tiers.append(maybe_canonical_tier)
|
|
94
|
+
return all_canonical_tiers
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _expected_candidate_tiers(own_tier: str) -> list[str]:
|
|
98
|
+
maybe_canonical_own_tier = canonical_tier_name(own_tier)
|
|
99
|
+
if maybe_canonical_own_tier is None:
|
|
100
|
+
raise ModelTierRunError(f"{UNKNOWN_OWN_TIER_MESSAGE}: {own_tier!r}")
|
|
101
|
+
if maybe_canonical_own_tier == GROK_MODEL_TIER:
|
|
102
|
+
return [GROK_MODEL_TIER]
|
|
103
|
+
floor_index = ALL_MODEL_TIERS.index(maybe_canonical_own_tier)
|
|
104
|
+
return list(ALL_MODEL_TIERS[: floor_index + 1])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _is_successful_attempt_outcome(
|
|
108
|
+
canonical_tier: str,
|
|
109
|
+
outcome_token: str,
|
|
110
|
+
) -> bool:
|
|
111
|
+
if canonical_tier == GROK_MODEL_TIER:
|
|
112
|
+
return outcome_token == SELF_BIND_SUCCESS_TOKEN
|
|
113
|
+
return outcome_token == SPAWN_SUCCESS_TOKEN
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def validate_model_tier_run(run: ModelTierRun) -> None:
|
|
117
|
+
"""Check that a spawn-walk log satisfies every ladder invariant.
|
|
118
|
+
|
|
119
|
+
::
|
|
120
|
+
|
|
121
|
+
validate_model_tier_run(ladder_walk) # ok: multi-tier ladder walk
|
|
122
|
+
validate_model_tier_run(self_bind) # ok: single-tier self-bind
|
|
123
|
+
validate_model_tier_run(broken_log) # flag: ModelTierRunError
|
|
124
|
+
|
|
125
|
+
Candidate tiers must match the floor slice (or ``["Grok"]`` alone). Tries
|
|
126
|
+
walk that slice in order; early stop only after ``spawned`` or ``self``.
|
|
127
|
+
A null selected_tier requires a full walk plus fallback_reason.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
run: The structured spawn-walk log to check.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
None when every invariant holds.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ModelTierRunError: When any invariant is violated.
|
|
137
|
+
"""
|
|
138
|
+
all_expected_candidates = _expected_candidate_tiers(run.own_tier)
|
|
139
|
+
maybe_canonical_candidates = _canonical_tier_list(run.candidate_tiers)
|
|
140
|
+
if maybe_canonical_candidates != all_expected_candidates:
|
|
141
|
+
raise ModelTierRunError(CANDIDATE_TIERS_MISMATCH_MESSAGE)
|
|
142
|
+
maybe_attempted_tiers = _canonical_tier_list(
|
|
143
|
+
[each_attempt[TIER_KEY] for each_attempt in run.attempts]
|
|
144
|
+
)
|
|
145
|
+
if maybe_attempted_tiers is None:
|
|
146
|
+
raise ModelTierRunError(ATTEMPT_TIER_OUT_OF_SLICE_MESSAGE)
|
|
147
|
+
all_attempted_tiers = maybe_attempted_tiers
|
|
148
|
+
if any(
|
|
149
|
+
each_tier not in all_expected_candidates for each_tier in all_attempted_tiers
|
|
150
|
+
):
|
|
151
|
+
raise ModelTierRunError(ATTEMPT_TIER_OUT_OF_SLICE_MESSAGE)
|
|
152
|
+
if all_attempted_tiers != all_expected_candidates[: len(all_attempted_tiers)]:
|
|
153
|
+
raise ModelTierRunError(ATTEMPT_ORDER_MISMATCH_MESSAGE)
|
|
154
|
+
_validate_selected_tier(
|
|
155
|
+
run=run,
|
|
156
|
+
all_attempted_tiers=all_attempted_tiers,
|
|
157
|
+
all_expected_candidates=all_expected_candidates,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _validate_selected_tier(
|
|
162
|
+
run: ModelTierRun,
|
|
163
|
+
all_attempted_tiers: list[str],
|
|
164
|
+
all_expected_candidates: list[str],
|
|
165
|
+
) -> None:
|
|
166
|
+
all_bound_tiers = [
|
|
167
|
+
each_tier
|
|
168
|
+
for each_tier, each_attempt in zip(
|
|
169
|
+
all_attempted_tiers, run.attempts, strict=True
|
|
170
|
+
)
|
|
171
|
+
if _is_successful_attempt_outcome(
|
|
172
|
+
canonical_tier=each_tier,
|
|
173
|
+
outcome_token=each_attempt[SPAWN_OUTCOME_KEY],
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
if all_bound_tiers:
|
|
177
|
+
maybe_canonical_selected = (
|
|
178
|
+
canonical_tier_name(run.selected_tier)
|
|
179
|
+
if run.selected_tier is not None
|
|
180
|
+
else None
|
|
181
|
+
)
|
|
182
|
+
if maybe_canonical_selected != all_bound_tiers[0]:
|
|
183
|
+
raise ModelTierRunError(SELECTED_TIER_MISMATCH_MESSAGE)
|
|
184
|
+
return
|
|
185
|
+
if run.selected_tier is not None:
|
|
186
|
+
raise ModelTierRunError(SELECTED_TIER_NOT_NULL_MESSAGE)
|
|
187
|
+
if not all_attempted_tiers or all_attempted_tiers != all_expected_candidates:
|
|
188
|
+
raise ModelTierRunError(INCOMPLETE_FALLBACK_WALK_MESSAGE)
|
|
189
|
+
if not run.fallback_reason:
|
|
190
|
+
raise ModelTierRunError(MISSING_FALLBACK_REASON_MESSAGE)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def load_model_tier_run_from_json_path(from_path: Path) -> ModelTierRun:
|
|
194
|
+
"""Load a ModelTierRun from a JSON spawn-walk log file.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
from_path: Path to a JSON object with ModelTierRun fields.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The parsed ModelTierRun.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
OSError: When the file cannot be read.
|
|
204
|
+
json.JSONDecodeError: When the file is not valid JSON.
|
|
205
|
+
KeyError: When a required field is missing.
|
|
206
|
+
TypeError: When a field has the wrong shape.
|
|
207
|
+
"""
|
|
208
|
+
parsed_payload = json.loads(from_path.read_text(encoding="utf-8"))
|
|
209
|
+
return ModelTierRun(
|
|
210
|
+
own_tier=parsed_payload["own_tier"],
|
|
211
|
+
candidate_tiers=list(parsed_payload["candidate_tiers"]),
|
|
212
|
+
attempts=list(parsed_payload["attempts"]),
|
|
213
|
+
selected_tier=parsed_payload.get("selected_tier"),
|
|
214
|
+
fallback_reason=parsed_payload.get("fallback_reason"),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main(all_cli_arguments: list[str]) -> int:
|
|
219
|
+
"""Validate a spawn-walk log JSON file from the command line.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
all_cli_arguments: Argument list without the program name.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
``0`` when the log is valid, ``1`` when an invariant fails, ``2`` when
|
|
226
|
+
the path or JSON was unusable.
|
|
227
|
+
"""
|
|
228
|
+
if len(all_cli_arguments) != 1:
|
|
229
|
+
print(CLI_USAGE_MESSAGE, file=sys.stderr)
|
|
230
|
+
return CLI_MISSING_PATH_EXIT_CODE
|
|
231
|
+
log_path = Path(all_cli_arguments[0])
|
|
232
|
+
try:
|
|
233
|
+
model_tier_run = load_model_tier_run_from_json_path(from_path=log_path)
|
|
234
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as load_error:
|
|
235
|
+
print(str(load_error), file=sys.stderr)
|
|
236
|
+
return CLI_INVALID_JSON_EXIT_CODE
|
|
237
|
+
try:
|
|
238
|
+
validate_model_tier_run(model_tier_run)
|
|
239
|
+
except ModelTierRunError as validation_error:
|
|
240
|
+
print(str(validation_error), file=sys.stderr)
|
|
241
|
+
return CLI_VALIDATION_FAILURE_EXIT_CODE
|
|
242
|
+
return CLI_SUCCESS_EXIT_CODE
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
raise SystemExit(main(sys.argv[1:]))
|