claude-dev-env 8.2.0 → 8.4.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/.agents/agents/poteto-agent.md +6 -0
- package/.agents/agents/test_agent_frontmatter.py +9 -9
- package/.agents/agents/test_pr_description_writer_style.py +1 -1
- package/.agents/agents-archived/ARCHIVE-MANIFEST.md +15 -0
- package/.agents/{agents → agents-archived}/code-quality-agent.md +1 -1
- package/.agents/{agents → agents-archived}/session-advisor.md +1 -1
- package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +2 -2
- package/.agents/skills/grok-spawn/SKILL.md +9 -6
- package/.agents/skills/grok-spawn/reference/flag-profiles.md +12 -8
- package/.agents/skills/issue-tracker/SKILL.md +2 -2
- package/.agents/skills/orchestrator/SKILL.md +16 -15
- package/.agents/skills/orchestrator-refresh/SKILL.md +2 -0
- package/.agents/skills/pull-request/SKILL.md +14 -5
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +13 -8
- package/_shared/advisor/reference/spawn-walk-log.md +2 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +169 -14
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +20 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +99 -0
- package/bin/install.agents-home.test.mjs +54 -94
- package/bin/install.mjs +31 -24
- package/bin/install.test.mjs +35 -0
- package/bin/resolve-package-managed-directory.test.mjs +1 -1
- package/bin/subagent-model-policy.test.mjs +15 -8
- package/docs/references/advisor-tool.md +32 -0
- package/docs/references/team-advisor-skill.md +1 -1
- package/docs/subagent-model-routing.md +6 -5
- package/hooks/blocking/tdd_enforcer.py +2 -3
- package/hooks/blocking/test_bash_post_call_dispatcher.py +7 -24
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -3
- package/hooks/hooks_constants/hosted_hook_runner.py +1 -1
- package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +3 -35
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +2 -10
- package/hooks/routing/subagent_model_routing.mjs +15 -0
- package/hooks/routing/subagent_model_routing.test.mjs +58 -25
- package/hooks/validators/README.md +0 -1
- package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +1 -2
- package/package.json +1 -1
- package/rules/git-workflow.md +3 -3
- package/rules/shell-invocation.md +0 -2
- package/rules/subagent-model-policy.json +5 -13
- package/rules/testing.md +0 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +3 -3
- package/scripts/grok_headless_runner.py +19 -1
- package/scripts/grok_worker_preflight.py +67 -1
- package/scripts/policy_lint/config/approved_test_pairs.py +9 -0
- package/scripts/resolve_advisor_model_route.mjs +2 -2
- package/scripts/subagent_model_policy.mjs +4 -1
- package/scripts/test_grok_headless_runner.py +35 -1
- package/scripts/test_grok_worker_preflight.py +73 -0
- package/scripts/tests/resolve_advisor_model_route.test.mjs +3 -3
- package/scripts/tests/test_policy_lint_rules_pairing.py +16 -0
- package/scripts/tests/test_rules.py +5 -5
- package/scripts/windows/pstack/pstack-command-shim.ps1 +41 -0
- package/scripts/windows/pstack/ship-pr.cmd +3 -0
- package/scripts/windows/pstack/watch-pr.cmd +3 -0
- package/hooks/advisory/msys_path_conversion_advisor.py +0 -200
- package/hooks/advisory/test_msys_path_conversion_advisor.py +0 -223
- package/hooks/hooks_constants/test_failure_recorder_constants.py +0 -34
- package/hooks/observability/test_failure_recorder.py +0 -221
- package/hooks/observability/test_test_failure_recorder.py +0 -173
- package/hooks/validators/VALIDATION_REPORT.md +0 -287
- package/hooks/validators/validator_defaults.py +0 -5
- /package/.agents/{agents → agents-archived}/clean-coder.md +0 -0
- /package/.agents/{agents → agents-archived}/git-commit-crafter.md +0 -0
- /package/.agents/{agents → agents-archived}/issue-tracker.md +0 -0
- /package/.agents/{agents → agents-archived}/plan-packet-validator.md +0 -0
- /package/.agents/{agents → agents-archived}/pr-description-writer.md +0 -0
- /package/.agents/{agents → agents-archived}/skill-writer-agent.md +0 -0
package/bin/install.test.mjs
CHANGED
|
@@ -209,6 +209,20 @@ test('core includeDirectories ships _shared and scripts for advisor protocol and
|
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
test('team-advisor records fallback repair and consult evidence', () => {
|
|
213
|
+
const skillPath = new URL('../.agents/skills/team-advisor/SKILL.md', import.meta.url);
|
|
214
|
+
const skillSource = readFileSync(skillPath, 'utf8');
|
|
215
|
+
assert.match(skillSource, /Mark `fallback_kind: broken` only when the selected bind or reply path fails/);
|
|
216
|
+
assert.match(skillSource, /Record the selected tier, fallback reason, and reply path/);
|
|
217
|
+
assert.match(skillSource, /changed evidence, validation, unresolved risks, and report-back status/);
|
|
218
|
+
assert.match(skillSource, /reference gap.*bind status separate/);
|
|
219
|
+
assert.match(skillSource, /selected_tier: Astra.*reply_path: native/);
|
|
220
|
+
assert.match(skillSource, /model-tier-run\.json/);
|
|
221
|
+
const advisorReferenceDocumentUrl = new URL('../docs/references/advisor-tool.md', import.meta.url);
|
|
222
|
+
assert.match(readFileSync(advisorReferenceDocumentUrl, 'utf8'), /"schema_version": 1/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
|
|
212
226
|
test('CORE_SKILLS ships issue-tracker so the core group installs the skill the SessionStart injector needs', () => {
|
|
213
227
|
assert.ok(
|
|
214
228
|
CORE_SKILLS.includes('issue-tracker'),
|
|
@@ -1168,6 +1182,8 @@ test('FOLDED_HOOK_RELATIVE_PATHS contains folded hooks plus retired entries', ()
|
|
|
1168
1182
|
|
|
1169
1183
|
test('retired hook registrations stay managed so reinstall removes them', () => {
|
|
1170
1184
|
const retiredPaths = new Set([
|
|
1185
|
+
'observability/test_failure_recorder.py',
|
|
1186
|
+
'advisory/msys_path_conversion_advisor.py',
|
|
1171
1187
|
'session/untracked_repo_detector.py',
|
|
1172
1188
|
'session/gh_pr_author_session_cleanup.py',
|
|
1173
1189
|
'observability/pr_description_writer_spawn_tracker.py',
|
|
@@ -2023,6 +2039,25 @@ test('copyTree copies AGENTS.md with agent definitions', () => {
|
|
|
2023
2039
|
});
|
|
2024
2040
|
|
|
2025
2041
|
|
|
2042
|
+
test('copyTree preserves an existing file named in the preserve set', () => {
|
|
2043
|
+
const sourceRoot = mkdtempSync(join(tmpdir(), 'cdev-copy-preserve-source-'));
|
|
2044
|
+
const destinationRoot = mkdtempSync(join(tmpdir(), 'cdev-copy-preserve-destination-'));
|
|
2045
|
+
try {
|
|
2046
|
+
const sourceFilePath = join(sourceRoot, 'poteto-agent.md');
|
|
2047
|
+
const destinationFilePath = join(destinationRoot, 'poteto-agent.md');
|
|
2048
|
+
writeFileSync(sourceFilePath, 'package wrapper\n');
|
|
2049
|
+
writeFileSync(destinationFilePath, 'user wrapper\n');
|
|
2050
|
+
|
|
2051
|
+
copyTree(sourceRoot, destinationRoot, { preserveExistingFileNames: ['poteto-agent.md'] });
|
|
2052
|
+
|
|
2053
|
+
assert.equal(readFileSync(destinationFilePath, 'utf8'), 'user wrapper\n');
|
|
2054
|
+
} finally {
|
|
2055
|
+
rmSync(sourceRoot, { recursive: true, force: true });
|
|
2056
|
+
rmSync(destinationRoot, { recursive: true, force: true });
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
|
|
2026
2061
|
const SHIPPED_README_NAME = 'README.md';
|
|
2027
2062
|
const INSTALLED_README_NAME = 'Readme.md';
|
|
2028
2063
|
const RETIRED_HOOK_RELATIVE_PATH = 'blocking/retired_gate.py';
|
|
@@ -26,7 +26,7 @@ import { resolvePackageManagedDirectory } from './resolve-package-managed-direct
|
|
|
26
26
|
|
|
27
27
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
28
28
|
const CANONICAL_SKILL_NAME = 'privacy-hygiene';
|
|
29
|
-
const CANONICAL_AGENT_FILE_NAME = '
|
|
29
|
+
const CANONICAL_AGENT_FILE_NAME = 'AGENTS.md';
|
|
30
30
|
|
|
31
31
|
test('prefers the .agents tree when both layouts exist', () => {
|
|
32
32
|
const sourceRoot = mkdtempSync(join(tmpdir(), 'cdev-src-agents-'));
|
|
@@ -25,7 +25,7 @@ function advisorRoute(request, options = {}) {
|
|
|
25
25
|
return route(request, {
|
|
26
26
|
trustedSessionMetadata: {
|
|
27
27
|
authorized: true,
|
|
28
|
-
registeredAgentType: '
|
|
28
|
+
registeredAgentType: 'team-advisor',
|
|
29
29
|
},
|
|
30
30
|
...options,
|
|
31
31
|
});
|
|
@@ -55,6 +55,13 @@ test('the shipped policy parses and names native model ids', () => {
|
|
|
55
55
|
assert.deepEqual(policy.advisorDefault, { model: 'astra', effort: 'medium' });
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
test('poteto plugin aliases use worker routing without advisor privileges', () => {
|
|
59
|
+
for (const agent_type of ['poteto-agent', 'pstack:poteto-agent']) {
|
|
60
|
+
const result = route({ agent_type, model: 'Astra', reasoning_effort: 'high' });
|
|
61
|
+
assert.deepEqual(result.selected, { model: 'gpt-6-astra', effort: 'low' });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
58
65
|
test('every approved pair returns its literal native pair', () => {
|
|
59
66
|
const expected = [
|
|
60
67
|
[['Luna', 'high'], ['gpt-5.6-luna', 'high']],
|
|
@@ -74,7 +81,7 @@ test('every approved pair returns its literal native pair', () => {
|
|
|
74
81
|
const advisor = advisorRoute({
|
|
75
82
|
model: 'Astra',
|
|
76
83
|
reasoning_effort: 'high',
|
|
77
|
-
agent_type: '
|
|
84
|
+
agent_type: 'team-advisor',
|
|
78
85
|
});
|
|
79
86
|
assert.equal(advisor.status, 'remapped');
|
|
80
87
|
assert.deepEqual(advisor.selected, { model: 'gpt-6-astra', effort: 'high' });
|
|
@@ -105,7 +112,7 @@ test('every automatic replacement returns its literal destination', () => {
|
|
|
105
112
|
|
|
106
113
|
test('advisor and worker Astra restrictions use role-specific replacements', () => {
|
|
107
114
|
assert.deepEqual(
|
|
108
|
-
advisorRoute({ model: 'Astra', reasoning_effort: 'xhigh', agent_type: '
|
|
115
|
+
advisorRoute({ model: 'Astra', reasoning_effort: 'xhigh', agent_type: 'team-advisor' }).selected,
|
|
109
116
|
{ model: 'gpt-6-astra', effort: 'medium' },
|
|
110
117
|
);
|
|
111
118
|
assert.deepEqual(
|
|
@@ -113,7 +120,7 @@ test('advisor and worker Astra restrictions use role-specific replacements', ()
|
|
|
113
120
|
{ model: 'gpt-6-astra', effort: 'low' },
|
|
114
121
|
);
|
|
115
122
|
assert.deepEqual(
|
|
116
|
-
advisorRoute({ model: 'Astra', reasoning_effort: 'high', agent_type: '
|
|
123
|
+
advisorRoute({ model: 'Astra', reasoning_effort: 'high', agent_type: 'team-advisor' }).selected,
|
|
117
124
|
{ model: 'gpt-6-astra', effort: 'high' },
|
|
118
125
|
);
|
|
119
126
|
});
|
|
@@ -122,7 +129,7 @@ test('registered advisor roles satisfy advisory authorization', () => {
|
|
|
122
129
|
const result = advisorRoute({
|
|
123
130
|
model: 'Astra',
|
|
124
131
|
reasoning_effort: 'high',
|
|
125
|
-
agent_type: '
|
|
132
|
+
agent_type: 'team-advisor',
|
|
126
133
|
});
|
|
127
134
|
|
|
128
135
|
assert.deepEqual(result.selected, { model: 'gpt-6-astra', effort: 'high' });
|
|
@@ -135,7 +142,7 @@ test('a policy from the first stack revision remains routable', () => {
|
|
|
135
142
|
{
|
|
136
143
|
model: 'Astra',
|
|
137
144
|
reasoning_effort: 'high',
|
|
138
|
-
agent_type: '
|
|
145
|
+
agent_type: 'team-advisor',
|
|
139
146
|
},
|
|
140
147
|
{ policy: legacyPolicy },
|
|
141
148
|
);
|
|
@@ -146,7 +153,7 @@ test('an advisor role claim cannot grant advisory authorization', () => {
|
|
|
146
153
|
const result = route({
|
|
147
154
|
model: 'Astra',
|
|
148
155
|
reasoning_effort: 'high',
|
|
149
|
-
agent_type: '
|
|
156
|
+
agent_type: 'team-advisor',
|
|
150
157
|
});
|
|
151
158
|
assert.equal(result.status, 'blocked');
|
|
152
159
|
assert.equal(result.diagnostic, 'advisor role is not trusted');
|
|
@@ -158,7 +165,7 @@ test('Astra Light resolves to Astra Low', () => {
|
|
|
158
165
|
});
|
|
159
166
|
|
|
160
167
|
test('missing advisor settings use the policy default', () => {
|
|
161
|
-
const result = advisorRoute({ agent_type: '
|
|
168
|
+
const result = advisorRoute({ agent_type: 'team-advisor' });
|
|
162
169
|
assert.deepEqual(result.selected, { model: 'gpt-6-astra', effort: 'medium' });
|
|
163
170
|
});
|
|
164
171
|
|
|
@@ -38,6 +38,38 @@ If your data points one way and the advisor points another: do not silently swit
|
|
|
38
38
|
|
|
39
39
|
Work a disagreement in this order: keep the observed evidence in the record, name the conflict plainly, ask the advisor which constraint breaks the tie, then act on the reconciled plan.
|
|
40
40
|
|
|
41
|
+
## Recovery record
|
|
42
|
+
|
|
43
|
+
Keep one versioned `evidence` object in the session-controlled `model-tier-run.json` record. A missing optional reference belongs in `reference.status`; it keeps the bind result independent. `fallback_kind: "broken"` identifies a bind or reply failure. On a Codex host, a successful native route records `selected_tier: "Astra"` and `reply_path: "native"`.
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"evidence": {
|
|
48
|
+
"schema_version": 1,
|
|
49
|
+
"reference": {
|
|
50
|
+
"path": "~/.claude/docs/references/advisor-tool.md",
|
|
51
|
+
"status": "missing",
|
|
52
|
+
"repair_action": "use the projected docs root",
|
|
53
|
+
"repair_result": "read"
|
|
54
|
+
},
|
|
55
|
+
"fallback": {
|
|
56
|
+
"selected_tier": "Astra",
|
|
57
|
+
"fallback_kind": null,
|
|
58
|
+
"fallback_reason": null,
|
|
59
|
+
"reply_path": "native"
|
|
60
|
+
},
|
|
61
|
+
"consult": {
|
|
62
|
+
"changed_evidence": ["native bind returned a reply"],
|
|
63
|
+
"validation": ["signal and session id read back"],
|
|
64
|
+
"unresolved_risks": ["optional reference projection"],
|
|
65
|
+
"report_back_status": "recorded"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The repair fields preserve the reference-path result. The fallback fields preserve route selection and bind or reply failures. The consult fields preserve first, recovery, and completion readback.
|
|
72
|
+
|
|
41
73
|
## Escalation shapes
|
|
42
74
|
|
|
43
75
|
Four shapes cover how a harder task gets more strength behind it. Route to the one that matches the work, not by default to the advisor.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| Doc | Holds |
|
|
8
8
|
|---|---|
|
|
9
|
-
|
|
|
9
|
+
| `.agents/skills/team-advisor/SKILL.md` | Sole-consumer bind wiring and constraints |
|
|
10
10
|
| `advisor-tool.md` | Consult timing, hard rule, how to treat advice |
|
|
11
11
|
| `~/.claude/_shared/advisor/advisor-protocol.md` | Session identity, host bind, floor, lifecycle |
|
|
12
12
|
| `agents/session-advisor.md` | ENDORSE / CORRECTION / PLAN / STOP |
|
|
@@ -53,15 +53,16 @@ The matching replacement is:
|
|
|
53
53
|
}
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Unknown values, invalid policy data,
|
|
57
|
-
spawn with a short diagnostic.
|
|
56
|
+
Unknown values, invalid policy data, unavailable replacement models, and
|
|
57
|
+
unresolved parent inheritance stop the spawn with a short diagnostic.
|
|
58
58
|
|
|
59
59
|
The direct spawn hook and advisor bridge use the same shared resolver. A Sol
|
|
60
60
|
Medium remap changes only the model and effort fields in the hook's tool input.
|
|
61
61
|
The hook returns an allow decision with the updated input and emits no remap
|
|
62
|
-
notice
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
notice when the selected model matches `gpt-*-luna`. Any route that resolves to
|
|
63
|
+
another model is denied unless the tool input carries the explicit
|
|
64
|
+
`flags: ["--advisor"]` bypass. Parent inheritance is denied because the hook
|
|
65
|
+
cannot verify the inherited model before the spawn.
|
|
65
66
|
|
|
66
67
|
The hook blocks advisor role claims because the current Codex hook input has no
|
|
67
68
|
host-provided role binding. The Python advisor bridge passes trusted session
|
|
@@ -4,9 +4,8 @@ Blocks a write to a production source file when no matching test was modified
|
|
|
4
4
|
within the freshness window, enforcing "TDD IS NON-NEGOTIABLE" from CLAUDE.md.
|
|
5
5
|
The gate records each candidate test's content hash at every sighting and
|
|
6
6
|
compares that hash on the next write. A first sighting requires content that
|
|
7
|
-
differs from HEAD. A
|
|
8
|
-
|
|
9
|
-
and applies while the candidate's content still matches what failed.
|
|
7
|
+
differs from HEAD. A stored failing pytest run is read before that fallback
|
|
8
|
+
and applies while the candidate's content still matches the stored hash.
|
|
10
9
|
Each concern lives in a ``tdd_enforcer_parts`` submodule; this entry wires them
|
|
11
10
|
into one PreToolUse gate and re-exports their surface for the test suite.
|
|
12
11
|
"""
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
One interpreter start runs every hosted PostToolUse hook a Bash call fires.
|
|
4
4
|
The unit tests pin selection and call order with a fake
|
|
5
|
-
runner; the end-to-end test
|
|
6
|
-
payload and confirms the recorder's own real side effect lands.
|
|
5
|
+
runner; the end-to-end test checks that an ordinary Bash call stays silent.
|
|
7
6
|
``hooks/conftest.py`` already puts the hooks and blocking directories on
|
|
8
7
|
sys.path for test collection, so this file needs no bootstrap of its own.
|
|
9
8
|
"""
|
|
@@ -23,12 +22,9 @@ from bash_post_call_dispatcher import (
|
|
|
23
22
|
)
|
|
24
23
|
from hooks_constants.bash_pre_tool_use_dispatcher_constants import BASH_TOOL_NAME
|
|
25
24
|
from hooks_constants.hosted_hook_runner import HostedHookRun
|
|
26
|
-
from tdd_enforcer_parts import content_hash_store
|
|
27
25
|
|
|
28
26
|
_ALL_EXPECTED_ROSTER_PATHS = [
|
|
29
|
-
"observability/test_failure_recorder.py",
|
|
30
27
|
"advisory/pr_done_reminder.py",
|
|
31
|
-
"advisory/msys_path_conversion_advisor.py",
|
|
32
28
|
]
|
|
33
29
|
|
|
34
30
|
|
|
@@ -63,11 +59,9 @@ def test_dispatch_forwards_hosted_hook_additional_context_as_one_payload(
|
|
|
63
59
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
64
60
|
) -> None:
|
|
65
61
|
context_by_hook = {
|
|
66
|
-
"test_failure_recorder.py": "",
|
|
67
62
|
"pr_done_reminder.py": json.dumps(
|
|
68
63
|
{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": "PR #42"}}
|
|
69
64
|
),
|
|
70
|
-
"msys_path_conversion_advisor.py": "",
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
|
|
@@ -124,29 +118,18 @@ def test_main_exits_zero_and_writes_nothing_to_stdout(
|
|
|
124
118
|
assert capsys.readouterr().out == ""
|
|
125
119
|
|
|
126
120
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
test_file.write_text("def test_fulfill(): assert False\n")
|
|
131
|
-
session_id = "post-dispatcher-session"
|
|
132
|
-
failing_output = (
|
|
133
|
-
"Error: Exit code 1\n"
|
|
134
|
-
"FAILED test_orders.py::test_fulfill - AssertionError\n"
|
|
135
|
-
"1 failed in 0.02s"
|
|
136
|
-
)
|
|
121
|
+
def test_end_to_end_hosted_hooks_leave_an_ordinary_call_silent(
|
|
122
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
123
|
+
) -> None:
|
|
137
124
|
payload = json.dumps(
|
|
138
125
|
{
|
|
139
|
-
"session_id": session_id,
|
|
140
126
|
"cwd": str(tmp_path),
|
|
141
127
|
"tool_name": "Bash",
|
|
142
|
-
"tool_input": {"command": "
|
|
143
|
-
"tool_response":
|
|
128
|
+
"tool_input": {"command": "git status"},
|
|
129
|
+
"tool_response": {"stdout": "clean", "stderr": ""},
|
|
144
130
|
}
|
|
145
131
|
)
|
|
146
132
|
|
|
147
133
|
dispatch(payload, BASH_TOOL_NAME)
|
|
148
134
|
|
|
149
|
-
|
|
150
|
-
state = json.loads(state_file.read_text())
|
|
151
|
-
entry = state[content_hash_store._state_key_for(test_file)]
|
|
152
|
-
assert entry[content_hash_store.STORED_FAILURE_EXIT_STATUS_KEY] == 1
|
|
135
|
+
assert capsys.readouterr().out == ""
|
|
@@ -15,7 +15,6 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
|
|
17
17
|
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
18
|
-
ALL_BASH_ONLY_TOOL_NAMES,
|
|
19
18
|
BashHostedHookEntry,
|
|
20
19
|
)
|
|
21
20
|
|
|
@@ -30,9 +29,7 @@ __all__ = [
|
|
|
30
29
|
]
|
|
31
30
|
|
|
32
31
|
ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
|
|
33
|
-
BashHostedHookEntry("observability/test_failure_recorder.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
34
32
|
BashHostedHookEntry("advisory/pr_done_reminder.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
|
|
35
|
-
BashHostedHookEntry("advisory/msys_path_conversion_advisor.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
36
33
|
)
|
|
37
34
|
|
|
38
35
|
EXIT_CODE_ERROR_PREFIX: str = "Error: Exit code "
|
|
@@ -89,7 +89,7 @@ def resolved_hook_script_path(relative_path: str) -> str:
|
|
|
89
89
|
|
|
90
90
|
::
|
|
91
91
|
|
|
92
|
-
"
|
|
92
|
+
"advisory/pr_done_reminder.py" -> <hooks>/advisory/pr_done_reminder.py
|
|
93
93
|
|
|
94
94
|
The root comes from this module's own location, so every dispatcher
|
|
95
95
|
resolves against the same directory however deep it sits.
|
|
@@ -1,48 +1,16 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
The advisory is a PostToolUse observer on Bash. It never blocks. When a git
|
|
4
|
-
call fails because Git Bash rewrote a ``<rev>:<path>`` argument before git saw
|
|
5
|
-
it, the advisory adds one loud note naming the two exports that turn the
|
|
6
|
-
rewriting off.
|
|
7
|
-
"""
|
|
1
|
+
"""Shared environment variable names for the MSYS path-conversion rewriter."""
|
|
8
2
|
|
|
9
3
|
from __future__ import annotations
|
|
10
4
|
|
|
11
5
|
__all__ = [
|
|
12
|
-
"ALL_MANGLED_ARGUMENT_MARKERS",
|
|
13
|
-
"MANGLED_ARGUMENT_CLOSING_QUOTE",
|
|
14
|
-
"ALL_MSYS_MANGLING_CHARACTERS",
|
|
15
6
|
"ALL_MSYS_PATH_CONVERSION_VARIABLE_NAMES",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"MANGLING_EXPLANATION_LINE",
|
|
19
|
-
"FIX_LINE",
|
|
20
|
-
"SAME_COMMAND_LINE",
|
|
21
|
-
"ADVISORY_LINE_SEPARATOR",
|
|
7
|
+
"MSYS_PATH_CONVERSION_VARIABLE_NAME",
|
|
8
|
+
"MSYS_ARGUMENT_CONVERSION_EXCLUSION_VARIABLE_NAME",
|
|
22
9
|
]
|
|
23
10
|
|
|
24
|
-
AMBIGUOUS_ARGUMENT_MARKER: str = "fatal: ambiguous argument '"
|
|
25
|
-
INVALID_OBJECT_NAME_MARKER: str = "fatal: invalid object name '"
|
|
26
|
-
ALL_MANGLED_ARGUMENT_MARKERS: tuple[str, ...] = (
|
|
27
|
-
AMBIGUOUS_ARGUMENT_MARKER,
|
|
28
|
-
INVALID_OBJECT_NAME_MARKER,
|
|
29
|
-
)
|
|
30
|
-
MANGLED_ARGUMENT_CLOSING_QUOTE: str = "'"
|
|
31
|
-
ALL_MSYS_MANGLING_CHARACTERS: frozenset[str] = frozenset({"\\", ";"})
|
|
32
11
|
MSYS_PATH_CONVERSION_VARIABLE_NAME: str = "MSYS_NO_PATHCONV"
|
|
33
12
|
MSYS_ARGUMENT_CONVERSION_EXCLUSION_VARIABLE_NAME: str = "MSYS2_ARG_CONV_EXCL"
|
|
34
13
|
ALL_MSYS_PATH_CONVERSION_VARIABLE_NAMES: tuple[str, ...] = (
|
|
35
14
|
MSYS_PATH_CONVERSION_VARIABLE_NAME,
|
|
36
15
|
MSYS_ARGUMENT_CONVERSION_EXCLUSION_VARIABLE_NAME,
|
|
37
16
|
)
|
|
38
|
-
MSYS_EXPORT_FIX_LINE: str = "export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'"
|
|
39
|
-
|
|
40
|
-
ADVISORY_HEADER: str = "=== MSYS PATH CONVERSION (context reminder, never a block) ==="
|
|
41
|
-
MANGLED_ARGUMENT_LINE_TEMPLATE: str = "Git saw: {argument}"
|
|
42
|
-
MANGLING_EXPLANATION_LINE: str = (
|
|
43
|
-
"Git Bash rewrote a <rev>:<path> argument before git saw it, turning the "
|
|
44
|
-
"colon into a semicolon and the slashes into backslashes."
|
|
45
|
-
)
|
|
46
|
-
FIX_LINE: str = "Fix: " + MSYS_EXPORT_FIX_LINE
|
|
47
|
-
SAME_COMMAND_LINE: str = "Put that export and the git command in the same command."
|
|
48
|
-
ADVISORY_LINE_SEPARATOR: str = "\n"
|
|
@@ -7,29 +7,21 @@ from hooks_constants.bash_post_call_dispatcher_constants import (
|
|
|
7
7
|
from hooks_constants.bash_pre_tool_use_dispatcher_constants import BASH_TOOL_NAME
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def test_roster_runs_the_pr_reminder() -> None:
|
|
11
11
|
all_script_paths = [
|
|
12
12
|
each_entry.script_relative_path for each_entry in ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES
|
|
13
13
|
]
|
|
14
14
|
assert all_script_paths == [
|
|
15
|
-
"observability/test_failure_recorder.py",
|
|
16
15
|
"advisory/pr_done_reminder.py",
|
|
17
|
-
"advisory/msys_path_conversion_advisor.py",
|
|
18
16
|
]
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
def test_pr_done_reminder_also_serves_the_powershell_tool() -> None:
|
|
22
|
-
reminder_entry = ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES[
|
|
20
|
+
reminder_entry = ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES[0]
|
|
23
21
|
assert reminder_entry.script_relative_path == "advisory/pr_done_reminder.py"
|
|
24
22
|
assert "PowerShell" in reminder_entry.applicable_tool_names
|
|
25
23
|
|
|
26
24
|
|
|
27
|
-
def test_msys_advisor_serves_the_bash_tool_only() -> None:
|
|
28
|
-
advisor_entry = ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES[-1]
|
|
29
|
-
assert advisor_entry.script_relative_path == "advisory/msys_path_conversion_advisor.py"
|
|
30
|
-
assert advisor_entry.applicable_tool_names == frozenset({"Bash"})
|
|
31
|
-
|
|
32
|
-
|
|
33
25
|
def test_context_forwarding_keys_match_the_hook_output_contract() -> None:
|
|
34
26
|
assert constants.POST_TOOL_USE_HOOK_EVENT_NAME == "PostToolUse"
|
|
35
27
|
assert constants.HOOK_SPECIFIC_OUTPUT_KEY == "hookSpecificOutput"
|
|
@@ -16,6 +16,13 @@ export function buildSubagentModelRoutingResponse(hookPayload, routingOptions =
|
|
|
16
16
|
if (routingDecision.status === 'blocked') {
|
|
17
17
|
return buildBlockedResponse(routingDecision.diagnostic);
|
|
18
18
|
}
|
|
19
|
+
if (routingDecision.status === 'inherited') {
|
|
20
|
+
return buildBlockedResponse('subagent model must resolve to gpt-*-luna');
|
|
21
|
+
}
|
|
22
|
+
if (!hasAdvisorBypass(hookPayload.tool_input)
|
|
23
|
+
&& !isLunaModel(routingDecision.selected?.model)) {
|
|
24
|
+
return buildBlockedResponse('only gpt-*-luna subagents are permitted');
|
|
25
|
+
}
|
|
19
26
|
if (routingDecision.status !== 'remapped') return {};
|
|
20
27
|
|
|
21
28
|
return buildAllowResponse(routingDecision.updatedInput);
|
|
@@ -52,6 +59,14 @@ function isTargetSubagentTool(hookPayload) {
|
|
|
52
59
|
return hookPayload.tool_name === targetToolName;
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
function isLunaModel(model) {
|
|
63
|
+
return typeof model === 'string' && /^gpt-.+-luna$/i.test(model);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasAdvisorBypass(toolInput) {
|
|
67
|
+
return Array.isArray(toolInput?.flags) && toolInput.flags.includes('--advisor');
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
function buildAllowResponse(updatedInput) {
|
|
56
71
|
return {
|
|
57
72
|
hookSpecificOutput: {
|
|
@@ -122,32 +122,65 @@ test('blocks malformed policy, unknown input, unavailable replacement, and untru
|
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
trustedSessionMetadata: {
|
|
138
|
-
authorized: true,
|
|
139
|
-
registeredAgentType: 'session-advisor',
|
|
140
|
-
},
|
|
125
|
+
test('blocks a trusted session advisor route that resolves to Astra', () => {
|
|
126
|
+
const hookResponse = buildSubagentModelRoutingResponse(
|
|
127
|
+
buildHookPayload({
|
|
128
|
+
agent_type: 'team-advisor',
|
|
129
|
+
model: 'Astra',
|
|
130
|
+
reasoning_effort: 'high',
|
|
131
|
+
}),
|
|
132
|
+
{
|
|
133
|
+
trustedSessionMetadata: {
|
|
134
|
+
authorized: true,
|
|
135
|
+
registeredAgentType: 'team-advisor',
|
|
141
136
|
},
|
|
142
|
-
|
|
137
|
+
},
|
|
138
|
+
);
|
|
143
139
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
assert.equal(hookResponse.hookSpecificOutput.permissionDecision, 'deny');
|
|
141
|
+
assert.equal(
|
|
142
|
+
hookResponse.hookSpecificOutput.permissionDecisionReason,
|
|
143
|
+
'only gpt-*-luna subagents are permitted',
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('allows an Astra route when the explicit advisor flag is present', () => {
|
|
148
|
+
const toolInput = {
|
|
149
|
+
model: 'Astra',
|
|
150
|
+
reasoning_effort: 'high',
|
|
151
|
+
flags: ['--advisor'],
|
|
152
|
+
};
|
|
153
|
+
const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload(toolInput));
|
|
154
|
+
assert.deepEqual(hookResponse.hookSpecificOutput.updatedInput, {
|
|
155
|
+
model: 'gpt-6-astra',
|
|
156
|
+
reasoning_effort: 'low',
|
|
157
|
+
flags: ['--advisor'],
|
|
149
158
|
});
|
|
150
|
-
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('does not treat unrelated flags as an advisor bypass', () => {
|
|
162
|
+
const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
|
|
163
|
+
model: 'Astra',
|
|
164
|
+
reasoning_effort: 'high',
|
|
165
|
+
flags: ['--worker'],
|
|
166
|
+
}));
|
|
167
|
+
assert.equal(hookResponse.hookSpecificOutput.permissionDecision, 'deny');
|
|
168
|
+
assert.equal(
|
|
169
|
+
hookResponse.hookSpecificOutput.permissionDecisionReason,
|
|
170
|
+
'only gpt-*-luna subagents are permitted',
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('blocks parent inheritance because the selected model is unresolved', () => {
|
|
175
|
+
const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
|
|
176
|
+
model: 'inherit-parent',
|
|
177
|
+
}));
|
|
178
|
+
assert.equal(hookResponse.hookSpecificOutput.permissionDecision, 'deny');
|
|
179
|
+
assert.equal(
|
|
180
|
+
hookResponse.hookSpecificOutput.permissionDecisionReason,
|
|
181
|
+
'subagent model must resolve to gpt-*-luna',
|
|
182
|
+
);
|
|
183
|
+
});
|
|
151
184
|
|
|
152
185
|
test('the standalone hook silently remaps native Sol Medium and preserves other fields', () => {
|
|
153
186
|
const hookExecution = spawnSync(
|
|
@@ -183,7 +216,7 @@ test('the standalone hook silently remaps native Sol Medium and preserves other
|
|
|
183
216
|
|
|
184
217
|
test('a session advisor name without trusted session metadata is blocked', () => {
|
|
185
218
|
const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
|
|
186
|
-
agent_type: '
|
|
219
|
+
agent_type: 'team-advisor',
|
|
187
220
|
model: 'Astra',
|
|
188
221
|
reasoning_effort: 'high',
|
|
189
222
|
}));
|
|
@@ -222,7 +255,7 @@ test('the standalone hook command blocks an advisor without host metadata', () =
|
|
|
222
255
|
{
|
|
223
256
|
encoding: 'utf8',
|
|
224
257
|
input: JSON.stringify(buildHookPayload({
|
|
225
|
-
agent_type: '
|
|
258
|
+
agent_type: 'team-advisor',
|
|
226
259
|
model: 'Astra',
|
|
227
260
|
reasoning_effort: 'high',
|
|
228
261
|
})),
|
|
@@ -37,7 +37,6 @@ The checks live in `python_style_checks.py`. Shared source-line splitting, funct
|
|
|
37
37
|
| `type_safety_checks.py` | Missing type hints and Any usage checks |
|
|
38
38
|
| `useless_test_checks.py` | Useless-test detection |
|
|
39
39
|
| `validator_base.py` | Shared validator dataclasses, source read, and parse |
|
|
40
|
-
| `validator_defaults.py` | Shared constants for the validators package |
|
|
41
40
|
|
|
42
41
|
## Checks Implemented
|
|
43
42
|
|
|
@@ -23,7 +23,7 @@ class RegularComponent extends Component { } // Line 4 - CAUGHT ✓
|
|
|
23
23
|
class OptimizedComponent extends PureComponent { } // Line 9 - MISSED ✗
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
**Impact:** HIGH - PureComponent is commonly used for performance optimization. Developers
|
|
26
|
+
**Impact:** HIGH - PureComponent is commonly used for performance optimization. Developers may bypass the check by selecting PureComponent where Component is expected.
|
|
27
27
|
|
|
28
28
|
**Fix:** Add to regex pattern:
|
|
29
29
|
```python
|
|
@@ -149,7 +149,6 @@ Consider for future iteration if multi-class files become common.
|
|
|
149
149
|
|
|
150
150
|
All test files in `test_files/` directory:
|
|
151
151
|
- `01-16_*.tsx` - Individual test cases
|
|
152
|
-
- `VALIDATION_REPORT.md` - Detailed test results
|
|
153
152
|
- `EXECUTIVE_SUMMARY.md` - This file
|
|
154
153
|
|
|
155
154
|
Run all tests:
|
package/package.json
CHANGED
package/rules/git-workflow.md
CHANGED
|
@@ -25,12 +25,12 @@ the next run. The merge stays in the repository. No tag is cut and the package n
|
|
|
25
25
|
|
|
26
26
|
Spot one by its head branch, which starts `release-please--branches--`, or by a body that
|
|
27
27
|
opens with the bot's own marker line. The description rules in this file, the
|
|
28
|
-
`
|
|
28
|
+
`pstack:poteto-agent` writing brief, and the house wording style all step aside for it. The
|
|
29
29
|
failure signature in the release job log reads
|
|
30
30
|
`could not parse pull request body as a release PR`.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
`pstack:poteto-agent` writes a title and body from the diff when you want one.
|
|
33
|
+
Publish the title and body file through
|
|
34
34
|
`~/.agents/skills/pull-request/scripts/pull_request.py`. That path is under the
|
|
35
35
|
agents home, not the repository. A worktree holds no `.agents/` copy.
|
|
36
36
|
|
|
@@ -33,5 +33,3 @@ export MSYS2_ARG_CONV_EXCL='origin/main:'; git show origin/main:.claude/settings
|
|
|
33
33
|
`MSYS2_ARG_CONV_EXCL` takes a semicolon-separated list of argument prefixes. Naming one prefix per detected token leaves every other argument in the command converting as before. The blanket pair `MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'` turns conversion off for the whole command, which changes a path the command meant to convert, so the rewriter does not emit it.
|
|
34
34
|
|
|
35
35
|
A quoted token is not a revision. `git commit -m "fix a/b:.py"` carries a slash and a dot in one token, and a rewrite there would break the message, so the detection skips a token holding whitespace or a quote.
|
|
36
|
-
|
|
37
|
-
`advisory/msys_path_conversion_advisor.py` (PostToolUse on Bash, hosted by `bash_post_call_dispatcher`) stays behind it. It reads a failed git call, looks for the mark MSYS leaves, and names the fix. It covers a shape the rewriter misses.
|
|
@@ -29,25 +29,17 @@
|
|
|
29
29
|
"default": "worker",
|
|
30
30
|
"worker": [
|
|
31
31
|
"worker",
|
|
32
|
+
"poteto-agent",
|
|
33
|
+
"pstack:poteto-agent",
|
|
32
34
|
"default",
|
|
33
35
|
"explorer",
|
|
34
36
|
"caveman",
|
|
35
|
-
"clean-coder",
|
|
36
|
-
"code-quality-agent",
|
|
37
37
|
"code-verifier",
|
|
38
38
|
"deep-research",
|
|
39
|
-
"docs-agent"
|
|
40
|
-
"git-commit-crafter",
|
|
41
|
-
"issue-tracker",
|
|
42
|
-
"plan-packet-validator",
|
|
43
|
-
"pr-description-writer",
|
|
44
|
-
"skill-writer-agent",
|
|
45
|
-
"theme-prompt-agent",
|
|
46
|
-
"theme-prompt-generator",
|
|
47
|
-
"translator"
|
|
39
|
+
"docs-agent"
|
|
48
40
|
],
|
|
49
|
-
"advisor": ["advisor", "
|
|
50
|
-
"trustedAdvisor": ["
|
|
41
|
+
"advisor": ["advisor", "team-advisor", "code-advisor"],
|
|
42
|
+
"trustedAdvisor": ["advisor", "team-advisor", "code-advisor"]
|
|
51
43
|
},
|
|
52
44
|
"approvedPairs": [
|
|
53
45
|
{"model": "luna", "effort": "high"},
|