tenseleyflow/loader / 4f5a867

Browse files

Add unified policy timeline entry contract

Authored by espadonne
SHA
4f5a8674002d7a1045b6410e59ca4c78a6aa01b6
Parents
f8f0c8a
Tree
aa3c0b9

2 changed files

StatusFile+-
M src/loader/runtime/workflow_policy.py 54 0
M tests/test_workflow_policy.py 22 0
src/loader/runtime/workflow_policy.pymodified
@@ -58,6 +58,12 @@ class WorkflowTimelineEntryKind(StrEnum):
58
     CLARIFY_EXIT = "clarify_exit"
58
     CLARIFY_EXIT = "clarify_exit"
59
     PLAN_REFRESH = "plan_refresh"
59
     PLAN_REFRESH = "plan_refresh"
60
     VERIFY_SKIP = "verify_skip"
60
     VERIFY_SKIP = "verify_skip"
61
+    COMPLETION_CHECK = "completion_check"
62
+    COMPLETION_CONTINUE = "completion_continue"
63
+    COMPLETION_COMPLETE = "completion_complete"
64
+    COMPLETION_FINALIZE = "completion_finalize"
65
+    REPAIR_RETRY = "repair_retry"
66
+    REPAIR_FAIL = "repair_fail"
61
 
67
 
62
 
68
 
63
 @dataclass(slots=True)
69
 @dataclass(slots=True)
@@ -291,6 +297,8 @@ class WorkflowTimelineEntry:
291
     clarify_pressure_kind: str | None = None
297
     clarify_pressure_kind: str | None = None
292
     pressure_pass_complete: bool = False
298
     pressure_pass_complete: bool = False
293
     missing_readiness_gates: list[str] = field(default_factory=list)
299
     missing_readiness_gates: list[str] = field(default_factory=list)
300
+    policy_stage: str | None = None
301
+    policy_outcome: str | None = None
294
     prompt_format: str | None = None
302
     prompt_format: str | None = None
295
     prompt_sections: list[str] = field(default_factory=list)
303
     prompt_sections: list[str] = field(default_factory=list)
296
     artifact_paths: list[str] = field(default_factory=list)
304
     artifact_paths: list[str] = field(default_factory=list)
@@ -314,6 +322,8 @@ class WorkflowTimelineEntry:
314
             "clarify_pressure_kind": self.clarify_pressure_kind,
322
             "clarify_pressure_kind": self.clarify_pressure_kind,
315
             "pressure_pass_complete": self.pressure_pass_complete,
323
             "pressure_pass_complete": self.pressure_pass_complete,
316
             "missing_readiness_gates": list(self.missing_readiness_gates),
324
             "missing_readiness_gates": list(self.missing_readiness_gates),
325
+            "policy_stage": self.policy_stage,
326
+            "policy_outcome": self.policy_outcome,
317
             "prompt_format": self.prompt_format,
327
             "prompt_format": self.prompt_format,
318
             "prompt_sections": list(self.prompt_sections),
328
             "prompt_sections": list(self.prompt_sections),
319
             "artifact_paths": list(self.artifact_paths),
329
             "artifact_paths": list(self.artifact_paths),
@@ -339,6 +349,8 @@ class WorkflowTimelineEntry:
339
             clarify_pressure_kind=_optional_text(data.get("clarify_pressure_kind")),
349
             clarify_pressure_kind=_optional_text(data.get("clarify_pressure_kind")),
340
             pressure_pass_complete=bool(data.get("pressure_pass_complete", False)),
350
             pressure_pass_complete=bool(data.get("pressure_pass_complete", False)),
341
             missing_readiness_gates=_string_list(data.get("missing_readiness_gates")),
351
             missing_readiness_gates=_string_list(data.get("missing_readiness_gates")),
352
+            policy_stage=_optional_text(data.get("policy_stage")),
353
+            policy_outcome=_optional_text(data.get("policy_outcome")),
342
             prompt_format=_optional_text(data.get("prompt_format")),
354
             prompt_format=_optional_text(data.get("prompt_format")),
343
             prompt_sections=_string_list(data.get("prompt_sections")),
355
             prompt_sections=_string_list(data.get("prompt_sections")),
344
             artifact_paths=_string_list(data.get("artifact_paths")),
356
             artifact_paths=_string_list(data.get("artifact_paths")),
