chipgraph 0.1.0__py3-none-any.whl

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 (252) hide show
  1. chipgraph/__init__.py +7 -0
  2. chipgraph/adapters/__init__.py +1 -0
  3. chipgraph/adapters/format/__init__.py +6 -0
  4. chipgraph/adapters/format/__main__.py +16 -0
  5. chipgraph/adapters/format/_yaml_lines.py +54 -0
  6. chipgraph/adapters/format/chip_yaml.py +323 -0
  7. chipgraph/adapters/format/qsoc_contract.py +246 -0
  8. chipgraph/adapters/llm/__init__.py +58 -0
  9. chipgraph/adapters/llm/_budget.py +173 -0
  10. chipgraph/adapters/llm/_errors.py +50 -0
  11. chipgraph/adapters/llm/_nda.py +30 -0
  12. chipgraph/adapters/llm/_retry.py +85 -0
  13. chipgraph/adapters/llm/anthropic.py +247 -0
  14. chipgraph/adapters/llm/decide_backend.py +179 -0
  15. chipgraph/adapters/llm/fake.py +87 -0
  16. chipgraph/adapters/llm/guard.py +68 -0
  17. chipgraph/adapters/parser/__init__.py +22 -0
  18. chipgraph/adapters/parser/cocotb.py +304 -0
  19. chipgraph/adapters/parser/generic_regex.py +75 -0
  20. chipgraph/adapters/parser/verible.py +55 -0
  21. chipgraph/adapters/parser/verilator.py +46 -0
  22. chipgraph/adapters/review/__init__.py +7 -0
  23. chipgraph/adapters/review/file.py +123 -0
  24. chipgraph/adapters/review/github.py +886 -0
  25. chipgraph/adapters/runner/__init__.py +7 -0
  26. chipgraph/adapters/runner/local.py +122 -0
  27. chipgraph/adapters/runtime/__init__.py +5 -0
  28. chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
  29. chipgraph/adapters/runtime/claude_code/agents.py +158 -0
  30. chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
  31. chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
  32. chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
  33. chipgraph/adapters/runtime/claude_code/loop.py +342 -0
  34. chipgraph/adapters/runtime/claude_code/previous.py +419 -0
  35. chipgraph/adapters/runtime/claude_code/review.py +492 -0
  36. chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
  37. chipgraph/adapters/runtime/claude_code/service.py +2285 -0
  38. chipgraph/adapters/tool/__init__.py +7 -0
  39. chipgraph/adapters/tool/cmd.py +229 -0
  40. chipgraph/adapters/tool/edalize.py +773 -0
  41. chipgraph/adapters/tool/filelist.py +259 -0
  42. chipgraph/adapters/tool/pyslang.py +485 -0
  43. chipgraph/adapters/vcs/__init__.py +1 -0
  44. chipgraph/adapters/vcs/git.py +521 -0
  45. chipgraph/app/__init__.py +10 -0
  46. chipgraph/app/baseline.py +339 -0
  47. chipgraph/app/build.py +332 -0
  48. chipgraph/app/checks.py +202 -0
  49. chipgraph/app/context.py +191 -0
  50. chipgraph/app/errors.py +11 -0
  51. chipgraph/app/executors.py +79 -0
  52. chipgraph/app/findings.py +203 -0
  53. chipgraph/app/ingest.py +523 -0
  54. chipgraph/checks/__init__.py +52 -0
  55. chipgraph/checks/_cdc_analyze.py +272 -0
  56. chipgraph/checks/_cdc_elaborate.py +455 -0
  57. chipgraph/checks/_common.py +268 -0
  58. chipgraph/checks/_cross.py +96 -0
  59. chipgraph/checks/_model.py +64 -0
  60. chipgraph/checks/_naming_pyslang.py +235 -0
  61. chipgraph/checks/_naming_rules.py +127 -0
  62. chipgraph/checks/_xref_syntax.py +260 -0
  63. chipgraph/checks/cdc_struct.py +235 -0
  64. chipgraph/checks/connect.py +353 -0
  65. chipgraph/checks/cross_chip.py +454 -0
  66. chipgraph/checks/duplicate.py +153 -0
  67. chipgraph/checks/filelist.py +323 -0
  68. chipgraph/checks/format.py +268 -0
  69. chipgraph/checks/generated.py +151 -0
  70. chipgraph/checks/hardcode.py +233 -0
  71. chipgraph/checks/layout.py +155 -0
  72. chipgraph/checks/naming.py +311 -0
  73. chipgraph/checks/plan_check.py +901 -0
  74. chipgraph/checks/ports_diff.py +203 -0
  75. chipgraph/checks/spec_schema.py +277 -0
  76. chipgraph/checks/style.py +910 -0
  77. chipgraph/checks/tb_static.py +599 -0
  78. chipgraph/checks/trace.py +224 -0
  79. chipgraph/checks/vendor_guard.py +369 -0
  80. chipgraph/checks/wrap_check.py +486 -0
  81. chipgraph/cli/__init__.py +1688 -0
  82. chipgraph/cli/next_steps.py +137 -0
  83. chipgraph/core/__init__.py +1 -0
  84. chipgraph/core/config/__init__.py +93 -0
  85. chipgraph/core/config/defaults.py +14 -0
  86. chipgraph/core/config/errors.py +32 -0
  87. chipgraph/core/config/loader.py +766 -0
  88. chipgraph/core/config/models.py +845 -0
  89. chipgraph/core/config/templates.py +340 -0
  90. chipgraph/core/contracts/__init__.py +75 -0
  91. chipgraph/core/contracts/_paths.py +26 -0
  92. chipgraph/core/contracts/agent.py +49 -0
  93. chipgraph/core/contracts/artifact.py +68 -0
  94. chipgraph/core/contracts/check.py +76 -0
  95. chipgraph/core/contracts/event.py +70 -0
  96. chipgraph/core/contracts/export.py +128 -0
  97. chipgraph/core/contracts/finding.py +171 -0
  98. chipgraph/core/contracts/gate.py +50 -0
  99. chipgraph/core/contracts/rule.py +177 -0
  100. chipgraph/core/contracts/types.py +45 -0
  101. chipgraph/core/engine/__init__.py +70 -0
  102. chipgraph/core/engine/agent_rule.py +1362 -0
  103. chipgraph/core/engine/baseline.py +283 -0
  104. chipgraph/core/engine/decide.py +462 -0
  105. chipgraph/core/engine/fanout.py +1358 -0
  106. chipgraph/core/engine/gate.py +170 -0
  107. chipgraph/core/engine/graph.py +647 -0
  108. chipgraph/core/engine/records.py +60 -0
  109. chipgraph/core/engine/rules.py +92 -0
  110. chipgraph/core/engine/scheduler.py +637 -0
  111. chipgraph/core/engine/select.py +1239 -0
  112. chipgraph/core/engine/targets.py +342 -0
  113. chipgraph/core/model/__init__.py +98 -0
  114. chipgraph/core/model/entities.py +244 -0
  115. chipgraph/core/model/export.py +16 -0
  116. chipgraph/core/model/ingest.py +693 -0
  117. chipgraph/core/model/json_value.py +4 -0
  118. chipgraph/core/model/keys.py +95 -0
  119. chipgraph/core/model/model.py +153 -0
  120. chipgraph/core/model/provenance.py +27 -0
  121. chipgraph/core/model/query.py +583 -0
  122. chipgraph/core/model/registry.py +80 -0
  123. chipgraph/core/model/relations.py +61 -0
  124. chipgraph/core/model/store.py +279 -0
  125. chipgraph/core/plugin_api/__init__.py +81 -0
  126. chipgraph/core/plugin_api/local.py +167 -0
  127. chipgraph/core/plugin_api/pack.py +159 -0
  128. chipgraph/core/plugin_api/protocols.py +177 -0
  129. chipgraph/core/plugin_api/registry.py +111 -0
  130. chipgraph/core/plugin_api/types.py +94 -0
  131. chipgraph/core/runtime/__init__.py +41 -0
  132. chipgraph/core/runtime/executor.py +79 -0
  133. chipgraph/core/runtime/queue.py +565 -0
  134. chipgraph/core/runtime/roles/__init__.py +91 -0
  135. chipgraph/core/runtime/roles/data/author.md +41 -0
  136. chipgraph/core/runtime/roles/data/critic.md +56 -0
  137. chipgraph/core/runtime/roles/data/planner.md +38 -0
  138. chipgraph/core/runtime/roles/data/researcher.md +37 -0
  139. chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
  140. chipgraph/core/runtime/roles/data/tb-author.md +48 -0
  141. chipgraph/core/runtime/roles/data/triage.md +34 -0
  142. chipgraph/core/runtime/roles/policy.py +299 -0
  143. chipgraph/core/runtime/roles/registry.py +52 -0
  144. chipgraph/core/runtime/roles/review.py +275 -0
  145. chipgraph/core/runtime/roles/schema.py +82 -0
  146. chipgraph/core/runtime/roles/skills.py +134 -0
  147. chipgraph/core/runtime/roles/spec.py +232 -0
  148. chipgraph/core/state/__init__.py +1 -0
  149. chipgraph/core/state/artifacts.py +156 -0
  150. chipgraph/core/state/backend.py +117 -0
  151. chipgraph/core/state/findings.py +151 -0
  152. chipgraph/core/state/handoff.py +411 -0
  153. chipgraph/core/state/idempotency.py +59 -0
  154. chipgraph/core/state/journal.py +238 -0
  155. chipgraph/core/state/layout.py +82 -0
  156. chipgraph/core/state/lock.py +195 -0
  157. chipgraph/core/state/trace.py +225 -0
  158. chipgraph/learn/__init__.py +96 -0
  159. chipgraph/learn/draft.py +187 -0
  160. chipgraph/learn/infer.py +580 -0
  161. chipgraph/learn/models.py +145 -0
  162. chipgraph/learn/try_run.py +228 -0
  163. chipgraph/mcp/__init__.py +12 -0
  164. chipgraph/mcp/model_tools.py +167 -0
  165. chipgraph/mcp/server.py +787 -0
  166. chipgraph/orgs/qnsc/naming-v1.yml +113 -0
  167. chipgraph/orgs/qnsc/profile.yml +5 -0
  168. chipgraph/packs/__init__.py +8 -0
  169. chipgraph/packs/assist/__init__.py +16 -0
  170. chipgraph/packs/assist/ask/__init__.py +58 -0
  171. chipgraph/packs/assist/ask/_db.py +57 -0
  172. chipgraph/packs/assist/ask/_project.py +230 -0
  173. chipgraph/packs/assist/ask/answer.py +266 -0
  174. chipgraph/packs/assist/ask/check.py +182 -0
  175. chipgraph/packs/assist/ask/contract.py +115 -0
  176. chipgraph/packs/assist/ask/documents.py +157 -0
  177. chipgraph/packs/assist/ask/fts.py +80 -0
  178. chipgraph/packs/assist/ask/retrieve.py +331 -0
  179. chipgraph/packs/assist/audit/__init__.py +383 -0
  180. chipgraph/packs/assist/pack.yml +13 -0
  181. chipgraph/packs/assist/triage/__init__.py +64 -0
  182. chipgraph/packs/assist/triage/contract.py +94 -0
  183. chipgraph/packs/assist/triage/parse.py +371 -0
  184. chipgraph/packs/assist/triage/question.py +384 -0
  185. chipgraph/packs/assist/triage/report.py +160 -0
  186. chipgraph/packs/assist/triage/rules.py +447 -0
  187. chipgraph/packs/assist/triage/run.py +224 -0
  188. chipgraph/packs/digital_rtl/__init__.py +19 -0
  189. chipgraph/packs/digital_rtl/layout.py +144 -0
  190. chipgraph/packs/digital_rtl/pack.yml +25 -0
  191. chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
  192. chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
  193. chipgraph/packs/digital_rtl/plan/context.py +82 -0
  194. chipgraph/packs/digital_rtl/plan/expand.py +94 -0
  195. chipgraph/packs/digital_rtl/plan/model.py +150 -0
  196. chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
  197. chipgraph/packs/digital_rtl/plan/show.py +176 -0
  198. chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
  199. chipgraph/packs/digital_rtl/pr/body.py +165 -0
  200. chipgraph/packs/digital_rtl/pr/gate.py +110 -0
  201. chipgraph/packs/digital_rtl/pr/open.py +397 -0
  202. chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
  203. chipgraph/packs/digital_rtl/rtl/context.py +272 -0
  204. chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
  205. chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
  206. chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
  207. chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
  208. chipgraph/packs/digital_rtl/rules/review.yml +31 -0
  209. chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
  210. chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
  211. chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
  212. chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
  213. chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
  214. chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
  215. chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
  216. chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
  217. chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
  218. chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
  219. chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
  220. chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
  221. chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
  222. chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
  223. chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
  224. chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
  225. chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
  226. chipgraph/packs/dv/__init__.py +15 -0
  227. chipgraph/packs/dv/declaration.py +392 -0
  228. chipgraph/packs/dv/interface.py +596 -0
  229. chipgraph/packs/dv/pack.yml +14 -0
  230. chipgraph/packs/dv/rules/tb_module.yml +42 -0
  231. chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
  232. chipgraph/packs/spec_core/__init__.py +9 -0
  233. chipgraph/packs/spec_core/extract/__init__.py +12 -0
  234. chipgraph/packs/spec_core/extract/_markdown.py +371 -0
  235. chipgraph/packs/spec_core/extract/_text.py +116 -0
  236. chipgraph/packs/spec_core/extract/mas.py +975 -0
  237. chipgraph/packs/spec_core/gen/__init__.py +11 -0
  238. chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
  239. chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
  240. chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
  241. chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
  242. chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
  243. chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
  244. chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
  245. chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
  246. chipgraph/packs/spec_core/pack.yml +6 -0
  247. chipgraph/py.typed +0 -0
  248. chipgraph-0.1.0.dist-info/METADATA +53 -0
  249. chipgraph-0.1.0.dist-info/RECORD +252 -0
  250. chipgraph-0.1.0.dist-info/WHEEL +4 -0
  251. chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
  252. chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,342 @@
