controlled-execution-system 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. ces/__init__.py +3 -0
  2. ces/brownfield/__init__.py +11 -0
  3. ces/brownfield/protocols.py +38 -0
  4. ces/brownfield/services/__init__.py +0 -0
  5. ces/brownfield/services/disposition_workflow.py +44 -0
  6. ces/brownfield/services/legacy_register.py +417 -0
  7. ces/cli/__init__.py +114 -0
  8. ces/cli/_async.py +44 -0
  9. ces/cli/_builder_flow.py +526 -0
  10. ces/cli/_builder_handoff.py +49 -0
  11. ces/cli/_builder_report.py +255 -0
  12. ces/cli/_context.py +81 -0
  13. ces/cli/_errors.py +96 -0
  14. ces/cli/_factory.py +293 -0
  15. ces/cli/_output.py +90 -0
  16. ces/cli/approve_cmd.py +502 -0
  17. ces/cli/audit_cmd.py +156 -0
  18. ces/cli/baseline_cmd.py +98 -0
  19. ces/cli/brownfield_cmd.py +440 -0
  20. ces/cli/calibrate_cmd.py +144 -0
  21. ces/cli/classify_cmd.py +121 -0
  22. ces/cli/doctor_cmd.py +181 -0
  23. ces/cli/dogfood_cmd.py +454 -0
  24. ces/cli/emergency_cmd.py +151 -0
  25. ces/cli/execute_cmd.py +213 -0
  26. ces/cli/gate_cmd.py +176 -0
  27. ces/cli/init_cmd.py +196 -0
  28. ces/cli/intake_cmd.py +121 -0
  29. ces/cli/manifest_cmd.py +198 -0
  30. ces/cli/report_cmd.py +79 -0
  31. ces/cli/review_cmd.py +409 -0
  32. ces/cli/run_cmd.py +1579 -0
  33. ces/cli/scan_cmd.py +204 -0
  34. ces/cli/setup_ci_cmd.py +86 -0
  35. ces/cli/spec_cmd.py +512 -0
  36. ces/cli/status_cmd.py +597 -0
  37. ces/cli/templates/__init__.py +1 -0
  38. ces/cli/templates/ci/__init__.py +1 -0
  39. ces/cli/templates/ci/github.yml +49 -0
  40. ces/cli/templates/ci/gitlab-ci.yml +37 -0
  41. ces/cli/templates/manifests/__init__.py +1 -0
  42. ces/cli/templates/manifests/python-library.yaml +38 -0
  43. ces/cli/templates/manifests/python-service.yaml +41 -0
  44. ces/cli/triage_cmd.py +158 -0
  45. ces/cli/vault_cmd.py +250 -0
  46. ces/control/__init__.py +1 -0
  47. ces/control/db/__init__.py +38 -0
  48. ces/control/db/base.py +77 -0
  49. ces/control/db/repository.py +497 -0
  50. ces/control/db/tables.py +365 -0
  51. ces/control/models/__init__.py +158 -0
  52. ces/control/models/architecture_blueprint.py +99 -0
  53. ces/control/models/audit_entry.py +70 -0
  54. ces/control/models/cascade_result.py +34 -0
  55. ces/control/models/debt_entry.py +51 -0
  56. ces/control/models/evidence_packet.py +136 -0
  57. ces/control/models/gate_evidence_packet.py +83 -0
  58. ces/control/models/gate_result.py +156 -0
  59. ces/control/models/intake.py +84 -0
  60. ces/control/models/interface_contract.py +36 -0
  61. ces/control/models/kill_switch_state.py +61 -0
  62. ces/control/models/knowledge_vault.py +55 -0
  63. ces/control/models/manifest.py +145 -0
  64. ces/control/models/merge_decision.py +44 -0
  65. ces/control/models/migration_control_pack.py +121 -0
  66. ces/control/models/oracle_result.py +36 -0
  67. ces/control/models/prl_item.py +49 -0
  68. ces/control/models/spec.py +62 -0
  69. ces/control/models/vision_anchor.py +51 -0
  70. ces/control/services/__init__.py +49 -0
  71. ces/control/services/audit_ledger.py +491 -0
  72. ces/control/services/cascade_invalidation.py +262 -0
  73. ces/control/services/classification.py +370 -0
  74. ces/control/services/classification_oracle.py +203 -0
  75. ces/control/services/gate_evaluator.py +242 -0
  76. ces/control/services/invalidation.py +131 -0
  77. ces/control/services/kill_switch.py +351 -0
  78. ces/control/services/manifest_manager.py +702 -0
  79. ces/control/services/merge_controller.py +296 -0
  80. ces/control/services/policy_engine.py +215 -0
  81. ces/control/services/workflow_engine.py +381 -0
  82. ces/control/spec/__init__.py +1 -0
  83. ces/control/spec/decomposer.py +125 -0
  84. ces/control/spec/parser.py +155 -0
  85. ces/control/spec/reconciler.py +32 -0
  86. ces/control/spec/template_loader.py +59 -0
  87. ces/control/spec/templates/__init__.py +1 -0
  88. ces/control/spec/templates/default.md +44 -0
  89. ces/control/spec/templates/default.yaml +26 -0
  90. ces/control/spec/tree.py +78 -0
  91. ces/control/spec/validator.py +57 -0
  92. ces/emergency/__init__.py +13 -0
  93. ces/emergency/protocols.py +39 -0
  94. ces/emergency/services/__init__.py +0 -0
  95. ces/emergency/services/emergency_service.py +214 -0
  96. ces/emergency/services/manifest_factory.py +104 -0
  97. ces/emergency/services/sla_timer.py +69 -0
  98. ces/execution/__init__.py +101 -0
  99. ces/execution/_subprocess_env.py +85 -0
  100. ces/execution/agent_runner.py +238 -0
  101. ces/execution/output_capture.py +110 -0
  102. ces/execution/providers/__init__.py +29 -0
  103. ces/execution/providers/bootstrap.py +114 -0
  104. ces/execution/providers/cli_provider.py +225 -0
  105. ces/execution/providers/demo_provider.py +193 -0
  106. ces/execution/providers/multi_model.py +108 -0
  107. ces/execution/providers/protocol.py +164 -0
  108. ces/execution/providers/registry.py +92 -0
  109. ces/execution/runtimes/__init__.py +13 -0
  110. ces/execution/runtimes/adapters.py +274 -0
  111. ces/execution/runtimes/protocol.py +64 -0
  112. ces/execution/runtimes/registry.py +62 -0
  113. ces/execution/sandbox.py +190 -0
  114. ces/harness/__init__.py +1 -0
  115. ces/harness/models/__init__.py +53 -0
  116. ces/harness/models/disclosure_set.py +33 -0
  117. ces/harness/models/guide_pack.py +73 -0
  118. ces/harness/models/harness_profile.py +78 -0
  119. ces/harness/models/hidden_check.py +53 -0
  120. ces/harness/models/observed_legacy.py +72 -0
  121. ces/harness/models/review_assignment.py +50 -0
  122. ces/harness/models/review_finding.py +81 -0
  123. ces/harness/models/self_correction_state.py +60 -0
  124. ces/harness/models/sensor_result.py +82 -0
  125. ces/harness/models/triage_result.py +96 -0
  126. ces/harness/prompts/__init__.py +1 -0
  127. ces/harness/prompts/review_prompts.py +142 -0
  128. ces/harness/protocols.py +95 -0
  129. ces/harness/sensors/__init__.py +53 -0
  130. ces/harness/sensors/_file_reader.py +53 -0
  131. ces/harness/sensors/accessibility.py +55 -0
  132. ces/harness/sensors/base.py +112 -0
  133. ces/harness/sensors/dependency.py +169 -0
  134. ces/harness/sensors/infrastructure.py +115 -0
  135. ces/harness/sensors/migration.py +154 -0
  136. ces/harness/sensors/performance.py +167 -0
  137. ces/harness/sensors/resilience.py +123 -0
  138. ces/harness/sensors/security.py +147 -0
  139. ces/harness/sensors/test_coverage.py +129 -0
  140. ces/harness/services/__init__.py +44 -0
  141. ces/harness/services/diff_extractor.py +326 -0
  142. ces/harness/services/evidence_synthesizer.py +606 -0
  143. ces/harness/services/findings_aggregator.py +178 -0
  144. ces/harness/services/guide_pack_builder.py +296 -0
  145. ces/harness/services/hidden_check_engine.py +225 -0
  146. ces/harness/services/review_executor.py +311 -0
  147. ces/harness/services/review_router.py +557 -0
  148. ces/harness/services/self_correction_manager.py +272 -0
  149. ces/harness/services/sensor_orchestrator.py +234 -0
  150. ces/harness/services/spec_authoring.py +209 -0
  151. ces/harness/services/spec_importer.py +66 -0
  152. ces/harness/services/trust_manager.py +340 -0
  153. ces/intake/__init__.py +15 -0
  154. ces/intake/protocols.py +71 -0
  155. ces/intake/questions/phase_questions.yaml +123 -0
  156. ces/intake/services/__init__.py +0 -0
  157. ces/intake/services/assumption_registry.py +254 -0
  158. ces/intake/services/interview_engine.py +386 -0
  159. ces/knowledge/__init__.py +16 -0
  160. ces/knowledge/protocols.py +44 -0
  161. ces/knowledge/services/__init__.py +0 -0
  162. ces/knowledge/services/note_ranker.py +101 -0
  163. ces/knowledge/services/trust_decay.py +129 -0
  164. ces/knowledge/services/vault_query_filter.py +92 -0
  165. ces/knowledge/services/vault_service.py +416 -0
  166. ces/local_store.py +1456 -0
  167. ces/observability/__init__.py +6 -0
  168. ces/observability/conventions.py +57 -0
  169. ces/observability/counters.py +85 -0
  170. ces/observability/metrics_bridge.py +220 -0
  171. ces/observability/otel.py +131 -0
  172. ces/observability/services/__init__.py +0 -0
  173. ces/observability/services/collector.py +116 -0
  174. ces/shared/__init__.py +1 -0
  175. ces/shared/base.py +49 -0
  176. ces/shared/config.py +38 -0
  177. ces/shared/crypto.py +279 -0
  178. ces/shared/enums.py +457 -0
  179. ces/shared/logging.py +76 -0
  180. controlled_execution_system-0.1.2.dist-info/METADATA +479 -0
  181. controlled_execution_system-0.1.2.dist-info/RECORD +184 -0
  182. controlled_execution_system-0.1.2.dist-info/WHEEL +4 -0
  183. controlled_execution_system-0.1.2.dist-info/entry_points.txt +2 -0
  184. controlled_execution_system-0.1.2.dist-info/licenses/LICENSE +21 -0
