@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.
@@ -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. permanent docs have been updated
297
- 3. any remaining follow-ups are recorded in tech debt or new plans
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
- placeholder = "- [ ] Add durable facts here as they emerge -> <destination-doc>"
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.strip().startswith("- [ ]"):
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 / plan_relative_path
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
- open_items = [item for item in extract_knowledge_items(text) if item.startswith("- [ ]")]
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).write_text(output)
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 / args.plan
1048
- if not plan_path.exists():
1049
- raise FileNotFoundError(f"Plan not found: {plan_path}")
1050
- item, item_id = append_knowledge_item(plan_path, args.fact, args.destination)
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 / args.plan
1070
- if not plan_path.exists():
1071
- raise FileNotFoundError(f"Plan not found: {plan_path}")
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
- args.fact,
1995
+ fact,
1076
1996
  args.destination,
1077
1997
  append=args.append,
1078
1998
  knowledge_id=args.id,
1079
- evidence=args.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 args.fact,
2004
+ "marked_written": args.id or fact,
1085
2005
  "destination": args.destination,
1086
- "evidence": args.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", required=True)
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")