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.
- ces/__init__.py +3 -0
- ces/brownfield/__init__.py +11 -0
- ces/brownfield/protocols.py +38 -0
- ces/brownfield/services/__init__.py +0 -0
- ces/brownfield/services/disposition_workflow.py +44 -0
- ces/brownfield/services/legacy_register.py +417 -0
- ces/cli/__init__.py +114 -0
- ces/cli/_async.py +44 -0
- ces/cli/_builder_flow.py +526 -0
- ces/cli/_builder_handoff.py +49 -0
- ces/cli/_builder_report.py +255 -0
- ces/cli/_context.py +81 -0
- ces/cli/_errors.py +96 -0
- ces/cli/_factory.py +293 -0
- ces/cli/_output.py +90 -0
- ces/cli/approve_cmd.py +502 -0
- ces/cli/audit_cmd.py +156 -0
- ces/cli/baseline_cmd.py +98 -0
- ces/cli/brownfield_cmd.py +440 -0
- ces/cli/calibrate_cmd.py +144 -0
- ces/cli/classify_cmd.py +121 -0
- ces/cli/doctor_cmd.py +181 -0
- ces/cli/dogfood_cmd.py +454 -0
- ces/cli/emergency_cmd.py +151 -0
- ces/cli/execute_cmd.py +213 -0
- ces/cli/gate_cmd.py +176 -0
- ces/cli/init_cmd.py +196 -0
- ces/cli/intake_cmd.py +121 -0
- ces/cli/manifest_cmd.py +198 -0
- ces/cli/report_cmd.py +79 -0
- ces/cli/review_cmd.py +409 -0
- ces/cli/run_cmd.py +1579 -0
- ces/cli/scan_cmd.py +204 -0
- ces/cli/setup_ci_cmd.py +86 -0
- ces/cli/spec_cmd.py +512 -0
- ces/cli/status_cmd.py +597 -0
- ces/cli/templates/__init__.py +1 -0
- ces/cli/templates/ci/__init__.py +1 -0
- ces/cli/templates/ci/github.yml +49 -0
- ces/cli/templates/ci/gitlab-ci.yml +37 -0
- ces/cli/templates/manifests/__init__.py +1 -0
- ces/cli/templates/manifests/python-library.yaml +38 -0
- ces/cli/templates/manifests/python-service.yaml +41 -0
- ces/cli/triage_cmd.py +158 -0
- ces/cli/vault_cmd.py +250 -0
- ces/control/__init__.py +1 -0
- ces/control/db/__init__.py +38 -0
- ces/control/db/base.py +77 -0
- ces/control/db/repository.py +497 -0
- ces/control/db/tables.py +365 -0
- ces/control/models/__init__.py +158 -0
- ces/control/models/architecture_blueprint.py +99 -0
- ces/control/models/audit_entry.py +70 -0
- ces/control/models/cascade_result.py +34 -0
- ces/control/models/debt_entry.py +51 -0
- ces/control/models/evidence_packet.py +136 -0
- ces/control/models/gate_evidence_packet.py +83 -0
- ces/control/models/gate_result.py +156 -0
- ces/control/models/intake.py +84 -0
- ces/control/models/interface_contract.py +36 -0
- ces/control/models/kill_switch_state.py +61 -0
- ces/control/models/knowledge_vault.py +55 -0
- ces/control/models/manifest.py +145 -0
- ces/control/models/merge_decision.py +44 -0
- ces/control/models/migration_control_pack.py +121 -0
- ces/control/models/oracle_result.py +36 -0
- ces/control/models/prl_item.py +49 -0
- ces/control/models/spec.py +62 -0
- ces/control/models/vision_anchor.py +51 -0
- ces/control/services/__init__.py +49 -0
- ces/control/services/audit_ledger.py +491 -0
- ces/control/services/cascade_invalidation.py +262 -0
- ces/control/services/classification.py +370 -0
- ces/control/services/classification_oracle.py +203 -0
- ces/control/services/gate_evaluator.py +242 -0
- ces/control/services/invalidation.py +131 -0
- ces/control/services/kill_switch.py +351 -0
- ces/control/services/manifest_manager.py +702 -0
- ces/control/services/merge_controller.py +296 -0
- ces/control/services/policy_engine.py +215 -0
- ces/control/services/workflow_engine.py +381 -0
- ces/control/spec/__init__.py +1 -0
- ces/control/spec/decomposer.py +125 -0
- ces/control/spec/parser.py +155 -0
- ces/control/spec/reconciler.py +32 -0
- ces/control/spec/template_loader.py +59 -0
- ces/control/spec/templates/__init__.py +1 -0
- ces/control/spec/templates/default.md +44 -0
- ces/control/spec/templates/default.yaml +26 -0
- ces/control/spec/tree.py +78 -0
- ces/control/spec/validator.py +57 -0
- ces/emergency/__init__.py +13 -0
- ces/emergency/protocols.py +39 -0
- ces/emergency/services/__init__.py +0 -0
- ces/emergency/services/emergency_service.py +214 -0
- ces/emergency/services/manifest_factory.py +104 -0
- ces/emergency/services/sla_timer.py +69 -0
- ces/execution/__init__.py +101 -0
- ces/execution/_subprocess_env.py +85 -0
- ces/execution/agent_runner.py +238 -0
- ces/execution/output_capture.py +110 -0
- ces/execution/providers/__init__.py +29 -0
- ces/execution/providers/bootstrap.py +114 -0
- ces/execution/providers/cli_provider.py +225 -0
- ces/execution/providers/demo_provider.py +193 -0
- ces/execution/providers/multi_model.py +108 -0
- ces/execution/providers/protocol.py +164 -0
- ces/execution/providers/registry.py +92 -0
- ces/execution/runtimes/__init__.py +13 -0
- ces/execution/runtimes/adapters.py +274 -0
- ces/execution/runtimes/protocol.py +64 -0
- ces/execution/runtimes/registry.py +62 -0
- ces/execution/sandbox.py +190 -0
- ces/harness/__init__.py +1 -0
- ces/harness/models/__init__.py +53 -0
- ces/harness/models/disclosure_set.py +33 -0
- ces/harness/models/guide_pack.py +73 -0
- ces/harness/models/harness_profile.py +78 -0
- ces/harness/models/hidden_check.py +53 -0
- ces/harness/models/observed_legacy.py +72 -0
- ces/harness/models/review_assignment.py +50 -0
- ces/harness/models/review_finding.py +81 -0
- ces/harness/models/self_correction_state.py +60 -0
- ces/harness/models/sensor_result.py +82 -0
- ces/harness/models/triage_result.py +96 -0
- ces/harness/prompts/__init__.py +1 -0
- ces/harness/prompts/review_prompts.py +142 -0
- ces/harness/protocols.py +95 -0
- ces/harness/sensors/__init__.py +53 -0
- ces/harness/sensors/_file_reader.py +53 -0
- ces/harness/sensors/accessibility.py +55 -0
- ces/harness/sensors/base.py +112 -0
- ces/harness/sensors/dependency.py +169 -0
- ces/harness/sensors/infrastructure.py +115 -0
- ces/harness/sensors/migration.py +154 -0
- ces/harness/sensors/performance.py +167 -0
- ces/harness/sensors/resilience.py +123 -0
- ces/harness/sensors/security.py +147 -0
- ces/harness/sensors/test_coverage.py +129 -0
- ces/harness/services/__init__.py +44 -0
- ces/harness/services/diff_extractor.py +326 -0
- ces/harness/services/evidence_synthesizer.py +606 -0
- ces/harness/services/findings_aggregator.py +178 -0
- ces/harness/services/guide_pack_builder.py +296 -0
- ces/harness/services/hidden_check_engine.py +225 -0
- ces/harness/services/review_executor.py +311 -0
- ces/harness/services/review_router.py +557 -0
- ces/harness/services/self_correction_manager.py +272 -0
- ces/harness/services/sensor_orchestrator.py +234 -0
- ces/harness/services/spec_authoring.py +209 -0
- ces/harness/services/spec_importer.py +66 -0
- ces/harness/services/trust_manager.py +340 -0
- ces/intake/__init__.py +15 -0
- ces/intake/protocols.py +71 -0
- ces/intake/questions/phase_questions.yaml +123 -0
- ces/intake/services/__init__.py +0 -0
- ces/intake/services/assumption_registry.py +254 -0
- ces/intake/services/interview_engine.py +386 -0
- ces/knowledge/__init__.py +16 -0
- ces/knowledge/protocols.py +44 -0
- ces/knowledge/services/__init__.py +0 -0
- ces/knowledge/services/note_ranker.py +101 -0
- ces/knowledge/services/trust_decay.py +129 -0
- ces/knowledge/services/vault_query_filter.py +92 -0
- ces/knowledge/services/vault_service.py +416 -0
- ces/local_store.py +1456 -0
- ces/observability/__init__.py +6 -0
- ces/observability/conventions.py +57 -0
- ces/observability/counters.py +85 -0
- ces/observability/metrics_bridge.py +220 -0
- ces/observability/otel.py +131 -0
- ces/observability/services/__init__.py +0 -0
- ces/observability/services/collector.py +116 -0
- ces/shared/__init__.py +1 -0
- ces/shared/base.py +49 -0
- ces/shared/config.py +38 -0
- ces/shared/crypto.py +279 -0
- ces/shared/enums.py +457 -0
- ces/shared/logging.py +76 -0
- controlled_execution_system-0.1.2.dist-info/METADATA +479 -0
- controlled_execution_system-0.1.2.dist-info/RECORD +184 -0
- controlled_execution_system-0.1.2.dist-info/WHEEL +4 -0
- controlled_execution_system-0.1.2.dist-info/entry_points.txt +2 -0
- controlled_execution_system-0.1.2.dist-info/licenses/LICENSE +21 -0
ces/__init__.py
ADDED
|
@@ -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
|