@hallucination-studio/harness-engine 1.0.0 → 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +185 -27
- package/bin/install.js +29 -17
- package/package.json +10 -4
- package/skills/harness-engine/SKILL.md +97 -0
- package/skills/harness-engine/agents/openai.yaml +4 -0
- package/skills/harness-engine/evals/cases.json +94 -0
- package/skills/harness-engine/evals/harness_engine_evals/__init__.py +1 -0
- package/skills/harness-engine/evals/harness_engine_evals/cases_frontend.py +211 -0
- package/skills/harness-engine/evals/harness_engine_evals/cases_lifecycle.py +1616 -0
- package/skills/harness-engine/evals/harness_engine_evals/helpers.py +155 -0
- package/skills/harness-engine/evals/harness_engine_evals/registry.py +55 -0
- package/skills/harness-engine/evals/harness_engine_evals/report.py +36 -0
- package/skills/harness-engine/evals/harness_engine_evals/runner.py +53 -0
- package/skills/harness-engine/evals/run_evals.py +14 -0
- package/skills/{harness-repo-bootstrap → harness-engine}/references/evaluation-loop.md +8 -2
- package/skills/harness-engine/references/evidence-first-evals.md +187 -0
- package/skills/harness-engine/references/exec-plans.md +59 -0
- package/skills/{harness-repo-bootstrap → harness-engine}/references/file-map.md +3 -3
- package/skills/{harness-repo-bootstrap → harness-engine}/references/knowledge-capture.md +2 -2
- package/skills/{harness-repo-bootstrap → harness-engine}/references/sop-index.md +3 -0
- package/skills/harness-engine/references/template-policy.md +17 -0
- package/skills/harness-engine/references/workflow.md +62 -0
- package/skills/harness-engine/scripts/harness_engine/__init__.py +1 -0
- package/skills/harness-engine/scripts/harness_engine/analysis.py +240 -0
- package/skills/harness-engine/scripts/harness_engine/checks.py +287 -0
- package/skills/harness-engine/scripts/harness_engine/cli.py +656 -0
- package/skills/harness-engine/scripts/harness_engine/common.py +977 -0
- package/skills/harness-engine/scripts/harness_engine/continuation.py +520 -0
- package/skills/harness-engine/scripts/harness_engine/git_ops.py +88 -0
- package/skills/harness-engine/scripts/harness_engine/knowledge.py +329 -0
- package/skills/harness-engine/scripts/harness_engine/plans.py +630 -0
- package/skills/harness-engine/scripts/harness_engine/templates.py +124 -0
- package/skills/harness-engine/scripts/manage_harness.py +14 -0
- package/skills/harness-repo-bootstrap/SKILL.md +0 -68
- package/skills/harness-repo-bootstrap/agents/openai.yaml +0 -4
- package/skills/harness-repo-bootstrap/evals/cases.json +0 -18
- package/skills/harness-repo-bootstrap/evals/run_evals.py +0 -337
- package/skills/harness-repo-bootstrap/references/exec-plans.md +0 -39
- package/skills/harness-repo-bootstrap/references/template-policy.md +0 -12
- package/skills/harness-repo-bootstrap/references/workflow.md +0 -47
- package/skills/harness-repo-bootstrap/scripts/manage_harness.py +0 -1181
- /package/skills/{harness-repo-bootstrap → harness-engine}/assets/repo-template/.keep +0 -0
- /package/skills/{harness-repo-bootstrap → harness-engine}/assets/sops/.keep +0 -0
- /package/skills/{harness-repo-bootstrap → harness-engine}/references/question-catalog.md +0 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
from .common import *
|
|
2
|
+
from .plans import find_section, phase_number_from_text, plan_title, replace_section, section_key_values, slugify, mark_state_dirty, sync_state_from_markdown, load_plan_state, save_plan_state
|
|
3
|
+
from .templates import DOC_FILES, ensure_parent
|
|
4
|
+
|
|
5
|
+
def default_workstream_id_from_plan(plan_path, text):
|
|
6
|
+
source = plan_path.stem
|
|
7
|
+
source = re.sub(r"^\d{4}-\d{2}-\d{2}-", "", source)
|
|
8
|
+
source = re.sub(r"phase[-_\s]*\d+", "", source, flags=re.IGNORECASE)
|
|
9
|
+
source = source.strip("-_ ")
|
|
10
|
+
if not source:
|
|
11
|
+
source = plan_title(text)
|
|
12
|
+
source = re.sub(r"phase[-_\s]*\d+", "", source, flags=re.IGNORECASE)
|
|
13
|
+
return slugify(source or "workstream")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def map_legacy_phase_mode(mode):
|
|
17
|
+
legacy = (mode or "").strip().lower()
|
|
18
|
+
if legacy in {"single-phase", "single", "none", "completed"}:
|
|
19
|
+
return "complete"
|
|
20
|
+
if legacy in {"multi-phase", "phased"}:
|
|
21
|
+
return "continue"
|
|
22
|
+
if legacy == "paused":
|
|
23
|
+
return "pause"
|
|
24
|
+
if legacy == "stopped":
|
|
25
|
+
return "stop"
|
|
26
|
+
return legacy
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def continuation_decision_for_plan(plan_path, text):
|
|
30
|
+
values = section_key_values(text, "Continuation Decision")
|
|
31
|
+
source = "continuation"
|
|
32
|
+
if values is None:
|
|
33
|
+
values = section_key_values(text, "Phase Continuity")
|
|
34
|
+
source = "phase"
|
|
35
|
+
detected_phase = phase_number_from_text(plan_path.stem) or phase_number_from_text(plan_title(text))
|
|
36
|
+
if values is None:
|
|
37
|
+
return {
|
|
38
|
+
"status": "missing",
|
|
39
|
+
"source": None,
|
|
40
|
+
"detected_phase": detected_phase,
|
|
41
|
+
"decision": None,
|
|
42
|
+
"workstream": None,
|
|
43
|
+
"next_target": None,
|
|
44
|
+
"next_action": None,
|
|
45
|
+
"closure_reason": None,
|
|
46
|
+
"resume_notes": None,
|
|
47
|
+
}
|
|
48
|
+
if source == "phase":
|
|
49
|
+
decision = map_legacy_phase_mode(values.get("mode", ""))
|
|
50
|
+
next_target = values.get("continuation")
|
|
51
|
+
else:
|
|
52
|
+
decision = values.get("decision", "").lower()
|
|
53
|
+
next_target = values.get("next_target") or values.get("continuation")
|
|
54
|
+
workstream = values.get("workstream")
|
|
55
|
+
next_action = values.get("next_action")
|
|
56
|
+
closure_reason = values.get("closure_reason")
|
|
57
|
+
resume_notes = values.get("resume_notes")
|
|
58
|
+
return {
|
|
59
|
+
"status": "present",
|
|
60
|
+
"source": source,
|
|
61
|
+
"detected_phase": detected_phase,
|
|
62
|
+
"decision": decision,
|
|
63
|
+
"workstream": workstream,
|
|
64
|
+
"next_target": next_target,
|
|
65
|
+
"next_action": next_action,
|
|
66
|
+
"closure_reason": closure_reason,
|
|
67
|
+
"resume_notes": resume_notes,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def phase_continuity_for_plan(plan_path, text):
|
|
72
|
+
return continuation_decision_for_plan(plan_path, text)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_empty_continuity_value(value):
|
|
76
|
+
if value is None:
|
|
77
|
+
return True
|
|
78
|
+
return value.strip().lower() in {"", "none", "pending", "unknown", "n/a", "-"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def target_exists_for_continuation(repo, next_target, workstream):
|
|
82
|
+
target = next_target.split("#", 1)[0].strip()
|
|
83
|
+
if target in {"", "none"}:
|
|
84
|
+
return False
|
|
85
|
+
if "workstreams.md" in target:
|
|
86
|
+
ledger = workstreams_path(repo)
|
|
87
|
+
return ledger.exists() and not is_empty_continuity_value(workstream) and workstream in ledger.read_text()
|
|
88
|
+
return (repo / target).exists()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def deferred_target_exists(repo, next_target):
|
|
92
|
+
target = next_target.split("#", 1)[0].strip()
|
|
93
|
+
if target in {"", "none"}:
|
|
94
|
+
return False
|
|
95
|
+
if "tech-debt-tracker.md" in target:
|
|
96
|
+
return (repo / "docs" / "exec-plans" / "tech-debt-tracker.md").exists()
|
|
97
|
+
return (repo / target).exists()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def continuation_decision_issues(repo, plan_path, plan_text):
|
|
101
|
+
continuity = continuation_decision_for_plan(plan_path, plan_text)
|
|
102
|
+
if continuity["status"] == "missing":
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
"severity": "error",
|
|
106
|
+
"code": "missing-continuation-decision",
|
|
107
|
+
"path": str(plan_path.relative_to(repo)),
|
|
108
|
+
"message": "Plan is missing a Continuation Decision section.",
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
issues = []
|
|
112
|
+
relative_plan = str(plan_path.relative_to(repo))
|
|
113
|
+
decision = continuity["decision"]
|
|
114
|
+
if is_empty_continuity_value(decision):
|
|
115
|
+
issues.append(
|
|
116
|
+
{
|
|
117
|
+
"severity": "error",
|
|
118
|
+
"code": "continuation-decision-pending",
|
|
119
|
+
"path": relative_plan,
|
|
120
|
+
"message": "Continuation Decision must be set before plan closure.",
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
return issues
|
|
124
|
+
if decision not in {"complete", "continue", "pause", "stop", "defer"}:
|
|
125
|
+
issues.append(
|
|
126
|
+
{
|
|
127
|
+
"severity": "error",
|
|
128
|
+
"code": "invalid-continuation-decision",
|
|
129
|
+
"path": relative_plan,
|
|
130
|
+
"message": "Continuation Decision must be one of complete, continue, pause, stop, or defer.",
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
return issues
|
|
134
|
+
workstream = continuity["workstream"]
|
|
135
|
+
next_target = continuity["next_target"]
|
|
136
|
+
next_action = continuity["next_action"]
|
|
137
|
+
closure_reason = continuity["closure_reason"]
|
|
138
|
+
resume_notes = continuity["resume_notes"]
|
|
139
|
+
if decision == "complete":
|
|
140
|
+
return issues
|
|
141
|
+
if decision in {"continue", "pause"} and is_empty_continuity_value(workstream):
|
|
142
|
+
issues.append(
|
|
143
|
+
{
|
|
144
|
+
"severity": "error",
|
|
145
|
+
"code": "missing-workstream",
|
|
146
|
+
"path": relative_plan,
|
|
147
|
+
"message": "Continue or pause decisions must name a resumable workstream.",
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
if decision in {"continue", "pause"} and is_empty_continuity_value(next_action):
|
|
151
|
+
issues.append(
|
|
152
|
+
{
|
|
153
|
+
"severity": "error",
|
|
154
|
+
"code": "missing-next-action",
|
|
155
|
+
"path": relative_plan,
|
|
156
|
+
"message": "Continue or pause decisions must record a concrete next action for recovery.",
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
if decision == "continue":
|
|
160
|
+
if is_empty_continuity_value(next_target):
|
|
161
|
+
issues.append(
|
|
162
|
+
{
|
|
163
|
+
"severity": "error",
|
|
164
|
+
"code": "missing-next-target",
|
|
165
|
+
"path": relative_plan,
|
|
166
|
+
"message": "Continue decisions must point to a next plan or workstream target.",
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
elif not target_exists_for_continuation(repo, next_target, workstream):
|
|
170
|
+
issues.append(
|
|
171
|
+
{
|
|
172
|
+
"severity": "error",
|
|
173
|
+
"code": "missing-continuation-target",
|
|
174
|
+
"path": relative_plan,
|
|
175
|
+
"message": "Continue decision points to a missing plan or missing workstream entry.",
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
if decision == "pause":
|
|
179
|
+
if is_empty_continuity_value(closure_reason):
|
|
180
|
+
issues.append(
|
|
181
|
+
{
|
|
182
|
+
"severity": "error",
|
|
183
|
+
"code": "missing-resume-condition",
|
|
184
|
+
"path": relative_plan,
|
|
185
|
+
"message": "Pause decisions must record the condition for resuming.",
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
if is_empty_continuity_value(resume_notes):
|
|
189
|
+
issues.append(
|
|
190
|
+
{
|
|
191
|
+
"severity": "error",
|
|
192
|
+
"code": "missing-resume-notes",
|
|
193
|
+
"path": relative_plan,
|
|
194
|
+
"message": "Pause decisions must include resume notes.",
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
if decision == "stop" and is_empty_continuity_value(closure_reason):
|
|
198
|
+
issues.append(
|
|
199
|
+
{
|
|
200
|
+
"severity": "error",
|
|
201
|
+
"code": "missing-closure-reason",
|
|
202
|
+
"path": relative_plan,
|
|
203
|
+
"message": "Stop decisions must explain why the work is ending.",
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
if decision == "defer":
|
|
207
|
+
if is_empty_continuity_value(next_target):
|
|
208
|
+
issues.append(
|
|
209
|
+
{
|
|
210
|
+
"severity": "error",
|
|
211
|
+
"code": "missing-deferred-target",
|
|
212
|
+
"path": relative_plan,
|
|
213
|
+
"message": "Defer decisions must record a tech-debt or follow-up target.",
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
elif not deferred_target_exists(repo, next_target):
|
|
217
|
+
issues.append(
|
|
218
|
+
{
|
|
219
|
+
"severity": "error",
|
|
220
|
+
"code": "missing-deferred-target",
|
|
221
|
+
"path": relative_plan,
|
|
222
|
+
"message": "Defer decision points to a missing tech-debt or follow-up target.",
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
if decision in {"continue", "pause"} and not is_empty_continuity_value(workstream):
|
|
226
|
+
ledger = workstreams_path(repo)
|
|
227
|
+
if not ledger.exists() or workstream not in ledger.read_text():
|
|
228
|
+
issues.append(
|
|
229
|
+
{
|
|
230
|
+
"severity": "error",
|
|
231
|
+
"code": "missing-workstream-ledger-entry",
|
|
232
|
+
"path": relative_plan,
|
|
233
|
+
"message": "Continue or pause decision names a workstream that is not recorded in workstreams.md.",
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
return issues
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def phase_continuity_issues(repo, plan_path, plan_text):
|
|
240
|
+
return continuation_decision_issues(repo, plan_path, plan_text)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def render_continuation_decision(decision, workstream, next_target, next_action, closure_reason, resume_notes):
|
|
244
|
+
return "\n".join(
|
|
245
|
+
[
|
|
246
|
+
f"Decision: {decision}",
|
|
247
|
+
f"Workstream: {workstream}",
|
|
248
|
+
f"Next target: {next_target}",
|
|
249
|
+
f"Next action: {next_action}",
|
|
250
|
+
f"Closure reason: {closure_reason}",
|
|
251
|
+
f"Resume notes: {resume_notes}",
|
|
252
|
+
]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def plan_goal_for_workstream(plan_path, explicit_goal=None):
|
|
257
|
+
if explicit_goal and not is_empty_continuity_value(explicit_goal):
|
|
258
|
+
return explicit_goal
|
|
259
|
+
text = plan_path.read_text()
|
|
260
|
+
lines = text.splitlines()
|
|
261
|
+
section_index = find_section(lines, "## Goal")
|
|
262
|
+
if section_index is not None:
|
|
263
|
+
goal_lines = []
|
|
264
|
+
for line in lines[section_index + 1 :]:
|
|
265
|
+
if line.startswith("## "):
|
|
266
|
+
break
|
|
267
|
+
stripped = line.strip()
|
|
268
|
+
if stripped:
|
|
269
|
+
goal_lines.append(stripped)
|
|
270
|
+
if goal_lines:
|
|
271
|
+
return " ".join(goal_lines)
|
|
272
|
+
title = plan_title(text)
|
|
273
|
+
return title or plan_path.stem
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def continuation_command_issues(repo, relative_plan, decision, workstream, next_target, next_action, closure_reason, resume_notes):
|
|
277
|
+
issues = []
|
|
278
|
+
decision = (decision or "").lower()
|
|
279
|
+
if decision not in {"complete", "continue", "pause", "stop", "defer"}:
|
|
280
|
+
issues.append(
|
|
281
|
+
{
|
|
282
|
+
"severity": "error",
|
|
283
|
+
"code": "invalid-continuation-decision",
|
|
284
|
+
"path": relative_plan,
|
|
285
|
+
"message": "Continuation Decision must be one of complete, continue, pause, stop, or defer.",
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
return issues
|
|
289
|
+
if decision in {"continue", "pause"} and is_empty_continuity_value(workstream):
|
|
290
|
+
issues.append(
|
|
291
|
+
{
|
|
292
|
+
"severity": "error",
|
|
293
|
+
"code": "missing-workstream",
|
|
294
|
+
"path": relative_plan,
|
|
295
|
+
"message": "Continue or pause decisions must name a resumable workstream.",
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
if decision in {"continue", "pause"} and is_empty_continuity_value(next_action):
|
|
299
|
+
issues.append(
|
|
300
|
+
{
|
|
301
|
+
"severity": "error",
|
|
302
|
+
"code": "missing-next-action",
|
|
303
|
+
"path": relative_plan,
|
|
304
|
+
"message": "Continue or pause decisions must record a concrete next action for recovery.",
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
if decision == "continue" and is_empty_continuity_value(next_target):
|
|
308
|
+
issues.append(
|
|
309
|
+
{
|
|
310
|
+
"severity": "error",
|
|
311
|
+
"code": "missing-next-target",
|
|
312
|
+
"path": relative_plan,
|
|
313
|
+
"message": "Continue decisions must point to a next plan or workstream target.",
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
if decision == "pause":
|
|
317
|
+
if is_empty_continuity_value(closure_reason):
|
|
318
|
+
issues.append(
|
|
319
|
+
{
|
|
320
|
+
"severity": "error",
|
|
321
|
+
"code": "missing-resume-condition",
|
|
322
|
+
"path": relative_plan,
|
|
323
|
+
"message": "Pause decisions must record the condition for resuming.",
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
if is_empty_continuity_value(resume_notes):
|
|
327
|
+
issues.append(
|
|
328
|
+
{
|
|
329
|
+
"severity": "error",
|
|
330
|
+
"code": "missing-resume-notes",
|
|
331
|
+
"path": relative_plan,
|
|
332
|
+
"message": "Pause decisions must include resume notes.",
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
if decision == "stop" and is_empty_continuity_value(closure_reason):
|
|
336
|
+
issues.append(
|
|
337
|
+
{
|
|
338
|
+
"severity": "error",
|
|
339
|
+
"code": "missing-closure-reason",
|
|
340
|
+
"path": relative_plan,
|
|
341
|
+
"message": "Stop decisions must explain why the work is ending.",
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
if decision == "defer":
|
|
345
|
+
if is_empty_continuity_value(next_target):
|
|
346
|
+
issues.append(
|
|
347
|
+
{
|
|
348
|
+
"severity": "error",
|
|
349
|
+
"code": "missing-deferred-target",
|
|
350
|
+
"path": relative_plan,
|
|
351
|
+
"message": "Defer decisions must record a tech-debt or follow-up target.",
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
elif not deferred_target_exists(repo, next_target):
|
|
355
|
+
issues.append(
|
|
356
|
+
{
|
|
357
|
+
"severity": "error",
|
|
358
|
+
"code": "missing-deferred-target",
|
|
359
|
+
"path": relative_plan,
|
|
360
|
+
"message": "Defer decision points to a missing tech-debt or follow-up target.",
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
return issues
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def update_continuation_decision(plan_path, decision, workstream, next_target, next_action, closure_reason, resume_notes):
|
|
367
|
+
text = plan_path.read_text()
|
|
368
|
+
decision = decision.lower()
|
|
369
|
+
resolved_workstream = workstream or (
|
|
370
|
+
default_workstream_id_from_plan(plan_path, text) if decision in {"continue", "pause"} else "none"
|
|
371
|
+
)
|
|
372
|
+
body = render_continuation_decision(
|
|
373
|
+
decision,
|
|
374
|
+
resolved_workstream,
|
|
375
|
+
next_target,
|
|
376
|
+
next_action,
|
|
377
|
+
closure_reason,
|
|
378
|
+
resume_notes,
|
|
379
|
+
)
|
|
380
|
+
if find_section(text.splitlines(), "## Continuation Decision") is None and find_section(text.splitlines(), "## Phase Continuity") is not None:
|
|
381
|
+
updated = replace_section(text, "Phase Continuity", body)
|
|
382
|
+
updated = updated.replace("## Phase Continuity", "## Continuation Decision", 1)
|
|
383
|
+
else:
|
|
384
|
+
updated = replace_section(text, "Continuation Decision", body)
|
|
385
|
+
plan_path.write_text(updated)
|
|
386
|
+
return {
|
|
387
|
+
"status": "updated",
|
|
388
|
+
"decision": decision,
|
|
389
|
+
"workstream": resolved_workstream,
|
|
390
|
+
"next_target": next_target,
|
|
391
|
+
"next_action": next_action,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def update_phase_continuity(plan_path, mode, workstream, current_phase, next_phase, continuation, next_action, closure_reason, resume_notes):
|
|
396
|
+
return update_continuation_decision(
|
|
397
|
+
plan_path,
|
|
398
|
+
map_legacy_phase_mode(mode),
|
|
399
|
+
workstream,
|
|
400
|
+
continuation,
|
|
401
|
+
next_action,
|
|
402
|
+
closure_reason,
|
|
403
|
+
resume_notes,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def workstreams_path(repo):
|
|
408
|
+
return repo / "docs" / "exec-plans" / "workstreams.md"
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def workstream_table_insert_index(lines):
|
|
412
|
+
index_heading = find_section(lines, "## Index")
|
|
413
|
+
if index_heading is None:
|
|
414
|
+
return len(lines)
|
|
415
|
+
index = index_heading + 1
|
|
416
|
+
while index < len(lines) and lines[index].strip() == "":
|
|
417
|
+
index += 1
|
|
418
|
+
while index < len(lines) and not lines[index].startswith("| ID |"):
|
|
419
|
+
if lines[index].startswith("## "):
|
|
420
|
+
return index
|
|
421
|
+
index += 1
|
|
422
|
+
if index >= len(lines):
|
|
423
|
+
return index_heading + 1
|
|
424
|
+
index += 1
|
|
425
|
+
if index < len(lines) and lines[index].startswith("| ---"):
|
|
426
|
+
index += 1
|
|
427
|
+
while index < len(lines) and lines[index].startswith("|"):
|
|
428
|
+
index += 1
|
|
429
|
+
return index
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def append_workstream_entry(repo, workstream_id, status, current_plan, last_completed_plan, next_action, goal, resume_notes):
|
|
433
|
+
target = workstreams_path(repo)
|
|
434
|
+
ensure_parent(target)
|
|
435
|
+
if not target.exists():
|
|
436
|
+
target.write_text(DOC_FILES["docs/exec-plans/workstreams.md"].format(marker=MANAGED_MARKER))
|
|
437
|
+
text = target.read_text()
|
|
438
|
+
today = datetime.now(UTC).strftime("%Y-%m-%d")
|
|
439
|
+
row = (
|
|
440
|
+
f"| {workstream_id} | {status} | {current_plan or 'none'} | "
|
|
441
|
+
f"{last_completed_plan or 'none'} | {next_action or 'none'} | {today} |"
|
|
442
|
+
)
|
|
443
|
+
lines = text.splitlines()
|
|
444
|
+
replaced = False
|
|
445
|
+
updated_lines = []
|
|
446
|
+
for line in lines:
|
|
447
|
+
if line.startswith(f"| {workstream_id} |"):
|
|
448
|
+
updated_lines.append(row)
|
|
449
|
+
replaced = True
|
|
450
|
+
else:
|
|
451
|
+
updated_lines.append(line)
|
|
452
|
+
if not replaced:
|
|
453
|
+
insert_index = workstream_table_insert_index(updated_lines)
|
|
454
|
+
updated_lines.insert(insert_index, row)
|
|
455
|
+
detail = (
|
|
456
|
+
f"Status: {status}\n"
|
|
457
|
+
f"Goal: {goal or 'Record the durable goal for this workstream.'}\n"
|
|
458
|
+
f"Current plan: {current_plan or 'none'}\n"
|
|
459
|
+
f"Last completed plan: {last_completed_plan or 'none'}\n"
|
|
460
|
+
f"Next action: {next_action or 'none'}\n"
|
|
461
|
+
f"Resume notes: {resume_notes or 'Read the current or last completed plan before continuing.'}\n"
|
|
462
|
+
f"Last updated: {today}"
|
|
463
|
+
)
|
|
464
|
+
updated_text = "\n".join(updated_lines).rstrip() + "\n"
|
|
465
|
+
updated_text = replace_section(updated_text, workstream_id, detail)
|
|
466
|
+
target.write_text(updated_text)
|
|
467
|
+
return target
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def update_workstreams_after_plan_close(repo, active_relative_plan, completed_relative_plan):
|
|
471
|
+
target = workstreams_path(repo)
|
|
472
|
+
if not target.exists():
|
|
473
|
+
return
|
|
474
|
+
lines = target.read_text().splitlines()
|
|
475
|
+
updated = []
|
|
476
|
+
current_plan_was_closed = False
|
|
477
|
+
for line in lines:
|
|
478
|
+
stripped = line.strip()
|
|
479
|
+
if stripped.startswith("|") and not stripped.startswith("| ---") and not stripped.startswith("| ID |"):
|
|
480
|
+
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
481
|
+
if len(cells) == 6:
|
|
482
|
+
if cells[2] == active_relative_plan:
|
|
483
|
+
cells[2] = "none"
|
|
484
|
+
if cells[3] == "none":
|
|
485
|
+
cells[3] = completed_relative_plan
|
|
486
|
+
if cells[3] == active_relative_plan:
|
|
487
|
+
cells[3] = completed_relative_plan
|
|
488
|
+
updated.append("| " + " | ".join(cells) + " |")
|
|
489
|
+
continue
|
|
490
|
+
if line == f"Current plan: {active_relative_plan}":
|
|
491
|
+
updated.append("Current plan: none")
|
|
492
|
+
current_plan_was_closed = True
|
|
493
|
+
continue
|
|
494
|
+
if line == f"Last completed plan: {active_relative_plan}":
|
|
495
|
+
updated.append(f"Last completed plan: {completed_relative_plan}")
|
|
496
|
+
current_plan_was_closed = False
|
|
497
|
+
continue
|
|
498
|
+
if current_plan_was_closed and line == "Last completed plan: none":
|
|
499
|
+
updated.append(f"Last completed plan: {completed_relative_plan}")
|
|
500
|
+
current_plan_was_closed = False
|
|
501
|
+
continue
|
|
502
|
+
updated.append(line)
|
|
503
|
+
if line.startswith("## "):
|
|
504
|
+
current_plan_was_closed = False
|
|
505
|
+
target.write_text("\n".join(updated).rstrip() + "\n")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def assert_phase_continuity_closed(repo, plan_path, plan_text):
|
|
509
|
+
issues = continuation_decision_issues(repo, plan_path, plan_text)
|
|
510
|
+
if issues:
|
|
511
|
+
messages = "\n".join(f"- {issue['code']}: {issue['message']}" for issue in issues)
|
|
512
|
+
raise PlanCloseError(
|
|
513
|
+
"continuation-decision-incomplete",
|
|
514
|
+
"Cannot close plan until the continuation decision is recorded:\n"
|
|
515
|
+
+ messages
|
|
516
|
+
+ "\nRecord a continuation decision before closing.",
|
|
517
|
+
{"issues": issues},
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from .common import *
|
|
2
|
+
from .templates import ensure_parent
|
|
3
|
+
|
|
4
|
+
def ensure_gitignore(repo):
|
|
5
|
+
path = repo / ".gitignore"
|
|
6
|
+
existing = path.read_text() if path.exists() else ""
|
|
7
|
+
block_lines = [GITIGNORE_BLOCK_START, *GITIGNORE_ENTRIES, GITIGNORE_BLOCK_END]
|
|
8
|
+
block = "\n".join(block_lines)
|
|
9
|
+
pattern = re.compile(
|
|
10
|
+
rf"(^|\n){re.escape(GITIGNORE_BLOCK_START)}\n.*?\n{re.escape(GITIGNORE_BLOCK_END)}(?=\n|$)",
|
|
11
|
+
flags=re.DOTALL,
|
|
12
|
+
)
|
|
13
|
+
if pattern.search(existing):
|
|
14
|
+
updated = pattern.sub(lambda match: match.group(1) + block, existing)
|
|
15
|
+
else:
|
|
16
|
+
prefix = existing.rstrip()
|
|
17
|
+
updated = f"{prefix}\n\n{block}" if prefix else block
|
|
18
|
+
updated = updated.rstrip() + "\n"
|
|
19
|
+
changed = updated != existing
|
|
20
|
+
if changed:
|
|
21
|
+
path.write_text(updated)
|
|
22
|
+
return {
|
|
23
|
+
"path": ".gitignore",
|
|
24
|
+
"updated": changed,
|
|
25
|
+
"entries": GITIGNORE_ENTRIES,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def clean_init_state(repo):
|
|
30
|
+
cleaned = []
|
|
31
|
+
for relative_dir in CLEAN_INIT_DIRS:
|
|
32
|
+
root = repo / relative_dir
|
|
33
|
+
if not root.exists():
|
|
34
|
+
continue
|
|
35
|
+
for path in sorted(root.rglob("*"), reverse=True):
|
|
36
|
+
if path.is_file() or path.is_symlink():
|
|
37
|
+
cleaned.append(str(path.relative_to(repo)))
|
|
38
|
+
path.unlink()
|
|
39
|
+
elif path.is_dir():
|
|
40
|
+
try:
|
|
41
|
+
path.rmdir()
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
return cleaned
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def git_tracked_harness_runtime_files(repo, roots=None):
|
|
48
|
+
if not (repo / ".git").exists():
|
|
49
|
+
return []
|
|
50
|
+
roots = roots or GIT_CLEAN_PATHS
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["git", "-C", str(repo), "ls-files", *roots],
|
|
53
|
+
text=True,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
check=False,
|
|
56
|
+
)
|
|
57
|
+
if result.returncode != 0:
|
|
58
|
+
raise RuntimeError(result.stderr.strip() or "git ls-files failed")
|
|
59
|
+
return [line for line in result.stdout.splitlines() if line.strip()]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def git_untrack_files(repo, paths):
|
|
63
|
+
if not paths:
|
|
64
|
+
return []
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
["git", "-C", str(repo), "rm", "-r", "--cached", "--", *paths],
|
|
67
|
+
text=True,
|
|
68
|
+
capture_output=True,
|
|
69
|
+
check=False,
|
|
70
|
+
)
|
|
71
|
+
if result.returncode != 0:
|
|
72
|
+
raise RuntimeError(result.stderr.strip() or "git rm --cached failed")
|
|
73
|
+
return paths
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def git_add_paths(repo, paths):
|
|
77
|
+
if not paths:
|
|
78
|
+
return []
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
["git", "-C", str(repo), "add", "--", *paths],
|
|
81
|
+
text=True,
|
|
82
|
+
capture_output=True,
|
|
83
|
+
check=False,
|
|
84
|
+
)
|
|
85
|
+
if result.returncode != 0:
|
|
86
|
+
raise RuntimeError(result.stderr.strip() or "git add failed")
|
|
87
|
+
return paths
|
|
88
|
+
|