py-harness-cli 0.3.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.
- finetune/__init__.py +1 -0
- finetune/agent_system.py +41 -0
- finetune/agent_traces.py +157 -0
- finetune/everyday.py +30 -0
- finetune/hf_ollama.py +158 -0
- finetune/huggingface_store.py +144 -0
- finetune/models.py +74 -0
- finetune/paths.py +11 -0
- finetune/python_vibe.py +788 -0
- finetune/splits.py +54 -0
- finetune/systems.py +9 -0
- harness/__init__.py +42 -0
- harness/__main__.py +8 -0
- harness/act/__init__.py +6 -0
- harness/act/autofix/__init__.py +110 -0
- harness/act/autofix/additions.py +217 -0
- harness/act/autofix/conflicts.py +124 -0
- harness/act/autofix/cover.py +419 -0
- harness/act/autofix/mechanical.py +151 -0
- harness/act/autofix/missing_imports.py +50 -0
- harness/act/autofix/moves.py +439 -0
- harness/act/autofix/names.py +339 -0
- harness/act/autofix/scaffold.py +224 -0
- harness/act/code.py +157 -0
- harness/act/gate.py +229 -0
- harness/act/parse.py +247 -0
- harness/act/patch_fix.py +138 -0
- harness/act/tools.py +244 -0
- harness/agent/__init__.py +11 -0
- harness/agent/dispatch.py +235 -0
- harness/agent/loop.py +699 -0
- harness/agent/options.py +144 -0
- harness/agent/policy.py +856 -0
- harness/agent/prompt.py +170 -0
- harness/cli.py +393 -0
- harness/editor_kit.py +265 -0
- harness/guard/__init__.py +6 -0
- harness/guard/fallbacks.py +6 -0
- harness/guard/loop_guard.py +57 -0
- harness/guard/python_vibe.py +68 -0
- harness/guard/run.py +41 -0
- harness/guard/types.py +19 -0
- harness/locate.py +767 -0
- harness/mcp_stdio.py +306 -0
- harness/memory/__init__.py +5 -0
- harness/memory/conversation.py +104 -0
- harness/model/__init__.py +6 -0
- harness/model/chat_backend.py +100 -0
- harness/model/engine.py +165 -0
- harness/model/ollama_generate.py +60 -0
- harness/model/openai_generate.py +156 -0
- harness/model/outbound.py +83 -0
- harness/model/route.py +90 -0
- harness/observe/__init__.py +6 -0
- harness/observe/eval_gate.py +80 -0
- harness/observe/eval_loop.py +185 -0
- harness/observe/eval_tasks.py +399 -0
- harness/observe/report_md.py +102 -0
- harness/observe/trace_record.py +79 -0
- harness/openai_api.py +81 -0
- harness/paths.py +88 -0
- harness/py.typed +0 -0
- harness/scan/__init__.py +6 -0
- harness/scan/app_spec.py +338 -0
- harness/scan/design.py +112 -0
- harness/scan/existing.py +131 -0
- harness/scan/layout.py +254 -0
- harness/scan/names.py +308 -0
- harness/scan/project_brief.py +287 -0
- harness/scan/project_docs.py +42 -0
- harness/scan/project_scan.py +49 -0
- harness/scan/repo_map.py +101 -0
- harness/secrets.py +39 -0
- harness/server.py +199 -0
- harness/ship/__init__.py +1 -0
- harness/ship/bot_pr.py +221 -0
- harness/ship/git_ship.py +262 -0
- harness/ship/identity.py +62 -0
- harness/ship/ticket.py +251 -0
- harness/skillkit/__init__.py +6 -0
- harness/skillkit/catalog.py +241 -0
- harness/skillkit/refuse_change.py +640 -0
- harness/skillkit/refuse_finish.py +295 -0
- harness/skillkit/target.py +238 -0
- harness/task.py +717 -0
- py_harness_cli-0.3.0.dist-info/METADATA +177 -0
- py_harness_cli-0.3.0.dist-info/RECORD +92 -0
- py_harness_cli-0.3.0.dist-info/WHEEL +5 -0
- py_harness_cli-0.3.0.dist-info/entry_points.txt +3 -0
- py_harness_cli-0.3.0.dist-info/licenses/LICENSE +202 -0
- py_harness_cli-0.3.0.dist-info/licenses/NOTICE +6 -0
- py_harness_cli-0.3.0.dist-info/top_level.txt +2 -0
harness/locate.py
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
"""Deterministic first steps for a small everyday model. No LLM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from harness.act.tools import grep_py, read_py
|
|
9
|
+
from harness.scan.names import undefined_names
|
|
10
|
+
from harness.task import looks_like_question, question_symbol
|
|
11
|
+
from harness.task import looks_like_add_feature
|
|
12
|
+
from harness.task import (
|
|
13
|
+
covered_symbol,
|
|
14
|
+
everyday_example_path,
|
|
15
|
+
everyday_skill_name,
|
|
16
|
+
named_project_file,
|
|
17
|
+
looks_like_bugfix,
|
|
18
|
+
looks_like_design_loop,
|
|
19
|
+
looks_like_everyday_code,
|
|
20
|
+
looks_like_fix_smell,
|
|
21
|
+
looks_like_new_package,
|
|
22
|
+
looks_like_refactor,
|
|
23
|
+
looks_like_review,
|
|
24
|
+
looks_like_write_tests,
|
|
25
|
+
smell_symbol,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_DEF = re.compile(r"\b(?:def|class)\s+([A-Za-z_][A-Za-z0-9_]*)")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def subject_of(task: str) -> str:
|
|
32
|
+
"""The longest dotted name in the task, or "".
|
|
33
|
+
|
|
34
|
+
A name like `result.stopped` is the strongest hint a task gives
|
|
35
|
+
about *where* in a file the work is, and it is what an excerpt
|
|
36
|
+
should be centred on. Without it the model was shown the first
|
|
37
|
+
3,500 characters and the last 800 of a 13,476-character file, and
|
|
38
|
+
the dict it had been asked to change was in neither.
|
|
39
|
+
"""
|
|
40
|
+
words = re.findall(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+", task)
|
|
41
|
+
dotted = [word for word in words if not word.endswith((".py", ".md", ".txt"))]
|
|
42
|
+
return max(dotted, key=len) if dotted else ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def signature_line(text: str, symbol: str) -> str:
|
|
46
|
+
if not symbol:
|
|
47
|
+
return ""
|
|
48
|
+
needle = f"def {symbol}("
|
|
49
|
+
for raw in text.splitlines():
|
|
50
|
+
line = raw.strip()
|
|
51
|
+
if line.startswith("#"):
|
|
52
|
+
continue
|
|
53
|
+
if raw.count(":") >= 2 and not raw.lstrip().startswith(("def ", "class ", "async ")):
|
|
54
|
+
line = raw.split(":", 2)[-1].strip()
|
|
55
|
+
if needle in line:
|
|
56
|
+
return line.rstrip()
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def return_annotation(signature: str) -> str:
|
|
61
|
+
if "->" not in signature:
|
|
62
|
+
return ""
|
|
63
|
+
return signature.split("->", 1)[1].rstrip(":").strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _compact(text: str) -> str:
|
|
67
|
+
return re.sub(r"\s+", "", text).lower()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_ASKS_RETURN = re.compile(r"\b(return|returns|returned|type|give back|output)\b", re.I)
|
|
71
|
+
MIN_DESCRIPTION_WORDS = 4
|
|
72
|
+
# Words a return-type answer must add beyond the type itself. The
|
|
73
|
+
# bare answer to "what does compute_total return?" was `"int"`, which
|
|
74
|
+
# is the annotation read back, not what the function does. Two extra
|
|
75
|
+
# words is enough for "compute_total sums int"; asking for four
|
|
76
|
+
# rejected answers a person would accept, and the loop then spent
|
|
77
|
+
# every remaining step asking again.
|
|
78
|
+
MIN_EXTRA_WORDS = 2
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def asks_what_it_returns(task: str) -> bool:
|
|
82
|
+
"""True for "what does X return?", false for "what does X do?"."""
|
|
83
|
+
return bool(_ASKS_RETURN.search(task))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def refuse_shallow_done(task: str, summary: str, signature: str) -> str:
|
|
87
|
+
"""Refuse an answer that is thinner than the question asked for.
|
|
88
|
+
|
|
89
|
+
Quoting the return type was added because the model answered "a tuple".
|
|
90
|
+
It was applied to every question, so "what does apply_discount do?" was
|
|
91
|
+
refused for the answer "it reduces a total by a whole percentage" —
|
|
92
|
+
the harness insisting on a worse reply than the one it was given.
|
|
93
|
+
"""
|
|
94
|
+
if not looks_like_question(task):
|
|
95
|
+
return ""
|
|
96
|
+
if not asks_what_it_returns(task):
|
|
97
|
+
# A question about behaviour wants a sentence, not a type name.
|
|
98
|
+
if len((summary or "").split()) >= MIN_DESCRIPTION_WORDS:
|
|
99
|
+
return ""
|
|
100
|
+
return (
|
|
101
|
+
"too thin. Action: done Summary: say in a sentence what it does, "
|
|
102
|
+
"from the code you read."
|
|
103
|
+
)
|
|
104
|
+
wanted = return_annotation(signature)
|
|
105
|
+
if not wanted:
|
|
106
|
+
return ""
|
|
107
|
+
compact = _compact(summary or "")
|
|
108
|
+
if _compact(wanted) not in compact:
|
|
109
|
+
return (
|
|
110
|
+
f"too thin. Action: done Summary: must quote {wanted} "
|
|
111
|
+
f"from {signature} and say what it computes."
|
|
112
|
+
)
|
|
113
|
+
extra = [
|
|
114
|
+
word
|
|
115
|
+
for word in re.findall(r"[A-Za-z0-9_]+", summary or "")
|
|
116
|
+
if _compact(word) not in _compact(wanted)
|
|
117
|
+
]
|
|
118
|
+
if len(extra) < MIN_EXTRA_WORDS:
|
|
119
|
+
return (
|
|
120
|
+
f"too thin. Action: done Summary: quote {wanted} and say in a "
|
|
121
|
+
"sentence what it computes, from the code you read."
|
|
122
|
+
)
|
|
123
|
+
return ""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def def_hit_path(grep_text: str, symbol: str) -> str:
|
|
127
|
+
wanted = symbol.strip()
|
|
128
|
+
if not wanted or grep_text.startswith(("(no hits)", "bad regex")):
|
|
129
|
+
return ""
|
|
130
|
+
fallback = ""
|
|
131
|
+
for line in grep_text.splitlines():
|
|
132
|
+
if line.startswith("#") or line.count(":") < 2:
|
|
133
|
+
continue
|
|
134
|
+
path, _ln, content = line.split(":", 2)
|
|
135
|
+
if not fallback:
|
|
136
|
+
fallback = path
|
|
137
|
+
if re.search(rf"\b(?:def|class)\s+{re.escape(wanted)}\b", content):
|
|
138
|
+
return path
|
|
139
|
+
return fallback
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def locate_py(project: Path, query: str, scope: str = "") -> tuple[str, str]:
|
|
143
|
+
if not query.strip():
|
|
144
|
+
return "locate needs Query:", ""
|
|
145
|
+
hits = grep_py(project, query, scope=scope)
|
|
146
|
+
symbol = query.removeprefix("def ").removeprefix("class ").split()[0]
|
|
147
|
+
path = def_hit_path(hits, symbol)
|
|
148
|
+
if not path:
|
|
149
|
+
return hits, ""
|
|
150
|
+
body = read_py(project, path)
|
|
151
|
+
return f"{hits}\n\n# auto-read {path}\n{body}", path
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def prelude(project: Path, task: str, scope: str = "") -> tuple[str, str]:
|
|
155
|
+
"""Search before the model runs, and say what to do with what was found.
|
|
156
|
+
|
|
157
|
+
Small models skip the first grep, so the harness does it for them and
|
|
158
|
+
hands over the file with an instruction attached.
|
|
159
|
+
|
|
160
|
+
Each kind of task needs a different opening, so each one is its own
|
|
161
|
+
function below and this only chooses between them. They were a single
|
|
162
|
+
if-chain of a hundred and forty lines, which made the shared tail at
|
|
163
|
+
the end read as if it belonged to whichever branch you had just
|
|
164
|
+
finished reading.
|
|
165
|
+
"""
|
|
166
|
+
if looks_like_new_package(task):
|
|
167
|
+
return "", ""
|
|
168
|
+
from harness.task import looks_like_app_overflow, package_noun
|
|
169
|
+
|
|
170
|
+
if looks_like_app_overflow(task):
|
|
171
|
+
from harness.scan.app_spec import overflow_edit_line
|
|
172
|
+
|
|
173
|
+
line = overflow_edit_line(task, project).rstrip(".")
|
|
174
|
+
dest = (
|
|
175
|
+
"pkg/config.py"
|
|
176
|
+
if "pkg/config.py" in line
|
|
177
|
+
else f"pkg/{package_noun(task)}.py"
|
|
178
|
+
)
|
|
179
|
+
return (f"{line}. Do not grep.\n", dest)
|
|
180
|
+
for opening in (
|
|
181
|
+
_opening_for_design_loop,
|
|
182
|
+
_opening_for_write_tests,
|
|
183
|
+
_opening_for_a_named_file,
|
|
184
|
+
):
|
|
185
|
+
found = opening(project, task, scope)
|
|
186
|
+
if found is not None:
|
|
187
|
+
return found
|
|
188
|
+
return _opening_found_by_symbol(project, task, scope)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _opening_for_design_loop(
|
|
192
|
+
project: Path, task: str, scope: str
|
|
193
|
+
) -> tuple[str, str] | None:
|
|
194
|
+
"""A review or refactor starts from the structure report, not a file."""
|
|
195
|
+
if not looks_like_design_loop(task):
|
|
196
|
+
return None
|
|
197
|
+
from harness.scan.design import design_is_clean, render_design_review
|
|
198
|
+
|
|
199
|
+
report = render_design_review(project, scope)
|
|
200
|
+
kind = "refactor" if looks_like_refactor(task) and not looks_like_review(task) else "review"
|
|
201
|
+
if design_is_clean(report):
|
|
202
|
+
next_line = (
|
|
203
|
+
"Next Action must be done. Summary: quote no structure findings."
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
next_line = (
|
|
207
|
+
"Next Action must be edit Path: pkg/<new_concern>.py with one function."
|
|
208
|
+
)
|
|
209
|
+
return f"Harness design review ({kind})\n{next_line}\n\n{report}", ""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _opening_for_write_tests(
|
|
213
|
+
project: Path, task: str, scope: str
|
|
214
|
+
) -> tuple[str, str] | None:
|
|
215
|
+
"""Writing a test needs the subject located and a destination chosen."""
|
|
216
|
+
if not looks_like_write_tests(task):
|
|
217
|
+
return None
|
|
218
|
+
symbol = covered_symbol(task) or question_symbol(task)
|
|
219
|
+
dest = named_project_file(task, project)
|
|
220
|
+
rel = dest.replace("\\", "/").lower()
|
|
221
|
+
if dest and "test" not in rel and not rel.split("/")[-1].startswith("test_"):
|
|
222
|
+
dest = ""
|
|
223
|
+
if not dest and symbol:
|
|
224
|
+
dest = f"tests/test_{symbol.split('.')[-1]}.py"
|
|
225
|
+
if not dest:
|
|
226
|
+
dest = "tests/test_module.py"
|
|
227
|
+
text, path = locate_py(project, symbol, scope) if symbol else ("", "")
|
|
228
|
+
header = (
|
|
229
|
+
f"Harness locate (write-tests) Query: {symbol or 'the function'}\n"
|
|
230
|
+
f"Next Action must be patch Path: {dest} Append: one AAA "
|
|
231
|
+
f"test_<unit>_<result> that calls {symbol or 'the function'}.\n"
|
|
232
|
+
"Do not edit the implementation. Do not ask."
|
|
233
|
+
)
|
|
234
|
+
return f"{header}\n\n{text}", path
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _opening_for_a_named_file(
|
|
238
|
+
project: Path, task: str, scope: str
|
|
239
|
+
) -> tuple[str, str] | None:
|
|
240
|
+
"""Open the file the task names, and say what may be done to it.
|
|
241
|
+
|
|
242
|
+
A task that names a file has already said which file to open.
|
|
243
|
+
Looking up a word out of that path instead found every file in the
|
|
244
|
+
project: "src/harness/model/engine.py" was searched for as
|
|
245
|
+
"harness".
|
|
246
|
+
|
|
247
|
+
`scope` is unused here and kept so every opening has one shape and
|
|
248
|
+
the caller can try them in turn.
|
|
249
|
+
"""
|
|
250
|
+
named = named_project_file(task, project)
|
|
251
|
+
if named:
|
|
252
|
+
try:
|
|
253
|
+
body = read_py(project, named, about=subject_of(task))
|
|
254
|
+
except (OSError, ValueError):
|
|
255
|
+
body = ""
|
|
256
|
+
if body:
|
|
257
|
+
if looks_like_question(task):
|
|
258
|
+
next_line = (
|
|
259
|
+
"Next Action must be done. Quote what this file does. "
|
|
260
|
+
"Do not grep, read, or edit."
|
|
261
|
+
)
|
|
262
|
+
elif reviews_one_named_file(task):
|
|
263
|
+
findings = named_file_review_summary(project, task)
|
|
264
|
+
extra = f"\n{findings}" if findings else ""
|
|
265
|
+
next_line = (
|
|
266
|
+
"Next Action must be done. Quote a defect from the "
|
|
267
|
+
"findings below. Do not patch, edit, or run."
|
|
268
|
+
f"{extra}"
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
next_line = (
|
|
272
|
+
f"Next Action must be patch Path: {named} with a Find: "
|
|
273
|
+
"line copied whole from the file below, and a Replace:. "
|
|
274
|
+
"Do not map or grep."
|
|
275
|
+
)
|
|
276
|
+
return (
|
|
277
|
+
f"Harness opened the file named in the task: {named}\n"
|
|
278
|
+
f"{next_line}\n"
|
|
279
|
+
f"Only {named} may be changed.\n\n"
|
|
280
|
+
f"# auto-read {named}\n{body}",
|
|
281
|
+
named,
|
|
282
|
+
)
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _opening_found_by_symbol(
|
|
287
|
+
project: Path, task: str, scope: str
|
|
288
|
+
) -> tuple[str, str]:
|
|
289
|
+
"""Nothing named a file, so find the symbol and say what to do with it."""
|
|
290
|
+
symbol = smell_symbol(task) if looks_like_fix_smell(task) else question_symbol(task)
|
|
291
|
+
if not symbol and looks_like_add_feature(task):
|
|
292
|
+
symbol = question_symbol(task) or ""
|
|
293
|
+
if not symbol:
|
|
294
|
+
return "", ""
|
|
295
|
+
text, path = locate_py(project, symbol, scope)
|
|
296
|
+
if looks_like_question(task):
|
|
297
|
+
kind = "question"
|
|
298
|
+
elif looks_like_fix_smell(task):
|
|
299
|
+
kind = "fix-smell"
|
|
300
|
+
elif looks_like_everyday_code(task):
|
|
301
|
+
kind = everyday_skill_name(task) or "everyday"
|
|
302
|
+
else:
|
|
303
|
+
kind = "add-feature"
|
|
304
|
+
header = f"Harness locate ({kind}) Query: {symbol}"
|
|
305
|
+
header += _what_to_do_next(project, task, symbol, text, path)
|
|
306
|
+
return f"{header}\n\n{text}", path
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _what_to_do_next(
|
|
310
|
+
project: Path, task: str, symbol: str, text: str, path: str
|
|
311
|
+
) -> str:
|
|
312
|
+
"""The instruction attached to what was found, or "" for none.
|
|
313
|
+
|
|
314
|
+
One line per kind of task. An 8B follows the first instruction it
|
|
315
|
+
sees, so there is exactly one and it names the action, the path and
|
|
316
|
+
the shape of the edit.
|
|
317
|
+
"""
|
|
318
|
+
header = ""
|
|
319
|
+
if looks_like_question(task) and path:
|
|
320
|
+
header += (
|
|
321
|
+
"\nNext Action must be done. Do not locate, grep, or read."
|
|
322
|
+
)
|
|
323
|
+
sig = signature_line(text, symbol)
|
|
324
|
+
if return_annotation(sig):
|
|
325
|
+
header += (
|
|
326
|
+
f"\nSummary must quote the -> type from: {sig} "
|
|
327
|
+
"and say what the function computes."
|
|
328
|
+
)
|
|
329
|
+
elif looks_like_fix_smell(task) and path:
|
|
330
|
+
header += (
|
|
331
|
+
"\nNext Action must be patch Find: the old def line "
|
|
332
|
+
"Replace: a readable snake_case name. Do not grep."
|
|
333
|
+
)
|
|
334
|
+
elif looks_like_everyday_code(task):
|
|
335
|
+
example = everyday_example_path(task)
|
|
336
|
+
header += (
|
|
337
|
+
f"\nNext Action must be edit Path: {example} with one function. "
|
|
338
|
+
"Do not grep. Do not emit curl."
|
|
339
|
+
)
|
|
340
|
+
elif looks_like_add_feature(task):
|
|
341
|
+
from harness.skillkit.target import pick_module
|
|
342
|
+
|
|
343
|
+
dest = path or pick_module(project, path, task)
|
|
344
|
+
header += (
|
|
345
|
+
f"\nNext Action must be patch Path: {dest} "
|
|
346
|
+
f"Append: def {symbol}(...). Do not grep. Do not create a second "
|
|
347
|
+
f"{Path(dest).stem}.py."
|
|
348
|
+
)
|
|
349
|
+
dest_path = Path(project) / dest
|
|
350
|
+
try:
|
|
351
|
+
dest_body = dest_path.read_text(encoding="utf-8")
|
|
352
|
+
except OSError:
|
|
353
|
+
dest_body = ""
|
|
354
|
+
names = [
|
|
355
|
+
name
|
|
356
|
+
for name in re.findall(r"^def \w+\((\w+)", dest_body, re.M)
|
|
357
|
+
if name not in {"self", "cls"}
|
|
358
|
+
]
|
|
359
|
+
# Only when the task left the argument open. `read_env_file(path)`
|
|
360
|
+
# has already said what it takes, and telling the model to use the
|
|
361
|
+
# neighbours' `prices` instead sent it round the loop until the
|
|
362
|
+
# steps ran out.
|
|
363
|
+
from harness.skillkit.refuse_change import task_names_arguments
|
|
364
|
+
|
|
365
|
+
if names and not task_names_arguments(task):
|
|
366
|
+
neighbor = max(set(names), key=names.count)
|
|
367
|
+
header += (
|
|
368
|
+
f" Neighbor functions take `{neighbor}`. Use the same "
|
|
369
|
+
"argument unless the task says otherwise."
|
|
370
|
+
)
|
|
371
|
+
return header
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
_QUESTION_WRITE = frozenset({"patch", "edit", "run"})
|
|
375
|
+
_QUESTION_REEXPLORE = frozenset({"read", "locate", "grep"})
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def refuse_redundant_locate(
|
|
379
|
+
task: str, action: str, prelude_ran: bool, project: Path | None = None
|
|
380
|
+
) -> str:
|
|
381
|
+
if action != "locate":
|
|
382
|
+
return ""
|
|
383
|
+
from harness.task import looks_like_app_overflow
|
|
384
|
+
|
|
385
|
+
if looks_like_app_overflow(task):
|
|
386
|
+
from harness.scan.app_spec import overflow_edit_line
|
|
387
|
+
|
|
388
|
+
return "already located. " + overflow_edit_line(task, project)
|
|
389
|
+
if looks_like_new_package(task):
|
|
390
|
+
from harness.task import looks_like_app_loop, package_noun
|
|
391
|
+
|
|
392
|
+
noun = package_noun(task)
|
|
393
|
+
if looks_like_app_loop(task):
|
|
394
|
+
return (
|
|
395
|
+
f"already located. Action: edit Path: pkg/{noun}.py with "
|
|
396
|
+
"urllib.request and argparse subcommand list. No curl. "
|
|
397
|
+
"Do not ask."
|
|
398
|
+
)
|
|
399
|
+
return (
|
|
400
|
+
f"already located. Action: edit Path: pkg/{noun}.py with one "
|
|
401
|
+
"function. Do not ask."
|
|
402
|
+
)
|
|
403
|
+
if not prelude_ran:
|
|
404
|
+
return ""
|
|
405
|
+
if looks_like_question(task):
|
|
406
|
+
return (
|
|
407
|
+
"already located. Action: done Summary: quote the -> type."
|
|
408
|
+
)
|
|
409
|
+
if looks_like_everyday_code(task):
|
|
410
|
+
example = everyday_example_path(task)
|
|
411
|
+
return (
|
|
412
|
+
f"already located. Action: edit Path: {example} with one function."
|
|
413
|
+
)
|
|
414
|
+
if looks_like_add_feature(task):
|
|
415
|
+
dest = ""
|
|
416
|
+
if project is not None:
|
|
417
|
+
from harness.skillkit.target import pick_module
|
|
418
|
+
|
|
419
|
+
dest = pick_module(project, "", task)
|
|
420
|
+
where = f" Path: {dest}" if dest else ""
|
|
421
|
+
return (
|
|
422
|
+
f"already located. Action: patch{where} Append: the new function."
|
|
423
|
+
)
|
|
424
|
+
return ""
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def refuse_app_overflow_explore(task: str, action: str) -> str:
|
|
428
|
+
"""Overflow already names pkg/pr_review.py. Live 8B grepped comment for 20 steps."""
|
|
429
|
+
from harness.task import looks_like_app_overflow
|
|
430
|
+
|
|
431
|
+
if not looks_like_app_overflow(task) or action not in {"grep", "locate", "map"}:
|
|
432
|
+
return ""
|
|
433
|
+
from harness.scan.app_spec import overflow_edit_line
|
|
434
|
+
|
|
435
|
+
return f"Do not grep. {overflow_edit_line(task)}"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def refuse_app_ask(task: str, action: str) -> str:
|
|
439
|
+
"""A greenfield CLI is not a clarifying question. Live 8B asked and stopped."""
|
|
440
|
+
from harness.task import looks_like_app_loop, package_noun
|
|
441
|
+
|
|
442
|
+
if action != "ask":
|
|
443
|
+
return ""
|
|
444
|
+
from harness.task import looks_like_app_overflow
|
|
445
|
+
|
|
446
|
+
if looks_like_app_overflow(task):
|
|
447
|
+
from harness.scan.app_spec import overflow_edit_line
|
|
448
|
+
|
|
449
|
+
return f"Do not ask. {overflow_edit_line(task)}"
|
|
450
|
+
if not looks_like_app_loop(task):
|
|
451
|
+
return ""
|
|
452
|
+
noun = package_noun(task)
|
|
453
|
+
return (
|
|
454
|
+
f"Do not ask. Action: edit Path: pkg/{noun}.py with urllib.request "
|
|
455
|
+
"and argparse subcommand list."
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def refuse_app_tests_first(task: str, project: Path | None, action: str, path: str) -> str:
|
|
460
|
+
"""Tests come after list/show exist. The live demo patched tests that were not there."""
|
|
461
|
+
from harness.task import looks_like_app_loop, package_noun
|
|
462
|
+
|
|
463
|
+
if not looks_like_app_loop(task) or action not in {"edit", "patch"}:
|
|
464
|
+
return ""
|
|
465
|
+
rel = (path or "").replace("\\", "/").lower()
|
|
466
|
+
if "test" not in rel:
|
|
467
|
+
return ""
|
|
468
|
+
if project is None:
|
|
469
|
+
return ""
|
|
470
|
+
from harness.scan.app_spec import required_gaps
|
|
471
|
+
|
|
472
|
+
keys = {gap.key for gap in required_gaps(project, task)}
|
|
473
|
+
if keys & {"http", "list", "show"}:
|
|
474
|
+
noun = package_noun(task)
|
|
475
|
+
return f"Implementation first. Action: edit Path: pkg/{noun}.py"
|
|
476
|
+
return ""
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def refuse_bugfix_tests_first(
|
|
480
|
+
task: str, project: Path | None, action: str, path: str
|
|
481
|
+
) -> str:
|
|
482
|
+
"""A named-file bugfix whose symbol already has a test is not a write-tests job.
|
|
483
|
+
|
|
484
|
+
Live 8B rewrote tests/test_util_stats.py on the everyday-ready fixture
|
|
485
|
+
and never patched compute_total.
|
|
486
|
+
"""
|
|
487
|
+
if not looks_like_bugfix(task) or action not in {"edit", "patch"}:
|
|
488
|
+
return ""
|
|
489
|
+
if project is None:
|
|
490
|
+
return ""
|
|
491
|
+
named = named_project_file(task, project)
|
|
492
|
+
if not named:
|
|
493
|
+
return ""
|
|
494
|
+
rel = (path or "").replace("\\", "/")
|
|
495
|
+
parts = [part for part in rel.split("/") if part]
|
|
496
|
+
if "tests" not in parts and not (parts and parts[-1].startswith("test_")):
|
|
497
|
+
return ""
|
|
498
|
+
from harness.skillkit.refuse_finish import tests_call
|
|
499
|
+
|
|
500
|
+
symbol = covered_symbol(task) or question_symbol(task)
|
|
501
|
+
if not symbol or not tests_call(project, symbol):
|
|
502
|
+
return ""
|
|
503
|
+
return (
|
|
504
|
+
f"The test already covers {symbol}. "
|
|
505
|
+
f"Action: patch Path: {named} with a Find: line copied whole from the file."
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def refuse_bugfix_explore(
|
|
510
|
+
task: str, project: Path | None, action: str, located_path: str = ""
|
|
511
|
+
) -> str:
|
|
512
|
+
"""The named impl is already open. Live 8B explored 12 steps and wrote nothing."""
|
|
513
|
+
if not looks_like_bugfix(task) or action not in {
|
|
514
|
+
"grep",
|
|
515
|
+
"locate",
|
|
516
|
+
"map",
|
|
517
|
+
"ask",
|
|
518
|
+
"read",
|
|
519
|
+
}:
|
|
520
|
+
return ""
|
|
521
|
+
if project is None:
|
|
522
|
+
return ""
|
|
523
|
+
named = named_project_file(task, project)
|
|
524
|
+
if not named:
|
|
525
|
+
return ""
|
|
526
|
+
if action == "read" and not located_path:
|
|
527
|
+
return ""
|
|
528
|
+
return (
|
|
529
|
+
f"Do not {action}. Action: patch Path: {named} with a Find: line "
|
|
530
|
+
"copied whole from the file."
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def refuse_app_wrong_path(task: str, action: str, path: str) -> str:
|
|
535
|
+
"""Live 8B wrote pkg/pull_viewer.py and pkg.py after the hint named pr_review."""
|
|
536
|
+
from harness.task import looks_like_app_loop, looks_like_app_overflow, package_noun
|
|
537
|
+
|
|
538
|
+
if action not in {"edit", "patch"}:
|
|
539
|
+
return ""
|
|
540
|
+
if not looks_like_app_loop(task) and not looks_like_app_overflow(task):
|
|
541
|
+
return ""
|
|
542
|
+
rel = (path or "").replace("\\", "/").lstrip("./")
|
|
543
|
+
if not rel:
|
|
544
|
+
return ""
|
|
545
|
+
noun = package_noun(task)
|
|
546
|
+
allowed = (
|
|
547
|
+
"pkg/__init__.py",
|
|
548
|
+
f"pkg/{noun}.py",
|
|
549
|
+
f"tests/test_{noun}.py",
|
|
550
|
+
"pkg/config.py",
|
|
551
|
+
)
|
|
552
|
+
if rel in allowed:
|
|
553
|
+
return ""
|
|
554
|
+
return f"The module is pkg/{noun}.py. Action: edit Path: pkg/{noun}.py"
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def refuse_write_tests_ask(task: str, action: str) -> str:
|
|
558
|
+
"""Cover-test jobs name the symbol. Asking where tests live wastes the step."""
|
|
559
|
+
if action != "ask" or not looks_like_write_tests(task):
|
|
560
|
+
return ""
|
|
561
|
+
symbol = covered_symbol(task)
|
|
562
|
+
dest = f"tests/test_{symbol}.py" if symbol else "tests/test_<unit>.py"
|
|
563
|
+
return (
|
|
564
|
+
"Do not ask. Action: patch Path: "
|
|
565
|
+
f"{dest} Append: one AAA test_<unit>_<result> method."
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
_INVENTED = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]{5,})\b")
|
|
570
|
+
_INVENTED_SKIP = frozenset(
|
|
571
|
+
{
|
|
572
|
+
"function",
|
|
573
|
+
"returns",
|
|
574
|
+
"return",
|
|
575
|
+
"empty",
|
|
576
|
+
"input",
|
|
577
|
+
"output",
|
|
578
|
+
"should",
|
|
579
|
+
"because",
|
|
580
|
+
"however",
|
|
581
|
+
"potential",
|
|
582
|
+
"defects",
|
|
583
|
+
"defect",
|
|
584
|
+
"errors",
|
|
585
|
+
"found",
|
|
586
|
+
"change",
|
|
587
|
+
"would",
|
|
588
|
+
"summary",
|
|
589
|
+
"action",
|
|
590
|
+
"module",
|
|
591
|
+
"caller",
|
|
592
|
+
"callers",
|
|
593
|
+
"formatted",
|
|
594
|
+
"present",
|
|
595
|
+
"values",
|
|
596
|
+
"counts",
|
|
597
|
+
"measured",
|
|
598
|
+
"estimated",
|
|
599
|
+
}
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def refuse_invented_review(task: str, summary: str, body: str) -> str:
|
|
604
|
+
"""Refuse a review that names a function the file does not contain.
|
|
605
|
+
|
|
606
|
+
Live 8B on a real tree invented compute_total and estimate_tokens after
|
|
607
|
+
reading an unrelated OpenSRE file. Demo-task prior, not a finding.
|
|
608
|
+
"""
|
|
609
|
+
from harness.task import looks_like_review_code
|
|
610
|
+
|
|
611
|
+
if not looks_like_review_code(task):
|
|
612
|
+
return ""
|
|
613
|
+
if not (summary or "").strip() or not (body or "").strip():
|
|
614
|
+
return ""
|
|
615
|
+
invented: list[str] = []
|
|
616
|
+
for name in _INVENTED.findall(summary):
|
|
617
|
+
if name.lower() in _INVENTED_SKIP:
|
|
618
|
+
continue
|
|
619
|
+
if name.lower() in task.lower():
|
|
620
|
+
continue
|
|
621
|
+
if "_" not in name:
|
|
622
|
+
continue
|
|
623
|
+
if name in body or f"def {name}" in body:
|
|
624
|
+
continue
|
|
625
|
+
invented.append(name)
|
|
626
|
+
if not invented:
|
|
627
|
+
return ""
|
|
628
|
+
return (
|
|
629
|
+
f"{invented[0]} is not in the file you read. "
|
|
630
|
+
"Action: done Summary: quote a name that is in # auto-read, or say "
|
|
631
|
+
"no defects found."
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def refuse_question_ask(task: str, action: str, located_path: str) -> str:
|
|
636
|
+
if action != "ask" or not looks_like_question(task):
|
|
637
|
+
return ""
|
|
638
|
+
if not located_path:
|
|
639
|
+
return ""
|
|
640
|
+
return (
|
|
641
|
+
"already located. Action: done Summary: quote the -> type from "
|
|
642
|
+
"# auto-read and say what the function computes."
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def reviews_one_named_file(task: str) -> bool:
|
|
647
|
+
"""True for "review src/orders.py for bugs", false for the design loop.
|
|
648
|
+
|
|
649
|
+
A structure review is allowed to edit, because the loop it drives moves
|
|
650
|
+
on to splitting a module. A review of one named file is not: it was
|
|
651
|
+
asked to report.
|
|
652
|
+
"""
|
|
653
|
+
from harness.task import looks_like_review_code, task_paths
|
|
654
|
+
|
|
655
|
+
return bool(task_paths(task)) and looks_like_review_code(task)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def named_file_review_summary(project: Path, task: str) -> str:
|
|
659
|
+
"""Quote compiler findings in a named file. Empty when there are none.
|
|
660
|
+
|
|
661
|
+
Live 8B on `review src/orders.py for bugs` was told to patch, then
|
|
662
|
+
refused, then burned the step budget. A hosted agent reads the file
|
|
663
|
+
once and names `subtotl`. This is that read, without a model turn.
|
|
664
|
+
"""
|
|
665
|
+
if not reviews_one_named_file(task):
|
|
666
|
+
return ""
|
|
667
|
+
named = named_project_file(task, project)
|
|
668
|
+
if not named:
|
|
669
|
+
return ""
|
|
670
|
+
try:
|
|
671
|
+
body = read_py(project, named, about=subject_of(task))
|
|
672
|
+
except (OSError, ValueError):
|
|
673
|
+
return ""
|
|
674
|
+
leftover = undefined_names(body)
|
|
675
|
+
if not leftover:
|
|
676
|
+
return ""
|
|
677
|
+
shown = ", ".join(leftover)
|
|
678
|
+
return f"Compiler findings: undefined name {shown} in {named} (used, never bound)."
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def refuse_question_write(task: str, action: str) -> str:
|
|
682
|
+
if reviews_one_named_file(task) and action in _QUESTION_WRITE:
|
|
683
|
+
return (
|
|
684
|
+
"Reviews do not edit. Action: done Summary: name the defect and "
|
|
685
|
+
"quote the line it is on."
|
|
686
|
+
)
|
|
687
|
+
if looks_like_design_loop(task):
|
|
688
|
+
return ""
|
|
689
|
+
if looks_like_question(task) and action in _QUESTION_WRITE:
|
|
690
|
+
return (
|
|
691
|
+
"Questions do not edit. "
|
|
692
|
+
"Action: done Summary: quote return or refuse from # auto-read."
|
|
693
|
+
)
|
|
694
|
+
return ""
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def refuse_thin_review(task: str, summary: str, report: str) -> str:
|
|
698
|
+
if not looks_like_review(task):
|
|
699
|
+
return ""
|
|
700
|
+
from harness.scan.design import design_is_clean
|
|
701
|
+
|
|
702
|
+
if design_is_clean(report):
|
|
703
|
+
if "no structure findings" in (summary or "").lower():
|
|
704
|
+
return ""
|
|
705
|
+
return (
|
|
706
|
+
"too thin. Action: done Summary: quote no structure findings"
|
|
707
|
+
)
|
|
708
|
+
keys = [
|
|
709
|
+
word
|
|
710
|
+
for word in ("SoC", "god", "outsized", "tests", "scripts", "split", "__init__")
|
|
711
|
+
if word.lower() in report.lower()
|
|
712
|
+
]
|
|
713
|
+
if not keys:
|
|
714
|
+
return ""
|
|
715
|
+
text = (summary or "").lower()
|
|
716
|
+
if any(key.lower() in text for key in keys):
|
|
717
|
+
return ""
|
|
718
|
+
return (
|
|
719
|
+
"too thin. Action: done Summary: quote one finding "
|
|
720
|
+
f"({', '.join(keys[:3])})"
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def refuse_design_dirty(task: str, report: str) -> str:
|
|
725
|
+
if not looks_like_design_loop(task):
|
|
726
|
+
return ""
|
|
727
|
+
from harness.scan.design import design_is_clean
|
|
728
|
+
|
|
729
|
+
if design_is_clean(report):
|
|
730
|
+
return ""
|
|
731
|
+
return (
|
|
732
|
+
"not done. Structure findings remain. "
|
|
733
|
+
"Action: edit Path: pkg/<new_concern>.py with one function. "
|
|
734
|
+
"Then the harness will re-scan."
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def refuse_redundant_explore(
|
|
739
|
+
task: str, action: str, path: str, located_path: str
|
|
740
|
+
) -> str:
|
|
741
|
+
if not looks_like_question(task) or not located_path:
|
|
742
|
+
return ""
|
|
743
|
+
if action not in _QUESTION_REEXPLORE:
|
|
744
|
+
return ""
|
|
745
|
+
rel = path.replace("\\", "/").lstrip("./")
|
|
746
|
+
located = located_path.replace("\\", "/").lstrip("./")
|
|
747
|
+
same = (not rel) or rel == located or located.endswith(rel) or rel.endswith(located)
|
|
748
|
+
if not same:
|
|
749
|
+
return ""
|
|
750
|
+
return (
|
|
751
|
+
f"already have # auto-read {located}. "
|
|
752
|
+
"Action: done Summary: quote return or refuse from that file."
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def refuse_early_done(task: str, last_path: str, located_path: str) -> str:
|
|
757
|
+
if not looks_like_question(task):
|
|
758
|
+
return ""
|
|
759
|
+
symbol = question_symbol(task)
|
|
760
|
+
if not symbol:
|
|
761
|
+
return ""
|
|
762
|
+
if located_path or (last_path and symbol.replace("_", "") in last_path.replace("_", "").lower()):
|
|
763
|
+
return ""
|
|
764
|
+
return (
|
|
765
|
+
f"not done. Harness or you must locate {symbol} first. "
|
|
766
|
+
f"Action: locate Query: {symbol}"
|
|
767
|
+
)
|