@hallucination-studio/harness-engine 1.0.0-beta.8.87407 → 1.0.0-beta.9.bb2cd30
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/README.md +23 -6
- package/package.json +8 -2
- package/skills/harness-repo-bootstrap/SKILL.md +18 -7
- package/skills/harness-repo-bootstrap/evals/cases.json +8 -0
- package/skills/harness-repo-bootstrap/evals/run_evals.py +453 -2
- package/skills/harness-repo-bootstrap/references/evaluation-loop.md +2 -0
- package/skills/harness-repo-bootstrap/references/exec-plans.md +14 -4
- package/skills/harness-repo-bootstrap/references/workflow.md +6 -0
- package/skills/harness-repo-bootstrap/scripts/manage_harness.py +1016 -22
|
@@ -9,6 +9,8 @@ from datetime import UTC, datetime
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
MANAGED_MARKER = "<!-- harness-repo-bootstrap:managed -->"
|
|
12
|
+
DEFAULT_KNOWLEDGE_PLACEHOLDER = "- [ ] Add durable facts here as they emerge -> <destination-doc>"
|
|
13
|
+
DEFAULT_DEFECT_PLACEHOLDER = "None."
|
|
12
14
|
PLAN_TEMPLATE = """# Execution Plan: {title}
|
|
13
15
|
|
|
14
16
|
## Goal
|
|
@@ -33,6 +35,40 @@ PLAN_TEMPLATE = """# Execution Plan: {title}
|
|
|
33
35
|
|
|
34
36
|
- Describe how the work will be verified.
|
|
35
37
|
|
|
38
|
+
## Quality Gate
|
|
39
|
+
|
|
40
|
+
Status: pending
|
|
41
|
+
Minimum score: 8.0
|
|
42
|
+
Average score: pending
|
|
43
|
+
Last scored: pending
|
|
44
|
+
|
|
45
|
+
| Dimension | Score | Notes |
|
|
46
|
+
| --- | ---: | --- |
|
|
47
|
+
| Product correctness | pending | Confirm the requested behavior is complete. |
|
|
48
|
+
| UX and operator clarity | pending | Confirm the user or operator experience is understandable. |
|
|
49
|
+
| Architecture and maintainability | pending | Confirm the implementation is clean and easy to change. |
|
|
50
|
+
| Reliability and observability | pending | Confirm the validation loop and failure handling are sufficient. |
|
|
51
|
+
| Security and data handling | pending | Confirm secrets and sensitive data are handled safely. |
|
|
52
|
+
|
|
53
|
+
## Defects To Resolve
|
|
54
|
+
|
|
55
|
+
{defect_section}
|
|
56
|
+
|
|
57
|
+
## Rework Required
|
|
58
|
+
|
|
59
|
+
- Pending quality score.
|
|
60
|
+
|
|
61
|
+
## Phase Continuity
|
|
62
|
+
|
|
63
|
+
Mode: single-phase
|
|
64
|
+
Workstream: none
|
|
65
|
+
Current phase: none
|
|
66
|
+
Next phase: none
|
|
67
|
+
Continuation: none
|
|
68
|
+
Next action: none
|
|
69
|
+
Closure reason: This plan is not part of a longer workstream.
|
|
70
|
+
Resume notes: none
|
|
71
|
+
|
|
36
72
|
## Durable Knowledge To Capture
|
|
37
73
|
|
|
38
74
|
{knowledge_section}
|
|
@@ -52,6 +88,7 @@ Read this file first, then follow the linked docs.
|
|
|
52
88
|
|
|
53
89
|
- Read `ARCHITECTURE.md` before changing boundaries, data flow, or integrations.
|
|
54
90
|
- Read `docs/PLANS.md` before starting multi-step execution work.
|
|
91
|
+
- Read `docs/exec-plans/workstreams.md` before resuming interrupted feature, refactor, reliability, or cleanup work.
|
|
55
92
|
- Read `docs/exec-plans/active/` before resuming in-flight work, and create a plan there for new multi-step work.
|
|
56
93
|
- Read `docs/QUALITY_SCORE.md` before evaluating tradeoffs or readiness.
|
|
57
94
|
- Read `docs/RELIABILITY.md` for runtime validation and failure handling.
|
|
@@ -70,8 +107,11 @@ Read this file first, then follow the linked docs.
|
|
|
70
107
|
|
|
71
108
|
- Keep durable decisions in repo docs, not only in chat.
|
|
72
109
|
- Keep active plans in `docs/exec-plans/active/`.
|
|
110
|
+
- Keep resumable feature, refactor, reliability, and cleanup work in `docs/exec-plans/workstreams.md`.
|
|
73
111
|
- Move completed plans to `docs/exec-plans/completed/`.
|
|
74
112
|
- Update plans during the work, not only at the end.
|
|
113
|
+
- Score completed work with `quality-score` before closing an execution plan.
|
|
114
|
+
- If `quality-score` fails, treat `## Rework Required` as the next implementation input and do not close the plan.
|
|
75
115
|
- Encode durable facts learned during execution into permanent docs before closing the task.
|
|
76
116
|
- Before handoff, run the local harness check: `python3 .codex/skills/harness-repo-bootstrap/scripts/manage_harness.py check --repo .`.
|
|
77
117
|
- Keep generated artifacts in `docs/generated/`.
|
|
@@ -146,6 +186,7 @@ DOC_FILES = {
|
|
|
146
186
|
|
|
147
187
|
- Put active execution plans in `docs/exec-plans/active/`.
|
|
148
188
|
- Move completed plans to `docs/exec-plans/completed/`.
|
|
189
|
+
- Track resumable multi-plan workstreams in `docs/exec-plans/workstreams.md`.
|
|
149
190
|
- Record cross-cutting follow-up work in `docs/exec-plans/tech-debt-tracker.md`.
|
|
150
191
|
|
|
151
192
|
## Authoring Rules
|
|
@@ -154,6 +195,7 @@ DOC_FILES = {
|
|
|
154
195
|
- Update plans during the work, not after the fact.
|
|
155
196
|
- Link to specs, decisions, and validation artifacts when they exist.
|
|
156
197
|
- Include a section for durable knowledge that must be written back into permanent docs.
|
|
198
|
+
- Include phase continuity when a plan is part of a multi-phase feature, refactor, reliability, or cleanup effort.
|
|
157
199
|
- Do not treat plans as the final home for product, architecture, or policy knowledge.
|
|
158
200
|
""",
|
|
159
201
|
"docs/PRODUCT_SENSE.md": """{marker}
|
|
@@ -236,6 +278,24 @@ DOC_FILES = {
|
|
|
236
278
|
# Tech Debt Tracker
|
|
237
279
|
|
|
238
280
|
Record follow-up work that should survive beyond a single execution plan.
|
|
281
|
+
""",
|
|
282
|
+
"docs/exec-plans/workstreams.md": """{marker}
|
|
283
|
+
# Workstreams
|
|
284
|
+
|
|
285
|
+
Use this ledger to recover interrupted feature, refactor, reliability, security, frontend, and cleanup work.
|
|
286
|
+
|
|
287
|
+
## Index
|
|
288
|
+
|
|
289
|
+
| ID | Status | Current Plan | Last Completed Plan | Next Action | Last Updated |
|
|
290
|
+
| --- | --- | --- | --- | --- | --- |
|
|
291
|
+
|
|
292
|
+
## Operating Rules
|
|
293
|
+
|
|
294
|
+
- Add a workstream when work spans multiple execution plans or may be resumed by another agent.
|
|
295
|
+
- Keep `Current Plan` pointed at the active plan when one exists.
|
|
296
|
+
- Keep `Last Completed Plan` pointed at the latest completed plan after `plan-close`.
|
|
297
|
+
- Keep `Next Action` concrete enough that another agent can resume without chat history.
|
|
298
|
+
- If a workstream is paused, record the restart condition in `Next Action`.
|
|
239
299
|
""",
|
|
240
300
|
"docs/exec-plans/active/README.md": """{marker}
|
|
241
301
|
# Active Execution Plans
|
|
@@ -253,6 +313,8 @@ Minimum contents:
|
|
|
253
313
|
- constraints
|
|
254
314
|
- steps
|
|
255
315
|
- validation
|
|
316
|
+
- quality gate
|
|
317
|
+
- phase continuity
|
|
256
318
|
- durable knowledge to capture
|
|
257
319
|
""",
|
|
258
320
|
"docs/exec-plans/active/_template.md": """{marker}
|
|
@@ -279,6 +341,36 @@ List product, architecture, reliability, security, or delivery constraints.
|
|
|
279
341
|
|
|
280
342
|
- Describe how the work will be verified.
|
|
281
343
|
|
|
344
|
+
## Quality Gate
|
|
345
|
+
|
|
346
|
+
Status: pending
|
|
347
|
+
Minimum score: 8.0
|
|
348
|
+
Average score: pending
|
|
349
|
+
Last scored: pending
|
|
350
|
+
|
|
351
|
+
| Dimension | Score | Notes |
|
|
352
|
+
| --- | ---: | --- |
|
|
353
|
+
| Product correctness | pending | Confirm the requested behavior is complete. |
|
|
354
|
+
| UX and operator clarity | pending | Confirm the user or operator experience is understandable. |
|
|
355
|
+
| Architecture and maintainability | pending | Confirm the implementation is clean and easy to change. |
|
|
356
|
+
| Reliability and observability | pending | Confirm the validation loop and failure handling is sufficient. |
|
|
357
|
+
| Security and data handling | pending | Confirm secrets and sensitive data are handled safely. |
|
|
358
|
+
|
|
359
|
+
## Rework Required
|
|
360
|
+
|
|
361
|
+
- Pending quality score.
|
|
362
|
+
|
|
363
|
+
## Phase Continuity
|
|
364
|
+
|
|
365
|
+
Mode: single-phase
|
|
366
|
+
Workstream: none
|
|
367
|
+
Current phase: none
|
|
368
|
+
Next phase: none
|
|
369
|
+
Continuation: none
|
|
370
|
+
Next action: none
|
|
371
|
+
Closure reason: This plan is not part of a longer workstream.
|
|
372
|
+
Resume notes: none
|
|
373
|
+
|
|
282
374
|
## Durable Knowledge To Capture
|
|
283
375
|
|
|
284
376
|
- List facts that must be written back into permanent docs before completion.
|
|
@@ -293,8 +385,10 @@ Summarize outcomes, follow-ups, and doc updates.
|
|
|
293
385
|
Move finished plans here after:
|
|
294
386
|
|
|
295
387
|
1. validation is complete
|
|
296
|
-
2.
|
|
297
|
-
3.
|
|
388
|
+
2. the quality gate has passed
|
|
389
|
+
3. phase continuity has been recorded for multi-phase work
|
|
390
|
+
4. permanent docs have been updated
|
|
391
|
+
5. any remaining follow-ups are recorded in workstreams, tech debt, or new plans
|
|
298
392
|
""",
|
|
299
393
|
"docs/generated/db-schema.md": """{marker}
|
|
300
394
|
# Generated DB Schema
|
|
@@ -399,6 +493,14 @@ QUESTION_CATALOG = [
|
|
|
399
493
|
},
|
|
400
494
|
]
|
|
401
495
|
|
|
496
|
+
QUALITY_DIMENSIONS = [
|
|
497
|
+
("product_correctness", "Product correctness"),
|
|
498
|
+
("ux_operator_clarity", "UX and operator clarity"),
|
|
499
|
+
("architecture_maintainability", "Architecture and maintainability"),
|
|
500
|
+
("reliability_observability", "Reliability and observability"),
|
|
501
|
+
("security_data_handling", "Security and data handling"),
|
|
502
|
+
]
|
|
503
|
+
|
|
402
504
|
|
|
403
505
|
def detect_languages(files):
|
|
404
506
|
ext_map = {}
|
|
@@ -597,11 +699,31 @@ def extract_knowledge_items(text):
|
|
|
597
699
|
return items
|
|
598
700
|
|
|
599
701
|
|
|
702
|
+
def extract_defect_items(text):
|
|
703
|
+
lines = text.splitlines()
|
|
704
|
+
section_index = find_section(lines, "## Defects To Resolve")
|
|
705
|
+
if section_index is None:
|
|
706
|
+
return []
|
|
707
|
+
items = []
|
|
708
|
+
for line in lines[section_index + 1 :]:
|
|
709
|
+
if line.startswith("## "):
|
|
710
|
+
break
|
|
711
|
+
stripped = line.strip()
|
|
712
|
+
if stripped.startswith("- ["):
|
|
713
|
+
items.append(stripped)
|
|
714
|
+
return items
|
|
715
|
+
|
|
716
|
+
|
|
600
717
|
def knowledge_id_for(fact, destination):
|
|
601
718
|
digest = hashlib.sha1(f"{clean_destination_text(destination)}\0{clean_fact_text(fact)}".encode()).hexdigest()
|
|
602
719
|
return f"hk-{digest[:10]}"
|
|
603
720
|
|
|
604
721
|
|
|
722
|
+
def defect_id_for(summary):
|
|
723
|
+
digest = hashlib.sha1(clean_fact_text(summary).encode()).hexdigest()
|
|
724
|
+
return f"bug-{digest[:10]}"
|
|
725
|
+
|
|
726
|
+
|
|
605
727
|
def parse_knowledge_item(item):
|
|
606
728
|
match = re.match(
|
|
607
729
|
r"- \[(?P<status>[ xX])\]\s+"
|
|
@@ -623,6 +745,29 @@ def parse_knowledge_item(item):
|
|
|
623
745
|
}
|
|
624
746
|
|
|
625
747
|
|
|
748
|
+
def parse_defect_item(item):
|
|
749
|
+
match = re.match(
|
|
750
|
+
r"- \[(?P<status>[ xX])\]\s+"
|
|
751
|
+
r"(?:\[(?:id|bug):(?P<id>[A-Za-z0-9_.:-]+)\]\s+)?"
|
|
752
|
+
r"\[(?P<severity>P[0-3])\]\s+"
|
|
753
|
+
r"(?P<summary>.*?)"
|
|
754
|
+
r"(?:\s+\|\s+evidence:\s+(?P<evidence>.*?))?"
|
|
755
|
+
r"(?:\s+\|\s+fix:\s+(?P<fix>.+))?$",
|
|
756
|
+
item.strip(),
|
|
757
|
+
)
|
|
758
|
+
if not match:
|
|
759
|
+
return None
|
|
760
|
+
return {
|
|
761
|
+
"status": "closed" if match.group("status").lower() == "x" else "open",
|
|
762
|
+
"id": match.group("id"),
|
|
763
|
+
"severity": match.group("severity"),
|
|
764
|
+
"summary": clean_fact_text(match.group("summary")),
|
|
765
|
+
"evidence": clean_fact_text(match.group("evidence")) if match.group("evidence") else None,
|
|
766
|
+
"fix": clean_fact_text(match.group("fix")) if match.group("fix") else None,
|
|
767
|
+
"raw": item,
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
|
|
626
771
|
def clean_fact_text(value):
|
|
627
772
|
cleaned = value.strip()
|
|
628
773
|
cleaned = cleaned.replace("`", "")
|
|
@@ -648,14 +793,482 @@ def replace_completion_notes(text, summary):
|
|
|
648
793
|
return "\n".join(new_lines).rstrip() + "\n"
|
|
649
794
|
|
|
650
795
|
|
|
796
|
+
def replace_section(text, heading, body):
|
|
797
|
+
lines = text.splitlines()
|
|
798
|
+
section_index = find_section(lines, f"## {heading}")
|
|
799
|
+
if section_index is None:
|
|
800
|
+
return text.rstrip() + f"\n\n## {heading}\n\n{body.rstrip()}\n"
|
|
801
|
+
end_index = len(lines)
|
|
802
|
+
for index in range(section_index + 1, len(lines)):
|
|
803
|
+
if lines[index].startswith("## "):
|
|
804
|
+
end_index = index
|
|
805
|
+
break
|
|
806
|
+
new_lines = lines[: section_index + 1] + ["", body.rstrip()] + lines[end_index:]
|
|
807
|
+
return "\n".join(new_lines).rstrip() + "\n"
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def quality_gate_for_plan(text):
|
|
811
|
+
lines = text.splitlines()
|
|
812
|
+
section_index = find_section(lines, "## Quality Gate")
|
|
813
|
+
if section_index is None:
|
|
814
|
+
return {"status": "missing", "minimum": None, "average": None, "scores": {}}
|
|
815
|
+
section_lines = []
|
|
816
|
+
for line in lines[section_index + 1 :]:
|
|
817
|
+
if line.startswith("## "):
|
|
818
|
+
break
|
|
819
|
+
section_lines.append(line)
|
|
820
|
+
section_text = "\n".join(section_lines)
|
|
821
|
+
status_match = re.search(r"^Status:\s*(?P<status>\w+)", section_text, flags=re.MULTILINE)
|
|
822
|
+
minimum_match = re.search(r"^Minimum score:\s*(?P<score>[0-9]+(?:\.[0-9]+)?)", section_text, flags=re.MULTILINE)
|
|
823
|
+
average_match = re.search(r"^Average score:\s*(?P<score>[0-9]+(?:\.[0-9]+)?)", section_text, flags=re.MULTILINE)
|
|
824
|
+
scores = {}
|
|
825
|
+
for _, label in QUALITY_DIMENSIONS:
|
|
826
|
+
row_match = re.search(
|
|
827
|
+
rf"^\|\s*{re.escape(label)}\s*\|\s*(?P<score>[0-9]+(?:\.[0-9]+)?)\s*\|",
|
|
828
|
+
section_text,
|
|
829
|
+
flags=re.MULTILINE,
|
|
830
|
+
)
|
|
831
|
+
if row_match:
|
|
832
|
+
scores[label] = float(row_match.group("score"))
|
|
833
|
+
return {
|
|
834
|
+
"status": status_match.group("status").lower() if status_match else "missing",
|
|
835
|
+
"minimum": float(minimum_match.group("score")) if minimum_match else None,
|
|
836
|
+
"average": float(average_match.group("score")) if average_match else None,
|
|
837
|
+
"scores": scores,
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def section_key_values(text, heading):
|
|
842
|
+
lines = text.splitlines()
|
|
843
|
+
section_index = find_section(lines, f"## {heading}")
|
|
844
|
+
if section_index is None:
|
|
845
|
+
return None
|
|
846
|
+
values = {}
|
|
847
|
+
for line in lines[section_index + 1 :]:
|
|
848
|
+
if line.startswith("## "):
|
|
849
|
+
break
|
|
850
|
+
if ":" not in line:
|
|
851
|
+
continue
|
|
852
|
+
key, value = line.split(":", 1)
|
|
853
|
+
normalized_key = key.strip().lower().replace(" ", "_")
|
|
854
|
+
values[normalized_key] = value.strip()
|
|
855
|
+
return values
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def phase_number_from_text(value):
|
|
859
|
+
match = re.search(r"\bphase[-_\s]*(?P<number>\d+)\b", value, flags=re.IGNORECASE)
|
|
860
|
+
if not match:
|
|
861
|
+
return None
|
|
862
|
+
return match.group("number")
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def plan_title(text):
|
|
866
|
+
for line in text.splitlines():
|
|
867
|
+
if line.startswith("# Execution Plan:"):
|
|
868
|
+
return line.split(":", 1)[1].strip()
|
|
869
|
+
return ""
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def default_workstream_id_from_plan(plan_path, text):
|
|
873
|
+
source = plan_path.stem
|
|
874
|
+
source = re.sub(r"^\d{4}-\d{2}-\d{2}-", "", source)
|
|
875
|
+
source = re.sub(r"phase[-_\s]*\d+", "", source, flags=re.IGNORECASE)
|
|
876
|
+
source = source.strip("-_ ")
|
|
877
|
+
if not source:
|
|
878
|
+
source = plan_title(text)
|
|
879
|
+
source = re.sub(r"phase[-_\s]*\d+", "", source, flags=re.IGNORECASE)
|
|
880
|
+
return slugify(source or "workstream")
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def phase_continuity_for_plan(plan_path, text):
|
|
884
|
+
values = section_key_values(text, "Phase Continuity")
|
|
885
|
+
detected_phase = phase_number_from_text(plan_path.stem) or phase_number_from_text(plan_title(text))
|
|
886
|
+
if values is None:
|
|
887
|
+
return {
|
|
888
|
+
"status": "missing",
|
|
889
|
+
"detected_phase": detected_phase,
|
|
890
|
+
"mode": None,
|
|
891
|
+
"workstream": None,
|
|
892
|
+
"current_phase": None,
|
|
893
|
+
"next_phase": None,
|
|
894
|
+
"continuation": None,
|
|
895
|
+
"next_action": None,
|
|
896
|
+
"closure_reason": None,
|
|
897
|
+
"resume_notes": None,
|
|
898
|
+
}
|
|
899
|
+
mode = values.get("mode", "").lower()
|
|
900
|
+
workstream = values.get("workstream")
|
|
901
|
+
current_phase = values.get("current_phase")
|
|
902
|
+
next_phase = values.get("next_phase")
|
|
903
|
+
continuation = values.get("continuation")
|
|
904
|
+
next_action = values.get("next_action")
|
|
905
|
+
closure_reason = values.get("closure_reason")
|
|
906
|
+
resume_notes = values.get("resume_notes")
|
|
907
|
+
return {
|
|
908
|
+
"status": "present",
|
|
909
|
+
"detected_phase": detected_phase,
|
|
910
|
+
"mode": mode,
|
|
911
|
+
"workstream": workstream,
|
|
912
|
+
"current_phase": current_phase,
|
|
913
|
+
"next_phase": next_phase,
|
|
914
|
+
"continuation": continuation,
|
|
915
|
+
"next_action": next_action,
|
|
916
|
+
"closure_reason": closure_reason,
|
|
917
|
+
"resume_notes": resume_notes,
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def is_empty_continuity_value(value):
|
|
922
|
+
if value is None:
|
|
923
|
+
return True
|
|
924
|
+
return value.strip().lower() in {"", "none", "pending", "unknown", "n/a", "-"}
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def phase_continuity_issues(repo, plan_path, plan_text):
|
|
928
|
+
continuity = phase_continuity_for_plan(plan_path, plan_text)
|
|
929
|
+
detected_phase = continuity["detected_phase"]
|
|
930
|
+
if continuity["status"] == "missing":
|
|
931
|
+
if detected_phase:
|
|
932
|
+
return [
|
|
933
|
+
{
|
|
934
|
+
"severity": "error",
|
|
935
|
+
"code": "missing-phase-continuity",
|
|
936
|
+
"path": str(plan_path.relative_to(repo)),
|
|
937
|
+
"message": "Phased plan is missing a Phase Continuity section.",
|
|
938
|
+
}
|
|
939
|
+
]
|
|
940
|
+
return []
|
|
941
|
+
mode = continuity["mode"]
|
|
942
|
+
if mode in {"single-phase", "single", "none"} and not detected_phase:
|
|
943
|
+
return []
|
|
944
|
+
issues = []
|
|
945
|
+
relative_plan = str(plan_path.relative_to(repo))
|
|
946
|
+
if mode not in {"multi-phase", "phased", "paused", "completed", "stopped"} and detected_phase:
|
|
947
|
+
issues.append(
|
|
948
|
+
{
|
|
949
|
+
"severity": "error",
|
|
950
|
+
"code": "phase-mode-not-declared",
|
|
951
|
+
"path": relative_plan,
|
|
952
|
+
"message": "Plan name indicates a phase, but Phase Continuity does not declare multi-phase, paused, completed, or stopped mode.",
|
|
953
|
+
}
|
|
954
|
+
)
|
|
955
|
+
if is_empty_continuity_value(continuity["workstream"]):
|
|
956
|
+
issues.append(
|
|
957
|
+
{
|
|
958
|
+
"severity": "error",
|
|
959
|
+
"code": "missing-workstream",
|
|
960
|
+
"path": relative_plan,
|
|
961
|
+
"message": "Phased or multi-plan work must name a workstream in Phase Continuity.",
|
|
962
|
+
}
|
|
963
|
+
)
|
|
964
|
+
if is_empty_continuity_value(continuity["current_phase"]):
|
|
965
|
+
issues.append(
|
|
966
|
+
{
|
|
967
|
+
"severity": "error",
|
|
968
|
+
"code": "missing-current-phase",
|
|
969
|
+
"path": relative_plan,
|
|
970
|
+
"message": "Phased or multi-plan work must record the current phase.",
|
|
971
|
+
}
|
|
972
|
+
)
|
|
973
|
+
continuation = continuity["continuation"]
|
|
974
|
+
closure_reason = continuity["closure_reason"]
|
|
975
|
+
next_action = continuity["next_action"]
|
|
976
|
+
if mode in {"completed", "stopped"}:
|
|
977
|
+
if is_empty_continuity_value(closure_reason):
|
|
978
|
+
issues.append(
|
|
979
|
+
{
|
|
980
|
+
"severity": "error",
|
|
981
|
+
"code": "missing-phase-closure-reason",
|
|
982
|
+
"path": relative_plan,
|
|
983
|
+
"message": "Completed or stopped workstreams must explain why no next phase is needed.",
|
|
984
|
+
}
|
|
985
|
+
)
|
|
986
|
+
return issues
|
|
987
|
+
if is_empty_continuity_value(continuation):
|
|
988
|
+
issues.append(
|
|
989
|
+
{
|
|
990
|
+
"severity": "error",
|
|
991
|
+
"code": "missing-continuation",
|
|
992
|
+
"path": relative_plan,
|
|
993
|
+
"message": "Multi-phase work must point to a next active plan, workstreams ledger, tech debt item, or explicit closure.",
|
|
994
|
+
}
|
|
995
|
+
)
|
|
996
|
+
elif "workstreams.md" in continuation and not is_empty_continuity_value(continuity["workstream"]):
|
|
997
|
+
ledger = workstreams_path(repo)
|
|
998
|
+
if not ledger.exists() or continuity["workstream"] not in ledger.read_text():
|
|
999
|
+
issues.append(
|
|
1000
|
+
{
|
|
1001
|
+
"severity": "error",
|
|
1002
|
+
"code": "missing-workstream-ledger-entry",
|
|
1003
|
+
"path": relative_plan,
|
|
1004
|
+
"message": "Phase Continuity points to workstreams.md, but the named workstream is not recorded there.",
|
|
1005
|
+
}
|
|
1006
|
+
)
|
|
1007
|
+
if is_empty_continuity_value(next_action):
|
|
1008
|
+
issues.append(
|
|
1009
|
+
{
|
|
1010
|
+
"severity": "error",
|
|
1011
|
+
"code": "missing-next-action",
|
|
1012
|
+
"path": relative_plan,
|
|
1013
|
+
"message": "Multi-phase work must record a concrete next action for recovery.",
|
|
1014
|
+
}
|
|
1015
|
+
)
|
|
1016
|
+
return issues
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def open_defects_for_plan(text):
|
|
1020
|
+
open_items = []
|
|
1021
|
+
for item in extract_defect_items(text):
|
|
1022
|
+
parsed = parse_defect_item(item)
|
|
1023
|
+
if parsed and parsed["status"] == "open":
|
|
1024
|
+
open_items.append(parsed)
|
|
1025
|
+
return open_items
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def render_quality_gate(scores, notes, minimum, open_defects=None):
|
|
1029
|
+
open_defects = open_defects or []
|
|
1030
|
+
average = sum(scores.values()) / len(scores)
|
|
1031
|
+
low_dimensions = [
|
|
1032
|
+
label for key, label in QUALITY_DIMENSIONS if scores[key] < minimum
|
|
1033
|
+
]
|
|
1034
|
+
passed = average >= minimum and not low_dimensions and not open_defects
|
|
1035
|
+
status = "pass" if passed else "fail"
|
|
1036
|
+
lines = [
|
|
1037
|
+
f"Status: {status}",
|
|
1038
|
+
f"Minimum score: {minimum:.1f}",
|
|
1039
|
+
f"Average score: {average:.1f}",
|
|
1040
|
+
f"Last scored: {datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ')}",
|
|
1041
|
+
"",
|
|
1042
|
+
"| Dimension | Score | Notes |",
|
|
1043
|
+
"| --- | ---: | --- |",
|
|
1044
|
+
]
|
|
1045
|
+
for key, label in QUALITY_DIMENSIONS:
|
|
1046
|
+
note = notes.get(key) or "No note provided."
|
|
1047
|
+
safe_note = note.replace("\n", " ").replace("|", "\\|").strip()
|
|
1048
|
+
lines.append(f"| {label} | {scores[key]:.1f} | {safe_note} |")
|
|
1049
|
+
return "\n".join(lines), passed, average, low_dimensions
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def render_rework_section(passed, average, minimum, low_dimensions, notes, open_defects=None):
|
|
1053
|
+
open_defects = open_defects or []
|
|
1054
|
+
if passed:
|
|
1055
|
+
return "None. Quality gate passed."
|
|
1056
|
+
lines = [
|
|
1057
|
+
f"- Rework implementation until every quality dimension is at least {minimum:.1f}; current average is {average:.1f}.",
|
|
1058
|
+
]
|
|
1059
|
+
for defect in open_defects:
|
|
1060
|
+
evidence = f" Evidence: {defect['evidence']}." if defect.get("evidence") else ""
|
|
1061
|
+
lines.append(
|
|
1062
|
+
f"- Resolve {defect['id']} ({defect['severity']}): {defect['summary']}.{evidence}"
|
|
1063
|
+
)
|
|
1064
|
+
for key, label in QUALITY_DIMENSIONS:
|
|
1065
|
+
if label in low_dimensions:
|
|
1066
|
+
note = notes.get(key) or "No note provided."
|
|
1067
|
+
lines.append(f"- Improve {label}: {note}")
|
|
1068
|
+
return "\n".join(lines)
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def update_quality_gate(plan_path, scores, notes, minimum):
|
|
1072
|
+
text = plan_path.read_text()
|
|
1073
|
+
open_defects = open_defects_for_plan(text)
|
|
1074
|
+
gate_text, passed, average, low_dimensions = render_quality_gate(scores, notes, minimum, open_defects)
|
|
1075
|
+
updated = replace_section(text, "Quality Gate", gate_text)
|
|
1076
|
+
updated = replace_section(
|
|
1077
|
+
updated,
|
|
1078
|
+
"Rework Required",
|
|
1079
|
+
render_rework_section(passed, average, minimum, low_dimensions, notes, open_defects),
|
|
1080
|
+
)
|
|
1081
|
+
plan_path.write_text(updated)
|
|
1082
|
+
return {
|
|
1083
|
+
"status": "pass" if passed else "fail",
|
|
1084
|
+
"minimum": minimum,
|
|
1085
|
+
"average": round(average, 1),
|
|
1086
|
+
"low_dimensions": low_dimensions,
|
|
1087
|
+
"open_defects": [defect["id"] for defect in open_defects],
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def assert_quality_gate_passed(plan_text):
|
|
1092
|
+
open_defects = open_defects_for_plan(plan_text)
|
|
1093
|
+
if open_defects:
|
|
1094
|
+
defects = "\n".join(
|
|
1095
|
+
f"- {defect['id']} ({defect['severity']}): {defect['summary']}" for defect in open_defects
|
|
1096
|
+
)
|
|
1097
|
+
raise RuntimeError(
|
|
1098
|
+
"Cannot close plan with unresolved defects:\n"
|
|
1099
|
+
+ defects
|
|
1100
|
+
+ "\nRun `defect-resolve`, re-run validation, and score again."
|
|
1101
|
+
)
|
|
1102
|
+
gate = quality_gate_for_plan(plan_text)
|
|
1103
|
+
if gate["status"] != "pass":
|
|
1104
|
+
raise RuntimeError(
|
|
1105
|
+
"Cannot close plan until the quality gate passes. "
|
|
1106
|
+
"Run `quality-score`, fix any `## Rework Required` items, then score again."
|
|
1107
|
+
)
|
|
1108
|
+
return gate
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def render_phase_continuity(mode, workstream, current_phase, next_phase, continuation, next_action, closure_reason, resume_notes):
|
|
1112
|
+
return "\n".join(
|
|
1113
|
+
[
|
|
1114
|
+
f"Mode: {mode}",
|
|
1115
|
+
f"Workstream: {workstream}",
|
|
1116
|
+
f"Current phase: {current_phase}",
|
|
1117
|
+
f"Next phase: {next_phase}",
|
|
1118
|
+
f"Continuation: {continuation}",
|
|
1119
|
+
f"Next action: {next_action}",
|
|
1120
|
+
f"Closure reason: {closure_reason}",
|
|
1121
|
+
f"Resume notes: {resume_notes}",
|
|
1122
|
+
]
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def update_phase_continuity(plan_path, mode, workstream, current_phase, next_phase, continuation, next_action, closure_reason, resume_notes):
|
|
1127
|
+
text = plan_path.read_text()
|
|
1128
|
+
detected_phase = phase_number_from_text(plan_path.stem) or phase_number_from_text(plan_title(text)) or "none"
|
|
1129
|
+
resolved_workstream = workstream or default_workstream_id_from_plan(plan_path, text)
|
|
1130
|
+
resolved_current_phase = current_phase or detected_phase
|
|
1131
|
+
body = render_phase_continuity(
|
|
1132
|
+
mode,
|
|
1133
|
+
resolved_workstream,
|
|
1134
|
+
resolved_current_phase,
|
|
1135
|
+
next_phase,
|
|
1136
|
+
continuation,
|
|
1137
|
+
next_action,
|
|
1138
|
+
closure_reason,
|
|
1139
|
+
resume_notes,
|
|
1140
|
+
)
|
|
1141
|
+
plan_path.write_text(replace_section(text, "Phase Continuity", body))
|
|
1142
|
+
return {
|
|
1143
|
+
"status": "updated",
|
|
1144
|
+
"mode": mode,
|
|
1145
|
+
"workstream": resolved_workstream,
|
|
1146
|
+
"current_phase": resolved_current_phase,
|
|
1147
|
+
"next_phase": next_phase,
|
|
1148
|
+
"continuation": continuation,
|
|
1149
|
+
"next_action": next_action,
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def workstreams_path(repo):
|
|
1154
|
+
return repo / "docs" / "exec-plans" / "workstreams.md"
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def workstream_table_insert_index(lines):
|
|
1158
|
+
index_heading = find_section(lines, "## Index")
|
|
1159
|
+
if index_heading is None:
|
|
1160
|
+
return len(lines)
|
|
1161
|
+
index = index_heading + 1
|
|
1162
|
+
while index < len(lines) and lines[index].strip() == "":
|
|
1163
|
+
index += 1
|
|
1164
|
+
while index < len(lines) and not lines[index].startswith("| ID |"):
|
|
1165
|
+
if lines[index].startswith("## "):
|
|
1166
|
+
return index
|
|
1167
|
+
index += 1
|
|
1168
|
+
if index >= len(lines):
|
|
1169
|
+
return index_heading + 1
|
|
1170
|
+
index += 1
|
|
1171
|
+
if index < len(lines) and lines[index].startswith("| ---"):
|
|
1172
|
+
index += 1
|
|
1173
|
+
while index < len(lines) and lines[index].startswith("|"):
|
|
1174
|
+
index += 1
|
|
1175
|
+
return index
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
def append_workstream_entry(repo, workstream_id, status, current_plan, last_completed_plan, next_action, goal, resume_notes):
|
|
1179
|
+
target = workstreams_path(repo)
|
|
1180
|
+
ensure_parent(target)
|
|
1181
|
+
if not target.exists():
|
|
1182
|
+
target.write_text(DOC_FILES["docs/exec-plans/workstreams.md"].format(marker=MANAGED_MARKER))
|
|
1183
|
+
text = target.read_text()
|
|
1184
|
+
today = datetime.now(UTC).strftime("%Y-%m-%d")
|
|
1185
|
+
row = (
|
|
1186
|
+
f"| {workstream_id} | {status} | {current_plan or 'none'} | "
|
|
1187
|
+
f"{last_completed_plan or 'none'} | {next_action or 'none'} | {today} |"
|
|
1188
|
+
)
|
|
1189
|
+
lines = text.splitlines()
|
|
1190
|
+
replaced = False
|
|
1191
|
+
updated_lines = []
|
|
1192
|
+
for line in lines:
|
|
1193
|
+
if line.startswith(f"| {workstream_id} |"):
|
|
1194
|
+
updated_lines.append(row)
|
|
1195
|
+
replaced = True
|
|
1196
|
+
else:
|
|
1197
|
+
updated_lines.append(line)
|
|
1198
|
+
if not replaced:
|
|
1199
|
+
insert_index = workstream_table_insert_index(updated_lines)
|
|
1200
|
+
updated_lines.insert(insert_index, row)
|
|
1201
|
+
detail = (
|
|
1202
|
+
f"Status: {status}\n"
|
|
1203
|
+
f"Goal: {goal or 'Record the durable goal for this workstream.'}\n"
|
|
1204
|
+
f"Current plan: {current_plan or 'none'}\n"
|
|
1205
|
+
f"Last completed plan: {last_completed_plan or 'none'}\n"
|
|
1206
|
+
f"Next action: {next_action or 'none'}\n"
|
|
1207
|
+
f"Resume notes: {resume_notes or 'Read the current or last completed plan before continuing.'}\n"
|
|
1208
|
+
f"Last updated: {today}"
|
|
1209
|
+
)
|
|
1210
|
+
updated_text = "\n".join(updated_lines).rstrip() + "\n"
|
|
1211
|
+
updated_text = replace_section(updated_text, workstream_id, detail)
|
|
1212
|
+
target.write_text(updated_text)
|
|
1213
|
+
return target
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def update_workstreams_after_plan_close(repo, active_relative_plan, completed_relative_plan):
|
|
1217
|
+
target = workstreams_path(repo)
|
|
1218
|
+
if not target.exists():
|
|
1219
|
+
return
|
|
1220
|
+
lines = target.read_text().splitlines()
|
|
1221
|
+
updated = []
|
|
1222
|
+
current_plan_was_closed = False
|
|
1223
|
+
for line in lines:
|
|
1224
|
+
stripped = line.strip()
|
|
1225
|
+
if stripped.startswith("|") and not stripped.startswith("| ---") and not stripped.startswith("| ID |"):
|
|
1226
|
+
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
1227
|
+
if len(cells) == 6:
|
|
1228
|
+
if cells[2] == active_relative_plan:
|
|
1229
|
+
cells[2] = "none"
|
|
1230
|
+
if cells[3] == "none":
|
|
1231
|
+
cells[3] = completed_relative_plan
|
|
1232
|
+
if cells[3] == active_relative_plan:
|
|
1233
|
+
cells[3] = completed_relative_plan
|
|
1234
|
+
updated.append("| " + " | ".join(cells) + " |")
|
|
1235
|
+
continue
|
|
1236
|
+
if line == f"Current plan: {active_relative_plan}":
|
|
1237
|
+
updated.append("Current plan: none")
|
|
1238
|
+
current_plan_was_closed = True
|
|
1239
|
+
continue
|
|
1240
|
+
if line == f"Last completed plan: {active_relative_plan}":
|
|
1241
|
+
updated.append(f"Last completed plan: {completed_relative_plan}")
|
|
1242
|
+
current_plan_was_closed = False
|
|
1243
|
+
continue
|
|
1244
|
+
if current_plan_was_closed and line == "Last completed plan: none":
|
|
1245
|
+
updated.append(f"Last completed plan: {completed_relative_plan}")
|
|
1246
|
+
current_plan_was_closed = False
|
|
1247
|
+
continue
|
|
1248
|
+
updated.append(line)
|
|
1249
|
+
if line.startswith("## "):
|
|
1250
|
+
current_plan_was_closed = False
|
|
1251
|
+
target.write_text("\n".join(updated).rstrip() + "\n")
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def assert_phase_continuity_closed(repo, plan_path, plan_text):
|
|
1255
|
+
issues = phase_continuity_issues(repo, plan_path, plan_text)
|
|
1256
|
+
if issues:
|
|
1257
|
+
messages = "\n".join(f"- {issue['code']}: {issue['message']}" for issue in issues)
|
|
1258
|
+
raise RuntimeError(
|
|
1259
|
+
"Cannot close plan until phase continuity is recorded:\n"
|
|
1260
|
+
+ messages
|
|
1261
|
+
+ "\nRun `phase-set` and update `workstreams.md` or `tech-debt-tracker.md` before closing."
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
|
|
651
1265
|
def append_knowledge_item(plan_path, fact, destination):
|
|
652
1266
|
text = plan_path.read_text()
|
|
653
1267
|
lines = text.splitlines()
|
|
654
1268
|
section_index = find_section(lines, "## Durable Knowledge To Capture")
|
|
655
1269
|
if section_index is None:
|
|
656
1270
|
raise ValueError("Plan is missing '## Durable Knowledge To Capture'")
|
|
657
|
-
|
|
658
|
-
filtered_lines = [line for line in lines if line.strip() != placeholder]
|
|
1271
|
+
filtered_lines = [line for line in lines if line.strip() != DEFAULT_KNOWLEDGE_PLACEHOLDER]
|
|
659
1272
|
insert_index = section_index + 1
|
|
660
1273
|
while insert_index < len(filtered_lines) and not filtered_lines[insert_index].startswith("## "):
|
|
661
1274
|
insert_index += 1
|
|
@@ -666,11 +1279,126 @@ def append_knowledge_item(plan_path, fact, destination):
|
|
|
666
1279
|
return item, item_id
|
|
667
1280
|
|
|
668
1281
|
|
|
1282
|
+
def render_open_defect_rework(open_defects):
|
|
1283
|
+
lines = ["- Resolve all open defects, then re-run validation and `quality-score`."]
|
|
1284
|
+
for defect in open_defects:
|
|
1285
|
+
evidence = f" Evidence: {defect['evidence']}." if defect.get("evidence") else ""
|
|
1286
|
+
lines.append(f"- Resolve {defect['id']} ({defect['severity']}): {defect['summary']}.{evidence}")
|
|
1287
|
+
return "\n".join(lines)
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def mark_quality_gate_blocked_by_defects(text):
|
|
1291
|
+
open_defects = open_defects_for_plan(text)
|
|
1292
|
+
if not open_defects:
|
|
1293
|
+
return text
|
|
1294
|
+
lines = text.splitlines()
|
|
1295
|
+
section_index = find_section(lines, "## Quality Gate")
|
|
1296
|
+
if section_index is None:
|
|
1297
|
+
gate_text = "\n".join(
|
|
1298
|
+
[
|
|
1299
|
+
"Status: fail",
|
|
1300
|
+
"Minimum score: 8.0",
|
|
1301
|
+
"Average score: pending",
|
|
1302
|
+
f"Last scored: {datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ')}",
|
|
1303
|
+
"",
|
|
1304
|
+
"Blocked by unresolved defects. Run `defect-resolve`, re-run validation, then run `quality-score`.",
|
|
1305
|
+
]
|
|
1306
|
+
)
|
|
1307
|
+
text = replace_section(text, "Quality Gate", gate_text)
|
|
1308
|
+
else:
|
|
1309
|
+
end_index = len(lines)
|
|
1310
|
+
for index in range(section_index + 1, len(lines)):
|
|
1311
|
+
if lines[index].startswith("## "):
|
|
1312
|
+
end_index = index
|
|
1313
|
+
break
|
|
1314
|
+
section_lines = lines[section_index + 1 : end_index]
|
|
1315
|
+
has_status = False
|
|
1316
|
+
updated_section = []
|
|
1317
|
+
for line in section_lines:
|
|
1318
|
+
if line.startswith("Status:"):
|
|
1319
|
+
updated_section.append("Status: fail")
|
|
1320
|
+
has_status = True
|
|
1321
|
+
elif line.startswith("Last scored:"):
|
|
1322
|
+
updated_section.append(f"Last scored: {datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ')}")
|
|
1323
|
+
else:
|
|
1324
|
+
updated_section.append(line)
|
|
1325
|
+
if not has_status:
|
|
1326
|
+
updated_section.insert(0, "Status: fail")
|
|
1327
|
+
lines = lines[: section_index + 1] + updated_section + lines[end_index:]
|
|
1328
|
+
text = "\n".join(lines).rstrip() + "\n"
|
|
1329
|
+
return replace_section(text, "Rework Required", render_open_defect_rework(open_defects))
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def append_defect_item(plan_path, severity, summary, evidence=None):
|
|
1333
|
+
text = plan_path.read_text()
|
|
1334
|
+
if find_section(text.splitlines(), "## Defects To Resolve") is None:
|
|
1335
|
+
text = replace_section(text, "Defects To Resolve", DEFAULT_DEFECT_PLACEHOLDER)
|
|
1336
|
+
lines = text.splitlines()
|
|
1337
|
+
section_index = find_section(lines, "## Defects To Resolve")
|
|
1338
|
+
if section_index is None:
|
|
1339
|
+
raise ValueError("Plan is missing '## Defects To Resolve'")
|
|
1340
|
+
filtered_lines = [line for line in lines if line.strip() != DEFAULT_DEFECT_PLACEHOLDER]
|
|
1341
|
+
insert_index = section_index + 1
|
|
1342
|
+
while insert_index < len(filtered_lines) and not filtered_lines[insert_index].startswith("## "):
|
|
1343
|
+
insert_index += 1
|
|
1344
|
+
item_id = defect_id_for(summary)
|
|
1345
|
+
safe_summary = clean_fact_text(summary)
|
|
1346
|
+
safe_evidence = clean_fact_text(evidence) if evidence else None
|
|
1347
|
+
item = f"- [ ] [bug:{item_id}] [{severity}] {safe_summary}"
|
|
1348
|
+
if safe_evidence:
|
|
1349
|
+
item = f"{item} | evidence: {safe_evidence}"
|
|
1350
|
+
updated_lines = filtered_lines[:insert_index] + [item] + filtered_lines[insert_index:]
|
|
1351
|
+
plan_path.write_text(mark_quality_gate_blocked_by_defects("\n".join(updated_lines).rstrip() + "\n"))
|
|
1352
|
+
return item, item_id
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def close_defect_line(line, fix_evidence):
|
|
1356
|
+
updated = line.replace("- [ ]", "- [x]", 1)
|
|
1357
|
+
if "| fix:" not in updated:
|
|
1358
|
+
updated = f"{updated} | fix: {fix_evidence}"
|
|
1359
|
+
return updated
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def mark_defect_resolved(plan_path, defect_id, fix_evidence):
|
|
1363
|
+
if not defect_id:
|
|
1364
|
+
raise ValueError("Provide --id to resolve a defect")
|
|
1365
|
+
if not fix_evidence:
|
|
1366
|
+
raise ValueError("Provide --fix-evidence or --fix-evidence-file to resolve a defect")
|
|
1367
|
+
lines = plan_path.read_text().splitlines()
|
|
1368
|
+
safe_fix = clean_fact_text(fix_evidence)
|
|
1369
|
+
replaced = False
|
|
1370
|
+
updated = []
|
|
1371
|
+
for line in lines:
|
|
1372
|
+
stripped = line.strip()
|
|
1373
|
+
parsed = parse_defect_item(stripped)
|
|
1374
|
+
if parsed and parsed["status"] == "open" and parsed["id"] == defect_id and not replaced:
|
|
1375
|
+
updated.append(close_defect_line(line, safe_fix))
|
|
1376
|
+
replaced = True
|
|
1377
|
+
else:
|
|
1378
|
+
updated.append(line)
|
|
1379
|
+
if not replaced:
|
|
1380
|
+
raise ValueError(f"Open defect not found for id: {defect_id}")
|
|
1381
|
+
text = "\n".join(updated).rstrip() + "\n"
|
|
1382
|
+
open_defects = open_defects_for_plan(text)
|
|
1383
|
+
if open_defects:
|
|
1384
|
+
text = replace_section(text, "Rework Required", render_open_defect_rework(open_defects))
|
|
1385
|
+
else:
|
|
1386
|
+
text = replace_section(
|
|
1387
|
+
text,
|
|
1388
|
+
"Rework Required",
|
|
1389
|
+
"Defects resolved. Re-run validation and `quality-score` before closing.",
|
|
1390
|
+
)
|
|
1391
|
+
plan_path.write_text(text)
|
|
1392
|
+
|
|
1393
|
+
|
|
669
1394
|
def mark_knowledge_items_closed(text):
|
|
670
1395
|
lines = text.splitlines()
|
|
671
1396
|
updated = []
|
|
1397
|
+
in_knowledge_section = False
|
|
672
1398
|
for line in lines:
|
|
673
|
-
if line.
|
|
1399
|
+
if line.startswith("## "):
|
|
1400
|
+
in_knowledge_section = line.strip().lower() == "## durable knowledge to capture"
|
|
1401
|
+
if in_knowledge_section and line.strip().startswith("- [ ]") and line.strip() != DEFAULT_KNOWLEDGE_PLACEHOLDER:
|
|
674
1402
|
updated.append(line.replace("- [ ]", "- [x]", 1))
|
|
675
1403
|
else:
|
|
676
1404
|
updated.append(line)
|
|
@@ -802,6 +1530,24 @@ def completed_plan_dir(repo):
|
|
|
802
1530
|
return repo / "docs" / "exec-plans" / "completed"
|
|
803
1531
|
|
|
804
1532
|
|
|
1533
|
+
def plan_path_from_arg(repo, plan_arg):
|
|
1534
|
+
raw_plan = Path(plan_arg)
|
|
1535
|
+
if raw_plan.is_absolute():
|
|
1536
|
+
plan_path = raw_plan.resolve()
|
|
1537
|
+
else:
|
|
1538
|
+
plan_path = (repo / raw_plan).resolve()
|
|
1539
|
+
|
|
1540
|
+
try:
|
|
1541
|
+
relative_plan = str(plan_path.relative_to(repo.resolve()))
|
|
1542
|
+
except ValueError as error:
|
|
1543
|
+
raise ValueError(f"Plan must be inside repo: {plan_arg}") from error
|
|
1544
|
+
|
|
1545
|
+
if not plan_path.exists():
|
|
1546
|
+
raise FileNotFoundError(f"Plan not found: {plan_path}")
|
|
1547
|
+
|
|
1548
|
+
return plan_path, relative_plan
|
|
1549
|
+
|
|
1550
|
+
|
|
805
1551
|
def create_plan(repo, slug, goal):
|
|
806
1552
|
plan_dir = active_plan_dir(repo)
|
|
807
1553
|
plan_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -813,6 +1559,7 @@ def create_plan(repo, slug, goal):
|
|
|
813
1559
|
content = PLAN_TEMPLATE.format(
|
|
814
1560
|
title=title.title(),
|
|
815
1561
|
goal=goal,
|
|
1562
|
+
defect_section=DEFAULT_DEFECT_PLACEHOLDER,
|
|
816
1563
|
knowledge_section="- [ ] Add durable facts here as they emerge -> <destination-doc>",
|
|
817
1564
|
)
|
|
818
1565
|
plan_path.write_text(content)
|
|
@@ -820,11 +1567,16 @@ def create_plan(repo, slug, goal):
|
|
|
820
1567
|
|
|
821
1568
|
|
|
822
1569
|
def close_plan(repo, plan_relative_path, summary, force):
|
|
823
|
-
plan_path = repo
|
|
824
|
-
if not plan_path.exists():
|
|
825
|
-
raise FileNotFoundError(f"Plan not found: {plan_path}")
|
|
1570
|
+
plan_path, active_relative_path = plan_path_from_arg(repo, plan_relative_path)
|
|
826
1571
|
text = plan_path.read_text()
|
|
827
|
-
|
|
1572
|
+
if not force:
|
|
1573
|
+
assert_quality_gate_passed(text)
|
|
1574
|
+
assert_phase_continuity_closed(repo, plan_path, text)
|
|
1575
|
+
open_items = [
|
|
1576
|
+
item
|
|
1577
|
+
for item in extract_knowledge_items(text)
|
|
1578
|
+
if item.startswith("- [ ]") and item != DEFAULT_KNOWLEDGE_PLACEHOLDER
|
|
1579
|
+
]
|
|
828
1580
|
if open_items and not force:
|
|
829
1581
|
raise RuntimeError(
|
|
830
1582
|
"Cannot close plan with unresolved durable knowledge items:\n" + "\n".join(open_items)
|
|
@@ -835,6 +1587,8 @@ def close_plan(repo, plan_relative_path, summary, force):
|
|
|
835
1587
|
destination = completed_dir / plan_path.name
|
|
836
1588
|
destination.write_text(updated_text)
|
|
837
1589
|
plan_path.unlink()
|
|
1590
|
+
completed_relative_path = str(destination.relative_to(repo))
|
|
1591
|
+
update_workstreams_after_plan_close(repo, active_relative_path, completed_relative_path)
|
|
838
1592
|
return destination, open_items
|
|
839
1593
|
|
|
840
1594
|
|
|
@@ -846,6 +1600,7 @@ def check_harness(repo):
|
|
|
846
1600
|
"docs/QUALITY_SCORE.md",
|
|
847
1601
|
"docs/RELIABILITY.md",
|
|
848
1602
|
"docs/SECURITY.md",
|
|
1603
|
+
"docs/exec-plans/workstreams.md",
|
|
849
1604
|
"docs/exec-plans/active/README.md",
|
|
850
1605
|
"docs/exec-plans/active/_template.md",
|
|
851
1606
|
"docs/exec-plans/completed/README.md",
|
|
@@ -869,7 +1624,40 @@ def check_harness(repo):
|
|
|
869
1624
|
if plan_path.name in {"README.md", "_template.md"}:
|
|
870
1625
|
continue
|
|
871
1626
|
relative_plan = str(plan_path.relative_to(repo))
|
|
1627
|
+
quality_gate = quality_gate_for_plan(plan_path.read_text())
|
|
1628
|
+
if quality_gate["status"] == "missing":
|
|
1629
|
+
issues.append(
|
|
1630
|
+
{
|
|
1631
|
+
"severity": "error",
|
|
1632
|
+
"code": "missing-quality-gate",
|
|
1633
|
+
"path": relative_plan,
|
|
1634
|
+
"message": "Active plan is missing a Quality Gate section.",
|
|
1635
|
+
}
|
|
1636
|
+
)
|
|
1637
|
+
elif quality_gate["status"] != "pass":
|
|
1638
|
+
issues.append(
|
|
1639
|
+
{
|
|
1640
|
+
"severity": "error",
|
|
1641
|
+
"code": "quality-gate-not-passing",
|
|
1642
|
+
"path": relative_plan,
|
|
1643
|
+
"message": "Active plan quality gate has not passed; score the work and finish rework before handoff.",
|
|
1644
|
+
}
|
|
1645
|
+
)
|
|
1646
|
+
for defect in open_defects_for_plan(plan_path.read_text()):
|
|
1647
|
+
issues.append(
|
|
1648
|
+
{
|
|
1649
|
+
"severity": "error",
|
|
1650
|
+
"code": "open-defect",
|
|
1651
|
+
"path": relative_plan,
|
|
1652
|
+
"id": defect["id"],
|
|
1653
|
+
"defect_severity": defect["severity"],
|
|
1654
|
+
"message": f"Active plan has an unresolved defect: {defect['summary']}",
|
|
1655
|
+
}
|
|
1656
|
+
)
|
|
1657
|
+
issues.extend(phase_continuity_issues(repo, plan_path, plan_path.read_text()))
|
|
872
1658
|
for item in extract_knowledge_items(plan_path.read_text()):
|
|
1659
|
+
if item == DEFAULT_KNOWLEDGE_PLACEHOLDER:
|
|
1660
|
+
continue
|
|
873
1661
|
parsed = parse_knowledge_item(item)
|
|
874
1662
|
if not parsed:
|
|
875
1663
|
issues.append(
|
|
@@ -905,6 +1693,34 @@ def check_harness(repo):
|
|
|
905
1693
|
}
|
|
906
1694
|
)
|
|
907
1695
|
|
|
1696
|
+
ledger = workstreams_path(repo)
|
|
1697
|
+
if ledger.exists():
|
|
1698
|
+
for index, line in enumerate(ledger.read_text().splitlines(), start=1):
|
|
1699
|
+
stripped = line.strip()
|
|
1700
|
+
if not stripped.startswith("|") or stripped.startswith("| ---") or stripped.startswith("| ID |"):
|
|
1701
|
+
continue
|
|
1702
|
+
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
1703
|
+
if len(cells) != 6:
|
|
1704
|
+
continue
|
|
1705
|
+
workstream_id, _, current_plan, last_completed_plan, _, _ = cells
|
|
1706
|
+
for label, plan_value in [
|
|
1707
|
+
("current plan", current_plan),
|
|
1708
|
+
("last completed plan", last_completed_plan),
|
|
1709
|
+
]:
|
|
1710
|
+
if plan_value in {"", "none", "n/a", "-"}:
|
|
1711
|
+
continue
|
|
1712
|
+
if not (repo / plan_value).exists():
|
|
1713
|
+
issues.append(
|
|
1714
|
+
{
|
|
1715
|
+
"severity": "error",
|
|
1716
|
+
"code": "missing-workstream-plan-reference",
|
|
1717
|
+
"path": str(ledger.relative_to(repo)),
|
|
1718
|
+
"line": index,
|
|
1719
|
+
"workstream": workstream_id,
|
|
1720
|
+
"message": f"Workstream {workstream_id} references missing {label}: {plan_value}",
|
|
1721
|
+
}
|
|
1722
|
+
)
|
|
1723
|
+
|
|
908
1724
|
return {
|
|
909
1725
|
"repo": str(repo),
|
|
910
1726
|
"status": "pass" if not issues else "fail",
|
|
@@ -1004,11 +1820,21 @@ def load_json(path):
|
|
|
1004
1820
|
def write_json(path, payload):
|
|
1005
1821
|
output = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
|
1006
1822
|
if path:
|
|
1007
|
-
Path(path)
|
|
1823
|
+
target = Path(path)
|
|
1824
|
+
ensure_parent(target)
|
|
1825
|
+
target.write_text(output)
|
|
1008
1826
|
else:
|
|
1009
1827
|
print(output, end="")
|
|
1010
1828
|
|
|
1011
1829
|
|
|
1830
|
+
def read_text_arg(value=None, file_path=None, label="value"):
|
|
1831
|
+
if value and file_path:
|
|
1832
|
+
raise ValueError(f"Use either --{label} or --{label}-file, not both")
|
|
1833
|
+
if file_path:
|
|
1834
|
+
return Path(file_path).read_text().strip()
|
|
1835
|
+
return value
|
|
1836
|
+
|
|
1837
|
+
|
|
1012
1838
|
def command_analyze(args):
|
|
1013
1839
|
repo = Path(args.repo).resolve()
|
|
1014
1840
|
analysis = analyze_repo(repo)
|
|
@@ -1044,14 +1870,43 @@ def command_plan_start(args):
|
|
|
1044
1870
|
|
|
1045
1871
|
def command_knowledge_log(args):
|
|
1046
1872
|
repo = Path(args.repo).resolve()
|
|
1047
|
-
plan_path = repo
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1873
|
+
plan_path, _ = plan_path_from_arg(repo, args.plan)
|
|
1874
|
+
fact = read_text_arg(args.fact, args.fact_file, "fact")
|
|
1875
|
+
if not fact:
|
|
1876
|
+
raise ValueError("Provide --fact or --fact-file")
|
|
1877
|
+
item, item_id = append_knowledge_item(plan_path, fact, args.destination)
|
|
1051
1878
|
result = {"repo": str(repo), "plan": str(plan_path), "id": item_id, "logged": item}
|
|
1052
1879
|
write_json(args.output, result)
|
|
1053
1880
|
|
|
1054
1881
|
|
|
1882
|
+
def command_defect_log(args):
|
|
1883
|
+
repo = Path(args.repo).resolve()
|
|
1884
|
+
plan_path, _ = plan_path_from_arg(repo, args.plan)
|
|
1885
|
+
summary = read_text_arg(args.summary, args.summary_file, "summary")
|
|
1886
|
+
evidence = read_text_arg(args.evidence, args.evidence_file, "evidence")
|
|
1887
|
+
if not summary:
|
|
1888
|
+
raise ValueError("Provide --summary or --summary-file")
|
|
1889
|
+
item, item_id = append_defect_item(plan_path, args.severity, summary, evidence=evidence)
|
|
1890
|
+
result = {"repo": str(repo), "plan": str(plan_path), "id": item_id, "logged": item, "status": "fail"}
|
|
1891
|
+
write_json(args.output, result)
|
|
1892
|
+
raise SystemExit(1)
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
def command_defect_resolve(args):
|
|
1896
|
+
repo = Path(args.repo).resolve()
|
|
1897
|
+
plan_path, _ = plan_path_from_arg(repo, args.plan)
|
|
1898
|
+
fix_evidence = read_text_arg(args.fix_evidence, args.fix_evidence_file, "fix-evidence")
|
|
1899
|
+
mark_defect_resolved(plan_path, args.id, fix_evidence)
|
|
1900
|
+
result = {
|
|
1901
|
+
"repo": str(repo),
|
|
1902
|
+
"plan": str(plan_path),
|
|
1903
|
+
"id": args.id,
|
|
1904
|
+
"status": "resolved",
|
|
1905
|
+
"fix_evidence": fix_evidence,
|
|
1906
|
+
}
|
|
1907
|
+
write_json(args.output, result)
|
|
1908
|
+
|
|
1909
|
+
|
|
1055
1910
|
def command_plan_close(args):
|
|
1056
1911
|
repo = Path(args.repo).resolve()
|
|
1057
1912
|
destination, unresolved = close_plan(repo, args.plan, args.summary, args.force)
|
|
@@ -1064,26 +1919,91 @@ def command_plan_close(args):
|
|
|
1064
1919
|
write_json(args.output, result)
|
|
1065
1920
|
|
|
1066
1921
|
|
|
1922
|
+
def score_arg(args, name):
|
|
1923
|
+
value = getattr(args, name)
|
|
1924
|
+
if value < 0 or value > 10:
|
|
1925
|
+
raise ValueError(f"{name.replace('_', '-')} must be between 0 and 10")
|
|
1926
|
+
return float(value)
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
def command_quality_score(args):
|
|
1930
|
+
repo = Path(args.repo).resolve()
|
|
1931
|
+
plan_path, _ = plan_path_from_arg(repo, args.plan)
|
|
1932
|
+
scores = {
|
|
1933
|
+
"product_correctness": score_arg(args, "product_correctness"),
|
|
1934
|
+
"ux_operator_clarity": score_arg(args, "ux_operator_clarity"),
|
|
1935
|
+
"architecture_maintainability": score_arg(args, "architecture_maintainability"),
|
|
1936
|
+
"reliability_observability": score_arg(args, "reliability_observability"),
|
|
1937
|
+
"security_data_handling": score_arg(args, "security_data_handling"),
|
|
1938
|
+
}
|
|
1939
|
+
notes = {
|
|
1940
|
+
"product_correctness": args.product_note,
|
|
1941
|
+
"ux_operator_clarity": args.ux_note,
|
|
1942
|
+
"architecture_maintainability": args.architecture_note,
|
|
1943
|
+
"reliability_observability": args.reliability_note,
|
|
1944
|
+
"security_data_handling": args.security_note,
|
|
1945
|
+
}
|
|
1946
|
+
result = update_quality_gate(plan_path, scores, notes, args.minimum)
|
|
1947
|
+
result.update({"repo": str(repo), "plan": str(plan_path)})
|
|
1948
|
+
write_json(args.output, result)
|
|
1949
|
+
if result["status"] != "pass":
|
|
1950
|
+
raise SystemExit(1)
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
def command_phase_set(args):
|
|
1954
|
+
repo = Path(args.repo).resolve()
|
|
1955
|
+
plan_path, _ = plan_path_from_arg(repo, args.plan)
|
|
1956
|
+
result = update_phase_continuity(
|
|
1957
|
+
plan_path,
|
|
1958
|
+
args.mode,
|
|
1959
|
+
args.workstream,
|
|
1960
|
+
args.current_phase,
|
|
1961
|
+
args.next_phase,
|
|
1962
|
+
args.continuation,
|
|
1963
|
+
args.next_action,
|
|
1964
|
+
args.closure_reason,
|
|
1965
|
+
args.resume_notes,
|
|
1966
|
+
)
|
|
1967
|
+
result.update({"repo": str(repo), "plan": str(plan_path)})
|
|
1968
|
+
write_json(args.output, result)
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
def command_workstream_upsert(args):
|
|
1972
|
+
repo = Path(args.repo).resolve()
|
|
1973
|
+
target = append_workstream_entry(
|
|
1974
|
+
repo,
|
|
1975
|
+
args.id,
|
|
1976
|
+
args.status,
|
|
1977
|
+
args.current_plan,
|
|
1978
|
+
args.last_completed_plan,
|
|
1979
|
+
args.next_action,
|
|
1980
|
+
args.goal,
|
|
1981
|
+
args.resume_notes,
|
|
1982
|
+
)
|
|
1983
|
+
result = {"repo": str(repo), "workstreams": str(target), "id": args.id, "status": "updated"}
|
|
1984
|
+
write_json(args.output, result)
|
|
1985
|
+
|
|
1986
|
+
|
|
1067
1987
|
def command_knowledge_mark_written(args):
|
|
1068
1988
|
repo = Path(args.repo).resolve()
|
|
1069
|
-
plan_path = repo
|
|
1070
|
-
|
|
1071
|
-
|
|
1989
|
+
plan_path, _ = plan_path_from_arg(repo, args.plan)
|
|
1990
|
+
fact = read_text_arg(args.fact, args.fact_file, "fact")
|
|
1991
|
+
evidence = read_text_arg(args.evidence, args.evidence_file, "evidence")
|
|
1072
1992
|
mark_single_knowledge_item_written(
|
|
1073
1993
|
repo,
|
|
1074
1994
|
plan_path,
|
|
1075
|
-
|
|
1995
|
+
fact,
|
|
1076
1996
|
args.destination,
|
|
1077
1997
|
append=args.append,
|
|
1078
1998
|
knowledge_id=args.id,
|
|
1079
|
-
evidence=
|
|
1999
|
+
evidence=evidence,
|
|
1080
2000
|
)
|
|
1081
2001
|
result = {
|
|
1082
2002
|
"repo": str(repo),
|
|
1083
2003
|
"plan": str(plan_path),
|
|
1084
|
-
"marked_written": args.id or
|
|
2004
|
+
"marked_written": args.id or fact,
|
|
1085
2005
|
"destination": args.destination,
|
|
1086
|
-
"evidence":
|
|
2006
|
+
"evidence": evidence,
|
|
1087
2007
|
}
|
|
1088
2008
|
write_json(args.output, result)
|
|
1089
2009
|
|
|
@@ -1139,18 +2059,41 @@ def build_parser():
|
|
|
1139
2059
|
knowledge_log = subparsers.add_parser("knowledge-log")
|
|
1140
2060
|
knowledge_log.add_argument("--repo", required=True)
|
|
1141
2061
|
knowledge_log.add_argument("--plan", required=True)
|
|
1142
|
-
knowledge_log.add_argument("--fact"
|
|
2062
|
+
knowledge_log.add_argument("--fact")
|
|
2063
|
+
knowledge_log.add_argument("--fact-file")
|
|
1143
2064
|
knowledge_log.add_argument("--destination", required=True)
|
|
1144
2065
|
knowledge_log.add_argument("--output")
|
|
1145
2066
|
knowledge_log.set_defaults(func=command_knowledge_log)
|
|
1146
2067
|
|
|
2068
|
+
defect_log = subparsers.add_parser("defect-log")
|
|
2069
|
+
defect_log.add_argument("--repo", required=True)
|
|
2070
|
+
defect_log.add_argument("--plan", required=True)
|
|
2071
|
+
defect_log.add_argument("--severity", choices=["P0", "P1", "P2", "P3"], required=True)
|
|
2072
|
+
defect_log.add_argument("--summary")
|
|
2073
|
+
defect_log.add_argument("--summary-file")
|
|
2074
|
+
defect_log.add_argument("--evidence")
|
|
2075
|
+
defect_log.add_argument("--evidence-file")
|
|
2076
|
+
defect_log.add_argument("--output")
|
|
2077
|
+
defect_log.set_defaults(func=command_defect_log)
|
|
2078
|
+
|
|
2079
|
+
defect_resolve = subparsers.add_parser("defect-resolve")
|
|
2080
|
+
defect_resolve.add_argument("--repo", required=True)
|
|
2081
|
+
defect_resolve.add_argument("--plan", required=True)
|
|
2082
|
+
defect_resolve.add_argument("--id", required=True)
|
|
2083
|
+
defect_resolve.add_argument("--fix-evidence")
|
|
2084
|
+
defect_resolve.add_argument("--fix-evidence-file")
|
|
2085
|
+
defect_resolve.add_argument("--output")
|
|
2086
|
+
defect_resolve.set_defaults(func=command_defect_resolve)
|
|
2087
|
+
|
|
1147
2088
|
knowledge_mark_written = subparsers.add_parser("knowledge-mark-written")
|
|
1148
2089
|
knowledge_mark_written.add_argument("--repo", required=True)
|
|
1149
2090
|
knowledge_mark_written.add_argument("--plan", required=True)
|
|
1150
2091
|
knowledge_mark_written.add_argument("--id")
|
|
1151
2092
|
knowledge_mark_written.add_argument("--fact")
|
|
2093
|
+
knowledge_mark_written.add_argument("--fact-file")
|
|
1152
2094
|
knowledge_mark_written.add_argument("--destination")
|
|
1153
2095
|
knowledge_mark_written.add_argument("--evidence")
|
|
2096
|
+
knowledge_mark_written.add_argument("--evidence-file")
|
|
1154
2097
|
knowledge_mark_written.add_argument("--append", action="store_true")
|
|
1155
2098
|
knowledge_mark_written.add_argument("--output")
|
|
1156
2099
|
knowledge_mark_written.set_defaults(func=command_knowledge_mark_written)
|
|
@@ -1163,6 +2106,57 @@ def build_parser():
|
|
|
1163
2106
|
plan_close.add_argument("--output")
|
|
1164
2107
|
plan_close.set_defaults(func=command_plan_close)
|
|
1165
2108
|
|
|
2109
|
+
quality_score = subparsers.add_parser("quality-score")
|
|
2110
|
+
quality_score.add_argument("--repo", required=True)
|
|
2111
|
+
quality_score.add_argument("--plan", required=True)
|
|
2112
|
+
quality_score.add_argument("--minimum", type=float, default=8.0)
|
|
2113
|
+
quality_score.add_argument("--product-correctness", type=float, required=True)
|
|
2114
|
+
quality_score.add_argument("--ux-operator-clarity", type=float, required=True)
|
|
2115
|
+
quality_score.add_argument("--architecture-maintainability", type=float, required=True)
|
|
2116
|
+
quality_score.add_argument("--reliability-observability", type=float, required=True)
|
|
2117
|
+
quality_score.add_argument("--security-data-handling", type=float, required=True)
|
|
2118
|
+
quality_score.add_argument("--product-note", default="")
|
|
2119
|
+
quality_score.add_argument("--ux-note", default="")
|
|
2120
|
+
quality_score.add_argument("--architecture-note", default="")
|
|
2121
|
+
quality_score.add_argument("--reliability-note", default="")
|
|
2122
|
+
quality_score.add_argument("--security-note", default="")
|
|
2123
|
+
quality_score.add_argument("--output")
|
|
2124
|
+
quality_score.set_defaults(func=command_quality_score)
|
|
2125
|
+
|
|
2126
|
+
phase_set = subparsers.add_parser("phase-set")
|
|
2127
|
+
phase_set.add_argument("--repo", required=True)
|
|
2128
|
+
phase_set.add_argument("--plan", required=True)
|
|
2129
|
+
phase_set.add_argument(
|
|
2130
|
+
"--mode",
|
|
2131
|
+
choices=["single-phase", "multi-phase", "paused", "completed", "stopped"],
|
|
2132
|
+
required=True,
|
|
2133
|
+
)
|
|
2134
|
+
phase_set.add_argument("--workstream")
|
|
2135
|
+
phase_set.add_argument("--current-phase")
|
|
2136
|
+
phase_set.add_argument("--next-phase", default="none")
|
|
2137
|
+
phase_set.add_argument("--continuation", default="none")
|
|
2138
|
+
phase_set.add_argument("--next-action", default="none")
|
|
2139
|
+
phase_set.add_argument("--closure-reason", default="none")
|
|
2140
|
+
phase_set.add_argument("--resume-notes", default="none")
|
|
2141
|
+
phase_set.add_argument("--output")
|
|
2142
|
+
phase_set.set_defaults(func=command_phase_set)
|
|
2143
|
+
|
|
2144
|
+
workstream_upsert = subparsers.add_parser("workstream-upsert")
|
|
2145
|
+
workstream_upsert.add_argument("--repo", required=True)
|
|
2146
|
+
workstream_upsert.add_argument("--id", required=True)
|
|
2147
|
+
workstream_upsert.add_argument(
|
|
2148
|
+
"--status",
|
|
2149
|
+
choices=["active", "paused", "completed", "stopped"],
|
|
2150
|
+
required=True,
|
|
2151
|
+
)
|
|
2152
|
+
workstream_upsert.add_argument("--current-plan", default="none")
|
|
2153
|
+
workstream_upsert.add_argument("--last-completed-plan", default="none")
|
|
2154
|
+
workstream_upsert.add_argument("--next-action", required=True)
|
|
2155
|
+
workstream_upsert.add_argument("--goal", default="")
|
|
2156
|
+
workstream_upsert.add_argument("--resume-notes", default="")
|
|
2157
|
+
workstream_upsert.add_argument("--output")
|
|
2158
|
+
workstream_upsert.set_defaults(func=command_workstream_upsert)
|
|
2159
|
+
|
|
1166
2160
|
check = subparsers.add_parser("check")
|
|
1167
2161
|
check.add_argument("--repo", required=True)
|
|
1168
2162
|
check.add_argument("--output")
|