ces/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Controlled Execution System - Deterministic governance for AI agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,11 @@
1
+ """Brownfield legacy behavior register (BROWN-01 to BROWN-03)."""
2
+
3
+ from ces.brownfield.protocols import LegacyRegisterProtocol
4
+ from ces.brownfield.services.disposition_workflow import DispositionWorkflow
5
+ from ces.brownfield.services.legacy_register import LegacyBehaviorService
6
+
7
+ __all__ = [
8
+ "DispositionWorkflow",
9
+ "LegacyBehaviorService",
10
+ "LegacyRegisterProtocol",
11
+ ]
@@ -0,0 +1,38 @@
1
+ """Protocols for brownfield subsystem dependency injection.
2
+
3
+ Defines the LegacyRegisterProtocol that governed services depend on
4
+ for brownfield behavior management. This decouples consumers from the
5
+ concrete LegacyBehaviorService implementation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional, Protocol, runtime_checkable
11
+
12
+ from ces.harness.models.observed_legacy import ObservedLegacyBehavior
13
+
14
+
15
+ @runtime_checkable
16
+ class LegacyRegisterProtocol(Protocol):
17
+ """Protocol for legacy behavior register dependency injection.
18
+
19
+ Any service needing to interact with the brownfield legacy behavior
20
+ register should depend on this protocol rather than the concrete
21
+ LegacyBehaviorService.
22
+ """
23
+
24
+ async def register_behavior(
25
+ self,
26
+ *,
27
+ system: str,
28
+ behavior_description: str,
29
+ inferred_by: str,
30
+ confidence: float,
31
+ source_manifest_id: Optional[str] = None,
32
+ ) -> ObservedLegacyBehavior:
33
+ """Register a newly observed legacy behavior."""
34
+ ...
35
+
36
+ async def get_pending_behaviors(self) -> list[ObservedLegacyBehavior]:
37
+ """Get all behaviors pending human disposition."""
38
+ ...
File without changes
@@ -0,0 +1,44 @@
1
+ """Disposition workflow state machine for legacy behaviors (BROWN-03).
2
+
3
+ Three-state workflow enforcing human review before PRL promotion:
4
+ pending -> reviewed -> promoted_to_prl
5
+ pending -> reviewed -> discarded
6
+
7
+ Invalid transitions raise TransitionNotAllowed (python-statemachine v3).
8
+ Reconstructable from any state via start_value parameter (D-11 pattern).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from statemachine import State, StateMachine
14
+
15
+
16
+ class DispositionWorkflow(StateMachine):
17
+ """Three-state disposition workflow for legacy behaviors (BROWN-03).
18
+
19
+ Enforces the invariant that legacy behaviors must be reviewed by a human
20
+ before they can be promoted to the PRL or discarded. This prevents
21
+ agents from auto-promoting inferred behaviors (T-05-15).
22
+
23
+ States:
24
+ pending: Initial state. Behavior has been observed but not reviewed.
25
+ reviewed: Human has reviewed and set a disposition.
26
+ promoted_to_prl: Final state. Behavior has been copied to PRL.
27
+ discarded: Final state. Behavior has been rejected.
28
+
29
+ Transitions:
30
+ review: pending -> reviewed
31
+ promote: reviewed -> promoted_to_prl
32
+ discard: reviewed -> discarded
33
+ """
34
+
35
+ # States
36
+ pending = State(initial=True)
37
+ reviewed = State()
38
+ promoted_to_prl = State(final=True)
39
+ discarded = State(final=True)
40
+
41
+ # Transitions
42
+ review = pending.to(reviewed)
43
+ promote = reviewed.to(promoted_to_prl)
44
+ discard = reviewed.to(discarded)
@@ -0,0 +1,417 @@
1
+ """Legacy behavior register service (BROWN-01, BROWN-02, BROWN-03).
2
+
3
+ Manages the Observed Legacy Behavior Register for brownfield projects.
4
+ Key invariants:
5
+ - BROWN-01: Stores inferred behaviors with confidence scores
6
+ - BROWN-02: register_behavior() NEVER creates a PRLItem directly.
7
+ Legacy behaviors go to the register, NOT the PRL, until human disposition.
8
+ - BROWN-03: Copy-on-promote creates a NEW PRLItem with back-reference
9
+ to the register entry. The original entry is PRESERVED (not deleted).
10
+
11
+ Threat mitigations:
12
+ - T-05-15: promote_to_prl requires approver string; DispositionWorkflow
13
+ state machine prevents skipping the review step.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import uuid
19
+ from datetime import datetime, timezone
20
+ from typing import TYPE_CHECKING, Optional
21
+
22
+ from ces.brownfield.services.disposition_workflow import DispositionWorkflow
23
+ from ces.control.models.prl_item import AcceptanceCriterion, PRLItem
24
+ from ces.harness.models.observed_legacy import ObservedLegacyBehavior
25
+ from ces.shared.enums import (
26
+ ActorType,
27
+ ArtifactStatus,
28
+ EventType,
29
+ LegacyDisposition,
30
+ Priority,
31
+ PRLItemType,
32
+ VerificationMethod,
33
+ )
34
+
35
+ if TYPE_CHECKING:
36
+ from ces.control.db.repository import LegacyBehaviorRepository
37
+ from ces.control.db.tables import LegacyBehaviorRow
38
+
39
+
40
+ class LegacyBehaviorService:
41
+ """Service managing the Observed Legacy Behavior Register.
42
+
43
+ Implements BROWN-01 (register), BROWN-02 (separate from PRL),
44
+ and BROWN-03 (disposition workflow with copy-on-promote).
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ repository: Optional[LegacyBehaviorRepository] = None,
50
+ audit_ledger: Optional[object] = None,
51
+ ) -> None:
52
+ """Initialize with optional repository and audit ledger.
53
+
54
+ Args:
55
+ repository: LegacyBehaviorRepository for DB persistence.
56
+ audit_ledger: Any object with append_event method for auditing.
57
+ """
58
+ self._repository = repository
59
+ self._audit_ledger = audit_ledger
60
+
61
+ async def register_behavior(
62
+ self,
63
+ *,
64
+ system: str,
65
+ behavior_description: str,
66
+ inferred_by: str,
67
+ confidence: float,
68
+ source_manifest_id: Optional[str] = None,
69
+ ) -> ObservedLegacyBehavior:
70
+ """Register a newly observed legacy behavior.
71
+
72
+ BROWN-01: Creates an ObservedLegacyBehavior entry in the register.
73
+ BROWN-02: This method NEVER creates a PRLItem. Legacy behaviors go
74
+ to the register, NOT the PRL, until human disposition.
75
+
76
+ Args:
77
+ system: The legacy system where behavior was observed.
78
+ behavior_description: Description of the observed behavior.
79
+ inferred_by: ID of the agent that inferred the behavior.
80
+ confidence: Confidence score (0.0 to 1.0).
81
+ source_manifest_id: Optional manifest ID that triggered discovery.
82
+
83
+ Returns:
84
+ The created ObservedLegacyBehavior domain model.
85
+ """
86
+ entry_id = f"OLB-{uuid.uuid4().hex[:12]}"
87
+ now = datetime.now(timezone.utc)
88
+
89
+ behavior = ObservedLegacyBehavior(
90
+ entry_id=entry_id,
91
+ system=system,
92
+ behavior_description=behavior_description,
93
+ inferred_by=inferred_by,
94
+ inferred_at=now,
95
+ confidence=confidence,
96
+ )
97
+
98
+ # Persist to DB if repository available
99
+ if self._repository is not None:
100
+ from ces.control.db.tables import LegacyBehaviorRow
101
+
102
+ row = LegacyBehaviorRow(
103
+ entry_id=behavior.entry_id,
104
+ system=behavior.system,
105
+ behavior_description=behavior.behavior_description,
106
+ inferred_by=behavior.inferred_by,
107
+ inferred_at=behavior.inferred_at,
108
+ confidence=behavior.confidence,
109
+ source_manifest_id=source_manifest_id,
110
+ )
111
+ await self._repository.save(row)
112
+
113
+ # Log to audit ledger
114
+ if self._audit_ledger is not None:
115
+ await self._audit_ledger.append_event( # type: ignore[attr-defined]
116
+ event_type=EventType.TRUTH_CHANGE,
117
+ actor=inferred_by,
118
+ actor_type=ActorType.AGENT,
119
+ action_summary=(
120
+ f"Registered legacy behavior {entry_id} from system '{system}': {behavior_description}"
121
+ ),
122
+ )
123
+
124
+ return behavior
125
+
126
+ async def get_pending_behaviors(self) -> list[ObservedLegacyBehavior]:
127
+ """Get all behaviors pending human disposition.
128
+
129
+ Returns only entries with disposition=None and discarded=False.
130
+
131
+ Returns:
132
+ List of pending ObservedLegacyBehavior entries.
133
+ """
134
+ if self._repository is None:
135
+ return []
136
+
137
+ rows = await self._repository.get_pending()
138
+ return [self._row_to_behavior(row) for row in rows]
139
+
140
+ async def get_behaviors_by_system(self, system: str) -> list[ObservedLegacyBehavior]:
141
+ """Get all behaviors for a specific legacy system.
142
+
143
+ Args:
144
+ system: The legacy system name to filter by.
145
+
146
+ Returns:
147
+ List of ObservedLegacyBehavior entries for the system.
148
+ """
149
+ if self._repository is None:
150
+ return []
151
+
152
+ rows = await self._repository.get_by_system(system)
153
+ return [self._row_to_behavior(row) for row in rows]
154
+
155
+ async def review_behavior(
156
+ self,
157
+ entry_id: str,
158
+ disposition: LegacyDisposition,
159
+ reviewed_by: str,
160
+ ) -> ObservedLegacyBehavior:
161
+ """Review a pending behavior and set its disposition.
162
+
163
+ Validates that the entry is pending (disposition is None), then
164
+ transitions through the DispositionWorkflow state machine.
165
+
166
+ Args:
167
+ entry_id: The behavior entry to review.
168
+ disposition: The disposition decision (PRESERVE, CHANGE, RETIRE, etc.).
169
+ reviewed_by: Human reviewer identifier.
170
+
171
+ Returns:
172
+ The updated ObservedLegacyBehavior.
173
+
174
+ Raises:
175
+ ValueError: If entry not found or not in pending state.
176
+ """
177
+ if self._repository is None:
178
+ msg = "Repository required for review_behavior"
179
+ raise RuntimeError(msg)
180
+
181
+ row = await self._repository.get_by_id(entry_id)
182
+ if row is None:
183
+ msg = f"Legacy behavior entry not found: {entry_id}"
184
+ raise ValueError(msg)
185
+
186
+ if row.disposition is not None:
187
+ msg = f"Entry {entry_id} already has disposition: {row.disposition}"
188
+ raise ValueError(msg)
189
+
190
+ # Validate state transition via DispositionWorkflow
191
+ wf = DispositionWorkflow()
192
+ wf.review()
193
+
194
+ now = datetime.now(timezone.utc)
195
+ updated_row = await self._repository.update_disposition(
196
+ entry_id=entry_id,
197
+ disposition=disposition.value,
198
+ reviewed_by=reviewed_by,
199
+ reviewed_at=now,
200
+ )
201
+
202
+ if updated_row is None:
203
+ msg = f"Failed to update disposition for {entry_id}"
204
+ raise ValueError(msg)
205
+
206
+ # Log to audit ledger
207
+ if self._audit_ledger is not None:
208
+ await self._audit_ledger.append_event( # type: ignore[attr-defined]
209
+ event_type=EventType.TRUTH_CHANGE,
210
+ actor=reviewed_by,
211
+ actor_type=ActorType.HUMAN,
212
+ action_summary=(f"Reviewed legacy behavior {entry_id}: disposition={disposition.value}"),
213
+ )
214
+
215
+ return self._row_to_behavior(updated_row)
216
+
217
+ async def promote_to_prl(
218
+ self,
219
+ entry_id: str,
220
+ approver: str,
221
+ acceptance_criteria: list[dict] | None = None,
222
+ negative_examples: list[str] | None = None,
223
+ ) -> tuple[ObservedLegacyBehavior, PRLItem]:
224
+ """Promote a reviewed behavior to the PRL via copy-on-promote (BROWN-03).
225
+
226
+ Creates a NEW PRLItem from the behavior description. The original
227
+ register entry is PRESERVED (not deleted) with a back-reference to
228
+ the new PRL item. This is the copy-on-promote invariant.
229
+
230
+ Args:
231
+ entry_id: The behavior entry to promote.
232
+ approver: Human approver identifier (T-05-15 requirement).
233
+ acceptance_criteria: Optional custom acceptance criteria.
234
+ negative_examples: Optional negative examples.
235
+
236
+ Returns:
237
+ Tuple of (updated register entry, new PRLItem).
238
+
239
+ Raises:
240
+ ValueError: If entry not found, not reviewed, or already discarded.
241
+ """
242
+ if self._repository is None:
243
+ msg = "Repository required for promote_to_prl"
244
+ raise RuntimeError(msg)
245
+
246
+ row = await self._repository.get_by_id(entry_id)
247
+ if row is None:
248
+ msg = f"Legacy behavior entry not found: {entry_id}"
249
+ raise ValueError(msg)
250
+
251
+ if row.disposition is None:
252
+ msg = f"Entry {entry_id} must be reviewed before promotion"
253
+ raise ValueError(msg)
254
+
255
+ if row.discarded:
256
+ msg = f"Entry {entry_id} is discarded and cannot be promoted"
257
+ raise ValueError(msg)
258
+
259
+ if row.promoted_to_prl_id is not None:
260
+ msg = f"Entry {entry_id} already promoted to {row.promoted_to_prl_id}"
261
+ raise ValueError(msg)
262
+
263
+ # Validate state transition via DispositionWorkflow
264
+ wf = DispositionWorkflow(start_value="reviewed")
265
+ wf.promote()
266
+
267
+ # Copy-on-promote (BROWN-03): Create a NEW PRLItem
268
+ now = datetime.now(timezone.utc)
269
+ prl_id = f"PRL-{uuid.uuid4().hex[:12]}"
270
+
271
+ criteria = acceptance_criteria or [
272
+ {
273
+ "criterion": "Behavior preserved as observed",
274
+ "verification_method": VerificationMethod.MANUAL,
275
+ }
276
+ ]
277
+ ac_list = [
278
+ AcceptanceCriterion(
279
+ criterion=c["criterion"],
280
+ verification_method=(
281
+ c["verification_method"]
282
+ if isinstance(c["verification_method"], VerificationMethod)
283
+ else VerificationMethod(c["verification_method"])
284
+ ),
285
+ )
286
+ for c in criteria
287
+ ]
288
+
289
+ prl_item = PRLItem(
290
+ schema_type="prl_item",
291
+ prl_id=prl_id,
292
+ type=PRLItemType.CONSTRAINT,
293
+ statement=row.behavior_description,
294
+ acceptance_criteria=tuple(ac_list),
295
+ negative_examples=tuple(negative_examples) if negative_examples else (),
296
+ priority=Priority.MEDIUM,
297
+ release_slice="brownfield-import",
298
+ legacy_disposition=LegacyDisposition.PRESERVE,
299
+ legacy_source_system=row.system,
300
+ status=ArtifactStatus.DRAFT,
301
+ version=1,
302
+ owner=approver,
303
+ created_at=now,
304
+ last_confirmed=now,
305
+ )
306
+
307
+ # Set back-reference on register entry (copy-on-promote preserves original)
308
+ promoted_row = await self._repository.mark_promoted(entry_id, prl_id)
309
+ if promoted_row is None:
310
+ msg = f"Failed to mark {entry_id} as promoted"
311
+ raise ValueError(msg)
312
+
313
+ # Log promotion to audit ledger
314
+ if self._audit_ledger is not None:
315
+ await self._audit_ledger.append_event( # type: ignore[attr-defined]
316
+ event_type=EventType.TRUTH_CHANGE,
317
+ actor=approver,
318
+ actor_type=ActorType.HUMAN,
319
+ action_summary=(
320
+ f"Promoted legacy behavior {entry_id} to PRL item {prl_id} (copy-on-promote, BROWN-03)"
321
+ ),
322
+ )
323
+
324
+ updated_entry = self._row_to_behavior(promoted_row)
325
+ return updated_entry, prl_item
326
+
327
+ async def discard_behavior(
328
+ self,
329
+ entry_id: str,
330
+ reviewed_by: str,
331
+ reason: str,
332
+ ) -> ObservedLegacyBehavior:
333
+ """Discard a behavior entry.
334
+
335
+ Marks the entry as discarded. Cannot discard an already-promoted entry.
336
+
337
+ Args:
338
+ entry_id: The behavior entry to discard.
339
+ reviewed_by: Human reviewer performing the discard.
340
+ reason: Reason for discarding.
341
+
342
+ Returns:
343
+ The updated ObservedLegacyBehavior.
344
+
345
+ Raises:
346
+ ValueError: If entry not found or already promoted.
347
+ """
348
+ if self._repository is None:
349
+ msg = "Repository required for discard_behavior"
350
+ raise RuntimeError(msg)
351
+
352
+ row = await self._repository.get_by_id(entry_id)
353
+ if row is None:
354
+ msg = f"Legacy behavior entry not found: {entry_id}"
355
+ raise ValueError(msg)
356
+
357
+ if row.promoted_to_prl_id is not None:
358
+ msg = f"Entry {entry_id} is already promoted to {row.promoted_to_prl_id} and cannot be discarded"
359
+ raise ValueError(msg)
360
+
361
+ # Transition through DispositionWorkflow
362
+ wf = DispositionWorkflow()
363
+ wf.review()
364
+ wf.discard()
365
+
366
+ now = datetime.now(timezone.utc)
367
+
368
+ # Update disposition to RETIRE and mark as discarded
369
+ updated_row = await self._repository.update_disposition(
370
+ entry_id=entry_id,
371
+ disposition=LegacyDisposition.RETIRE.value,
372
+ reviewed_by=reviewed_by,
373
+ reviewed_at=now,
374
+ )
375
+
376
+ if updated_row is None:
377
+ msg = f"Failed to update disposition for {entry_id}"
378
+ raise ValueError(msg)
379
+
380
+ # Mark discarded by setting the discarded flag on the row directly
381
+ updated_row.discarded = True
382
+ await self._repository.save(updated_row)
383
+
384
+ # Log to audit ledger
385
+ if self._audit_ledger is not None:
386
+ await self._audit_ledger.append_event( # type: ignore[attr-defined]
387
+ event_type=EventType.TRUTH_CHANGE,
388
+ actor=reviewed_by,
389
+ actor_type=ActorType.HUMAN,
390
+ action_summary=(f"Discarded legacy behavior {entry_id}: {reason}"),
391
+ )
392
+
393
+ return self._row_to_behavior(updated_row)
394
+
395
+ @staticmethod
396
+ def _row_to_behavior(row: LegacyBehaviorRow) -> ObservedLegacyBehavior:
397
+ """Convert a DB row to an ObservedLegacyBehavior domain model.
398
+
399
+ Args:
400
+ row: A LegacyBehaviorRow instance from the database.
401
+
402
+ Returns:
403
+ ObservedLegacyBehavior domain model.
404
+ """
405
+ return ObservedLegacyBehavior(
406
+ entry_id=row.entry_id,
407
+ system=row.system,
408
+ behavior_description=row.behavior_description,
409
+ inferred_by=row.inferred_by,
410
+ inferred_at=row.inferred_at,
411
+ confidence=row.confidence,
412
+ disposition=(LegacyDisposition(row.disposition) if row.disposition else None),
413
+ reviewed_by=row.reviewed_by,
414
+ reviewed_at=row.reviewed_at,
415
+ promoted_to_prl_id=row.promoted_to_prl_id,
416
+ discarded=row.discarded,
417
+ )
ces/cli/__init__.py ADDED
@@ -0,0 +1,114 @@
1
+ """CES CLI entry point using Typer for the local builder-first workflow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from ces.cli import (
8
+ approve_cmd,
9
+ audit_cmd,
10
+ baseline_cmd,
11
+ brownfield_cmd,
12
+ calibrate_cmd,
13
+ classify_cmd,
14
+ doctor_cmd,
15
+ dogfood_cmd,
16
+ emergency_cmd,
17
+ execute_cmd,
18
+ gate_cmd,
19
+ init_cmd,
20
+ intake_cmd,
21
+ manifest_cmd,
22
+ report_cmd,
23
+ review_cmd,
24
+ run_cmd,
25
+ scan_cmd,
26
+ setup_ci_cmd,
27
+ spec_cmd,
28
+ status_cmd,
29
+ triage_cmd,
30
+ vault_cmd,
31
+ )
32
+ from ces.cli._output import set_json_mode
33
+
34
+ _ROOT_HELP = """Builder-first governed AI delivery for local repos.
35
+
36
+ Start Here:
37
+ `ces build` Describe the change and let CES guide the workflow
38
+ `ces continue` Resume the latest builder session
39
+ `ces explain` Summarize the latest request, blockers, and next step
40
+ `ces status` Show builder-first status; add `--expert` for the full expert view
41
+
42
+ Advanced Governance:
43
+ `ces manifest`, `classify`, `review`, `triage`, `approve`, `audit`, `gate`
44
+ """
45
+
46
+ app = typer.Typer(
47
+ name="ces",
48
+ help=_ROOT_HELP,
49
+ rich_markup_mode="rich",
50
+ )
51
+
52
+
53
+ @app.callback()
54
+ def main(
55
+ json_output: bool = typer.Option(
56
+ False,
57
+ "--json",
58
+ help="Output results as JSON instead of Rich tables.",
59
+ ),
60
+ ) -> None:
61
+ """Controlled Execution System - Deterministic governance for AI agents."""
62
+ set_json_mode(json_output)
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Core commands -- always available (no optional deps)
67
+ # ---------------------------------------------------------------------------
68
+
69
+ app.command(name="init", help="Optional manual setup before your first builder-first run.")(init_cmd.init_project)
70
+ app.command(name="manifest", help="Advanced governance: generate a task manifest from natural language.")(
71
+ manifest_cmd.create_manifest
72
+ )
73
+ app.command(
74
+ name="build",
75
+ help=(
76
+ "Describe what you want to build and let CES run the local workflow. "
77
+ "Tip: CES_DEMO_MODE=1 only affects optional LLM-backed steps; "
78
+ "`ces build` still needs Codex CLI or Claude Code."
79
+ ),
80
+ )(run_cmd.run_task)
81
+ app.command(name="continue", help="Resume the latest saved builder brief without re-entering context.")(
82
+ run_cmd.continue_task
83
+ )
84
+ app.command(name="explain", help="Explain the latest builder brief and current CES state in plain language.")(
85
+ run_cmd.explain_task
86
+ )
87
+ app.command(name="run", help="Legacy alias for the guided local-first build flow.")(run_cmd.run_task)
88
+
89
+ app.command(name="classify", help="Classify a task manifest.")(classify_cmd.classify_task)
90
+ app.command(name="execute", help="Execute an agent task locally within manifest boundaries.")(execute_cmd.execute_task)
91
+ app.command(name="review", help="Run review pipeline and display evidence summary.")(review_cmd.review_task)
92
+ app.command(name="triage", help="Pre-screen evidence with triage color.")(triage_cmd.triage_evidence)
93
+ app.command(name="approve", help="Approve or reject evidence.")(approve_cmd.approve_evidence)
94
+ app.command(name="gate", help="Evaluate a phase gate.")(gate_cmd.evaluate_gate)
95
+ app.command(name="intake", help="Run intake interview for a phase.")(intake_cmd.run_intake)
96
+ app.command(name="calibrate", help="Run hidden check calibration probes.")(calibrate_cmd.run_calibration)
97
+ app.add_typer(vault_cmd.vault_app, name="vault")
98
+ app.add_typer(spec_cmd.spec_app, name="spec")
99
+ app.command(name="status", help="Show builder-first project status. Use --expert for the full expert view.")(
100
+ status_cmd.show_status
101
+ )
102
+ app.command(name="dogfood", help="Use CES to review its own changes.")(dogfood_cmd.dogfood)
103
+ app.command(name="doctor", help="Run pre-flight checks (Python, providers, extras, project dir).")(
104
+ doctor_cmd.run_doctor
105
+ )
106
+ app.command(name="setup-ci", help="Generate a CI gating workflow for the chosen provider (github|gitlab).")(
107
+ setup_ci_cmd.setup_ci
108
+ )
109
+ app.command(name="scan", help="Inventory the repository: modules, generated code, CODEOWNERS.")(scan_cmd.scan)
110
+ app.command(name="baseline", help="Capture a day-0 sensor snapshot under .ces/baseline/.")(baseline_cmd.baseline)
111
+ app.command(name="audit", help="Inspect the local audit ledger.")(audit_cmd.query_audit)
112
+ app.add_typer(report_cmd.report_app, name="report")
113
+ app.add_typer(brownfield_cmd.brownfield_app, name="brownfield")
114
+ app.add_typer(emergency_cmd.emergency_app, name="emergency")
ces/cli/_async.py ADDED
@@ -0,0 +1,44 @@
1
+ """Async wrapper for Typer commands.
2
+
3
+ Typer commands are synchronous, but CES services are async.
4
+ This module provides a decorator that bridges the gap by calling
5
+ asyncio.run() inside a synchronous wrapper.
6
+
7
+ Exports:
8
+ run_async: Decorator that wraps an async function for Typer.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ from functools import wraps
15
+ from typing import Any, Callable, Coroutine, TypeVar
16
+
17
+ T = TypeVar("T")
18
+
19
+
20
+ def run_async(func: Callable[..., Coroutine[Any, Any, T]]) -> Callable[..., T]:
21
+ """Wrap an async function so it can be used as a Typer command.
22
+
23
+ Uses asyncio.run() to execute the coroutine synchronously.
24
+ Preserves the original function's name, docstring, and type hints
25
+ so Typer can introspect parameters for --help generation.
26
+
27
+ Args:
28
+ func: An async function to wrap.
29
+
30
+ Returns:
31
+ A synchronous wrapper that calls asyncio.run(func(...)).
32
+
33
+ Example::
34
+
35
+ @run_async
36
+ async def my_command(name: str) -> None:
37
+ await some_async_operation(name)
38
+ """
39
+
40
+ @wraps(func)
41
+ def wrapper(*args: Any, **kwargs: Any) -> T:
42
+ return asyncio.run(func(*args, **kwargs))
43
+
44
+ return wrapper