lemming-cli 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.
- lemming/__init__.py +1 -0
- lemming/api/__init__.py +5 -0
- lemming/api/auth.py +34 -0
- lemming/api/auth_test.py +40 -0
- lemming/api/config.py +85 -0
- lemming/api/config_test.py +84 -0
- lemming/api/conftest.py +204 -0
- lemming/api/context.py +39 -0
- lemming/api/context_test.py +62 -0
- lemming/api/directories.py +57 -0
- lemming/api/directories_test.py +94 -0
- lemming/api/files.py +116 -0
- lemming/api/files_test.py +163 -0
- lemming/api/hooks.py +15 -0
- lemming/api/hooks_test.py +33 -0
- lemming/api/logging.py +16 -0
- lemming/api/logging_test.py +59 -0
- lemming/api/loop.py +45 -0
- lemming/api/loop_test.py +58 -0
- lemming/api/main.py +53 -0
- lemming/api/main_test.py +30 -0
- lemming/api/tasks.py +170 -0
- lemming/api/tasks_test.py +426 -0
- lemming/api.py +5 -0
- lemming/api_test.py +7 -0
- lemming/cli/__init__.py +12 -0
- lemming/cli/config.py +67 -0
- lemming/cli/config_test.py +50 -0
- lemming/cli/context.py +40 -0
- lemming/cli/context_test.py +49 -0
- lemming/cli/hooks.py +147 -0
- lemming/cli/hooks_test.py +50 -0
- lemming/cli/main.py +42 -0
- lemming/cli/main_test.py +21 -0
- lemming/cli/operations.py +226 -0
- lemming/cli/operations_test.py +44 -0
- lemming/cli/progress.py +54 -0
- lemming/cli/progress_test.py +40 -0
- lemming/cli/readability_cli.py +28 -0
- lemming/cli/readability_cli_test.py +57 -0
- lemming/cli/tasks.py +529 -0
- lemming/cli/tasks_test.py +168 -0
- lemming/cli.py +5 -0
- lemming/cli_test.py +22 -0
- lemming/conftest.py +13 -0
- lemming/hooks.py +145 -0
- lemming/hooks_test.py +180 -0
- lemming/integration_test.py +299 -0
- lemming/main.py +6 -0
- lemming/main_test.py +44 -0
- lemming/models.py +88 -0
- lemming/models_test.py +91 -0
- lemming/orchestrator.py +407 -0
- lemming/orchestrator_test.py +468 -0
- lemming/paths.py +245 -0
- lemming/paths_test.py +186 -0
- lemming/persistence.py +179 -0
- lemming/persistence_test.py +150 -0
- lemming/prompts/hooks/readability.md +42 -0
- lemming/prompts/hooks/roadmap.md +61 -0
- lemming/prompts/hooks/testing.md +44 -0
- lemming/prompts/taskrunner.md +69 -0
- lemming/prompts.py +354 -0
- lemming/prompts_test.py +421 -0
- lemming/providers.py +190 -0
- lemming/providers_test.py +73 -0
- lemming/runner.py +327 -0
- lemming/runner_test.py +378 -0
- lemming/tasks/__init__.py +75 -0
- lemming/tasks/lifecycle.py +331 -0
- lemming/tasks/lifecycle_test.py +312 -0
- lemming/tasks/operations.py +258 -0
- lemming/tasks/operations_test.py +172 -0
- lemming/tasks/progress.py +29 -0
- lemming/tasks/progress_test.py +22 -0
- lemming/tasks/queries.py +128 -0
- lemming/tasks/queries_test.py +233 -0
- lemming/web/dashboard.spec.js +350 -0
- lemming/web/dashboard.test.js +998 -0
- lemming/web/favicon.js +40 -0
- lemming/web/favicon.spec.js +242 -0
- lemming/web/files.html +375 -0
- lemming/web/files.spec.js +97 -0
- lemming/web/index.html +983 -0
- lemming/web/index.js +753 -0
- lemming/web/logs.html +358 -0
- lemming/web/logs.test.js +195 -0
- lemming/web/mancha.js +58 -0
- lemming/web/screenshots.spec.js +328 -0
- lemming_cli-0.1.0.dist-info/METADATA +314 -0
- lemming_cli-0.1.0.dist-info/RECORD +94 -0
- lemming_cli-0.1.0.dist-info/WHEEL +4 -0
- lemming_cli-0.1.0.dist-info/entry_points.txt +2 -0
- lemming_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Task Runner
|
|
2
|
+
|
|
3
|
+
You are an autonomous AI coding agent managed by the 'Lemming' orchestrator.
|
|
4
|
+
|
|
5
|
+
## The Project Roadmap
|
|
6
|
+
|
|
7
|
+
{{roadmap}}{{progress}}
|
|
8
|
+
|
|
9
|
+
## Your Assignment
|
|
10
|
+
|
|
11
|
+
Your CURRENT, EXCLUSIVE task is: **{{description}}**
|
|
12
|
+
|
|
13
|
+
## Critical Directives
|
|
14
|
+
|
|
15
|
+
1. **Execute:** Write the code to fulfill the current task. Run any necessary
|
|
16
|
+
tests. **Your operations should be idempotent** — your process may be killed
|
|
17
|
+
at any time (e.g. due to a timeout) and the task retried from scratch. Design
|
|
18
|
+
your work so that re-running it on a partially modified workspace produces
|
|
19
|
+
the correct result: check whether changes already exist before applying them,
|
|
20
|
+
use create-or-update patterns, and avoid operations that fail if run twice.
|
|
21
|
+
2. **DO NOT edit `{{tasks_file_name}}` directly.** You must use the Lemming CLI
|
|
22
|
+
API.
|
|
23
|
+
3. **Task Management:** You may manipulate the task list to add or insert new
|
|
24
|
+
tasks at any position in the queue. However, you should generally only do
|
|
25
|
+
this if it is explicitly requested by the user or clearly necessary to
|
|
26
|
+
complete your current assignment. Use
|
|
27
|
+
`lemming --tasks-file {{tasks_file_path}} --help` for the full list of
|
|
28
|
+
available commands. Assume the Lemming queue is in a running state and will
|
|
29
|
+
pick up new tasks automatically.
|
|
30
|
+
- **Logs:** If this is a retry or you need context from a previous task's
|
|
31
|
+
execution, you can read the full runner log with
|
|
32
|
+
`lemming --tasks-file {{tasks_file_path}} logs [<id>]`. If no ID is
|
|
33
|
+
provided, it shows the log for the currently active task.
|
|
34
|
+
|
|
35
|
+
- **Context Isolation:** Be aware that newly scheduled tasks will start with
|
|
36
|
+
a fresh, empty conversation history. Their only context is the global
|
|
37
|
+
roadmap, previously recorded progress from completed tasks, and the state
|
|
38
|
+
of the file system.
|
|
39
|
+
- **Self-Contained Descriptions:** Because of this isolation, you MUST write
|
|
40
|
+
extremely thorough, self-contained descriptions for any new tasks you
|
|
41
|
+
schedule. Reference specific task IDs, file paths, and exact symbols so the
|
|
42
|
+
new task knows exactly what to do.
|
|
43
|
+
- **Complex Handoffs:** If you need to pass extensive context (like detailed
|
|
44
|
+
architectural plans, specific error traces, or large code snippets) to a
|
|
45
|
+
downstream task, do not rely on the brief progress messages alone. Instead,
|
|
46
|
+
create a dedicated file in the workspace to hold this context, and
|
|
47
|
+
explicitly mention its path in the new task's description. Use this
|
|
48
|
+
technique judiciously to avoid creating a mess of orphaned files in the
|
|
49
|
+
project.
|
|
50
|
+
|
|
51
|
+
4. **Progress:** Your first action should be to record a brief progress entry
|
|
52
|
+
describing how you intend to approach this task — e.g. which files you will
|
|
53
|
+
modify, what strategy you will use. This lets the operator verify your
|
|
54
|
+
direction early. Then continue recording progress as you work: each time you
|
|
55
|
+
complete a meaningful step, discover something relevant, or hit a problem,
|
|
56
|
+
record it immediately. If your process is killed, recorded progress carries
|
|
57
|
+
over to the next attempt. At least one progress entry is required before
|
|
58
|
+
completing or failing a task:
|
|
59
|
+
`lemming --tasks-file {{tasks_file_path}} progress {{task_id}} '<what you did or found>'`
|
|
60
|
+
5. **Success:** When you have completely finished and verified the task, and
|
|
61
|
+
recorded ALL relevant progress (at least one is required), run:
|
|
62
|
+
`lemming --tasks-file {{tasks_file_path}} complete {{task_id}}`
|
|
63
|
+
6. **Failure/Blocker:** If you hit a technical roadblock, cannot fix a bug, or
|
|
64
|
+
are unable to complete the task, after recording ALL relevant progress (at
|
|
65
|
+
least one is required), run:
|
|
66
|
+
`lemming --tasks-file {{tasks_file_path}} fail {{task_id}}`
|
|
67
|
+
|
|
68
|
+
7. Stop and exit after running either the complete or fail command.
|
|
69
|
+
{{time_limit_section}}
|
lemming/prompts.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Prompt template loading and rendering for runners and orchestrator hooks."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import pathlib
|
|
5
|
+
|
|
6
|
+
from . import paths, runner, tasks
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def ensure_hooks_symlinked():
|
|
12
|
+
"""Ensures built-in hooks are symlinked in the global hooks directory.
|
|
13
|
+
|
|
14
|
+
If a hook already exists (as a file or symlink), it is not overwritten.
|
|
15
|
+
This allows users to override them by replacing the symlink with their
|
|
16
|
+
own file.
|
|
17
|
+
"""
|
|
18
|
+
global_hooks_dir = paths.get_global_hooks_dir()
|
|
19
|
+
global_hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
|
|
21
|
+
base_path = pathlib.Path(__file__).parent / "prompts" / "hooks"
|
|
22
|
+
if not base_path.exists():
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
for f in base_path.glob("*.md"):
|
|
26
|
+
target = global_hooks_dir / f.name
|
|
27
|
+
# We only create the symlink if NOTHING exists there yet.
|
|
28
|
+
# This respects manual deletion as a way to "disable" the global
|
|
29
|
+
# symlink, but the hook will still be available as a built-in
|
|
30
|
+
# fallback unless disabled in the project's tasks.yml.
|
|
31
|
+
if not target.exists() and not target.is_symlink():
|
|
32
|
+
try:
|
|
33
|
+
target.symlink_to(f.absolute())
|
|
34
|
+
except (OSError, PermissionError) as e:
|
|
35
|
+
logger.error(
|
|
36
|
+
"Failed to create symlink for hook %s: %s", target, e
|
|
37
|
+
)
|
|
38
|
+
# If we hit permission issues, we want to know, not just
|
|
39
|
+
# skip silently
|
|
40
|
+
if isinstance(e, PermissionError):
|
|
41
|
+
raise e
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_prompt(name: str, tasks_file: pathlib.Path | None = None) -> str:
|
|
45
|
+
"""Loads a prompt template from the project, global, or built-in hooks.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
name: Name of the prompt template (without .md extension).
|
|
49
|
+
tasks_file: Optional path to the tasks file to look for local hooks.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The content of the prompt template.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
FileNotFoundError: If the prompt template does not exist.
|
|
56
|
+
"""
|
|
57
|
+
# 1. Look for local hooks in the project directory
|
|
58
|
+
if tasks_file:
|
|
59
|
+
working_dir = paths.get_working_dir(tasks_file)
|
|
60
|
+
local_hook_path = working_dir / ".lemming" / "hooks" / f"{name}.md"
|
|
61
|
+
if local_hook_path.exists():
|
|
62
|
+
return local_hook_path.read_text(encoding="utf-8")
|
|
63
|
+
|
|
64
|
+
# 2. Look in global hooks directory (~/.local/lemming/hooks)
|
|
65
|
+
global_hooks_dir = paths.get_global_hooks_dir()
|
|
66
|
+
global_hook_path = global_hooks_dir / f"{name}.md"
|
|
67
|
+
if global_hook_path.exists():
|
|
68
|
+
content = global_hook_path.read_text(encoding="utf-8")
|
|
69
|
+
if content.strip():
|
|
70
|
+
return content
|
|
71
|
+
|
|
72
|
+
# 3. Look in built-in prompts directory (fallback)
|
|
73
|
+
base_path = pathlib.Path(__file__).parent / "prompts"
|
|
74
|
+
|
|
75
|
+
# Try exact name first (e.g. taskrunner)
|
|
76
|
+
path = base_path / f"{name}.md"
|
|
77
|
+
if path.exists():
|
|
78
|
+
return path.read_text(encoding="utf-8")
|
|
79
|
+
|
|
80
|
+
# Try hooks subdirectory
|
|
81
|
+
path = base_path / "hooks" / f"{name}.md"
|
|
82
|
+
if path.exists():
|
|
83
|
+
return path.read_text(encoding="utf-8")
|
|
84
|
+
|
|
85
|
+
raise FileNotFoundError(f"Prompt template {name} not found")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def list_hooks(tasks_file: pathlib.Path | None = None) -> list[str]:
|
|
89
|
+
"""Lists available orchestrator hooks.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
tasks_file: Optional path to the tasks file to look for local hooks.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
A list of hook names.
|
|
96
|
+
"""
|
|
97
|
+
hooks = set()
|
|
98
|
+
|
|
99
|
+
# 1. Look for local hooks in the project directory
|
|
100
|
+
if tasks_file:
|
|
101
|
+
working_dir = paths.get_working_dir(tasks_file)
|
|
102
|
+
local_hooks_dir = working_dir / ".lemming" / "hooks"
|
|
103
|
+
if local_hooks_dir.exists():
|
|
104
|
+
for f in local_hooks_dir.glob("*.md"):
|
|
105
|
+
hooks.add(f.stem)
|
|
106
|
+
|
|
107
|
+
# 2. Look in global hooks directory
|
|
108
|
+
global_hooks_dir = paths.get_global_hooks_dir()
|
|
109
|
+
if global_hooks_dir.exists():
|
|
110
|
+
for f in global_hooks_dir.glob("*.md"):
|
|
111
|
+
hooks.add(f.stem)
|
|
112
|
+
|
|
113
|
+
# 3. Look in built-in prompts directory (always included)
|
|
114
|
+
base_path = pathlib.Path(__file__).parent / "prompts" / "hooks"
|
|
115
|
+
if base_path.exists():
|
|
116
|
+
for f in base_path.glob("*.md"):
|
|
117
|
+
hooks.add(f.stem)
|
|
118
|
+
|
|
119
|
+
# Ensure 'roadmap' is always last
|
|
120
|
+
hooks_list = sorted(list(hooks))
|
|
121
|
+
if "roadmap" in hooks_list:
|
|
122
|
+
hooks_list.remove("roadmap")
|
|
123
|
+
hooks_list.append("roadmap")
|
|
124
|
+
|
|
125
|
+
return hooks_list
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _format_roadmap(
|
|
129
|
+
data: tasks.Roadmap, current_task_id: str | None = None
|
|
130
|
+
) -> str:
|
|
131
|
+
"""Formats the roadmap for inclusion in prompts.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
data: The current Roadmap.
|
|
135
|
+
current_task_id: ID of the task currently being executed, if any.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
A formatted roadmap string.
|
|
139
|
+
"""
|
|
140
|
+
roadmap_str = (
|
|
141
|
+
f"## Project Context\n{data.context or 'No context provided.'}\n\n"
|
|
142
|
+
)
|
|
143
|
+
roadmap_str += "## Roadmap\n"
|
|
144
|
+
|
|
145
|
+
completed_tasks = [
|
|
146
|
+
t
|
|
147
|
+
for t in data.tasks
|
|
148
|
+
if (t.requested_status or t.status) == tasks.TaskStatus.COMPLETED
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
for i, t in enumerate(data.tasks):
|
|
152
|
+
effective_status = t.requested_status or t.status
|
|
153
|
+
if effective_status == tasks.TaskStatus.COMPLETED:
|
|
154
|
+
marker = "[COMPLETED]"
|
|
155
|
+
elif effective_status == tasks.TaskStatus.FAILED:
|
|
156
|
+
marker = f"[FAILED - {t.attempts}/{data.config.retries} attempt(s)]"
|
|
157
|
+
elif (
|
|
158
|
+
t.status == tasks.TaskStatus.IN_PROGRESS or t.id == current_task_id
|
|
159
|
+
):
|
|
160
|
+
marker = "[IN PROGRESS]"
|
|
161
|
+
elif t.attempts > 0:
|
|
162
|
+
marker = (
|
|
163
|
+
f"[PENDING - {t.attempts}/{data.config.retries}"
|
|
164
|
+
" attempt(s) so far]"
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
marker = "[PENDING]"
|
|
168
|
+
|
|
169
|
+
# Bold the current task to make it stand out
|
|
170
|
+
if t.id == current_task_id:
|
|
171
|
+
roadmap_str += f"- **{marker} ({t.id}) {t.description}**\n"
|
|
172
|
+
else:
|
|
173
|
+
roadmap_str += f"- {marker} ({t.id}) {t.description}\n"
|
|
174
|
+
|
|
175
|
+
if t.progress:
|
|
176
|
+
# For completed tasks, only show progress for the last 5 to
|
|
177
|
+
# keep the prompt concise
|
|
178
|
+
if effective_status == tasks.TaskStatus.COMPLETED:
|
|
179
|
+
completed_index = completed_tasks.index(t)
|
|
180
|
+
if len(completed_tasks) - completed_index <= 5:
|
|
181
|
+
for o in t.progress:
|
|
182
|
+
roadmap_str += f" - {o}\n"
|
|
183
|
+
else:
|
|
184
|
+
for o in t.progress:
|
|
185
|
+
roadmap_str += f" - {o}\n"
|
|
186
|
+
|
|
187
|
+
return roadmap_str
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def prepare_hook_prompt(
|
|
191
|
+
hook_name: str,
|
|
192
|
+
data: tasks.Roadmap,
|
|
193
|
+
finished_task: tasks.Task,
|
|
194
|
+
tasks_file: pathlib.Path,
|
|
195
|
+
) -> str:
|
|
196
|
+
"""Prepares the prompt for a specific orchestrator hook.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
hook_name: Name of the orchestrator hook (e.g. "roadmap").
|
|
200
|
+
data: The current Roadmap.
|
|
201
|
+
finished_task: The Task that just finished executing.
|
|
202
|
+
tasks_file: Path to the tasks YAML file.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
The fully rendered hook prompt string.
|
|
206
|
+
"""
|
|
207
|
+
roadmap_str = _format_roadmap(data, current_task_id=finished_task.id)
|
|
208
|
+
|
|
209
|
+
# Use requested_status when available — it reflects the actual outcome
|
|
210
|
+
# (e.g. FAILED) while status may still be IN_PROGRESS during hook execution.
|
|
211
|
+
result_status = finished_task.requested_status or finished_task.status
|
|
212
|
+
|
|
213
|
+
finished_str = f"Task ID: {finished_task.id}\n"
|
|
214
|
+
finished_str += f"Description: {finished_task.description}\n"
|
|
215
|
+
finished_str += f"Result: {result_status}\n"
|
|
216
|
+
finished_str += (
|
|
217
|
+
f"Attempts: {finished_task.attempts}/{data.config.retries}\n"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if (
|
|
221
|
+
finished_task.attempts >= data.config.retries
|
|
222
|
+
and result_status == tasks.TaskStatus.FAILED
|
|
223
|
+
):
|
|
224
|
+
finished_str += "\n!!! WARNING: FINAL ATTEMPT FAILED !!!\n"
|
|
225
|
+
finished_str += (
|
|
226
|
+
f"This task has reached the maximum of"
|
|
227
|
+
f" {data.config.retries} attempts.\n"
|
|
228
|
+
)
|
|
229
|
+
finished_str += (
|
|
230
|
+
"Unless you intervene NOW (by resetting it with a new approach,\n"
|
|
231
|
+
)
|
|
232
|
+
finished_str += (
|
|
233
|
+
"editing it, or replacing it), the entire orchestrator loop will\n"
|
|
234
|
+
)
|
|
235
|
+
finished_str += "ABORT and the project will fail.\n"
|
|
236
|
+
|
|
237
|
+
if finished_task.progress:
|
|
238
|
+
finished_str += "Progress recorded during this attempt:\n"
|
|
239
|
+
for o in finished_task.progress:
|
|
240
|
+
finished_str += f"- {o}\n"
|
|
241
|
+
|
|
242
|
+
# Include the last 100 lines of the runner log for the finished task.
|
|
243
|
+
# We filter out 'Command:' lines because they contain the full previous
|
|
244
|
+
# prompt and cause exponential escaping growth when prompts are re-quoted.
|
|
245
|
+
log_file = paths.get_log_file(tasks_file, finished_task.id)
|
|
246
|
+
if log_file.exists():
|
|
247
|
+
try:
|
|
248
|
+
with open(log_file, "r", encoding="utf-8") as f:
|
|
249
|
+
lines = f.readlines()
|
|
250
|
+
filtered = [
|
|
251
|
+
line for line in lines if not line.startswith("Command: ")
|
|
252
|
+
]
|
|
253
|
+
last_lines = [line.rstrip() for line in filtered[-100:]]
|
|
254
|
+
finished_str += (
|
|
255
|
+
"\nExecution log of THIS task (last 100 lines):\n"
|
|
256
|
+
)
|
|
257
|
+
finished_str += "```\n"
|
|
258
|
+
finished_str += "\n".join(last_lines)
|
|
259
|
+
finished_str += "\n```\n"
|
|
260
|
+
except Exception as e:
|
|
261
|
+
finished_str += f"\n(Could not read log file: {e})\n"
|
|
262
|
+
|
|
263
|
+
tasks_file_str = runner._pretty_quote(str(tasks_file))
|
|
264
|
+
prompt_template = load_prompt(hook_name, tasks_file)
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
prompt_template.replace("{{roadmap}}", roadmap_str)
|
|
268
|
+
.replace("{{finished_task}}", finished_str)
|
|
269
|
+
.replace("{{finished_task_id}}", finished_task.id)
|
|
270
|
+
.replace("{{tasks_file_name}}", tasks_file.name)
|
|
271
|
+
.replace("{{tasks_file_path}}", tasks_file_str)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def prepare_prompt(
|
|
276
|
+
data: tasks.Roadmap,
|
|
277
|
+
task: tasks.Task,
|
|
278
|
+
tasks_file: pathlib.Path,
|
|
279
|
+
time_limit: int = 0,
|
|
280
|
+
) -> str:
|
|
281
|
+
"""Prepares the runner prompt based on the current roadmap state.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
data: The current Roadmap.
|
|
285
|
+
task: The Task being executed.
|
|
286
|
+
tasks_file: Path to the tasks YAML file.
|
|
287
|
+
time_limit: Maximum execution time in minutes. 0 means no limit.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
The fully rendered prompt string.
|
|
291
|
+
"""
|
|
292
|
+
roadmap_str = _format_roadmap(data, current_task_id=task.id)
|
|
293
|
+
|
|
294
|
+
# Add parent task context if it's from another project
|
|
295
|
+
if task.parent and task.parent_tasks_file:
|
|
296
|
+
try:
|
|
297
|
+
parent_tasks_path = pathlib.Path(task.parent_tasks_file)
|
|
298
|
+
if parent_tasks_path.exists():
|
|
299
|
+
parent_roadmap = tasks.load_tasks(parent_tasks_path)
|
|
300
|
+
parent_task = next(
|
|
301
|
+
(t for t in parent_roadmap.tasks if t.id == task.parent),
|
|
302
|
+
None,
|
|
303
|
+
)
|
|
304
|
+
if parent_task:
|
|
305
|
+
roadmap_str += (
|
|
306
|
+
"\n## Parent Task Context (From root project)\n"
|
|
307
|
+
)
|
|
308
|
+
roadmap_str += f"- [ ] {parent_task.description}\n"
|
|
309
|
+
if parent_task.progress:
|
|
310
|
+
for progress_item in parent_task.progress:
|
|
311
|
+
roadmap_str += f" - {progress_item}\n"
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
progress_str = ""
|
|
316
|
+
if task.progress:
|
|
317
|
+
progress_str = "### Progress from Previous Attempts on THIS Task\n"
|
|
318
|
+
for progress_item in task.progress:
|
|
319
|
+
progress_str += f"- {progress_item}\n"
|
|
320
|
+
progress_str += "\n"
|
|
321
|
+
|
|
322
|
+
time_limit_section = ""
|
|
323
|
+
if time_limit > 0:
|
|
324
|
+
time_limit_section = (
|
|
325
|
+
f"\n## Time Limit\n\n"
|
|
326
|
+
f"You have a hard time limit of **{time_limit} minutes**."
|
|
327
|
+
f" If you exceed it, your\n"
|
|
328
|
+
f"process will be killed and any unrecorded progress will be"
|
|
329
|
+
f" lost.\n\n"
|
|
330
|
+
f"- **Record progress early and often.** Don't wait until the"
|
|
331
|
+
f" end. If you\n"
|
|
332
|
+
f" are killed, your recorded progress will be passed to the"
|
|
333
|
+
f" next attempt.\n"
|
|
334
|
+
f"- **If the work is too large** for {time_limit} minutes,"
|
|
335
|
+
f" break it into smaller\n"
|
|
336
|
+
f" sub-tasks using `lemming` and complete what you can.\n"
|
|
337
|
+
f"- **Leverage background tasks and subagents** if your runner"
|
|
338
|
+
f" supports\n"
|
|
339
|
+
f" them. Long-running operations (builds, test suites, large"
|
|
340
|
+
f" refactors)\n"
|
|
341
|
+
f" are good candidates for parallel execution."
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
tasks_file_str = runner._pretty_quote(str(tasks_file))
|
|
345
|
+
prompt_template = load_prompt("taskrunner", tasks_file)
|
|
346
|
+
return (
|
|
347
|
+
prompt_template.replace("{{roadmap}}", roadmap_str)
|
|
348
|
+
.replace("{{progress}}", progress_str)
|
|
349
|
+
.replace("{{description}}", task.description)
|
|
350
|
+
.replace("{{tasks_file_name}}", tasks_file.name)
|
|
351
|
+
.replace("{{tasks_file_path}}", tasks_file_str)
|
|
352
|
+
.replace("{{task_id}}", task.id)
|
|
353
|
+
.replace("{{time_limit_section}}", time_limit_section)
|
|
354
|
+
)
|