devrites 3.2.2 → 3.2.4
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/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/docs/architecture.md +6 -2
- package/docs/command-map.md +3 -3
- package/docs/engine/agent-contract.md +3 -2
- package/docs/harness-compliance.md +2 -2
- package/docs/orchestration.md +15 -12
- package/docs/porting-to-a-new-harness.md +2 -1
- package/docs/skills.md +7 -2
- package/engine/hooks.go +39 -4
- package/engine/hooks_agent_dispatch.go +1193 -0
- package/engine/hooks_workspace.go +78 -10
- package/engine/internal/forge/forge.go +0 -2
- package/engine/internal/harness/compliance.go +1 -1
- package/engine/internal/harness/harness.go +3 -0
- package/engine/internal/lib/doubtcoverage.go +2 -12
- package/engine/internal/lib/reconcile.go +4 -29
- package/engine/internal/lib/reconcile_test.go +14 -10
- package/engine/main.go +4 -1
- package/engine/tests/hook_test.go +877 -1
- package/engine/tests/lib_parity_test.go +5 -17
- package/engine/tests/parity_reconcile_test.go +1 -1
- package/engine/tests/parity_test.go +2 -6
- package/pack/.claude/skills/devrites-api-interface/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-audit/SKILL.md +5 -6
- package/pack/.claude/skills/devrites-browser-proof/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-doubt/SKILL.md +2 -1
- package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-interview/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-lib/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-lib/reference/model-tiers.md +4 -3
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +4 -5
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +15 -8
- package/pack/.claude/skills/devrites-prose-craft/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-refresh-indexes/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-source-driven/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-ux-shape/SKILL.md +1 -0
- package/pack/.claude/skills/rite/SKILL.md +1 -0
- package/pack/.claude/skills/rite-adopt/SKILL.md +1 -0
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +1 -0
- package/pack/.claude/skills/rite-build/SKILL.md +2 -1
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +3 -4
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +11 -12
- package/pack/.claude/skills/rite-clarify/SKILL.md +1 -0
- package/pack/.claude/skills/rite-converge/SKILL.md +1 -0
- package/pack/.claude/skills/rite-customize/SKILL.md +1 -0
- package/pack/.claude/skills/rite-define/SKILL.md +1 -0
- package/pack/.claude/skills/rite-doctor/SKILL.md +1 -0
- package/pack/.claude/skills/rite-dogfood/SKILL.md +1 -0
- package/pack/.claude/skills/rite-explain/SKILL.md +4 -2
- package/pack/.claude/skills/rite-frame/SKILL.md +1 -0
- package/pack/.claude/skills/rite-handoff/SKILL.md +1 -0
- package/pack/.claude/skills/rite-learn/SKILL.md +1 -0
- package/pack/.claude/skills/rite-plan/SKILL.md +1 -0
- package/pack/.claude/skills/rite-polish/SKILL.md +1 -0
- package/pack/.claude/skills/rite-pov/SKILL.md +1 -0
- package/pack/.claude/skills/rite-pr-feedback/SKILL.md +1 -0
- package/pack/.claude/skills/rite-pressure-test/SKILL.md +1 -0
- package/pack/.claude/skills/rite-prototype/SKILL.md +1 -0
- package/pack/.claude/skills/rite-prove/SKILL.md +1 -0
- package/pack/.claude/skills/rite-quick/SKILL.md +1 -0
- package/pack/.claude/skills/rite-resolve/SKILL.md +1 -0
- package/pack/.claude/skills/rite-review/SKILL.md +1 -0
- package/pack/.claude/skills/rite-seal/SKILL.md +1 -0
- package/pack/.claude/skills/rite-seal/reference/phase-contract.md +2 -3
- package/pack/.claude/skills/rite-seal/reference/risk-and-rollback.md +10 -12
- package/pack/.claude/skills/rite-ship/SKILL.md +1 -0
- package/pack/.claude/skills/rite-spec/SKILL.md +1 -0
- package/pack/.claude/skills/rite-status/SKILL.md +1 -0
- package/pack/.claude/skills/rite-temper/SKILL.md +3 -2
- package/pack/.claude/skills/rite-upgrade/SKILL.md +3 -1
- package/pack/.claude/skills/rite-vet/SKILL.md +1 -0
- package/pack/.claude/skills/rite-zoom-out/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-api-interface/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-audit/SKILL.md +5 -6
- package/pack/generated/claude/skills/devrites-browser-proof/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-doubt/SKILL.md +2 -1
- package/pack/generated/claude/skills/devrites-frontend-craft/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-interview/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/reference/model-tiers.md +4 -3
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +4 -5
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +15 -8
- package/pack/generated/claude/skills/devrites-prose-craft/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-refresh-indexes/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-ux-shape/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-adopt/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-build/SKILL.md +2 -1
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +3 -4
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +11 -12
- package/pack/generated/claude/skills/rite-clarify/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-converge/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-customize/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-define/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-doctor/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-dogfood/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-explain/SKILL.md +4 -2
- package/pack/generated/claude/skills/rite-frame/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-handoff/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-learn/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-plan/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-polish/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-pov/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-pr-feedback/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-prototype/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-prove/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-quick/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-resolve/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-review/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-seal/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +2 -3
- package/pack/generated/claude/skills/rite-seal/reference/risk-and-rollback.md +10 -12
- package/pack/generated/claude/skills/rite-ship/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-spec/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-status/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-temper/SKILL.md +3 -2
- package/pack/generated/claude/skills/rite-upgrade/SKILL.md +3 -1
- package/pack/generated/claude/skills/rite-vet/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-zoom-out/SKILL.md +1 -0
- package/pack/generated/codex/AGENTS.md +2 -4
- package/pack/generated/codex/hooks.json +24 -5
- package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-audit/SKILL.md +10 -10
- package/pack/generated/codex/skills/devrites-browser-proof/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-doubt/SKILL.md +7 -5
- package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-interview/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-lib/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-lib/reference/model-tiers.md +4 -3
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +4 -5
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +15 -8
- package/pack/generated/codex/skills/devrites-prose-craft/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-refresh-indexes/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-ux-shape/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-adopt/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-build/SKILL.md +7 -5
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +3 -4
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +11 -12
- package/pack/generated/codex/skills/rite-clarify/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-converge/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-customize/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-define/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-doctor/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-dogfood/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-explain/SKILL.md +9 -6
- package/pack/generated/codex/skills/rite-frame/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-handoff/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-learn/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-plan/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-polish/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-pov/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-prototype/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-prove/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-quick/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-resolve/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-review/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-seal/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +2 -3
- package/pack/generated/codex/skills/rite-seal/reference/risk-and-rollback.md +10 -12
- package/pack/generated/codex/skills/rite-ship/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-spec/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-status/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-temper/SKILL.md +8 -6
- package/pack/generated/codex/skills/rite-upgrade/SKILL.md +8 -5
- package/pack/generated/codex/skills/rite-vet/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-zoom-out/SKILL.md +6 -4
- package/package.json +1 -1
- package/scripts/codex-generate.sh +32 -18
- package/scripts/live-hosts/agent-result.schema.json +2 -1
- package/scripts/live-hosts/claude.sh +2 -2
- package/scripts/live-hosts/codex.sh +4 -0
- package/scripts/live-hosts/fake-host.py +1 -8
- package/scripts/live-hosts/host-transport.py +1 -2
- package/scripts/run-agent-contract-evals.py +3 -3
- package/scripts/run-evals.sh +3 -34
- package/scripts/validate-frontmatter.py +34 -1
- package/engine/testdata/golden/TestParityA1Guard/inline-fallback-silent.golden +0 -1
- package/engine/testdata/golden/TestParityDoubtCoverage/arg=inline.golden +0 -3
- package/engine/testdata/golden/TestParityDoubtCoverage/arg=missing-inline.golden +0 -3
- package/scripts/eval-runner.py +0 -275
- package/scripts/eval-scorer.mjs +0 -46
- package/scripts/render-eval-summary.py +0 -48
- /package/engine/testdata/golden/TestParityReconcile/{inline-fallback → close-window}/snapshot.golden +0 -0
- /package/engine/testdata/golden/TestParityReconcile/{inline-fallback.golden → close-window.golden} +0 -0
|
@@ -273,8 +273,8 @@ def validate_matrix(data: Any) -> dict[str, Any]:
|
|
|
273
273
|
raise ContractError("scenario.role_class is unsupported")
|
|
274
274
|
if scenario["simulate"] not in {
|
|
275
275
|
"accepted",
|
|
276
|
+
"dispatch-unavailable",
|
|
276
277
|
"generic-fallback",
|
|
277
|
-
"inline-fallback",
|
|
278
278
|
"interrupted",
|
|
279
279
|
"malformed",
|
|
280
280
|
"missing",
|
|
@@ -313,7 +313,7 @@ def validate_matrix(data: Any) -> dict[str, Any]:
|
|
|
313
313
|
},
|
|
314
314
|
"scenario.expect",
|
|
315
315
|
)
|
|
316
|
-
if expect["execution_mode"] not in {"named", "generic", "
|
|
316
|
+
if expect["execution_mode"] not in {"named", "generic", "none"}:
|
|
317
317
|
raise ContractError("execution mode is unsupported")
|
|
318
318
|
if expect["guard_strength"] not in {
|
|
319
319
|
"enforced",
|
|
@@ -778,7 +778,7 @@ def validate_result(
|
|
|
778
778
|
exact_keys(
|
|
779
779
|
execution, {"mode", "guard_strength", "independence"}, "result execution"
|
|
780
780
|
)
|
|
781
|
-
if execution["mode"] not in {"named", "generic", "
|
|
781
|
+
if execution["mode"] not in {"named", "generic", "none"}:
|
|
782
782
|
raise ContractError("result execution mode is unsupported")
|
|
783
783
|
if execution["guard_strength"] not in {
|
|
784
784
|
"enforced",
|
package/scripts/run-evals.sh
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# scripts/run-evals.sh: validate the structure of DevRites trigger evals.
|
|
3
3
|
#
|
|
4
|
-
# Schema check + summary.
|
|
5
|
-
# Live execution requires CLAUDE_API_KEY and is manual-only, never CI default.
|
|
6
|
-
# CI runs this script to enforce the shape and
|
|
4
|
+
# Schema check + summary. CI runs this script to enforce the shape and
|
|
7
5
|
# catch broken JSON, missing skills, and empty/one-sided corpora. Invocation-policy
|
|
8
6
|
# shape is enforced by run-routing-evals.py in scripts/validate.sh.
|
|
9
7
|
#
|
|
10
8
|
# Usage:
|
|
11
9
|
# scripts/run-evals.sh # validate every evals/*.json
|
|
12
10
|
# scripts/run-evals.sh evals/rite-spec.json # validate one file
|
|
13
|
-
# scripts/run-evals.sh --live evals/*.json # execute live model evals too
|
|
14
11
|
|
|
15
12
|
set -euo pipefail
|
|
16
13
|
|
|
17
14
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
18
15
|
EVALS_DIR="$ROOT/evals"
|
|
19
|
-
LIVE=0
|
|
20
|
-
ARGS=()
|
|
21
|
-
for arg in "$@"; do
|
|
22
|
-
case "$arg" in
|
|
23
|
-
--live) LIVE=1 ;;
|
|
24
|
-
*) ARGS+=("$arg") ;;
|
|
25
|
-
esac
|
|
26
|
-
done
|
|
27
16
|
|
|
28
|
-
if [[
|
|
29
|
-
FILES=("
|
|
17
|
+
if [[ $# -gt 0 ]]; then
|
|
18
|
+
FILES=("$@")
|
|
30
19
|
else
|
|
31
20
|
if [[ ! -d "$EVALS_DIR" ]]; then
|
|
32
21
|
echo "No evals/ directory at $EVALS_DIR" >&2
|
|
@@ -141,23 +130,3 @@ printf 'Validated %d eval files; %d failed.\n' "$TOTAL" "$FAILED"
|
|
|
141
130
|
if [[ $FAILED -gt 0 ]]; then
|
|
142
131
|
exit 1
|
|
143
132
|
fi
|
|
144
|
-
|
|
145
|
-
if [[ "$LIVE" -ne 1 ]]; then
|
|
146
|
-
echo
|
|
147
|
-
echo "Live model evals disabled by default. To run them manually:"
|
|
148
|
-
echo " CLAUDE_API_KEY=sk-... scripts/run-evals.sh --live evals/*.json"
|
|
149
|
-
exit 0
|
|
150
|
-
fi
|
|
151
|
-
|
|
152
|
-
if [[ -z "${CLAUDE_API_KEY:-}" ]]; then
|
|
153
|
-
echo "error: --live requires CLAUDE_API_KEY." >&2
|
|
154
|
-
exit 2
|
|
155
|
-
fi
|
|
156
|
-
if ! command -v python3 >/dev/null 2>&1; then
|
|
157
|
-
echo "error: --live requires python3." >&2
|
|
158
|
-
exit 2
|
|
159
|
-
fi
|
|
160
|
-
|
|
161
|
-
echo
|
|
162
|
-
echo "Executing live trigger evals via scripts/eval-runner.py …"
|
|
163
|
-
exec python3 "$ROOT/scripts/eval-runner.py" "${FILES[@]}"
|
|
@@ -6,10 +6,12 @@ Exits non-zero if any file fails. Uses PyYAML if present, else a minimal parser
|
|
|
6
6
|
(frontmatter here is simple key: value, no nested structures needed).
|
|
7
7
|
"""
|
|
8
8
|
import sys
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
9
11
|
|
|
10
12
|
KNOWN_SKILL_FIELDS = {
|
|
11
13
|
"name", "description", "argument-hint", "user-invocable",
|
|
12
|
-
"disable-model-invocation",
|
|
14
|
+
"disable-model-invocation", "required-agent-roles",
|
|
13
15
|
}
|
|
14
16
|
KNOWN_AGENT_FIELDS = {
|
|
15
17
|
"name", "description", "tools", "disallowedTools", "model", "permissionMode",
|
|
@@ -22,6 +24,7 @@ DESCRIPTION_WORD_LIMITS = {
|
|
|
22
24
|
"library": 60,
|
|
23
25
|
"explicit": 30,
|
|
24
26
|
}
|
|
27
|
+
AGENT_ROLE_RE = re.compile(r"^devrites-[a-z0-9-]+$")
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
def extract_frontmatter(text):
|
|
@@ -88,6 +91,26 @@ def is_agent(path):
|
|
|
88
91
|
return "/agents/" in path.replace("\\", "/")
|
|
89
92
|
|
|
90
93
|
|
|
94
|
+
def required_agent_roles(path, value):
|
|
95
|
+
raw = str(value).strip()
|
|
96
|
+
if raw == "none":
|
|
97
|
+
return [], None
|
|
98
|
+
if not raw:
|
|
99
|
+
return [], "required-agent-roles must be 'none' or a comma-separated role list"
|
|
100
|
+
roles = [role.strip() for role in raw.split(",")]
|
|
101
|
+
if any(not AGENT_ROLE_RE.fullmatch(role) for role in roles):
|
|
102
|
+
return [], "required-agent-roles contains an invalid role"
|
|
103
|
+
if len(set(roles)) != len(roles):
|
|
104
|
+
return [], "required-agent-roles contains a duplicate role"
|
|
105
|
+
skill_path = Path(path).resolve()
|
|
106
|
+
agents_dir = skill_path.parent.parent.parent / "agents"
|
|
107
|
+
if agents_dir.is_dir():
|
|
108
|
+
missing = [role for role in roles if not (agents_dir / (role + ".md")).is_file()]
|
|
109
|
+
if missing:
|
|
110
|
+
return [], "required-agent-roles references missing agent(s): " + ", ".join(missing)
|
|
111
|
+
return roles, None
|
|
112
|
+
|
|
113
|
+
|
|
91
114
|
def main(argv):
|
|
92
115
|
files = argv[1:]
|
|
93
116
|
if not files:
|
|
@@ -123,6 +146,16 @@ def main(argv):
|
|
|
123
146
|
% (path, ", ".join(unknown)))
|
|
124
147
|
errors += 1
|
|
125
148
|
continue
|
|
149
|
+
if not is_agent(path):
|
|
150
|
+
if "required-agent-roles" not in data:
|
|
151
|
+
print("ERROR %s: missing 'required-agent-roles' (use 'none' when no fresh agent is mandatory)" % path)
|
|
152
|
+
errors += 1
|
|
153
|
+
continue
|
|
154
|
+
_, role_err = required_agent_roles(path, data.get("required-agent-roles"))
|
|
155
|
+
if role_err:
|
|
156
|
+
print("ERROR %s: %s" % (path, role_err))
|
|
157
|
+
errors += 1
|
|
158
|
+
continue
|
|
126
159
|
# description length cap is 1024 chars per Anthropic SKILL.md spec
|
|
127
160
|
warn = ""
|
|
128
161
|
desc = str(data.get("description", ""))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
exit 0
|
package/scripts/eval-runner.py
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""DevRites trigger-eval runner.
|
|
3
|
-
|
|
4
|
-
Wraps the Anthropic SDK to execute a trigger eval against a Claude
|
|
5
|
-
model. Off by default in CI (schema-only validation lives in
|
|
6
|
-
`scripts/run-evals.sh`); this runner is invoked explicitly when
|
|
7
|
-
`CLAUDE_API_KEY` is set.
|
|
8
|
-
|
|
9
|
-
Inputs
|
|
10
|
-
------
|
|
11
|
-
A trigger-eval JSON of the shape DevRites ships under `evals/`:
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
"skill": "rite-spec",
|
|
15
|
-
"description": "...",
|
|
16
|
-
"queries": [
|
|
17
|
-
{"text": "...", "expected": "should_trigger"|"should_not_trigger", "rationale": "..."}
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
Plus the pack at `pack/.claude/skills/` (so the runner can read the candidate
|
|
22
|
-
skills' descriptions and present them to the model).
|
|
23
|
-
|
|
24
|
-
What it does per query
|
|
25
|
-
----------------------
|
|
26
|
-
1. Loads each skill's `name` + `description` from `pack/.claude/skills/*/SKILL.md`.
|
|
27
|
-
2. Sends the query as a `user` message to Claude with a system prompt
|
|
28
|
-
instructing it to pick exactly one skill name from the candidate list, or
|
|
29
|
-
"none" if no skill applies. We do NOT ask the model to invoke a tool: we
|
|
30
|
-
ask it to predict which skill the harness would have triggered. That keeps
|
|
31
|
-
the eval cheap (no agentic loop, one round-trip per query) and faithful to
|
|
32
|
-
what skill discovery decides.
|
|
33
|
-
3. Compares the predicted skill name to the expected verdict:
|
|
34
|
-
- `should_trigger` → predicted name must equal the eval's `skill` field.
|
|
35
|
-
- `should_not_trigger` → predicted name must NOT equal the eval's `skill`.
|
|
36
|
-
|
|
37
|
-
Outputs
|
|
38
|
-
-------
|
|
39
|
-
A per-query line and a final accuracy summary. Exit code 1 if accuracy is
|
|
40
|
-
below `--min-accuracy` (default 0.90).
|
|
41
|
-
|
|
42
|
-
Usage
|
|
43
|
-
-----
|
|
44
|
-
CLAUDE_API_KEY=sk-... ./scripts/eval-runner.py evals/rite-spec.json
|
|
45
|
-
CLAUDE_API_KEY=sk-... ./scripts/eval-runner.py --min-accuracy 0.9 evals/*.json
|
|
46
|
-
|
|
47
|
-
The runner is intentionally thin (~200 lines). It is not the eval-viewer in
|
|
48
|
-
Anthropic's `skill-creator`: it just provides the missing "did the model
|
|
49
|
-
pick the right skill" signal so we can answer that question without firing
|
|
50
|
-
up the full Claude Code harness.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
from __future__ import annotations
|
|
54
|
-
|
|
55
|
-
import argparse
|
|
56
|
-
import json
|
|
57
|
-
import os
|
|
58
|
-
import re
|
|
59
|
-
import sys
|
|
60
|
-
from dataclasses import dataclass
|
|
61
|
-
from pathlib import Path
|
|
62
|
-
|
|
63
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
64
|
-
SKILLS_DIR = ROOT / "pack" / ".claude" / "skills"
|
|
65
|
-
|
|
66
|
-
SYSTEM_TEMPLATE = """\
|
|
67
|
-
You are the DevRites skill router. Your job is to predict which DevRites
|
|
68
|
-
skill (if any) should fire for a given user message.
|
|
69
|
-
|
|
70
|
-
Each candidate skill is described below. Read the descriptions carefully:
|
|
71
|
-
DevRites' triggers are encoded in them.
|
|
72
|
-
|
|
73
|
-
For the user message at the end, respond with exactly one line of the form:
|
|
74
|
-
|
|
75
|
-
name: <skill-name>
|
|
76
|
-
|
|
77
|
-
…where <skill-name> is the name of the skill that should fire, or the
|
|
78
|
-
literal token `none` if no DevRites skill applies. Do not explain. Do not
|
|
79
|
-
suggest. Do not output anything except that one line.
|
|
80
|
-
|
|
81
|
-
# Candidate skills
|
|
82
|
-
|
|
83
|
-
{skills}
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@dataclass
|
|
88
|
-
class Skill:
|
|
89
|
-
name: str
|
|
90
|
-
description: str
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@dataclass
|
|
94
|
-
class Query:
|
|
95
|
-
text: str
|
|
96
|
-
expected: str
|
|
97
|
-
rationale: str
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@dataclass
|
|
101
|
-
class Outcome:
|
|
102
|
-
query: Query
|
|
103
|
-
predicted: str
|
|
104
|
-
correct: bool
|
|
105
|
-
false_positive: bool # expected should_not_trigger but skill fired
|
|
106
|
-
false_negative: bool # expected should_trigger but skill didn't fire
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def load_skills() -> list[Skill]:
|
|
110
|
-
skills: list[Skill] = []
|
|
111
|
-
for skill_dir in sorted(SKILLS_DIR.iterdir()):
|
|
112
|
-
skill_md = skill_dir / "SKILL.md"
|
|
113
|
-
if not skill_md.is_file():
|
|
114
|
-
continue
|
|
115
|
-
body = skill_md.read_text(encoding="utf-8")
|
|
116
|
-
# Lightweight YAML frontmatter parse: we only need name + description.
|
|
117
|
-
m = re.match(r"^---\n(.*?)\n---", body, re.DOTALL)
|
|
118
|
-
if not m:
|
|
119
|
-
continue
|
|
120
|
-
front = m.group(1)
|
|
121
|
-
name_m = re.search(r"^name:\s*(\S+)", front, re.MULTILINE)
|
|
122
|
-
desc_m = re.search(r"^description:\s*(.+)$", front, re.MULTILINE)
|
|
123
|
-
if not name_m or not desc_m:
|
|
124
|
-
continue
|
|
125
|
-
skills.append(Skill(name=name_m.group(1), description=desc_m.group(1).strip()))
|
|
126
|
-
return skills
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def render_system(skills: list[Skill]) -> str:
|
|
130
|
-
lines = []
|
|
131
|
-
for s in skills:
|
|
132
|
-
lines.append(f"- **{s.name}**: {s.description}")
|
|
133
|
-
return SYSTEM_TEMPLATE.format(skills="\n".join(lines))
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def load_eval(path: Path) -> tuple[str, list[Query]]:
|
|
137
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
138
|
-
queries = [
|
|
139
|
-
Query(text=q["text"], expected=q["expected"], rationale=q.get("rationale", ""))
|
|
140
|
-
for q in data["queries"]
|
|
141
|
-
]
|
|
142
|
-
return data["skill"], queries
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def predict(client, model: str, system: str, query_text: str) -> str:
|
|
146
|
-
response = client.messages.create(
|
|
147
|
-
model=model,
|
|
148
|
-
max_tokens=64,
|
|
149
|
-
system=system,
|
|
150
|
-
messages=[{"role": "user", "content": query_text}],
|
|
151
|
-
)
|
|
152
|
-
text = "".join(block.text for block in response.content if getattr(block, "text", None))
|
|
153
|
-
m = re.search(r"^\s*name:\s*([A-Za-z0-9_\-]+)", text, re.MULTILINE)
|
|
154
|
-
if not m:
|
|
155
|
-
return "none"
|
|
156
|
-
name = m.group(1).strip().lower()
|
|
157
|
-
return name
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def score(target_skill: str, predicted: str, expected: str) -> bool:
|
|
161
|
-
predicted = predicted.lower()
|
|
162
|
-
target = target_skill.lower()
|
|
163
|
-
if expected == "should_trigger":
|
|
164
|
-
return predicted == target
|
|
165
|
-
if expected == "should_not_trigger":
|
|
166
|
-
return predicted != target
|
|
167
|
-
raise ValueError(f"unknown expected verdict: {expected!r}")
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def run_one(client, model: str, system: str, eval_path: Path) -> tuple[int, int, list[Outcome]]:
|
|
171
|
-
target_skill, queries = load_eval(eval_path)
|
|
172
|
-
outcomes: list[Outcome] = []
|
|
173
|
-
correct = 0
|
|
174
|
-
for q in queries:
|
|
175
|
-
predicted = predict(client, model, system, q.text)
|
|
176
|
-
ok = score(target_skill, predicted, q.expected)
|
|
177
|
-
fp = (q.expected == "should_not_trigger" and not ok)
|
|
178
|
-
fn = (q.expected == "should_trigger" and not ok)
|
|
179
|
-
outcomes.append(Outcome(query=q, predicted=predicted, correct=ok, false_positive=fp, false_negative=fn))
|
|
180
|
-
if ok:
|
|
181
|
-
correct += 1
|
|
182
|
-
return correct, len(queries), outcomes
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def main() -> int:
|
|
186
|
-
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
187
|
-
parser.add_argument("eval_files", nargs="+", type=Path, help="Eval JSON files to execute")
|
|
188
|
-
parser.add_argument("--model", default=os.environ.get("DEVRITES_EVAL_MODEL", "claude-haiku-4-5-20251001"))
|
|
189
|
-
parser.add_argument("--min-accuracy", type=float, default=0.90,
|
|
190
|
-
help="Per-eval minimum correct/total (default: 0.90)")
|
|
191
|
-
parser.add_argument("--max-false-positives", type=int, default=2,
|
|
192
|
-
help="Per-eval max should_not_trigger queries that fired (default: 2)")
|
|
193
|
-
parser.add_argument("--verbose", action="store_true", help="Print every per-query line")
|
|
194
|
-
parser.add_argument("--summary-file", type=Path, default=None,
|
|
195
|
-
help="Write a machine-readable summary (one JSON line per eval) to this file")
|
|
196
|
-
args = parser.parse_args()
|
|
197
|
-
|
|
198
|
-
if not os.environ.get("CLAUDE_API_KEY"):
|
|
199
|
-
sys.stderr.write("error: CLAUDE_API_KEY is not set; cannot run live eval.\n")
|
|
200
|
-
return 2
|
|
201
|
-
|
|
202
|
-
try:
|
|
203
|
-
import anthropic
|
|
204
|
-
except ImportError:
|
|
205
|
-
sys.stderr.write(
|
|
206
|
-
"error: anthropic SDK not installed. Run `pip install anthropic` first.\n"
|
|
207
|
-
)
|
|
208
|
-
return 2
|
|
209
|
-
|
|
210
|
-
client = anthropic.Anthropic(api_key=os.environ["CLAUDE_API_KEY"])
|
|
211
|
-
skills = load_skills()
|
|
212
|
-
system = render_system(skills)
|
|
213
|
-
|
|
214
|
-
total_correct = 0
|
|
215
|
-
total_queries = 0
|
|
216
|
-
total_fp = 0
|
|
217
|
-
total_fn = 0
|
|
218
|
-
per_file_failed = 0
|
|
219
|
-
summary_lines: list[str] = []
|
|
220
|
-
|
|
221
|
-
for eval_path in args.eval_files:
|
|
222
|
-
if eval_path.name == "routing-baseline.json":
|
|
223
|
-
continue
|
|
224
|
-
if not eval_path.is_file():
|
|
225
|
-
sys.stderr.write(f"skip: {eval_path} not a file\n")
|
|
226
|
-
continue
|
|
227
|
-
print(f"== {eval_path} ==")
|
|
228
|
-
correct, n, outcomes = run_one(client, args.model, system, eval_path)
|
|
229
|
-
fp = sum(1 for o in outcomes if o.false_positive)
|
|
230
|
-
fn = sum(1 for o in outcomes if o.false_negative)
|
|
231
|
-
total_correct += correct
|
|
232
|
-
total_queries += n
|
|
233
|
-
total_fp += fp
|
|
234
|
-
total_fn += fn
|
|
235
|
-
accuracy = correct / n if n else 0.0
|
|
236
|
-
if args.verbose:
|
|
237
|
-
for o in outcomes:
|
|
238
|
-
marker = "✓" if o.correct else "✗"
|
|
239
|
-
tag = "FP" if o.false_positive else ("FN" if o.false_negative else " ")
|
|
240
|
-
print(f" {marker} {tag} [{o.query.expected:20s}] predicted={o.predicted:24s} : {o.query.text[:60]}")
|
|
241
|
-
print(f" accuracy: {correct}/{n} = {accuracy:.0%} (FP={fp}, FN={fn})")
|
|
242
|
-
failed_reasons = []
|
|
243
|
-
if accuracy < args.min_accuracy:
|
|
244
|
-
failed_reasons.append(f"accuracy {accuracy:.0%} < {args.min_accuracy:.0%}")
|
|
245
|
-
if fp > args.max_false_positives:
|
|
246
|
-
failed_reasons.append(f"false-positives {fp} > {args.max_false_positives}")
|
|
247
|
-
if failed_reasons:
|
|
248
|
-
per_file_failed += 1
|
|
249
|
-
print(f" FAIL: {'; '.join(failed_reasons)}")
|
|
250
|
-
if args.summary_file:
|
|
251
|
-
summary_lines.append(json.dumps({
|
|
252
|
-
"file": str(eval_path),
|
|
253
|
-
"skill": load_eval(eval_path)[0],
|
|
254
|
-
"correct": correct,
|
|
255
|
-
"total": n,
|
|
256
|
-
"accuracy": round(accuracy, 4),
|
|
257
|
-
"false_positives": fp,
|
|
258
|
-
"false_negatives": fn,
|
|
259
|
-
"passed": not failed_reasons,
|
|
260
|
-
}))
|
|
261
|
-
|
|
262
|
-
overall = total_correct / total_queries if total_queries else 0.0
|
|
263
|
-
print()
|
|
264
|
-
print(f"Overall: {total_correct}/{total_queries} = {overall:.0%} (FP={total_fp}, FN={total_fn}, model={args.model})")
|
|
265
|
-
if args.summary_file:
|
|
266
|
-
args.summary_file.write_text("\n".join(summary_lines) + "\n", encoding="utf-8")
|
|
267
|
-
print(f"Summary written to {args.summary_file}")
|
|
268
|
-
if per_file_failed:
|
|
269
|
-
print(f"{per_file_failed} eval file(s) below threshold")
|
|
270
|
-
return 1
|
|
271
|
-
return 0
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if __name__ == "__main__":
|
|
275
|
-
sys.exit(main())
|
package/scripts/eval-scorer.mjs
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Deterministic fixture scorer for DevRites evals: no LLM judge, no deps.
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
|
|
5
|
-
function norm(s) {
|
|
6
|
-
return String(s || '')
|
|
7
|
-
.normalize('NFKC')
|
|
8
|
-
.toLowerCase()
|
|
9
|
-
.replace(/[\p{P}\p{S}]+/gu, ' ')
|
|
10
|
-
.replace(/\s+/g, ' ')
|
|
11
|
-
.trim();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function hit(text, kw) {
|
|
15
|
-
const t = norm(text);
|
|
16
|
-
const k = norm(kw);
|
|
17
|
-
if (!k) return false;
|
|
18
|
-
if (t.includes(k)) return true;
|
|
19
|
-
const parts = k.split(' ').filter(Boolean);
|
|
20
|
-
return parts.length > 1 && parts.every((p) => t.includes(p));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function matchFinding(output, keywords) {
|
|
24
|
-
const kws = Array.isArray(keywords) ? keywords : [];
|
|
25
|
-
const matches = kws.filter((k) => hit(output, k)).length;
|
|
26
|
-
const needed = Math.max(2, Math.ceil(kws.length * 0.4));
|
|
27
|
-
return { matches, needed, matched: kws.length > 0 && matches >= Math.min(needed, kws.length) };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function score(output, groundTruth) {
|
|
31
|
-
const findings = Array.isArray(groundTruth?.findings) ? groundTruth.findings : [];
|
|
32
|
-
const rows = findings.map((f) => ({ id: f.id || f.name || '', ...matchFinding(output, f.keywords || []) }));
|
|
33
|
-
const found = rows.filter((r) => r.matched).length;
|
|
34
|
-
return { found, total: findings.length, score: findings.length ? found / findings.length : 1, rows };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
38
|
-
const [, , outputPath, truthPath] = process.argv;
|
|
39
|
-
if (!outputPath || !truthPath) {
|
|
40
|
-
console.error('usage: node scripts/eval-scorer.mjs <model-output.txt> <ground-truth.json>');
|
|
41
|
-
process.exit(2);
|
|
42
|
-
}
|
|
43
|
-
const result = score(readFileSync(outputPath, 'utf8'), JSON.parse(readFileSync(truthPath, 'utf8')));
|
|
44
|
-
console.log(JSON.stringify(result, null, 2));
|
|
45
|
-
process.exit(result.found === result.total ? 0 : 1);
|
|
46
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Render a GitHub-Actions-flavored markdown table from an eval summary JSONL.
|
|
3
|
-
|
|
4
|
-
Reads eval-summary.jsonl (one JSON object per line, written by
|
|
5
|
-
`scripts/eval-runner.py --summary-file`) and prints a markdown table to
|
|
6
|
-
stdout. Used by `.github/workflows/evals.yml` to populate the job summary.
|
|
7
|
-
"""
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
import pathlib
|
|
13
|
-
import sys
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def main() -> int:
|
|
17
|
-
path = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "eval-summary.jsonl")
|
|
18
|
-
model = os.environ.get("DEVRITES_EVAL_MODEL", "claude-haiku-4-5-20251001")
|
|
19
|
-
if not path.is_file():
|
|
20
|
-
print(f"(no summary file found at {path})")
|
|
21
|
-
return 0
|
|
22
|
-
print(f"## DevRites trigger evals\n")
|
|
23
|
-
print(f"Model: `{model}`\n")
|
|
24
|
-
print("| Skill | Correct | Accuracy | FP | FN | Passed |")
|
|
25
|
-
print("|---|---|---|---|---|---|")
|
|
26
|
-
total_correct = total_n = total_fp = total_fn = 0
|
|
27
|
-
any_failed = False
|
|
28
|
-
for line in path.read_text(encoding="utf-8").splitlines():
|
|
29
|
-
line = line.strip()
|
|
30
|
-
if not line:
|
|
31
|
-
continue
|
|
32
|
-
r = json.loads(line)
|
|
33
|
-
tick = "✅" if r["passed"] else "❌"
|
|
34
|
-
if not r["passed"]:
|
|
35
|
-
any_failed = True
|
|
36
|
-
total_correct += r["correct"]
|
|
37
|
-
total_n += r["total"]
|
|
38
|
-
total_fp += r["false_positives"]
|
|
39
|
-
total_fn += r["false_negatives"]
|
|
40
|
-
print(f"| `{r['skill']}` | {r['correct']}/{r['total']} | {r['accuracy']:.0%} | {r['false_positives']} | {r['false_negatives']} | {tick} |")
|
|
41
|
-
if total_n:
|
|
42
|
-
overall = total_correct / total_n
|
|
43
|
-
print(f"| **overall** | **{total_correct}/{total_n}** | **{overall:.0%}** | **{total_fp}** | **{total_fn}** | {'✅' if not any_failed else '❌'} |")
|
|
44
|
-
return 0
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if __name__ == "__main__":
|
|
48
|
-
sys.exit(main())
|
/package/engine/testdata/golden/TestParityReconcile/{inline-fallback → close-window}/snapshot.golden
RENAMED
|
File without changes
|
/package/engine/testdata/golden/TestParityReconcile/{inline-fallback.golden → close-window.golden}
RENAMED
|
File without changes
|