@@ -386,6 +398,48 @@ class WorkflowTimelineEntry:
386
             artifact_paths=list(artifact_paths or []),
398
             artifact_paths=list(artifact_paths or []),
387
         )
399
         )
388
 
400
 
401
+    @classmethod
402
+    def accountability(
403
+        cls,
404
+        *,
405
+        kind: WorkflowTimelineEntryKind,
406
+        mode: WorkflowMode | str,
407
+        reason_code: str,
408
+        summary: str,
409
+        policy_stage: str | None = None,
410
+        policy_outcome: str | None = None,
411
+        decision_kind: WorkflowDecisionKind | str | None = WorkflowDecisionKind.FORCED,
412
+        prompt_format: str | None = None,
413
+        prompt_sections: list[str] | None = None,
414
+        signal_summary: list[str] | None = None,
415
+        evidence_summary: list[str] | None = None,
416
+        artifact_paths: list[str] | None = None,
417
+    ) -> WorkflowTimelineEntry:
418
+        """Build one typed non-routing accountability entry."""
419
+
420
+        resolved_mode = mode.value if isinstance(mode, WorkflowMode) else str(mode)
421
+        if isinstance(decision_kind, WorkflowDecisionKind):
422
+            resolved_decision_kind = decision_kind.value
423
+        elif decision_kind is None:
424
+            resolved_decision_kind = None
425
+        else:
426
+            resolved_decision_kind = str(decision_kind)
427
+        return cls(
428
+            timestamp=_utc_now(),
429
+            kind=kind.value,
430
+            mode=resolved_mode,
431
+            reason_code=reason_code,
432
+            summary=summary,
433
+            decision_kind=resolved_decision_kind,
434
+            signal_summary=list(signal_summary or []),
435
+            evidence_summary=list(evidence_summary or []),
436
+            policy_stage=policy_stage,
437
+            policy_outcome=policy_outcome,
438
+            prompt_format=prompt_format,
439
+            prompt_sections=list(prompt_sections or []),
440
+            artifact_paths=list(artifact_paths or []),
441
+        )
442
+
389
 
443
 
390
 class WorkflowPolicy:
444
 class WorkflowPolicy:
391
     """Scored workflow-policy engine for route and clarify decisions."""
445
     """Scored workflow-policy engine for route and clarify decisions."""
tests/test_workflow_policy.pymodified
@@ -5,6 +5,7 @@ from __future__ import annotations
5
 from loader.runtime.clarify_strategy import ClarifySnapshot
5
 from loader.runtime.clarify_strategy import ClarifySnapshot
6
 from loader.runtime.workflow import (
6
 from loader.runtime.workflow import (
7
     ArtifactEvidenceKind,
7
     ArtifactEvidenceKind,
8
+    WorkflowDecisionKind,
8
     WorkflowMode,
9
     WorkflowMode,
9
     WorkflowPolicy,
10
     WorkflowPolicy,
10
     WorkflowTimelineEntry,
11
     WorkflowTimelineEntry,
@@ -147,3 +148,24 @@ def test_workflow_timeline_entry_round_trips() -> None:
147
     restored = WorkflowTimelineEntry.from_dict(entry.to_dict())
148
     restored = WorkflowTimelineEntry.from_dict(entry.to_dict())
148
 
149
 
149
     assert restored == entry
150
     assert restored == entry
151
+
152
+
153
+def test_workflow_accountability_entry_round_trips() -> None:
154
+    entry = WorkflowTimelineEntry.accountability(
155
+        kind=WorkflowTimelineEntryKind.COMPLETION_CONTINUE,
156
+        mode=WorkflowMode.EXECUTE,
157
+        reason_code="verification_failed_reentry",
158
+        summary="completion: verification failed; returning to execute for fixes",
159
+        policy_stage="definition_of_done",
160
+        policy_outcome="continue",
161
+        decision_kind=WorkflowDecisionKind.FORCED,
162
+        prompt_format="native",
163
+        prompt_sections=["Runtime Config", "Workflow Context"],
164
+        signal_summary=["stage=definition_of_done"],
165
+        evidence_summary=["verification contradiction: pytest still failed"],
166
+        artifact_paths=["/tmp/verification.md"],
167
+    )
168
+
169
+    restored = WorkflowTimelineEntry.from_dict(entry.to_dict())
170
+
171
+    assert restored == entry