1
+ """The M2-02a agent rule loop, one submitted attempt at a time (DESIGN.md 5.2).
2
+
3
+ In an in-process runtime `AgentRuleExecutor` runs the whole loop. In runtime
4
+ `claude-code` the user's Claude Code session does each attempt and `submit` checks it,
5
+ so this module applies the same pieces (`chipgraph.core.engine.agent_rule`) to one
6
+ attempt and keeps the loop's count on the task record (`TaskBudgetState`):
7
+
8
+ - checks: `attempt_checks` (inputs, outputs, writes) and the rule's checks, each once;
9
+ - label: `FailureClassifier` over the default `LabelMap`, the one the in-process loop
10
+ uses (deterministic rules only: no model is asked here);
11
+ - budget: `AttemptBudget.for_rule`, restored from the record. An `infra` failure (a
12
+ check that errored, a tool that cannot run) uses no try, and at most
13
+ `max_infra_retries` (2, as in process) of them are retried; two failed tries in a
14
+ row with the same outputs and failures stop the task early (`stagnation`). Tokens and
15
+ cost are not known per task in this runtime (the session reports them only for the
16
+ whole run), so the token and cost caps of the in-process loop do not apply here;
17
+ - redo: `render_feedback`, the bounded redo instruction for the next try. For a role
18
+ that must not see some artifacts (the testbench Author and the RTL, the RTL Author
19
+ and the testbench), what the answer and the redo text show of the failures goes
20
+ through `FeedbackFilter` first: issues in a denied file are dropped, denied paths in
21
+ messages become `<rtl>` (`<tb>`), source excerpt lines are cut, and the redo text says
22
+ that something was withheld (`FeedbackFilter.note`). The label is still taken from
23
+ the full results.
24
+
25
+ `max_dispatches` (tries plus infra retries) is the hard bound on how often one task is
26
+ handed out in a budget cycle: every submit either uses a try or an infra retry, and
27
+ the loop stops when either runs out, so the bound is never reached by a live task.
28
+ `TaskQueue.dispatch` and `next_task` both refuse to go past it.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from collections.abc import Mapping, Sequence
34
+ from dataclasses import dataclass
35
+ from typing import Any
36
+
37
+ from chipgraph.core.contracts import AgentResult, CheckResult, Issue, RuleSpec
38
+ from chipgraph.core.contracts.types import FailureLabel, ModelTier
39
+ from chipgraph.core.engine.agent_rule import (
40
+ DEFAULT_LABEL_MAP,
41
+ DEFAULT_MAX_INFRA_RETRIES,
42
+ AttemptBudget,
43
+ BudgetState,
44
+ FailureClassifier,
45
+ FeedbackFilter,
46
+ failing,
47
+ render_feedback,
48
+ )
49
+ from chipgraph.core.runtime.queue import AgentTaskRecord, BudgetStop, TaskBudgetState, TaskStatus
50
+
51
+ MAX_INFRA_RETRIES = DEFAULT_MAX_INFRA_RETRIES
52
+ """Submits of one task that may fail on infra without using a try (as in process)."""
53
+
54
+ REVIEW_CHECK = "agent.review"
55
+ """Synthetic check id of a review task's reply (invalid JSON, schema, evidence)."""
56
+
57
+ MAX_FAILED_CHECKS = 10
58
+ MAX_ISSUES_PER_CHECK = 8
59
+ MAX_MSG_CHARS = 300
60
+ REDO_MAX_CHARS = 6000
61
+
62
+ _CLASSIFIER = FailureClassifier(label_map=DEFAULT_LABEL_MAP)
63
+
64
+
65
+ def max_dispatches(tries: int, max_infra_retries: int = MAX_INFRA_RETRIES) -> int:
66
+ """The most dispatches of one task in a budget cycle: its tries plus infra retries."""
67
+ return tries + max_infra_retries
68
+
69
+
70
+ def dispatch_limit(record: AgentTaskRecord) -> int:
71
+ """The record's dispatch bound (one from before it was stored: the default)."""
72
+ if record.max_dispatches is not None:
73
+ return record.max_dispatches
74
+ return max_dispatches(record.tries)
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class Verdict:
79
+ """What one submitted attempt comes to: the record's next status and budget state."""
80
+
81
+ status: TaskStatus
82
+ label: FailureLabel | None
83
+ counted: bool
84
+ stop: BudgetStop | None
85
+ tier: ModelTier
86
+ state: TaskBudgetState
87
+ tries: int
88
+ failed: tuple[CheckResult, ...]
89
+ questions: tuple[str, ...] = ()
90
+
91
+ @property
92
+ def accepted(self) -> bool:
93
+ return self.status == "accepted"
94
+
95
+ @property
96
+ def tries_left(self) -> int:
97
+ return max(self.tries - self.state.tries_used, 0)
98
+
99
+
100
+ def _cut(text: str, limit: int) -> str:
101
+ return text if len(text) <= limit else text[: max(limit - 1, 0)] + "…"
102
+
103
+
104
+ def issue_line(issue: Issue) -> str:
105
+ """`file:line: [rule] msg`, as the redo instruction writes an issue."""
106
+ where = issue.file or ""
107
+ if where and issue.line is not None:
108
+ where = f"{where}:{issue.line}"
109
+ rule = f"[{issue.rule}] " if issue.rule else ""
110
+ return f"{where + ': ' if where else ''}{rule}{issue.msg}".strip()
111
+
112
+
113
+ def _errors(result: CheckResult) -> list[Issue]:
114
+ return [i for i in result.issues if i.severity == "error"] or list(result.issues)
115
+
116
+
117
+ def failure_line(result: CheckResult) -> str:
118
+ """One line for a failing check: its id, status and first issue (or log tail)."""
119
+ issues = _errors(result)
120
+ first = _cut(issue_line(issues[0]), 200) if issues else _cut(result.log_tail.strip(), 200)
121
+ more = f" (+{len(issues) - 1} more)" if len(issues) > 1 else ""
122
+ return f"{result.check_id}: {result.status}" + (f": {first}{more}" if first else "")
123
+
124
+
125
+ def failed_checks(results: Sequence[CheckResult]) -> list[dict[str, Any]]:
126
+ """The failing checks for `submit`'s answer: id, status, issues (capped), and the log
127
+ tail when no error issue explains the failure."""
128
+ shown: list[dict[str, Any]] = []
129
+ for result in failing(results)[:MAX_FAILED_CHECKS]:
130
+ issues = _errors(result)
131
+ entry: dict[str, Any] = {
132
+ "check_id": result.check_id,
133
+ "status": result.status,
134
+ "issues": [
135
+ {
136
+ "at": f"{i.file}:{i.line}" if i.file and i.line else i.file,
137
+ "rule": i.rule,
138
+ "msg": _cut(i.msg, MAX_MSG_CHARS),
139
+ }
140
+ for i in issues[:MAX_ISSUES_PER_CHECK]
141
+ ],
142
+ }
143
+ if len(issues) > MAX_ISSUES_PER_CHECK:
144
+ entry["more_issues"] = len(issues) - MAX_ISSUES_PER_CHECK
145
+ if not any(i.severity == "error" for i in issues) and result.log_tail.strip():
146
+ entry["log_tail"] = result.log_tail.strip()[-600:]
147
+ shown.append(entry)
148
+ return shown
149
+
150
+
151
+ def review_result(problems: Sequence[str]) -> CheckResult:
152
+ """A review task's reply as a check: `fail` with one issue per problem."""
153
+ return CheckResult(
154
+ check_id=REVIEW_CHECK,
155
+ status="fail" if problems else "pass",
156
+ issues=tuple(Issue(rule="review.invalid", msg=p) for p in problems),
157
+ duration_s=0.0,
158
+ idempotency_key=f"{REVIEW_CHECK}:{len(problems)}",
159
+ )
160
+
161
+
162
+ def _budget(rule: RuleSpec, prior: TaskBudgetState) -> AttemptBudget:
163
+ budget = AttemptBudget.for_rule(rule)
164
+ budget.state = BudgetState(
165
+ tries_used=prior.tries_used,
166
+ failed_tries=prior.failed_tries,
167
+ infra_failures=prior.infra_failures,
168
+ last_signature=prior.last_signature,
169
+ stagnant=prior.stagnant,
170
+ )
171
+ return budget
172
+
173
+
174
+ async def judge(
175
+ rule: RuleSpec,
176
+ record: AgentTaskRecord,
177
+ results: Sequence[CheckResult],
178
+ agent: AgentResult,
179
+ output_hashes: Mapping[str, str],
180
+ *,
181
+ max_infra_retries: int = MAX_INFRA_RETRIES,
182
+ shown: FeedbackFilter | None = None,
183
+ ) -> Verdict:
184
+ """Count one submitted attempt against the task's budget and say what comes next.
185
+
186
+ `results` are every check of the attempt (the engine's and the rule's);
187
+ `output_hashes` the task's outputs as the attempt left them (for stagnation).
188
+ `shown` filters what the verdict's failures, failure lines, questions and redo text
189
+ show of `results` (see the module docstring); the label and the budget use them all.
190
+ """
191
+ prior = record.state
192
+ budget = _budget(rule, prior)
193
+ tier = record.current_tier
194
+ bad = failing(results)
195
+ common: dict[str, Any] = {"dispatches": prior.dispatches, "tier": tier}
196
+
197
+ def state(**update: Any) -> TaskBudgetState:
198
+ s = budget.state
199
+ return TaskBudgetState(
200
+ tries_used=s.tries_used,
201
+ failed_tries=s.failed_tries,
202
+ infra_failures=s.infra_failures,
203
+ last_signature=s.last_signature,
204
+ stagnant=s.stagnant,
205
+ output_hashes=dict(output_hashes),
206
+ **common,
207
+ **update,
208
+ )
209
+
210
+ if not bad:
211
+ budget.record_pass()
212
+ return Verdict(
213
+ status="accepted",
214
+ label=None,
215
+ counted=True,
216
+ stop=None,
217
+ tier=tier,
218
+ state=state(),
219
+ tries=budget.tries,
220
+ failed=(),
221
+ )
222
+
223
+ label = await _CLASSIFIER.classify(results, agent)
224
+ visible: Sequence[CheckResult] = results
225
+ withheld = 0
226
+ note = ""
227
+ if shown is not None:
228
+ visible, withheld = shown.apply(results)
229
+ note = shown.note
230
+ bad_shown = failing(visible)
231
+ lines = tuple(failure_line(r) for r in bad_shown[:MAX_FAILED_CHECKS])
232
+ ids = tuple(r.check_id for r in bad)
233
+ stop: BudgetStop | None = None
234
+ questions: tuple[str, ...] = ()
235
+ if label == "infra":
236
+ budget.record_infra()
237
+ counted = False
238
+ if budget.state.infra_failures > max_infra_retries:
239
+ stop = "infra"
240
+ redo = render_feedback(
241
+ visible, "infra", max_chars=1500, withheld=withheld, withheld_note=note
242
+ )
243
+ else:
244
+ budget.record_failure(results, output_hashes)
245
+ counted = True
246
+ if label == "planning":
247
+ stop = "planning"
248
+ questions = tuple(
249
+ issue_line(i) for r in bad_shown for i in r.issues if i.severity == "error"
250
+ )[:10] or ("the checks point at the spec or plan; a person must decide",)
251
+ else:
252
+ reason = budget.exhausted # tries | stagnation (no token or cost caps here)
253
+ if reason == "tries" or reason == "stagnation":
254
+ stop = reason
255
+ redo = render_feedback(
256
+ visible,
257
+ label,
258
+ attempt=budget.state.tries_used,
259
+ max_chars=REDO_MAX_CHARS,
260
+ withheld=withheld,
261
+ withheld_note=note,
262
+ )
263
+ status: TaskStatus
264
+ if stop == "planning":
265
+ status = "needs_human"
266
+ elif stop is not None:
267
+ status = "budget_exhausted"
268
+ else:
269
+ status = "rejected"
270
+ return Verdict(
271
+ status=status,
272
+ label=label,
273
+ counted=counted,
274
+ stop=stop,
275
+ tier=tier,
276
+ state=state(label=label, failed_checks=ids, failures=lines, redo=redo, stop=stop),
277
+ tries=budget.tries,
278
+ failed=bad_shown,
279
+ questions=questions,
280
+ )
281
+
282
+
283
+ # --- the stop, for the journal and HANDOFF.md ---------------------------------------------
284
+
285
+ _STOP_STATUS: dict[str, str] = {"planning": "needs_human"}
286
+
287
+
288
+ def stop_info(record: AgentTaskRecord, *, stopped_earlier: bool = False) -> dict[str, Any]:
289
+ """The `agent` payload of a stopped task's `rule_fail` (what `AgentStopInfo` reads)."""
290
+ state = record.state
291
+ reason = state.stop
292
+ return {
293
+ "instance": record.task_id,
294
+ "status": _STOP_STATUS.get(reason or "", "budget_exhausted"),
295
+ "tries": state.tries_used,
296
+ "max_tries": record.tries,
297
+ "infra_failures": state.infra_failures,
298
+ "calls": state.dispatches,
299
+ "reason": reason,
300
+ "label": state.label,
301
+ "tier": state.tier,
302
+ "failed_checks": list(state.failed_checks),
303
+ "failures": list(state.failures),
304
+ "tokens": None,
305
+ "cost": None,
306
+ "stopped_earlier": stopped_earlier,
307
+ }
308
+
309
+
310
+ _STOP_HEAD = {
311
+ "tries": "budget exhausted (tries)",
312
+ "stagnation": "stopped early: two tries in a row left the same outputs and failures",
313
+ "infra": "stopped: infrastructure errors persisted",
314
+ "planning": "stopped: the checks point at the spec or plan",
315
+ }
316
+
317
+
318
+ def stop_message(info: Mapping[str, Any]) -> str:
319
+ """The `rule_fail` message of a stopped task, worded as the in-process loop's."""
320
+ tries = f"{info['tries']} of {info['max_tries']} tries"
321
+ infra = int(info["infra_failures"])
322
+ if infra:
323
+ tries += f", {infra} infra failure{'' if infra == 1 else 's'}"
324
+ failed = ", ".join(info["failed_checks"]) or "none"
325
+ head = _STOP_HEAD.get(str(info["reason"]), "stopped")
326
+ return f"agent {head} after {tries}; last failures [{info['label']}]: {failed}"
327
+
328
+
329
+ __all__ = [
330
+ "MAX_INFRA_RETRIES",
331
+ "REVIEW_CHECK",
332
+ "Verdict",
333
+ "dispatch_limit",
334
+ "failed_checks",
335
+ "failure_line",
336
+ "issue_line",
337
+ "judge",
338
+ "max_dispatches",
339
+ "review_result",
340
+ "stop_info",
341
+ "stop_message",
342
+ ]