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.
Files changed (196) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/docs/architecture.md +6 -2
  4. package/docs/command-map.md +3 -3
  5. package/docs/engine/agent-contract.md +3 -2
  6. package/docs/harness-compliance.md +2 -2
  7. package/docs/orchestration.md +15 -12
  8. package/docs/porting-to-a-new-harness.md +2 -1
  9. package/docs/skills.md +7 -2
  10. package/engine/hooks.go +39 -4
  11. package/engine/hooks_agent_dispatch.go +1193 -0
  12. package/engine/hooks_workspace.go +78 -10
  13. package/engine/internal/forge/forge.go +0 -2
  14. package/engine/internal/harness/compliance.go +1 -1
  15. package/engine/internal/harness/harness.go +3 -0
  16. package/engine/internal/lib/doubtcoverage.go +2 -12
  17. package/engine/internal/lib/reconcile.go +4 -29
  18. package/engine/internal/lib/reconcile_test.go +14 -10
  19. package/engine/main.go +4 -1
  20. package/engine/tests/hook_test.go +877 -1
  21. package/engine/tests/lib_parity_test.go +5 -17
  22. package/engine/tests/parity_reconcile_test.go +1 -1
  23. package/engine/tests/parity_test.go +2 -6
  24. package/pack/.claude/skills/devrites-api-interface/SKILL.md +1 -0
  25. package/pack/.claude/skills/devrites-audit/SKILL.md +5 -6
  26. package/pack/.claude/skills/devrites-browser-proof/SKILL.md +1 -0
  27. package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +1 -0
  28. package/pack/.claude/skills/devrites-doubt/SKILL.md +2 -1
  29. package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +1 -0
  30. package/pack/.claude/skills/devrites-interview/SKILL.md +1 -0
  31. package/pack/.claude/skills/devrites-lib/SKILL.md +1 -0
  32. package/pack/.claude/skills/devrites-lib/reference/model-tiers.md +4 -3
  33. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +4 -5
  34. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +15 -8
  35. package/pack/.claude/skills/devrites-prose-craft/SKILL.md +1 -0
  36. package/pack/.claude/skills/devrites-refresh-indexes/SKILL.md +1 -0
  37. package/pack/.claude/skills/devrites-source-driven/SKILL.md +1 -0
  38. package/pack/.claude/skills/devrites-ux-shape/SKILL.md +1 -0
  39. package/pack/.claude/skills/rite/SKILL.md +1 -0
  40. package/pack/.claude/skills/rite-adopt/SKILL.md +1 -0
  41. package/pack/.claude/skills/rite-autocomplete/SKILL.md +1 -0
  42. package/pack/.claude/skills/rite-build/SKILL.md +2 -1
  43. package/pack/.claude/skills/rite-build/reference/phase-contract.md +3 -4
  44. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +11 -12
  45. package/pack/.claude/skills/rite-clarify/SKILL.md +1 -0
  46. package/pack/.claude/skills/rite-converge/SKILL.md +1 -0
  47. package/pack/.claude/skills/rite-customize/SKILL.md +1 -0
  48. package/pack/.claude/skills/rite-define/SKILL.md +1 -0
  49. package/pack/.claude/skills/rite-doctor/SKILL.md +1 -0
  50. package/pack/.claude/skills/rite-dogfood/SKILL.md +1 -0
  51. package/pack/.claude/skills/rite-explain/SKILL.md +4 -2
  52. package/pack/.claude/skills/rite-frame/SKILL.md +1 -0
  53. package/pack/.claude/skills/rite-handoff/SKILL.md +1 -0
  54. package/pack/.claude/skills/rite-learn/SKILL.md +1 -0
  55. package/pack/.claude/skills/rite-plan/SKILL.md +1 -0
  56. package/pack/.claude/skills/rite-polish/SKILL.md +1 -0
  57. package/pack/.claude/skills/rite-pov/SKILL.md +1 -0
  58. package/pack/.claude/skills/rite-pr-feedback/SKILL.md +1 -0
  59. package/pack/.claude/skills/rite-pressure-test/SKILL.md +1 -0
  60. package/pack/.claude/skills/rite-prototype/SKILL.md +1 -0
  61. package/pack/.claude/skills/rite-prove/SKILL.md +1 -0
  62. package/pack/.claude/skills/rite-quick/SKILL.md +1 -0
  63. package/pack/.claude/skills/rite-resolve/SKILL.md +1 -0
  64. package/pack/.claude/skills/rite-review/SKILL.md +1 -0
  65. package/pack/.claude/skills/rite-seal/SKILL.md +1 -0
  66. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +2 -3
  67. package/pack/.claude/skills/rite-seal/reference/risk-and-rollback.md +10 -12
  68. package/pack/.claude/skills/rite-ship/SKILL.md +1 -0
  69. package/pack/.claude/skills/rite-spec/SKILL.md +1 -0
  70. package/pack/.claude/skills/rite-status/SKILL.md +1 -0
  71. package/pack/.claude/skills/rite-temper/SKILL.md +3 -2
  72. package/pack/.claude/skills/rite-upgrade/SKILL.md +3 -1
  73. package/pack/.claude/skills/rite-vet/SKILL.md +1 -0
  74. package/pack/.claude/skills/rite-zoom-out/SKILL.md +1 -0
  75. package/pack/generated/claude/skills/devrites-api-interface/SKILL.md +1 -0
  76. package/pack/generated/claude/skills/devrites-audit/SKILL.md +5 -6
  77. package/pack/generated/claude/skills/devrites-browser-proof/SKILL.md +1 -0
  78. package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +1 -0
  79. package/pack/generated/claude/skills/devrites-doubt/SKILL.md +2 -1
  80. package/pack/generated/claude/skills/devrites-frontend-craft/SKILL.md +1 -0
  81. package/pack/generated/claude/skills/devrites-interview/SKILL.md +1 -0
  82. package/pack/generated/claude/skills/devrites-lib/SKILL.md +1 -0
  83. package/pack/generated/claude/skills/devrites-lib/reference/model-tiers.md +4 -3
  84. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +4 -5
  85. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +15 -8
  86. package/pack/generated/claude/skills/devrites-prose-craft/SKILL.md +1 -0
  87. package/pack/generated/claude/skills/devrites-refresh-indexes/SKILL.md +1 -0
  88. package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +1 -0
  89. package/pack/generated/claude/skills/devrites-ux-shape/SKILL.md +1 -0
  90. package/pack/generated/claude/skills/rite/SKILL.md +1 -0
  91. package/pack/generated/claude/skills/rite-adopt/SKILL.md +1 -0
  92. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +1 -0
  93. package/pack/generated/claude/skills/rite-build/SKILL.md +2 -1
  94. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +3 -4
  95. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +11 -12
  96. package/pack/generated/claude/skills/rite-clarify/SKILL.md +1 -0
  97. package/pack/generated/claude/skills/rite-converge/SKILL.md +1 -0
  98. package/pack/generated/claude/skills/rite-customize/SKILL.md +1 -0
  99. package/pack/generated/claude/skills/rite-define/SKILL.md +1 -0
  100. package/pack/generated/claude/skills/rite-doctor/SKILL.md +1 -0
  101. package/pack/generated/claude/skills/rite-dogfood/SKILL.md +1 -0
  102. package/pack/generated/claude/skills/rite-explain/SKILL.md +4 -2
  103. package/pack/generated/claude/skills/rite-frame/SKILL.md +1 -0
  104. package/pack/generated/claude/skills/rite-handoff/SKILL.md +1 -0
  105. package/pack/generated/claude/skills/rite-learn/SKILL.md +1 -0
  106. package/pack/generated/claude/skills/rite-plan/SKILL.md +1 -0
  107. package/pack/generated/claude/skills/rite-polish/SKILL.md +1 -0
  108. package/pack/generated/claude/skills/rite-pov/SKILL.md +1 -0
  109. package/pack/generated/claude/skills/rite-pr-feedback/SKILL.md +1 -0
  110. package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +1 -0
  111. package/pack/generated/claude/skills/rite-prototype/SKILL.md +1 -0
  112. package/pack/generated/claude/skills/rite-prove/SKILL.md +1 -0
  113. package/pack/generated/claude/skills/rite-quick/SKILL.md +1 -0
  114. package/pack/generated/claude/skills/rite-resolve/SKILL.md +1 -0
  115. package/pack/generated/claude/skills/rite-review/SKILL.md +1 -0
  116. package/pack/generated/claude/skills/rite-seal/SKILL.md +1 -0
  117. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +2 -3
  118. package/pack/generated/claude/skills/rite-seal/reference/risk-and-rollback.md +10 -12
  119. package/pack/generated/claude/skills/rite-ship/SKILL.md +1 -0
  120. package/pack/generated/claude/skills/rite-spec/SKILL.md +1 -0
  121. package/pack/generated/claude/skills/rite-status/SKILL.md +1 -0
  122. package/pack/generated/claude/skills/rite-temper/SKILL.md +3 -2
  123. package/pack/generated/claude/skills/rite-upgrade/SKILL.md +3 -1
  124. package/pack/generated/claude/skills/rite-vet/SKILL.md +1 -0
  125. package/pack/generated/claude/skills/rite-zoom-out/SKILL.md +1 -0
  126. package/pack/generated/codex/AGENTS.md +2 -4
  127. package/pack/generated/codex/hooks.json +24 -5
  128. package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +6 -4
  129. package/pack/generated/codex/skills/devrites-audit/SKILL.md +10 -10
  130. package/pack/generated/codex/skills/devrites-browser-proof/SKILL.md +6 -4
  131. package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +6 -4
  132. package/pack/generated/codex/skills/devrites-doubt/SKILL.md +7 -5
  133. package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +6 -4
  134. package/pack/generated/codex/skills/devrites-interview/SKILL.md +6 -4
  135. package/pack/generated/codex/skills/devrites-lib/SKILL.md +6 -4
  136. package/pack/generated/codex/skills/devrites-lib/reference/model-tiers.md +4 -3
  137. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +4 -5
  138. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +15 -8
  139. package/pack/generated/codex/skills/devrites-prose-craft/SKILL.md +6 -4
  140. package/pack/generated/codex/skills/devrites-refresh-indexes/SKILL.md +6 -4
  141. package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +6 -4
  142. package/pack/generated/codex/skills/devrites-ux-shape/SKILL.md +6 -4
  143. package/pack/generated/codex/skills/rite/SKILL.md +6 -4
  144. package/pack/generated/codex/skills/rite-adopt/SKILL.md +6 -4
  145. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +6 -4
  146. package/pack/generated/codex/skills/rite-build/SKILL.md +7 -5
  147. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +3 -4
  148. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +11 -12
  149. package/pack/generated/codex/skills/rite-clarify/SKILL.md +6 -4
  150. package/pack/generated/codex/skills/rite-converge/SKILL.md +6 -4
  151. package/pack/generated/codex/skills/rite-customize/SKILL.md +6 -4
  152. package/pack/generated/codex/skills/rite-define/SKILL.md +6 -4
  153. package/pack/generated/codex/skills/rite-doctor/SKILL.md +6 -4
  154. package/pack/generated/codex/skills/rite-dogfood/SKILL.md +6 -4
  155. package/pack/generated/codex/skills/rite-explain/SKILL.md +9 -6
  156. package/pack/generated/codex/skills/rite-frame/SKILL.md +6 -4
  157. package/pack/generated/codex/skills/rite-handoff/SKILL.md +6 -4
  158. package/pack/generated/codex/skills/rite-learn/SKILL.md +6 -4
  159. package/pack/generated/codex/skills/rite-plan/SKILL.md +6 -4
  160. package/pack/generated/codex/skills/rite-polish/SKILL.md +6 -4
  161. package/pack/generated/codex/skills/rite-pov/SKILL.md +6 -4
  162. package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +6 -4
  163. package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -4
  164. package/pack/generated/codex/skills/rite-prototype/SKILL.md +6 -4
  165. package/pack/generated/codex/skills/rite-prove/SKILL.md +6 -4
  166. package/pack/generated/codex/skills/rite-quick/SKILL.md +6 -4
  167. package/pack/generated/codex/skills/rite-resolve/SKILL.md +6 -4
  168. package/pack/generated/codex/skills/rite-review/SKILL.md +6 -4
  169. package/pack/generated/codex/skills/rite-seal/SKILL.md +6 -4
  170. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +2 -3
  171. package/pack/generated/codex/skills/rite-seal/reference/risk-and-rollback.md +10 -12
  172. package/pack/generated/codex/skills/rite-ship/SKILL.md +6 -4
  173. package/pack/generated/codex/skills/rite-spec/SKILL.md +6 -4
  174. package/pack/generated/codex/skills/rite-status/SKILL.md +6 -4
  175. package/pack/generated/codex/skills/rite-temper/SKILL.md +8 -6
  176. package/pack/generated/codex/skills/rite-upgrade/SKILL.md +8 -5
  177. package/pack/generated/codex/skills/rite-vet/SKILL.md +6 -4
  178. package/pack/generated/codex/skills/rite-zoom-out/SKILL.md +6 -4
  179. package/package.json +1 -1
  180. package/scripts/codex-generate.sh +32 -18
  181. package/scripts/live-hosts/agent-result.schema.json +2 -1
  182. package/scripts/live-hosts/claude.sh +2 -2
  183. package/scripts/live-hosts/codex.sh +4 -0
  184. package/scripts/live-hosts/fake-host.py +1 -8
  185. package/scripts/live-hosts/host-transport.py +1 -2
  186. package/scripts/run-agent-contract-evals.py +3 -3
  187. package/scripts/run-evals.sh +3 -34
  188. package/scripts/validate-frontmatter.py +34 -1
  189. package/engine/testdata/golden/TestParityA1Guard/inline-fallback-silent.golden +0 -1
  190. package/engine/testdata/golden/TestParityDoubtCoverage/arg=inline.golden +0 -3
  191. package/engine/testdata/golden/TestParityDoubtCoverage/arg=missing-inline.golden +0 -3
  192. package/scripts/eval-runner.py +0 -275
  193. package/scripts/eval-scorer.mjs +0 -46
  194. package/scripts/render-eval-summary.py +0 -48
  195. /package/engine/testdata/golden/TestParityReconcile/{inline-fallback → close-window}/snapshot.golden +0 -0
  196. /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", "inline"}:
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", "inline"}:
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",
@@ -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. Does NOT invoke a model unless --live is passed.
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 [[ ${#ARGS[@]} -gt 0 ]]; then
29
- FILES=("${ARGS[@]}")
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,3 +0,0 @@
1
- exit 0
2
- doubt-coverage: inline build (.reconcile-inline): footprint heuristic n/a; no MISSING
3
- verdict recorded. Verify by hand that each stood decision carries a devrites-doubt verdict.
@@ -1,3 +0,0 @@
1
- exit 3
2
- doubt-coverage: SKIPPED: a '## Decisions stood' entry in decisions.md has doubt: MISSING.
3
- A decision was stood and recorded but never doubted. Re-dispatch doubt or escalate.
@@ -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())
@@ -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())