forge-autonomy 2.3.1__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.
- forge/__init__.py +35 -0
- forge/__main__.py +20 -0
- forge/_version.py +38 -0
- forge/action_derivation_canary_closure.py +129 -0
- forge/action_derivation_evidence.py +101 -0
- forge/action_derivation_qualification.py +71 -0
- forge/action_derivation_reattempt.py +113 -0
- forge/ai_architect/__init__.py +5 -0
- forge/ai_architect/provider_registry.py +82 -0
- forge/architecture/__init__.py +8 -0
- forge/architecture/advisor.py +37 -0
- forge/architecture/presentation.py +21 -0
- forge/architecture/workspace.py +169 -0
- forge/autonomous_orchestrator.py +201 -0
- forge/business/__init__.py +9 -0
- forge/business/advisor.py +44 -0
- forge/business/ingress.py +186 -0
- forge/business/presentation.py +74 -0
- forge/business/workspace.py +141 -0
- forge/canary.py +134 -0
- forge/capabilities/__init__.py +11 -0
- forge/capabilities/delegation.py +150 -0
- forge/completion/__init__.py +5 -0
- forge/completion/assessor.py +53 -0
- forge/constitutional/__init__.py +5 -0
- forge/constitutional/assessor.py +38 -0
- forge/core/__init__.py +5 -0
- forge/core/json_store.py +24 -0
- forge/decision_evidence/__init__.py +5 -0
- forge/decision_evidence/repository.py +68 -0
- forge/dispatcher/__init__.py +13 -0
- forge/dispatcher/dispatcher.py +194 -0
- forge/execution/__init__.py +13 -0
- forge/execution/loop.py +346 -0
- forge/execution/presentation.py +14 -0
- forge/foundation/__init__.py +10 -0
- forge/foundation/loader.py +378 -0
- forge/generation_two.py +89 -0
- forge/generation_two_execution.py +179 -0
- forge/governance/__init__.py +26 -0
- forge/governance/execution_policy.py +83 -0
- forge/governance/profiles.py +93 -0
- forge/governance_authority.py +199 -0
- forge/intake.py +136 -0
- forge/integration/__init__.py +7 -0
- forge/integration/coordinator.py +100 -0
- forge/integration/repository.py +54 -0
- forge/knowledge/__init__.py +6 -0
- forge/knowledge/consumer.py +39 -0
- forge/knowledge/registry.py +65 -0
- forge/lifecycle/__init__.py +17 -0
- forge/lifecycle/store.py +445 -0
- forge/mission_amendment.py +35 -0
- forge/mission_scheduler.py +264 -0
- forge/models/__init__.py +561 -0
- forge/models/action.py +53 -0
- forge/models/action_derivation.py +284 -0
- forge/models/agent_policy.py +252 -0
- forge/models/ai_architect_provider.py +230 -0
- forge/models/ai_architect_session.py +147 -0
- forge/models/ai_provider_registry.py +140 -0
- forge/models/architecture_mission.py +113 -0
- forge/models/architecture_reasoning.py +257 -0
- forge/models/architecture_review.py +232 -0
- forge/models/codex_runtime_prompt.py +227 -0
- forge/models/constitutional_validation.py +109 -0
- forge/models/decision_evidence.py +202 -0
- forge/models/execution_host.py +289 -0
- forge/models/foundation.py +192 -0
- forge/models/historical_intent.py +212 -0
- forge/models/integration.py +173 -0
- forge/models/intent.py +265 -0
- forge/models/intent_authoring.py +119 -0
- forge/models/mission.py +301 -0
- forge/models/mission_candidate.py +108 -0
- forge/models/mission_planner.py +219 -0
- forge/models/mission_recommendation.py +222 -0
- forge/models/phase_completion.py +162 -0
- forge/models/planning.py +162 -0
- forge/models/producer.py +164 -0
- forge/models/prompt_artifact.py +234 -0
- forge/models/proposal.py +148 -0
- forge/models/runtime_prompt.py +245 -0
- forge/models/solution_template.py +64 -0
- forge/operator_identity.py +135 -0
- forge/planner/__init__.py +28 -0
- forge/planner/action_derivation.py +265 -0
- forge/planner/engine.py +49 -0
- forge/planner/openai_responses.py +660 -0
- forge/planner/provider_adapter.py +81 -0
- forge/planning/__init__.py +6 -0
- forge/planning/loader.py +232 -0
- forge/planning/registry.py +38 -0
- forge/portfolio_intelligence.py +110 -0
- forge/programme_authorization.py +194 -0
- forge/prompts/__init__.py +6 -0
- forge/prompts/codex_cli.py +121 -0
- forge/prompts/generator.py +106 -0
- forge/proposals/__init__.py +5 -0
- forge/proposals/generator.py +90 -0
- forge/provider_security.py +210 -0
- forge/qualification/__init__.py +13 -0
- forge/qualification/bootstrap_sequence.py +266 -0
- forge/qualification/generation_one.py +103 -0
- forge/qualification/installed_smoke.py +29 -0
- forge/recommendations/__init__.py +5 -0
- forge/recommendations/engine.py +113 -0
- forge/repository_truth.py +105 -0
- forge/review/__init__.py +5 -0
- forge/review/engine.py +80 -0
- forge/runtime/__init__.py +34 -0
- forge/runtime/bootstrap.py +360 -0
- forge/runtime/database.py +2191 -0
- forge/runtime/evidence.py +355 -0
- forge/runtime/execution_context.py +173 -0
- forge/runtime/runner.py +402 -0
- forge/runtime/service.py +127 -0
- forge/scheduler/__init__.py +7 -0
- forge/scheduler/adapter.py +245 -0
- forge/scheduler/ep_http_adapter.py +231 -0
- forge/scheduler/ep_v12.py +182 -0
- forge/scheduler/scheduler.py +180 -0
- forge/schemas/capability-registry-1.0.schema.json +23 -0
- forge/schemas/capability.schema.json +17 -0
- forge/schemas/common.schema.json +9 -0
- forge/schemas/decision-evidence-1.0.schema.json +20 -0
- forge/schemas/engineering-goal-0.5.schema.json +16 -0
- forge/schemas/engineering-increment-proposal-0.5.schema.json +20 -0
- forge/schemas/engineering-mode.schema.json +8 -0
- forge/schemas/engineering-plan-0.5.schema.json +29 -0
- forge/schemas/engineering-prompt-artifact-0.7.schema.json +26 -0
- forge/schemas/engineering-proposal-0.6.schema.json +75 -0
- forge/schemas/evidence-reference-0.5.schema.json +16 -0
- forge/schemas/foundation-document.schema.json +26 -0
- forge/schemas/governance-profile.schema.json +8 -0
- forge/schemas/knowledge-source-0.4.schema.json +23 -0
- forge/schemas/knowledge-source.schema.json +18 -0
- forge/schemas/mission-candidate-1.0.schema.json +27 -0
- forge/schemas/mission-candidate-1.1.schema.json +15 -0
- forge/schemas/phase-completion-1.0.schema.json +60 -0
- forge/schemas/planning-document-0.5.schema.json +17 -0
- forge/schemas/repository-catalog.schema.json +24 -0
- forge/schemas/repository.schema.json +18 -0
- forge/schemas/solution-template-1.0.schema.json +14 -0
- forge/schemas/workspace.schema.json +18 -0
- forge/solutions/__init__.py +6 -0
- forge/solutions/catalogue.py +54 -0
- forge/solutions/generator.py +64 -0
- forge/state/__init__.py +19 -0
- forge/state/mission_state.py +429 -0
- forge/workspace/__init__.py +1 -0
- forge_autonomy-2.3.1.dist-info/METADATA +474 -0
- forge_autonomy-2.3.1.dist-info/RECORD +156 -0
- forge_autonomy-2.3.1.dist-info/WHEEL +5 -0
- forge_autonomy-2.3.1.dist-info/entry_points.txt +2 -0
- forge_autonomy-2.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Deterministic capability assessment and delegated-work records.
|
|
2
|
+
|
|
3
|
+
The registry describes available execution capabilities. It never invokes a
|
|
4
|
+
provider: Forge retains the Mission and records only the bounded Engineering
|
|
5
|
+
Action it asks another provider to complete.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Mapping
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CAPABILITY_REGISTRY_SCHEMA_VERSION = "1.0"
|
|
16
|
+
DELEGATION_SCHEMA_VERSION = "1.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CapabilityAvailability(str, Enum):
|
|
20
|
+
AVAILABLE = "available"
|
|
21
|
+
UNAVAILABLE = "unavailable"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CapabilityOwner(str, Enum):
|
|
25
|
+
FORGE = "internal_forge"
|
|
26
|
+
HUMAN = "human"
|
|
27
|
+
EXTERNAL_FORGE_RUNNER = "external_forge_runner"
|
|
28
|
+
EXTERNAL_AI_AGENT = "external_ai_agent"
|
|
29
|
+
EXTERNAL_APPLICATION = "external_application"
|
|
30
|
+
PROFESSIONAL_SERVICE = "professional_service"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CapabilityExecutionMode(str, Enum):
|
|
34
|
+
INTERNAL = "internal"
|
|
35
|
+
DELEGATED = "delegated"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DelegationApprovalState(str, Enum):
|
|
39
|
+
NOT_REQUIRED = "not_required"
|
|
40
|
+
PENDING = "pending"
|
|
41
|
+
APPROVED = "approved"
|
|
42
|
+
REJECTED = "rejected"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DelegationResultState(str, Enum):
|
|
46
|
+
PENDING = "pending"
|
|
47
|
+
RECEIVED = "received"
|
|
48
|
+
ACCEPTED = "accepted"
|
|
49
|
+
REJECTED = "rejected"
|
|
50
|
+
ADDITIONAL_WORK_REQUIRED = "additional_work_required"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class CapabilityRegistration:
|
|
55
|
+
id: str
|
|
56
|
+
name: str
|
|
57
|
+
owner: CapabilityOwner
|
|
58
|
+
availability: CapabilityAvailability
|
|
59
|
+
execution_mode: CapabilityExecutionMode
|
|
60
|
+
preferred_provider: CapabilityOwner
|
|
61
|
+
trust_level: str
|
|
62
|
+
approval_required: bool = False
|
|
63
|
+
schema_version: str = CAPABILITY_REGISTRY_SCHEMA_VERSION
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
if self.schema_version != CAPABILITY_REGISTRY_SCHEMA_VERSION or not all((self.id, self.name, self.trust_level)):
|
|
67
|
+
raise ValueError("capability registration requires supported version, identity, name, and trust level")
|
|
68
|
+
if self.execution_mode is CapabilityExecutionMode.INTERNAL and self.owner is not CapabilityOwner.FORGE:
|
|
69
|
+
raise ValueError("internal execution must be owned by Forge")
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict[str, object]:
|
|
72
|
+
document = asdict(self)
|
|
73
|
+
for name in ("owner", "availability", "execution_mode", "preferred_provider"):
|
|
74
|
+
document[name] = document[name].value # type: ignore[union-attr]
|
|
75
|
+
return document
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class CapabilityAssessment:
|
|
80
|
+
capability_id: str
|
|
81
|
+
available: bool
|
|
82
|
+
execution_mode: CapabilityExecutionMode
|
|
83
|
+
selected_provider: CapabilityOwner
|
|
84
|
+
alternatives_considered: tuple[CapabilityOwner, ...]
|
|
85
|
+
rationale: str
|
|
86
|
+
confidence: int
|
|
87
|
+
approval_required: bool
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict[str, object]:
|
|
90
|
+
return {"capability_id": self.capability_id, "available": self.available,
|
|
91
|
+
"execution_mode": self.execution_mode.value, "selected_provider": self.selected_provider.value,
|
|
92
|
+
"alternatives_considered": [item.value for item in self.alternatives_considered],
|
|
93
|
+
"rationale": self.rationale, "confidence": self.confidence,
|
|
94
|
+
"approval_required": self.approval_required}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class CapabilityRegistry:
|
|
98
|
+
"""Immutable, deterministically ordered capability configuration."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, registrations: tuple[CapabilityRegistration, ...]) -> None:
|
|
101
|
+
identifiers = [item.id for item in registrations]
|
|
102
|
+
if not registrations or len(identifiers) != len(set(identifiers)):
|
|
103
|
+
raise ValueError("capability registry requires uniquely identified registrations")
|
|
104
|
+
self._registrations = {item.id: item for item in registrations}
|
|
105
|
+
|
|
106
|
+
def assess(self, capability_id: str) -> CapabilityAssessment:
|
|
107
|
+
registration = self._registrations.get(capability_id)
|
|
108
|
+
if registration is None:
|
|
109
|
+
raise ValueError("required capability is not registered")
|
|
110
|
+
available = registration.availability is CapabilityAvailability.AVAILABLE and registration.execution_mode is CapabilityExecutionMode.INTERNAL
|
|
111
|
+
alternatives = tuple(sorted((owner for owner in CapabilityOwner if owner is not registration.preferred_provider), key=lambda item: item.value))
|
|
112
|
+
return CapabilityAssessment(
|
|
113
|
+
capability_id, available, registration.execution_mode,
|
|
114
|
+
CapabilityOwner.FORGE if available else registration.preferred_provider,
|
|
115
|
+
alternatives,
|
|
116
|
+
"internal capability available" if available else "required capability is unavailable for internal execution",
|
|
117
|
+
100 if available else 90, registration.approval_required,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class DelegationRequest:
|
|
123
|
+
id: str
|
|
124
|
+
mission_id: str
|
|
125
|
+
action_id: str
|
|
126
|
+
capability_id: str
|
|
127
|
+
provider: CapabilityOwner
|
|
128
|
+
reason: str
|
|
129
|
+
decision_evidence: tuple[str, ...]
|
|
130
|
+
requested_at: str
|
|
131
|
+
approval_state: DelegationApprovalState
|
|
132
|
+
alternatives_considered: tuple[CapabilityOwner, ...]
|
|
133
|
+
confidence: int
|
|
134
|
+
result_state: DelegationResultState = DelegationResultState.PENDING
|
|
135
|
+
verification: Mapping[str, object] | None = None
|
|
136
|
+
schema_version: str = DELEGATION_SCHEMA_VERSION
|
|
137
|
+
|
|
138
|
+
def __post_init__(self) -> None:
|
|
139
|
+
if self.schema_version != DELEGATION_SCHEMA_VERSION or not all((self.id, self.mission_id, self.action_id, self.capability_id, self.reason, self.requested_at)):
|
|
140
|
+
raise ValueError("delegation request requires complete identity, rationale, and timestamp")
|
|
141
|
+
if not 0 <= self.confidence <= 100:
|
|
142
|
+
raise ValueError("delegation confidence must be between 0 and 100")
|
|
143
|
+
|
|
144
|
+
def to_dict(self) -> dict[str, object]:
|
|
145
|
+
document = asdict(self)
|
|
146
|
+
document["provider"] = self.provider.value
|
|
147
|
+
document["approval_state"] = self.approval_state.value
|
|
148
|
+
document["result_state"] = self.result_state.value
|
|
149
|
+
document["alternatives_considered"] = [item.value for item in self.alternatives_considered]
|
|
150
|
+
return document
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Pure deterministic assessment of declared Phase Completion 1.0 evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from forge.models import (
|
|
8
|
+
AssessmentFinding,
|
|
9
|
+
AssessmentStatus,
|
|
10
|
+
CompletionEvidence,
|
|
11
|
+
CriterionOutcome,
|
|
12
|
+
EngineeringPhase,
|
|
13
|
+
PhaseAssessment,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PhaseCompletionAssessor:
|
|
18
|
+
"""Assess declared evidence only; never fetch, invent, or mutate evidence."""
|
|
19
|
+
|
|
20
|
+
def assess(self, phase: EngineeringPhase, evidence: Iterable[CompletionEvidence]) -> PhaseAssessment:
|
|
21
|
+
declared = tuple(evidence)
|
|
22
|
+
criterion_ids = {criterion.id for criterion in phase.criteria}
|
|
23
|
+
evidence_by_criterion: dict[str, list[CompletionEvidence]] = {}
|
|
24
|
+
findings: list[AssessmentFinding] = []
|
|
25
|
+
for item in declared:
|
|
26
|
+
if item.criterion_id not in criterion_ids:
|
|
27
|
+
findings.append(AssessmentFinding(item.criterion_id, "UNKNOWN_CRITERION", "Evidence references no declared phase criterion."))
|
|
28
|
+
continue
|
|
29
|
+
evidence_by_criterion.setdefault(item.criterion_id, []).append(item)
|
|
30
|
+
|
|
31
|
+
has_criterion_evidence = bool(evidence_by_criterion)
|
|
32
|
+
for criterion in phase.criteria:
|
|
33
|
+
if not criterion.required:
|
|
34
|
+
continue
|
|
35
|
+
criterion_evidence = evidence_by_criterion.get(criterion.id, [])
|
|
36
|
+
if not criterion_evidence:
|
|
37
|
+
findings.append(AssessmentFinding(criterion.id, "MISSING_EVIDENCE", "Required criterion has no declared evidence."))
|
|
38
|
+
elif any(item.outcome is CriterionOutcome.FAIL for item in criterion_evidence):
|
|
39
|
+
findings.append(AssessmentFinding(criterion.id, "FAILED_CRITERION", "Required criterion has declared failing evidence."))
|
|
40
|
+
elif not any(item.outcome is CriterionOutcome.PASS for item in criterion_evidence):
|
|
41
|
+
findings.append(AssessmentFinding(criterion.id, "UNRESOLVED_CRITERION", "Required criterion has no passing evidence."))
|
|
42
|
+
|
|
43
|
+
ordered_findings = tuple(sorted(findings))
|
|
44
|
+
required_unresolved = any(finding.code != "UNKNOWN_CRITERION" for finding in ordered_findings)
|
|
45
|
+
if not has_criterion_evidence:
|
|
46
|
+
status = AssessmentStatus.NOT_STARTED
|
|
47
|
+
elif required_unresolved or any(finding.code == "UNKNOWN_CRITERION" for finding in ordered_findings):
|
|
48
|
+
status = AssessmentStatus.IN_PROGRESS
|
|
49
|
+
elif phase.completion_declaration is None:
|
|
50
|
+
status = AssessmentStatus.READY
|
|
51
|
+
else:
|
|
52
|
+
status = AssessmentStatus.COMPLETE
|
|
53
|
+
return PhaseAssessment(phase.id, status, ordered_findings)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Pure deterministic assessment of declared Constitutional Validation input."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from forge.models import (
|
|
8
|
+
ConstitutionalAssessment,
|
|
9
|
+
ConstitutionalAssessmentStatus,
|
|
10
|
+
ConstitutionalFinding,
|
|
11
|
+
ConstitutionalFindingSeverity,
|
|
12
|
+
ConstitutionalRule,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConstitutionalAssessor:
|
|
17
|
+
"""Assess supplied architectural findings; never retrieve, enforce, or mutate."""
|
|
18
|
+
|
|
19
|
+
def assess(
|
|
20
|
+
self,
|
|
21
|
+
subject_id: str,
|
|
22
|
+
applicable_rules: Iterable[ConstitutionalRule],
|
|
23
|
+
findings: Iterable[ConstitutionalFinding],
|
|
24
|
+
) -> ConstitutionalAssessment:
|
|
25
|
+
"""Derive an assessment with fixed severity precedence and stable ordering."""
|
|
26
|
+
rules = tuple(sorted(applicable_rules))
|
|
27
|
+
ordered_findings = tuple(sorted(findings))
|
|
28
|
+
if not rules:
|
|
29
|
+
if ordered_findings:
|
|
30
|
+
raise ValueError("constitutional findings require at least one applicable rule")
|
|
31
|
+
status = ConstitutionalAssessmentStatus.NOT_APPLICABLE
|
|
32
|
+
elif any(finding.severity is ConstitutionalFindingSeverity.VIOLATION for finding in ordered_findings):
|
|
33
|
+
status = ConstitutionalAssessmentStatus.VIOLATION
|
|
34
|
+
elif ordered_findings:
|
|
35
|
+
status = ConstitutionalAssessmentStatus.WARNING
|
|
36
|
+
else:
|
|
37
|
+
status = ConstitutionalAssessmentStatus.PASS
|
|
38
|
+
return ConstitutionalAssessment(subject_id, rules, status, ordered_findings)
|
forge/core/__init__.py
ADDED
forge/core/json_store.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Deterministic, human-readable local JSON persistence for foundation data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JsonStore:
|
|
11
|
+
"""Persist a single document locally without database or network dependencies."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, path: Path) -> None:
|
|
14
|
+
self.path = path
|
|
15
|
+
|
|
16
|
+
def save(self, document: Mapping[str, Any]) -> None:
|
|
17
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
self.path.write_text(
|
|
19
|
+
json.dumps(document, indent=2, sort_keys=True, ensure_ascii=False) + "\n",
|
|
20
|
+
encoding="utf-8",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def load(self) -> dict[str, Any]:
|
|
24
|
+
return json.loads(self.path.read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Append-only local Repository Truth store for immutable Decision Evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from forge.models.decision_evidence import DecisionEvidence, DecisionReference
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DecisionEvidenceRepositoryError(ValueError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DecisionEvidenceRepository:
|
|
18
|
+
"""Persists one immutable document per decision and rejects unresolved references."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, path: Path, *, reference_resolver: Callable[[DecisionReference], bool]) -> None:
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
self._connection = sqlite3.connect(path)
|
|
23
|
+
self._connection.row_factory = sqlite3.Row
|
|
24
|
+
self._reference_resolver = reference_resolver
|
|
25
|
+
self._connection.executescript("""
|
|
26
|
+
CREATE TABLE IF NOT EXISTS decision_evidence (
|
|
27
|
+
decision_id TEXT PRIMARY KEY, content_digest TEXT NOT NULL UNIQUE, document TEXT NOT NULL
|
|
28
|
+
);
|
|
29
|
+
CREATE TRIGGER IF NOT EXISTS decision_evidence_no_update BEFORE UPDATE ON decision_evidence
|
|
30
|
+
BEGIN SELECT RAISE(ABORT, 'decision evidence is immutable'); END;
|
|
31
|
+
CREATE TRIGGER IF NOT EXISTS decision_evidence_no_delete BEFORE DELETE ON decision_evidence
|
|
32
|
+
BEGIN SELECT RAISE(ABORT, 'decision evidence is immutable'); END;
|
|
33
|
+
""")
|
|
34
|
+
self._connection.commit()
|
|
35
|
+
|
|
36
|
+
def close(self) -> None:
|
|
37
|
+
self._connection.close()
|
|
38
|
+
|
|
39
|
+
def __enter__(self) -> "DecisionEvidenceRepository":
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(self, *_: object) -> None:
|
|
43
|
+
self.close()
|
|
44
|
+
|
|
45
|
+
def append(self, evidence: DecisionEvidence) -> DecisionEvidence:
|
|
46
|
+
references = (evidence.repository_context, evidence.mission_context, evidence.repository_maturity_reference,
|
|
47
|
+
*evidence.evidence_references, *evidence.execution_evidence_references,
|
|
48
|
+
evidence.confidence.repository_truth, evidence.confidence.architecture_review,
|
|
49
|
+
evidence.confidence.execution_evidence, evidence.confidence.mission_state)
|
|
50
|
+
if not all(self._reference_resolver(reference) for reference in references):
|
|
51
|
+
raise DecisionEvidenceRepositoryError("all Decision Evidence references must resolve to Repository Truth")
|
|
52
|
+
try:
|
|
53
|
+
with self._connection:
|
|
54
|
+
self._connection.execute("INSERT INTO decision_evidence VALUES (?, ?, ?)",
|
|
55
|
+
(evidence.id, evidence.content_digest, json.dumps(evidence.to_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=False)))
|
|
56
|
+
except sqlite3.IntegrityError as error:
|
|
57
|
+
raise DecisionEvidenceRepositoryError("decision evidence id or immutable content already exists") from error
|
|
58
|
+
return evidence
|
|
59
|
+
|
|
60
|
+
def get(self, decision_id: str) -> DecisionEvidence:
|
|
61
|
+
row = self._connection.execute("SELECT document FROM decision_evidence WHERE decision_id = ?", (decision_id,)).fetchone()
|
|
62
|
+
if row is None:
|
|
63
|
+
raise DecisionEvidenceRepositoryError(f"unknown decision evidence: {decision_id}")
|
|
64
|
+
return DecisionEvidence.from_dict(json.loads(row["document"]))
|
|
65
|
+
|
|
66
|
+
def list(self) -> tuple[DecisionEvidence, ...]:
|
|
67
|
+
rows = self._connection.execute("SELECT document FROM decision_evidence ORDER BY decision_id").fetchall()
|
|
68
|
+
return tuple(DecisionEvidence.from_dict(json.loads(row["document"])) for row in rows)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Approved Mission Queue and deterministic Mission Dispatcher."""
|
|
2
|
+
|
|
3
|
+
from .dispatcher import (
|
|
4
|
+
BOOTSTRAP_MISSION_SEQUENCE,
|
|
5
|
+
ApprovedMissionQueue,
|
|
6
|
+
DispatcherStatus,
|
|
7
|
+
MissionDispatcher,
|
|
8
|
+
MissionDispatcherError,
|
|
9
|
+
MissionDispatcherStore,
|
|
10
|
+
MissionDispatchRecord,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = ["BOOTSTRAP_MISSION_SEQUENCE", "ApprovedMissionQueue", "DispatcherStatus", "MissionDispatcher", "MissionDispatcherError", "MissionDispatcherStore", "MissionDispatchRecord"]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Persistent, FIFO dispatch of Architecture-approved Missions only.
|
|
2
|
+
|
|
3
|
+
This boundary activates Mission work. It does not create Missions, plan
|
|
4
|
+
Intents or Actions, render prompts, invoke an Execution Host, or execute.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
import sqlite3
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Callable, Protocol
|
|
14
|
+
|
|
15
|
+
from forge.architecture import ArchitectureWorkspace
|
|
16
|
+
from forge.intake import MissionIntake
|
|
17
|
+
from forge.models.architecture_mission import ArchitectureMission
|
|
18
|
+
from forge.state import MissionExecutionStatus, MissionStateStore
|
|
19
|
+
|
|
20
|
+
# Historical portfolio identifier set retained for the bootstrap regression
|
|
21
|
+
# harness only. It does not affect operational queue ordering.
|
|
22
|
+
BOOTSTRAP_MISSION_SEQUENCE = ("MISSION-0001", "MISSION-0002", "MISSION-0003", "MISSION-0004", "MISSION-0005")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DispatcherStatus(str, Enum):
|
|
26
|
+
PENDING = "PENDING"
|
|
27
|
+
ACTIVE = "ACTIVE"
|
|
28
|
+
COMPLETED = "COMPLETED"
|
|
29
|
+
BLOCKED = "BLOCKED"
|
|
30
|
+
FAILED = "FAILED"
|
|
31
|
+
ARCHIVED = "ARCHIVED"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MissionDispatcherError(ValueError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class MissionDispatchRecord:
|
|
40
|
+
mission_id: str
|
|
41
|
+
status: DispatcherStatus
|
|
42
|
+
sequence: int
|
|
43
|
+
occurred_at: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CompletionHook(Protocol):
|
|
47
|
+
def __call__(self, mission_id: str) -> None: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ApprovedMissionQueue:
|
|
51
|
+
"""A read-only, governance-filtered queue; recommendations never enter it."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, workspace: ArchitectureWorkspace) -> None:
|
|
54
|
+
self._workspace = workspace
|
|
55
|
+
|
|
56
|
+
def missions(self) -> tuple[ArchitectureMission, ...]:
|
|
57
|
+
return self._workspace.approved_for_engineering()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MissionDispatcherStore:
|
|
61
|
+
"""Local durable dispatcher lifecycle with a database-enforced one-active invariant."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, path: Path) -> None:
|
|
64
|
+
self._connection = sqlite3.connect(path)
|
|
65
|
+
self._connection.row_factory = sqlite3.Row
|
|
66
|
+
self._connection.executescript("""
|
|
67
|
+
CREATE TABLE IF NOT EXISTS mission_dispatch (
|
|
68
|
+
mission_id TEXT PRIMARY KEY, status TEXT NOT NULL, sequence INTEGER NOT NULL, occurred_at TEXT NOT NULL
|
|
69
|
+
);
|
|
70
|
+
CREATE UNIQUE INDEX IF NOT EXISTS one_active_mission ON mission_dispatch(status) WHERE status = 'ACTIVE';
|
|
71
|
+
""")
|
|
72
|
+
self._connection.commit()
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
self._connection.close()
|
|
76
|
+
|
|
77
|
+
def active(self) -> MissionDispatchRecord | None:
|
|
78
|
+
row = self._connection.execute("SELECT * FROM mission_dispatch WHERE status = 'ACTIVE'").fetchone()
|
|
79
|
+
return None if row is None else self._record(row)
|
|
80
|
+
|
|
81
|
+
def get(self, mission_id: str) -> MissionDispatchRecord | None:
|
|
82
|
+
row = self._connection.execute("SELECT * FROM mission_dispatch WHERE mission_id = ?", (mission_id,)).fetchone()
|
|
83
|
+
return None if row is None else self._record(row)
|
|
84
|
+
|
|
85
|
+
def records(self) -> tuple[MissionDispatchRecord, ...]:
|
|
86
|
+
return tuple(self._record(row) for row in self._connection.execute("SELECT * FROM mission_dispatch ORDER BY sequence, mission_id"))
|
|
87
|
+
|
|
88
|
+
def activate(self, mission_id: str, *, occurred_at: str) -> MissionDispatchRecord:
|
|
89
|
+
if self.active() is not None:
|
|
90
|
+
raise MissionDispatcherError("only one Mission may be active")
|
|
91
|
+
existing = self.get(mission_id)
|
|
92
|
+
if existing and existing.status not in {DispatcherStatus.PENDING, DispatcherStatus.BLOCKED, DispatcherStatus.FAILED}:
|
|
93
|
+
raise MissionDispatcherError("Mission dispatch lifecycle cannot be activated")
|
|
94
|
+
sequence = existing.sequence if existing else len(self.records()) + 1
|
|
95
|
+
try:
|
|
96
|
+
with self._connection:
|
|
97
|
+
if existing:
|
|
98
|
+
self._connection.execute("UPDATE mission_dispatch SET status = ?, occurred_at = ? WHERE mission_id = ?", (DispatcherStatus.ACTIVE.value, occurred_at, mission_id))
|
|
99
|
+
else:
|
|
100
|
+
self._connection.execute("INSERT INTO mission_dispatch VALUES (?, ?, ?, ?)", (mission_id, DispatcherStatus.ACTIVE.value, sequence, occurred_at))
|
|
101
|
+
except sqlite3.IntegrityError as error:
|
|
102
|
+
raise MissionDispatcherError("only one Mission may be active") from error
|
|
103
|
+
return self.get(mission_id) # type: ignore[return-value]
|
|
104
|
+
|
|
105
|
+
def pending(self, mission_id: str, *, occurred_at: str) -> MissionDispatchRecord:
|
|
106
|
+
if self.get(mission_id) is not None:
|
|
107
|
+
raise MissionDispatcherError("Mission already has a dispatch record")
|
|
108
|
+
with self._connection:
|
|
109
|
+
self._connection.execute(
|
|
110
|
+
"INSERT INTO mission_dispatch VALUES (?, ?, ?, ?)",
|
|
111
|
+
(mission_id, DispatcherStatus.PENDING.value, len(self.records()) + 1, occurred_at),
|
|
112
|
+
)
|
|
113
|
+
return self.get(mission_id) # type: ignore[return-value]
|
|
114
|
+
|
|
115
|
+
def transition(self, mission_id: str, status: DispatcherStatus, *, occurred_at: str) -> MissionDispatchRecord:
|
|
116
|
+
current = self.get(mission_id)
|
|
117
|
+
if current is None or current.status is not DispatcherStatus.ACTIVE:
|
|
118
|
+
raise MissionDispatcherError("only the active Mission may change terminal dispatch state")
|
|
119
|
+
if status not in {DispatcherStatus.COMPLETED, DispatcherStatus.BLOCKED, DispatcherStatus.FAILED, DispatcherStatus.ARCHIVED}:
|
|
120
|
+
raise MissionDispatcherError("dispatch transition must be terminal or explicitly blocked")
|
|
121
|
+
with self._connection:
|
|
122
|
+
self._connection.execute("UPDATE mission_dispatch SET status = ?, occurred_at = ? WHERE mission_id = ?", (status.value, occurred_at, mission_id))
|
|
123
|
+
return self.get(mission_id) # type: ignore[return-value]
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _record(row: sqlite3.Row) -> MissionDispatchRecord:
|
|
127
|
+
return MissionDispatchRecord(row["mission_id"], DispatcherStatus(row["status"]), row["sequence"], row["occurred_at"])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class MissionDispatcher:
|
|
131
|
+
"""Activate one approved Mission, then wait for its independently verified completion."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, queue: ApprovedMissionQueue, intake: MissionIntake, states: MissionStateStore, store: MissionDispatcherStore, *, clock: Callable[[], str], architecture_review: CompletionHook | None = None, recommendations: CompletionHook | None = None) -> None:
|
|
134
|
+
self._queue, self._intake, self._states, self._store = queue, intake, states, store
|
|
135
|
+
self._clock, self._architecture_review, self._recommendations = clock, architecture_review, recommendations
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def is_idle(self) -> bool:
|
|
139
|
+
return self._store.active() is None and not any(item.id not in {record.mission_id for record in self._store.records()} for item in self._queue.missions())
|
|
140
|
+
|
|
141
|
+
def dispatch(self) -> MissionDispatchRecord | None:
|
|
142
|
+
active = self.resume()
|
|
143
|
+
if active:
|
|
144
|
+
return active
|
|
145
|
+
if any(item.status in {DispatcherStatus.BLOCKED, DispatcherStatus.FAILED} for item in self._store.records()):
|
|
146
|
+
return None
|
|
147
|
+
completed = {item.mission_id for item in self._store.records() if item.status in {DispatcherStatus.COMPLETED, DispatcherStatus.ARCHIVED}}
|
|
148
|
+
mission = next((item for item in self._queue.missions() if item.id not in completed), None)
|
|
149
|
+
if mission is None:
|
|
150
|
+
return None
|
|
151
|
+
if self._store.get(mission.id) is None:
|
|
152
|
+
self._store.pending(mission.id, occurred_at=self._clock())
|
|
153
|
+
self._intake.admit_approved_mission(mission)
|
|
154
|
+
return self._store.activate(mission.id, occurred_at=self._clock())
|
|
155
|
+
|
|
156
|
+
def resume(self) -> MissionDispatchRecord | None:
|
|
157
|
+
active = self._store.active()
|
|
158
|
+
if active is None:
|
|
159
|
+
return None
|
|
160
|
+
state = self._states.get(active.mission_id)
|
|
161
|
+
if state.status in {MissionExecutionStatus.COMPLETED, MissionExecutionStatus.ARCHIVED}:
|
|
162
|
+
raise MissionDispatcherError("active dispatcher record has terminal Mission State")
|
|
163
|
+
return active
|
|
164
|
+
|
|
165
|
+
def complete(self, mission_id: str) -> MissionDispatchRecord | None:
|
|
166
|
+
state = self._states.get(mission_id)
|
|
167
|
+
if state.status is not MissionExecutionStatus.COMPLETED:
|
|
168
|
+
raise MissionDispatcherError("Mission completion requires verified completed Mission State")
|
|
169
|
+
evidence = getattr(state, "execution_evidence", None)
|
|
170
|
+
if evidence is not None and (not evidence.get("receipt_id") or evidence.get("outcome") != "complete"):
|
|
171
|
+
raise MissionDispatcherError("Mission completion requires host-issued complete execution evidence")
|
|
172
|
+
self._store.transition(mission_id, DispatcherStatus.COMPLETED, occurred_at=self._clock())
|
|
173
|
+
if self._architecture_review:
|
|
174
|
+
self._architecture_review(mission_id)
|
|
175
|
+
if self._recommendations:
|
|
176
|
+
self._recommendations(mission_id)
|
|
177
|
+
return self.dispatch()
|
|
178
|
+
|
|
179
|
+
def hold(self, mission_id: str, status: MissionExecutionStatus) -> MissionDispatchRecord:
|
|
180
|
+
"""Reflect a loop-owned deterministic pause without advancing the queue."""
|
|
181
|
+
if status not in {MissionExecutionStatus.BLOCKED, MissionExecutionStatus.FAILED}:
|
|
182
|
+
raise MissionDispatcherError("dispatcher hold requires blocked or failed Mission State")
|
|
183
|
+
state = self._states.get(mission_id)
|
|
184
|
+
if state.status is not status:
|
|
185
|
+
raise MissionDispatcherError("dispatcher hold requires matching durable Mission State")
|
|
186
|
+
dispatch_status = DispatcherStatus.BLOCKED if status is MissionExecutionStatus.BLOCKED else DispatcherStatus.FAILED
|
|
187
|
+
return self._store.transition(mission_id, dispatch_status, occurred_at=self._clock())
|
|
188
|
+
|
|
189
|
+
def recover(self, mission_id: str) -> MissionDispatchRecord:
|
|
190
|
+
"""Reactivate an explicitly recovered Mission; it does not plan or execute it."""
|
|
191
|
+
state = self._states.get(mission_id)
|
|
192
|
+
if state.status not in {MissionExecutionStatus.READY, MissionExecutionStatus.ACTIVE}:
|
|
193
|
+
raise MissionDispatcherError("dispatcher recovery requires a recovered Mission State")
|
|
194
|
+
return self._store.activate(mission_id, occurred_at=self._clock())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Autonomous, single-Mission execution orchestration."""
|
|
2
|
+
|
|
3
|
+
from .loop import (
|
|
4
|
+
ExecutionLoop,
|
|
5
|
+
ExecutionLoopError,
|
|
6
|
+
ExecutionLoopObservability,
|
|
7
|
+
RecoveryAuthorization,
|
|
8
|
+
)
|
|
9
|
+
from .presentation import render_execution_decision_evidence_reference
|
|
10
|
+
from forge.governance import ApprovalRecord, ExecutionPolicy, ExecutionPolicyKind, PauseBoundary
|
|
11
|
+
|
|
12
|
+
__all__ = ["ApprovalRecord", "ExecutionLoop", "ExecutionLoopError", "ExecutionLoopObservability",
|
|
13
|
+
"ExecutionPolicy", "ExecutionPolicyKind", "PauseBoundary", "RecoveryAuthorization", "render_execution_decision_evidence_reference"]
|