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/task.py
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
"""Read the user's task and report what kind of work it asks for.
|
|
2
|
+
|
|
3
|
+
Every layer needs to know whether the task is a question, a request to add
|
|
4
|
+
code, a rename, or a request to create a package. These functions are the
|
|
5
|
+
single place that decision is made, so the layers above agree with each
|
|
6
|
+
other. This module imports nothing from the rest of the harness.
|
|
7
|
+
|
|
8
|
+
Rule shared by all of the writer functions below: a question is never a
|
|
9
|
+
write, so each one returns False when the task is a question.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
QUESTION_PREFIXES = (
|
|
17
|
+
"what ",
|
|
18
|
+
"why ",
|
|
19
|
+
"how ",
|
|
20
|
+
"where ",
|
|
21
|
+
"which ",
|
|
22
|
+
"explain ",
|
|
23
|
+
"list ",
|
|
24
|
+
"who ",
|
|
25
|
+
)
|
|
26
|
+
_SYMBOL = re.compile(r"\b([a-z_][a-z0-9_]{4,})\b")
|
|
27
|
+
_DOES_SYMBOL = re.compile(r"\bdoes\s+([a-z_][a-z0-9_]*)\b")
|
|
28
|
+
# A name written with brackets was typed on purpose. Eleven of the
|
|
29
|
+
# fifteen benchmark tasks spell the subject that way — `word_count(text)`
|
|
30
|
+
# — and taking it is right every time, where reading left to right takes
|
|
31
|
+
# `module` out of "create a new module with a function word_count(text)".
|
|
32
|
+
# A run then wrote a function literally called `module` and said so.
|
|
33
|
+
_CALLED = re.compile(r"\b([a-z_][a-z0-9_]*)\s*\(")
|
|
34
|
+
_SYMBOL_SKIP = frozenset(
|
|
35
|
+
{
|
|
36
|
+
# The word a task opens with is what to do, not what to do it to.
|
|
37
|
+
# Asked to "create a new module and move the helpers into it", the
|
|
38
|
+
# harness took `create` for a function name, found the word in a
|
|
39
|
+
# test file and answered "already has a test for create".
|
|
40
|
+
"build",
|
|
41
|
+
"create",
|
|
42
|
+
"delete",
|
|
43
|
+
"make",
|
|
44
|
+
"move",
|
|
45
|
+
"remove",
|
|
46
|
+
"rename",
|
|
47
|
+
"split",
|
|
48
|
+
"write",
|
|
49
|
+
"about",
|
|
50
|
+
"after",
|
|
51
|
+
"and",
|
|
52
|
+
"apply",
|
|
53
|
+
"are",
|
|
54
|
+
"before",
|
|
55
|
+
"but",
|
|
56
|
+
"can",
|
|
57
|
+
"did",
|
|
58
|
+
"does",
|
|
59
|
+
"explain",
|
|
60
|
+
"feature",
|
|
61
|
+
"for",
|
|
62
|
+
"from",
|
|
63
|
+
"function",
|
|
64
|
+
"had",
|
|
65
|
+
"has",
|
|
66
|
+
"how",
|
|
67
|
+
"not",
|
|
68
|
+
"refuse",
|
|
69
|
+
"return",
|
|
70
|
+
"source",
|
|
71
|
+
"that",
|
|
72
|
+
"the",
|
|
73
|
+
"this",
|
|
74
|
+
"was",
|
|
75
|
+
"what",
|
|
76
|
+
"where",
|
|
77
|
+
"which",
|
|
78
|
+
"who",
|
|
79
|
+
"why",
|
|
80
|
+
"with",
|
|
81
|
+
"you",
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
_ADD_START = re.compile(r"^(please\s+)?(add|implement|introduce|create)\b|new feature")
|
|
85
|
+
_SMELL = re.compile(
|
|
86
|
+
r"\b(code smell|smell|rename|readable name|human[- ]readable|clean up|cleanup)\b"
|
|
87
|
+
)
|
|
88
|
+
_PACKAGE = re.compile(
|
|
89
|
+
r"\b(scaffold|new package|new project|"
|
|
90
|
+
r"create a package|create a project|create a pkg|"
|
|
91
|
+
r"design and develop|"
|
|
92
|
+
r"(?:design|develop|build|create) an? (?:small )?"
|
|
93
|
+
r"(?:cli(?:\s+app|\s+tool|\s+application)?|"
|
|
94
|
+
r"command[ -]line(?:\s+app|\s+tool)?|"
|
|
95
|
+
r"app(?:lication)?|tool))\b",
|
|
96
|
+
re.I,
|
|
97
|
+
)
|
|
98
|
+
_GITHUB_PR_APP = re.compile(
|
|
99
|
+
r"\b(github|gh)\b.*\b(pr|prs|pull requests?)\b|"
|
|
100
|
+
r"\b(pr|prs|pull requests?)\b.*\b(github|gh)\b",
|
|
101
|
+
re.I,
|
|
102
|
+
)
|
|
103
|
+
_SNAKE_NAME = re.compile(r"\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b")
|
|
104
|
+
_PACKAGE_NOUN_SKIP = _SYMBOL_SKIP | frozenset(
|
|
105
|
+
{
|
|
106
|
+
"app",
|
|
107
|
+
"application",
|
|
108
|
+
"cli",
|
|
109
|
+
"command",
|
|
110
|
+
"design",
|
|
111
|
+
"develop",
|
|
112
|
+
"github",
|
|
113
|
+
"line",
|
|
114
|
+
"package",
|
|
115
|
+
"project",
|
|
116
|
+
"pull",
|
|
117
|
+
"request",
|
|
118
|
+
"requests",
|
|
119
|
+
"reviewing",
|
|
120
|
+
"scaffold",
|
|
121
|
+
"script",
|
|
122
|
+
"small",
|
|
123
|
+
"tool",
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
_REVIEW = re.compile(
|
|
127
|
+
r"\b(review|system design|architecture|project structure|design review)\b"
|
|
128
|
+
)
|
|
129
|
+
_REFACTOR = re.compile(
|
|
130
|
+
r"\b(refactor|extract (a )?function|split (the )?module|move function)\b"
|
|
131
|
+
)
|
|
132
|
+
_RENAME = re.compile(
|
|
133
|
+
r"\brename\s+([A-Za-z_][A-Za-z0-9_]*)\s+to\s+([A-Za-z_][A-Za-z0-9_]*)",
|
|
134
|
+
re.I,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def looks_like_question(task: str) -> bool:
|
|
139
|
+
text = task.strip().lower()
|
|
140
|
+
return text.endswith("?") or text.startswith(QUESTION_PREFIXES)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_FILE_OPERATION = re.compile(
|
|
144
|
+
r"\b(move|rename|split|delete|remove|extract)\b.{0,60}"
|
|
145
|
+
r"(\.py\b|\bmodule\b|\bfile\b|\bpackage\b|\bfolder\b)",
|
|
146
|
+
re.I,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def looks_like_file_operation(task: str) -> bool:
|
|
151
|
+
"""Moving, renaming, splitting or deleting a file, not editing one.
|
|
152
|
+
|
|
153
|
+
These have no symbol to work on, and guessing one is worse than
|
|
154
|
+
having none: asked to "create a new module and move the helpers into
|
|
155
|
+
it", the harness took a word out of the task, looked for a test
|
|
156
|
+
naming it, found one, and answered "already has a test for create".
|
|
157
|
+
"""
|
|
158
|
+
lowered = task.lower()
|
|
159
|
+
if _FILE_OPERATION.search(lowered):
|
|
160
|
+
return True
|
|
161
|
+
return bool(
|
|
162
|
+
re.search(r"\b(create|add)\b.{0,40}\b(new )?(module|file|package)\b", lowered)
|
|
163
|
+
and re.search(r"\b(move|into it|and put)\b", lowered)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def question_symbol(task: str) -> str:
|
|
168
|
+
"""The first word in the task that could be a symbol name."""
|
|
169
|
+
called = [
|
|
170
|
+
name
|
|
171
|
+
for name in _CALLED.findall(task.lower())
|
|
172
|
+
if name not in _SYMBOL_SKIP
|
|
173
|
+
]
|
|
174
|
+
if called:
|
|
175
|
+
return called[0]
|
|
176
|
+
does = _DOES_SYMBOL.search(task.lower())
|
|
177
|
+
if does:
|
|
178
|
+
name = does.group(1)
|
|
179
|
+
if name not in _SYMBOL_SKIP:
|
|
180
|
+
return name
|
|
181
|
+
hits = [word for word in _SYMBOL.findall(task.lower()) if word not in _SYMBOL_SKIP]
|
|
182
|
+
return hits[0] if hits else ""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def looks_like_new_package(task: str) -> bool:
|
|
186
|
+
if looks_like_question(task):
|
|
187
|
+
return False
|
|
188
|
+
return bool(_PACKAGE.search(task.strip().lower()))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def looks_like_app_loop(task: str) -> bool:
|
|
192
|
+
"""A greenfield CLI that talks to GitHub: list / show / comment, not a weekday copy."""
|
|
193
|
+
return looks_like_new_package(task) and mentions_cli(task) and mentions_http(task)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def looks_like_app_overflow(task: str) -> bool:
|
|
197
|
+
"""A later typed run for comment / pagination / config. Not a new package."""
|
|
198
|
+
if looks_like_question(task) or looks_like_new_package(task):
|
|
199
|
+
return False
|
|
200
|
+
text = task.strip().lower()
|
|
201
|
+
if re.search(r"\bcomment\b", text) and re.search(
|
|
202
|
+
r"\b(subcommand|argparse|mocked)\b", text
|
|
203
|
+
):
|
|
204
|
+
return True
|
|
205
|
+
return bool(re.search(r"\b(pagination|config file|path\.home)\b", text))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def mentions_cli(task: str) -> bool:
|
|
209
|
+
"""True when the task names a CLI, even if it is also a new package."""
|
|
210
|
+
return bool(_SCRIPT.search(_without_paths(task)))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def mentions_http(task: str) -> bool:
|
|
214
|
+
"""True when the work talks to an HTTP API, including GitHub PRs."""
|
|
215
|
+
if _HTTP.search(task):
|
|
216
|
+
return True
|
|
217
|
+
lowered = task.lower()
|
|
218
|
+
if "url" in lowered and "json" in lowered:
|
|
219
|
+
return True
|
|
220
|
+
return bool(_GITHUB_PR_APP.search(task))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def package_noun(task: str) -> str:
|
|
224
|
+
"""The module name for a new-package task: pkg/<noun>.py.
|
|
225
|
+
|
|
226
|
+
`question_symbol` takes the first five-letter word, so
|
|
227
|
+
"design and develop a CLI" would become `pkg/design.py`. A snake_case
|
|
228
|
+
name in the task wins; a GitHub-PR app is `pr_review`; otherwise the
|
|
229
|
+
last content word after "for", then a leftover symbol, then `service`.
|
|
230
|
+
"""
|
|
231
|
+
text = task.strip().lower()
|
|
232
|
+
for name in _SNAKE_NAME.findall(text):
|
|
233
|
+
if name not in _PACKAGE_NOUN_SKIP:
|
|
234
|
+
return name
|
|
235
|
+
if _GITHUB_PR_APP.search(text) or looks_like_app_overflow(task):
|
|
236
|
+
return "pr_review"
|
|
237
|
+
purpose = re.search(r"\bfor\s+(.+)$", text)
|
|
238
|
+
if purpose:
|
|
239
|
+
words = [
|
|
240
|
+
word
|
|
241
|
+
for word in re.findall(r"[a-z][a-z0-9]{2,}", purpose.group(1))
|
|
242
|
+
if word not in _PACKAGE_NOUN_SKIP
|
|
243
|
+
]
|
|
244
|
+
if len(words) == 1:
|
|
245
|
+
return words[0]
|
|
246
|
+
if len(words) >= 2:
|
|
247
|
+
return f"{words[0]}_{words[1]}"
|
|
248
|
+
symbol = question_symbol(task)
|
|
249
|
+
if symbol and symbol not in _PACKAGE_NOUN_SKIP:
|
|
250
|
+
return symbol
|
|
251
|
+
return "service"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def looks_like_fix_smell(task: str) -> bool:
|
|
255
|
+
if looks_like_question(task):
|
|
256
|
+
return False
|
|
257
|
+
text = task.strip().lower()
|
|
258
|
+
# "find code smells" is a report. "fix the code smell" / rename is a write.
|
|
259
|
+
if re.search(r"\b(find|report|list|show)\b", text) and "smell" in text:
|
|
260
|
+
return False
|
|
261
|
+
return bool(_SMELL.search(text))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
_SHIP = re.compile(
|
|
265
|
+
r"(\bopen a pr\b|\bcreate a pr\b|\bopen a pull request\b|"
|
|
266
|
+
r"\bcommit\b|\bpush\b|\bmerge\b)",
|
|
267
|
+
re.I,
|
|
268
|
+
)
|
|
269
|
+
_ISSUE_NUM = re.compile(r"(?:#|issue\s+#?|pr\s+#?)(\d+)", re.I)
|
|
270
|
+
_TICKET_WORK = re.compile(
|
|
271
|
+
r"\b(fix|do|implement|write|add|address|handle|cover)\b", re.I
|
|
272
|
+
)
|
|
273
|
+
_PR_REF = re.compile(r"\b(pr|pull request)\s+#?\d+", re.I)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def looks_like_ship(task: str) -> bool:
|
|
277
|
+
"""Commit, push, open a PR, or merge — not merely “fix #50”."""
|
|
278
|
+
if looks_like_question(task) or looks_like_new_package(task):
|
|
279
|
+
return False
|
|
280
|
+
return bool(_SHIP.search(task))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def looks_like_merge(task: str) -> bool:
|
|
284
|
+
return looks_like_ship(task) and bool(re.search(r"\bmerge\b", task, re.I))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def issue_number(task: str) -> str:
|
|
288
|
+
match = _ISSUE_NUM.search(task)
|
|
289
|
+
return match.group(1) if match else ""
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def looks_like_ticket(task: str) -> bool:
|
|
293
|
+
return bool(issue_number(task))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def looks_like_pr_ref(task: str) -> bool:
|
|
297
|
+
return bool(_PR_REF.search(task))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def looks_like_ticket_work(task: str) -> bool:
|
|
301
|
+
"""Do the work the ticket names, not only open or merge a PR."""
|
|
302
|
+
if not looks_like_ticket(task) or looks_like_question(task):
|
|
303
|
+
return False
|
|
304
|
+
if looks_like_merge(task) and not _TICKET_WORK.search(task):
|
|
305
|
+
return False
|
|
306
|
+
return bool(_TICKET_WORK.search(task))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def looks_like_refactor(task: str) -> bool:
|
|
310
|
+
if looks_like_question(task) or looks_like_new_package(task) or looks_like_ship(task):
|
|
311
|
+
return False
|
|
312
|
+
return bool(_REFACTOR.search(task.lower()))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def looks_like_design_loop(task: str) -> bool:
|
|
316
|
+
"""Review, then one-split, then review again until the scan is clean.
|
|
317
|
+
|
|
318
|
+
"review src/orders.py for bugs" is a report on one file. That is not
|
|
319
|
+
this loop. Live 8B on main spent eight turns fighting review-design
|
|
320
|
+
after a named-file review.
|
|
321
|
+
"""
|
|
322
|
+
if (
|
|
323
|
+
task_paths(task)
|
|
324
|
+
and looks_like_review_code(task)
|
|
325
|
+
and not looks_like_refactor(task)
|
|
326
|
+
):
|
|
327
|
+
return False
|
|
328
|
+
return looks_like_review(task) or looks_like_refactor(task)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def looks_like_review(task: str) -> bool:
|
|
332
|
+
if looks_like_new_package(task) or looks_like_ship(task):
|
|
333
|
+
return False
|
|
334
|
+
if looks_like_refactor(task) and not re.search(r"\breview\b", task, re.I):
|
|
335
|
+
return False
|
|
336
|
+
return bool(_REVIEW.search(task.lower()))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
_BUG = re.compile(r"\b(fix|bug|nameerror|crash|defect)\b", re.I)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def looks_like_bugfix(task: str) -> bool:
|
|
343
|
+
"""A concrete fix that is not a rename, package, review, or ship."""
|
|
344
|
+
if looks_like_question(task):
|
|
345
|
+
return False
|
|
346
|
+
if (
|
|
347
|
+
looks_like_new_package(task)
|
|
348
|
+
or looks_like_fix_smell(task)
|
|
349
|
+
or looks_like_ship(task)
|
|
350
|
+
or looks_like_review(task)
|
|
351
|
+
or looks_like_refactor(task)
|
|
352
|
+
):
|
|
353
|
+
return False
|
|
354
|
+
return bool(_BUG.search(task))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
_SCRIPT = re.compile(
|
|
358
|
+
r"\b(script|cli|argparse|argv|command[ -]line|__main__)\b", re.I
|
|
359
|
+
)
|
|
360
|
+
_HTTP = re.compile(
|
|
361
|
+
r"\b(http api|rest api|fetch json|urllib|call the api|"
|
|
362
|
+
r"get json|post json|\bcurl\b|http get|http post|"
|
|
363
|
+
# A job that names a url and json is an HTTP call however it is worded.
|
|
364
|
+
r"gets? an? url|download the json|read json from|from a url)\b",
|
|
365
|
+
re.I,
|
|
366
|
+
)
|
|
367
|
+
_ANALYTICS = re.compile(
|
|
368
|
+
r"\b(analytics|group by|groupby|tally|histogram|counter|csv)\b", re.I
|
|
369
|
+
)
|
|
370
|
+
_ALGO = re.compile(
|
|
371
|
+
r"\b(algorithm|binary search|linked list|hash map|two pointers?|"
|
|
372
|
+
r"breadth.first|depth.first|\bbfs\b|\bdfs\b|quicksort|mergesort|"
|
|
373
|
+
r"implement (a |an )?(stack|queue|heap))\b",
|
|
374
|
+
re.I,
|
|
375
|
+
)
|
|
376
|
+
_PLATFORM = re.compile(
|
|
377
|
+
r"\b("
|
|
378
|
+
r"pathlib|file.?system|cross.?platform|platform engineer|"
|
|
379
|
+
r"path helper|os\.path|USERPROFILE|ntpath|"
|
|
380
|
+
r"venv|virtual.?env|python\.exe|bin/python|"
|
|
381
|
+
r"Path\.home|tempfile|line.?ending"
|
|
382
|
+
r")\b",
|
|
383
|
+
re.I,
|
|
384
|
+
)
|
|
385
|
+
_OPS = re.compile(
|
|
386
|
+
r"\b("
|
|
387
|
+
r"devops|dev.?ops|\bci\b|continuous integration|"
|
|
388
|
+
r"workflow|pipeline|ci workflow"
|
|
389
|
+
r")\b",
|
|
390
|
+
re.I,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _without_paths(task: str) -> str:
|
|
395
|
+
"""Drop file paths so `tests/cli/foo.py` is not a CLI-script job."""
|
|
396
|
+
text = re.sub(r"\b[\w.-]+(?:/[\w.-]+)+\b", " ", task)
|
|
397
|
+
return re.sub(r"\S+\.py\b", " ", text)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def looks_like_script(task: str) -> bool:
|
|
401
|
+
if looks_like_question(task) or looks_like_new_package(task) or looks_like_ship(task):
|
|
402
|
+
return False
|
|
403
|
+
return bool(_SCRIPT.search(_without_paths(task)))
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def looks_like_http_client(task: str) -> bool:
|
|
407
|
+
if looks_like_question(task) or looks_like_new_package(task) or looks_like_ship(task):
|
|
408
|
+
return False
|
|
409
|
+
return mentions_http(task)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def looks_like_analytics(task: str) -> bool:
|
|
413
|
+
if looks_like_question(task) or looks_like_new_package(task) or looks_like_ship(task):
|
|
414
|
+
return False
|
|
415
|
+
return bool(_ANALYTICS.search(task))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def looks_like_algorithm(task: str) -> bool:
|
|
419
|
+
if looks_like_question(task) or looks_like_new_package(task) or looks_like_ship(task):
|
|
420
|
+
return False
|
|
421
|
+
return bool(_ALGO.search(task))
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def looks_like_platform(task: str) -> bool:
|
|
425
|
+
"""Path / venv / OS-layout work. Small files, many platforms."""
|
|
426
|
+
if looks_like_question(task) or looks_like_new_package(task) or looks_like_ship(task):
|
|
427
|
+
return False
|
|
428
|
+
if looks_like_ops(task):
|
|
429
|
+
return False
|
|
430
|
+
return bool(_PLATFORM.search(task))
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def looks_like_ops(task: str) -> bool:
|
|
434
|
+
"""CI / workflow YAML. Not a function add, not a path helper."""
|
|
435
|
+
if looks_like_question(task) or looks_like_new_package(task) or looks_like_ship(task):
|
|
436
|
+
return False
|
|
437
|
+
if looks_like_write_tests(task):
|
|
438
|
+
return False
|
|
439
|
+
return bool(_OPS.search(task))
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def looks_like_everyday_code(task: str) -> bool:
|
|
443
|
+
"""Simple script, HTTP client, tally, algorithm, path helper, or CI YAML."""
|
|
444
|
+
return (
|
|
445
|
+
looks_like_script(task)
|
|
446
|
+
or looks_like_http_client(task)
|
|
447
|
+
or looks_like_analytics(task)
|
|
448
|
+
or looks_like_algorithm(task)
|
|
449
|
+
or looks_like_platform(task)
|
|
450
|
+
or looks_like_ops(task)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def everyday_skill_name(task: str) -> str:
|
|
455
|
+
"""The kit skill for a script / HTTP / tally / algorithm / path / CI task."""
|
|
456
|
+
if looks_like_ops(task):
|
|
457
|
+
return "write-workflow"
|
|
458
|
+
if looks_like_platform(task):
|
|
459
|
+
return "write-paths"
|
|
460
|
+
if looks_like_http_client(task):
|
|
461
|
+
return "call-http"
|
|
462
|
+
if looks_like_analytics(task):
|
|
463
|
+
return "analyze-data"
|
|
464
|
+
if looks_like_algorithm(task):
|
|
465
|
+
return "write-algorithm"
|
|
466
|
+
if looks_like_script(task):
|
|
467
|
+
return "write-script"
|
|
468
|
+
return ""
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
_EVERYDAY_PATH = {
|
|
472
|
+
"write-script": "pkg/weekday_name.py",
|
|
473
|
+
"call-http": "pkg/fetch_json.py",
|
|
474
|
+
"analyze-data": "pkg/tally.py",
|
|
475
|
+
"write-algorithm": "pkg/index_of.py",
|
|
476
|
+
"write-paths": "pkg/paths.py",
|
|
477
|
+
"write-workflow": ".github/workflows/tests.yml",
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def everyday_example_path(task: str) -> str:
|
|
482
|
+
"""The Path: the skill wants the 8B to copy. Keep in sync with SKILL.md."""
|
|
483
|
+
return _EVERYDAY_PATH.get(everyday_skill_name(task), "pkg/<noun>.py")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
_WRITE_TESTS = re.compile(r"\bwrite tests?\b|\badd tests?\b|\bunit tests?\b", re.I)
|
|
487
|
+
_COVERED = re.compile(
|
|
488
|
+
r"\b(?:for|cover)\s+(?:the\s+)?"
|
|
489
|
+
r"([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\b"
|
|
490
|
+
)
|
|
491
|
+
_COVERED_SKIP = frozenset(
|
|
492
|
+
{
|
|
493
|
+
# Pronouns are not function names. "…and a unit test for it" read as
|
|
494
|
+
# "cover the function `it`", so a task asking to create a function
|
|
495
|
+
# was handled as a request to test one that already exists.
|
|
496
|
+
"everything",
|
|
497
|
+
"file",
|
|
498
|
+
"it",
|
|
499
|
+
"module",
|
|
500
|
+
"some",
|
|
501
|
+
"that",
|
|
502
|
+
"them",
|
|
503
|
+
"these",
|
|
504
|
+
"this",
|
|
505
|
+
"those",
|
|
506
|
+
"test",
|
|
507
|
+
"tests",
|
|
508
|
+
"unit",
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def looks_like_write_tests(task: str) -> bool:
|
|
514
|
+
"""Cover an existing function. Not “add X and a test” (that is add-feature)."""
|
|
515
|
+
if looks_like_question(task) or looks_like_new_package(task) or looks_like_ship(task):
|
|
516
|
+
return False
|
|
517
|
+
if not _WRITE_TESTS.search(task):
|
|
518
|
+
return False
|
|
519
|
+
# "add multiply and a unit test" starts with add. "write tests for X" covers X.
|
|
520
|
+
if _ADD_START.search(task.strip().lower()) and not covered_symbol(task):
|
|
521
|
+
return False
|
|
522
|
+
return True
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def covered_symbol(task: str) -> str:
|
|
526
|
+
"""`write tests for apply_discount` → apply_discount.
|
|
527
|
+
|
|
528
|
+
Also `OrderService`, `OrdersController.handle`.
|
|
529
|
+
"""
|
|
530
|
+
match = _COVERED.search(task)
|
|
531
|
+
if not match:
|
|
532
|
+
return ""
|
|
533
|
+
name = match.group(1)
|
|
534
|
+
return "" if name.lower() in _COVERED_SKIP else name
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def looks_like_add_feature(task: str) -> bool:
|
|
538
|
+
text = task.strip().lower()
|
|
539
|
+
if looks_like_question(text):
|
|
540
|
+
return False
|
|
541
|
+
if looks_like_write_tests(task):
|
|
542
|
+
return False
|
|
543
|
+
if looks_like_ops(task) or looks_like_platform(task):
|
|
544
|
+
return False
|
|
545
|
+
if (
|
|
546
|
+
looks_like_new_package(text)
|
|
547
|
+
or looks_like_fix_smell(text)
|
|
548
|
+
or looks_like_ship(text)
|
|
549
|
+
or looks_like_review(text)
|
|
550
|
+
or looks_like_refactor(text)
|
|
551
|
+
):
|
|
552
|
+
return False
|
|
553
|
+
return bool(_ADD_START.search(text)) or looks_like_everyday_code(task)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def rename_pair(task: str) -> tuple[str, str]:
|
|
557
|
+
"""`rename calc to total_price` -> ("calc", "total_price")."""
|
|
558
|
+
match = _RENAME.search(task)
|
|
559
|
+
return (match.group(1), match.group(2)) if match else ("", "")
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
_SMELL_SKIP = frozenset(
|
|
563
|
+
{
|
|
564
|
+
"and",
|
|
565
|
+
"clean",
|
|
566
|
+
"cleanup",
|
|
567
|
+
"code",
|
|
568
|
+
"for",
|
|
569
|
+
"human",
|
|
570
|
+
"into",
|
|
571
|
+
"name",
|
|
572
|
+
"names",
|
|
573
|
+
"readable",
|
|
574
|
+
"rename",
|
|
575
|
+
"smell",
|
|
576
|
+
"that",
|
|
577
|
+
"the",
|
|
578
|
+
"this",
|
|
579
|
+
"with",
|
|
580
|
+
}
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def smell_symbol(task: str) -> str:
|
|
585
|
+
"""The name a rename task is about: `rename calc to x` -> "calc"."""
|
|
586
|
+
old, _new = rename_pair(task)
|
|
587
|
+
if old:
|
|
588
|
+
return old
|
|
589
|
+
names = re.findall(r"\b([a-z_][a-z0-9_]{2,})\b", task.lower())
|
|
590
|
+
hits = [name for name in names if name not in _SMELL_SKIP]
|
|
591
|
+
return hits[-1] if hits else ""
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def rename_target(task: str) -> str:
|
|
595
|
+
"""The name a rename task wants: `rename calc to x` -> "x"."""
|
|
596
|
+
return rename_pair(task)[1]
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
_REVIEW_CODE = re.compile(
|
|
600
|
+
r"\b(review|check|find bugs?|find errors?|defects?|audit|"
|
|
601
|
+
r"code smells?)\b"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def looks_like_review_code(task: str) -> bool:
|
|
606
|
+
"""True when the task asks for problems to be reported, not fixed."""
|
|
607
|
+
text = task.strip().lower()
|
|
608
|
+
if looks_like_new_package(text) or looks_like_ship(text):
|
|
609
|
+
return False
|
|
610
|
+
if looks_like_add_feature(text) or looks_like_fix_smell(text):
|
|
611
|
+
return False
|
|
612
|
+
if looks_like_write_tests(text):
|
|
613
|
+
return False
|
|
614
|
+
return bool(_REVIEW_CODE.search(text))
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
_IDENTIFIER = re.compile(r"[A-Za-z_]\w*_\w+|\w+\s*\(|\S+\.py\b|\b[A-Za-z]\w*/\S+")
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def names_something_concrete(task: str) -> bool:
|
|
621
|
+
"""True when the task names a file, a call, or a snake_case identifier.
|
|
622
|
+
|
|
623
|
+
`question_symbol` will return any five-letter word, including ordinary
|
|
624
|
+
English like "clean", so it cannot be used on its own to decide whether
|
|
625
|
+
the agent has been told what to work on.
|
|
626
|
+
"""
|
|
627
|
+
return bool(_IDENTIFIER.search(task))
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def looks_unclear(task: str) -> bool:
|
|
631
|
+
"""True when the task is short and names nothing the agent can search for.
|
|
632
|
+
|
|
633
|
+
A task like "clean this up" gives the agent no file and no symbol, so
|
|
634
|
+
asking one question is better than guessing which file to change.
|
|
635
|
+
"""
|
|
636
|
+
text = task.strip()
|
|
637
|
+
if looks_like_question(text) or looks_like_ship(text):
|
|
638
|
+
return False
|
|
639
|
+
if names_something_concrete(text):
|
|
640
|
+
return False
|
|
641
|
+
# If the kind of work is recognised, the agent has a skill and a first
|
|
642
|
+
# action for it, so a short task is still workable.
|
|
643
|
+
known = (
|
|
644
|
+
looks_like_add_feature,
|
|
645
|
+
looks_like_everyday_code,
|
|
646
|
+
looks_like_fix_smell,
|
|
647
|
+
looks_like_new_package,
|
|
648
|
+
looks_like_review_code,
|
|
649
|
+
looks_like_design_loop,
|
|
650
|
+
looks_like_bugfix,
|
|
651
|
+
)
|
|
652
|
+
if any(check(text) for check in known):
|
|
653
|
+
return False
|
|
654
|
+
return len(text.split()) <= 6
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
_TASK_PATH = re.compile(r"[\w./\\-]+\.(?:py|pyi|md)\b")
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def task_paths(task: str) -> tuple[str, ...]:
|
|
661
|
+
"""File paths named in the task, with forward slashes, in order.
|
|
662
|
+
|
|
663
|
+
A task that says "in src/harness/model/engine.py ..." has already told
|
|
664
|
+
the agent which file to open. Searching for a word out of that path
|
|
665
|
+
instead finds every file in the project.
|
|
666
|
+
"""
|
|
667
|
+
seen: list[str] = []
|
|
668
|
+
for hit in _TASK_PATH.findall(task):
|
|
669
|
+
cleaned = hit.replace("\\", "/").strip("./,;:()[]'\"")
|
|
670
|
+
if cleaned and cleaned not in seen:
|
|
671
|
+
seen.append(cleaned)
|
|
672
|
+
return tuple(seen)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def named_project_file(task: str, project) -> str:
|
|
676
|
+
"""The one file the task names that exists in the project, or "".
|
|
677
|
+
|
|
678
|
+
Empty when the task names none, or names more than one, because then
|
|
679
|
+
there is nothing unambiguous to act on.
|
|
680
|
+
"""
|
|
681
|
+
from pathlib import Path as _Path
|
|
682
|
+
|
|
683
|
+
root = _Path(project).resolve()
|
|
684
|
+
found = [rel for rel in task_paths(task) if (root / rel).is_file()]
|
|
685
|
+
return found[0] if len(found) == 1 else ""
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
_WALK_FILES = re.compile(
|
|
689
|
+
r"\b(under|inside|recursively|every file|all files|folder|directory|"
|
|
690
|
+
r"file size|newest|oldest)\b"
|
|
691
|
+
)
|
|
692
|
+
_ARCHIVE = re.compile(r"\b(zip|unzip|tar|archive|compress|extract)\b")
|
|
693
|
+
_COMPARE = re.compile(
|
|
694
|
+
r"\b(compare|diff|differ|different|changed|missing from|not in)\b"
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def looks_like_walk_files(task: str) -> bool:
|
|
699
|
+
"""True when the job is to reach files under a folder, not one known path."""
|
|
700
|
+
text = task.strip().lower()
|
|
701
|
+
if looks_like_question(text) or looks_like_ship(text):
|
|
702
|
+
return False
|
|
703
|
+
return bool(_WALK_FILES.search(text))
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def looks_like_archive(task: str) -> bool:
|
|
707
|
+
text = task.strip().lower()
|
|
708
|
+
if looks_like_question(text) or looks_like_ship(text):
|
|
709
|
+
return False
|
|
710
|
+
return bool(_ARCHIVE.search(text))
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def looks_like_compare(task: str) -> bool:
|
|
714
|
+
text = task.strip().lower()
|
|
715
|
+
if looks_like_question(text) or looks_like_ship(text):
|
|
716
|
+
return False
|
|
717
|
+
return bool(_COMPARE.search(text))
|