arkaos 2.13.0 → 2.15.0

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 (51) hide show
  1. package/VERSION +1 -1
  2. package/arka/skills/forge/SKILL.md +649 -0
  3. package/config/constitution.yaml +8 -0
  4. package/config/hooks/post-tool-use.sh +43 -0
  5. package/config/hooks/session-start.sh +24 -0
  6. package/config/hooks/user-prompt-submit.sh +25 -1
  7. package/core/forge/__init__.py +104 -0
  8. package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
  9. package/core/forge/__pycache__/complexity.cpython-313.pyc +0 -0
  10. package/core/forge/__pycache__/handoff.cpython-313.pyc +0 -0
  11. package/core/forge/__pycache__/persistence.cpython-313.pyc +0 -0
  12. package/core/forge/__pycache__/renderer.cpython-313.pyc +0 -0
  13. package/core/forge/__pycache__/schema.cpython-313.pyc +0 -0
  14. package/core/forge/complexity.py +125 -0
  15. package/core/forge/handoff.py +100 -0
  16. package/core/forge/persistence.py +308 -0
  17. package/core/forge/renderer.py +261 -0
  18. package/core/forge/schema.py +213 -0
  19. package/core/synapse/__init__.py +2 -2
  20. package/core/synapse/__pycache__/__init__.cpython-313.pyc +0 -0
  21. package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
  22. package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
  23. package/core/synapse/engine.py +4 -2
  24. package/core/synapse/layers.py +49 -0
  25. package/core/sync/__init__.py +25 -0
  26. package/core/sync/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/core/sync/__pycache__/descriptor_syncer.cpython-313.pyc +0 -0
  28. package/core/sync/__pycache__/discovery.cpython-313.pyc +0 -0
  29. package/core/sync/__pycache__/engine.cpython-313.pyc +0 -0
  30. package/core/sync/__pycache__/manifest.cpython-313.pyc +0 -0
  31. package/core/sync/__pycache__/mcp_syncer.cpython-313.pyc +0 -0
  32. package/core/sync/__pycache__/reporter.cpython-313.pyc +0 -0
  33. package/core/sync/__pycache__/schema.cpython-313.pyc +0 -0
  34. package/core/sync/__pycache__/settings_syncer.cpython-313.pyc +0 -0
  35. package/core/sync/descriptor_syncer.py +166 -0
  36. package/core/sync/discovery.py +256 -0
  37. package/core/sync/engine.py +177 -0
  38. package/core/sync/features/forge.yaml +16 -0
  39. package/core/sync/features/quality-gate.yaml +15 -0
  40. package/core/sync/features/spec-gate.yaml +15 -0
  41. package/core/sync/features/workflow-tiers.yaml +19 -0
  42. package/core/sync/manifest.py +87 -0
  43. package/core/sync/mcp_syncer.py +255 -0
  44. package/core/sync/reporter.py +178 -0
  45. package/core/sync/schema.py +94 -0
  46. package/core/sync/settings_syncer.py +121 -0
  47. package/core/workflow/state_reader.sh +25 -1
  48. package/departments/ops/skills/update/SKILL.md +69 -0
  49. package/installer/update.js +14 -0
  50. package/package.json +1 -1
  51. package/pyproject.toml +1 -1
@@ -0,0 +1,213 @@
1
+ """Forge schema — Pydantic models and enums for the ArkaOS Intelligent Planning Engine.
2
+
3
+ The Forge analyses incoming requests, scores complexity across five dimensions,
4
+ selects the appropriate execution tier (shallow / standard / deep), and emits a
5
+ structured ForgePlan that downstream agents consume.
6
+ """
7
+
8
+ from enum import Enum
9
+ from typing import List, Optional
10
+
11
+ from pydantic import BaseModel, Field, field_validator
12
+
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Enums
16
+ # ---------------------------------------------------------------------------
17
+
18
+ class ForgeTier(str, Enum):
19
+ """Execution tier determined by complexity score."""
20
+ SHALLOW = "shallow"
21
+ STANDARD = "standard"
22
+ DEEP = "deep"
23
+
24
+
25
+ class ForgeStatus(str, Enum):
26
+ """Lifecycle status of a ForgePlan."""
27
+ DRAFT = "draft"
28
+ REVIEWING = "reviewing"
29
+ APPROVED = "approved"
30
+ EXECUTING = "executing"
31
+ COMPLETED = "completed"
32
+ REJECTED = "rejected"
33
+ CANCELLED = "cancelled"
34
+ ARCHIVED = "archived"
35
+
36
+
37
+ class ExplorerLens(str, Enum):
38
+ """The analytical perspective used when exploring a plan."""
39
+ PRAGMATIC = "pragmatic" # Focus on fastest viable path
40
+ ARCHITECTURAL = "architectural" # Focus on long-term design health
41
+ CONTRARIAN = "contrarian" # Challenge assumptions, surface risks
42
+
43
+
44
+ class RiskSeverity(str, Enum):
45
+ """Severity level for identified risks."""
46
+ LOW = "low"
47
+ MEDIUM = "medium"
48
+ HIGH = "high"
49
+
50
+
51
+ class ExecutionPathType(str, Enum):
52
+ """Type of execution artefact that fulfils a plan step."""
53
+ SKILL = "skill"
54
+ WORKFLOW = "workflow"
55
+ ENTERPRISE_WORKFLOW = "enterprise_workflow"
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Models
60
+ # ---------------------------------------------------------------------------
61
+
62
+ class ComplexityDimensions(BaseModel):
63
+ """Five-axis complexity breakdown, each scored 0–100."""
64
+
65
+ scope: int = Field(default=0, description="Breadth of change across the codebase or system.")
66
+ dependencies: int = Field(default=0, description="Number and criticality of upstream/downstream dependencies.")
67
+ ambiguity: int = Field(default=0, description="How unclear or under-specified the requirements are.")
68
+ risk: int = Field(default=0, description="Potential for breakage, data loss, or security impact.")
69
+ novelty: int = Field(default=0, description="How unlike existing patterns this work is.")
70
+
71
+ @field_validator("scope", "dependencies", "ambiguity", "risk", "novelty", mode="before")
72
+ @classmethod
73
+ def clamp_to_range(cls, v: int) -> int:
74
+ """Clamp dimension value to [0, 100]."""
75
+ return max(0, min(100, int(v)))
76
+
77
+
78
+ class ComplexityScore(BaseModel):
79
+ """Aggregated complexity result produced by the Complexity Scorer."""
80
+
81
+ score: int = Field(default=0, description="Composite 0–100 score derived from all dimensions.")
82
+ tier: ForgeTier = Field(default=ForgeTier.SHALLOW, description="Execution tier selected based on the composite score.")
83
+ dimensions: ComplexityDimensions = Field(default_factory=ComplexityDimensions, description="Per-dimension breakdown.")
84
+ similar_plans: List[str] = Field(
85
+ default_factory=list,
86
+ description="IDs of previously completed plans with similar profiles.",
87
+ )
88
+ reused_patterns: List[str] = Field(
89
+ default_factory=list,
90
+ description="Named patterns from the ArkaOS pattern library reused in this plan.",
91
+ )
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Approach & Critic Models
96
+ # ---------------------------------------------------------------------------
97
+
98
+ class KeyDecision(BaseModel):
99
+ """A single decision made during exploration."""
100
+ decision: str
101
+ rationale: str = ""
102
+
103
+
104
+ class PhaseDeliverable(BaseModel):
105
+ """A deliverable within a plan phase."""
106
+ name: str
107
+ deliverables: list[str] = Field(default_factory=list)
108
+ effort: str = "medium"
109
+
110
+
111
+ class ExplorerApproach(BaseModel):
112
+ """Output from a single explorer agent."""
113
+ explorer: ExplorerLens
114
+ summary: str = ""
115
+ key_decisions: list[KeyDecision] = Field(default_factory=list)
116
+ phases: list[PhaseDeliverable] = Field(default_factory=list)
117
+ estimated_total_effort: str = "medium"
118
+ risks: list[str] = Field(default_factory=list)
119
+ reuses_patterns: list[str] = Field(default_factory=list)
120
+
121
+
122
+ class RejectedElement(BaseModel):
123
+ """An element rejected by the critic with reason."""
124
+ element: str
125
+ reason: str
126
+
127
+
128
+ class IdentifiedRisk(BaseModel):
129
+ """A risk identified by the critic with mitigation."""
130
+ risk: str
131
+ mitigation: str = ""
132
+ severity: RiskSeverity = RiskSeverity.LOW
133
+
134
+
135
+ class CriticVerdict(BaseModel):
136
+ """Synthesis output from the Plan Critic."""
137
+ synthesis: dict[str, list[str]] = Field(default_factory=dict)
138
+ rejected_elements: list[RejectedElement] = Field(default_factory=list)
139
+ risks: list[IdentifiedRisk] = Field(default_factory=list)
140
+ confidence: float = 0.0
141
+ estimated_phases: int = 0
142
+ estimated_departments: list[str] = Field(default_factory=list)
143
+
144
+ def is_valid(self) -> bool:
145
+ """Check critic rules: must reject >= 1 and identify >= 1 risk."""
146
+ return len(self.rejected_elements) >= 1 and len(self.risks) >= 1
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # ForgePlan and Context Models
151
+ # ---------------------------------------------------------------------------
152
+
153
+ class ForgeContext(BaseModel):
154
+ """Snapshot of repo state when forge was initiated."""
155
+ repo: str
156
+ branch: str
157
+ commit_at_forge: str
158
+ arkaos_version: str
159
+ prompt: str
160
+ context_refreshed: bool = False
161
+
162
+
163
+ class PlanPhase(BaseModel):
164
+ """A single phase in the forge plan."""
165
+ name: str
166
+ department: str
167
+ agents: list[str] = Field(default_factory=list)
168
+ deliverables: list[str] = Field(default_factory=list)
169
+ acceptance_criteria: list[str] = Field(default_factory=list)
170
+ depends_on: list[str] = Field(default_factory=list)
171
+ context_from_forge: dict[str, list[str]] = Field(default_factory=dict)
172
+
173
+
174
+ class ExecutionPath(BaseModel):
175
+ """How the plan will be executed after approval."""
176
+ type: ExecutionPathType = ExecutionPathType.SKILL
177
+ target: str = ""
178
+ departments: list[str] = Field(default_factory=list)
179
+ estimated_commits: int = 0
180
+
181
+
182
+ class ForgeGovernance(BaseModel):
183
+ """Governance metadata for a forge plan."""
184
+ constitution_check: str = "pending"
185
+ violations: list[str] = Field(default_factory=list)
186
+ quality_gate_required: bool = True
187
+ branch_strategy: str = ""
188
+
189
+
190
+ class ForgePlan(BaseModel):
191
+ """Complete forge plan — the primary artifact of The Forge."""
192
+ id: str
193
+ name: str
194
+ created_at: str = ""
195
+ forged_by: str = ""
196
+ version: int = 1
197
+
198
+ context: ForgeContext
199
+ complexity: ComplexityScore = Field(default_factory=ComplexityScore)
200
+ approaches: list[ExplorerApproach] = Field(default_factory=list)
201
+ critic: CriticVerdict = Field(default_factory=CriticVerdict)
202
+
203
+ plan_phases: list[PlanPhase] = Field(default_factory=list)
204
+ goal: str = ""
205
+ execution_path: ExecutionPath = Field(default_factory=ExecutionPath)
206
+
207
+ governance: ForgeGovernance = Field(default_factory=ForgeGovernance)
208
+
209
+ status: ForgeStatus = ForgeStatus.DRAFT
210
+ approved_at: Optional[str] = None
211
+ approved_by: Optional[str] = None
212
+ executed_at: Optional[str] = None
213
+ completion_notes: Optional[str] = None
@@ -5,7 +5,7 @@ and 65% context reduction through intelligent filtering.
5
5
  """
6
6
 
7
7
  from core.synapse.engine import SynapseEngine
8
- from core.synapse.layers import Layer, LayerResult
8
+ from core.synapse.layers import Layer, LayerResult, ForgeContextLayer
9
9
  from core.synapse.cache import LayerCache
10
10
 
11
- __all__ = ["SynapseEngine", "Layer", "LayerResult", "LayerCache"]
11
+ __all__ = ["SynapseEngine", "Layer", "LayerResult", "LayerCache", "ForgeContextLayer"]
@@ -28,7 +28,7 @@ class SynapseResult:
28
28
 
29
29
 
30
30
  class SynapseEngine:
31
- """8-layer context injection engine.
31
+ """9-layer context injection engine.
32
32
 
33
33
  Computes all registered layers, caches results per TTL,
34
34
  filters empty results, and combines into a compact context string.
@@ -155,7 +155,7 @@ def create_default_engine(
155
155
  agents_registry: dict[str, dict] | None = None,
156
156
  vector_store: Any = None,
157
157
  ) -> SynapseEngine:
158
- """Create a SynapseEngine with all 8 default layers.
158
+ """Create a SynapseEngine with all 9 default layers.
159
159
 
160
160
  Args:
161
161
  constitution_compressed: Compressed Constitution string for L0.
@@ -169,6 +169,7 @@ def create_default_engine(
169
169
  ConstitutionLayer, DepartmentLayer, AgentLayer,
170
170
  ProjectLayer, BranchLayer, CommandHintsLayer,
171
171
  QualityGateLayer, TimeLayer, KnowledgeRetrievalLayer,
172
+ ForgeContextLayer,
172
173
  )
173
174
 
174
175
  engine = SynapseEngine()
@@ -184,5 +185,6 @@ def create_default_engine(
184
185
  engine.register_layer(CommandHintsLayer(commands=commands))
185
186
  engine.register_layer(QualityGateLayer())
186
187
  engine.register_layer(TimeLayer())
188
+ engine.register_layer(ForgeContextLayer())
187
189
 
188
190
  return engine
@@ -525,3 +525,52 @@ class KnowledgeRetrievalLayer(Layer):
525
525
  layer_id=self.id, tag=tag, content=content,
526
526
  tokens_est=total_tokens, compute_ms=ms, cached=False,
527
527
  )
528
+
529
+
530
+ # --- L8: Forge Context ---
531
+
532
+ class ForgeContextLayer(Layer):
533
+ """L8: Active forge plan context — decisions, risks, rejected approaches."""
534
+
535
+ @property
536
+ def id(self) -> str:
537
+ return "L8"
538
+
539
+ @property
540
+ def name(self) -> str:
541
+ return "ForgeContext"
542
+
543
+ @property
544
+ def cache_ttl(self) -> int:
545
+ return 0
546
+
547
+ @property
548
+ def priority(self) -> int:
549
+ return 80
550
+
551
+ def compute(self, ctx: PromptContext) -> LayerResult:
552
+ start = time.time()
553
+ try:
554
+ from core.forge.persistence import get_active_plan
555
+ plan = get_active_plan()
556
+ except Exception:
557
+ plan = None
558
+ if plan is None:
559
+ return LayerResult(layer_id=self.id, tag="", content="", tokens_est=0, compute_ms=0, cached=False)
560
+ tag = f"[forge:{plan.id}]"
561
+ parts = [f"Forge plan: {plan.id} ({plan.status.value})"]
562
+ if plan.critic.confidence > 0:
563
+ decisions = []
564
+ for source, elements in plan.critic.synthesis.items():
565
+ decisions.extend(elements)
566
+ if decisions:
567
+ parts.append(f"Decisions: {'; '.join(decisions[:5])}")
568
+ rejected = [r.element for r in plan.critic.rejected_elements]
569
+ if rejected:
570
+ parts.append(f"Rejected: {'; '.join(rejected[:3])}")
571
+ risks = [r.risk for r in plan.critic.risks]
572
+ if risks:
573
+ parts.append(f"Risks: {'; '.join(risks[:3])}")
574
+ content = " | ".join(parts)
575
+ ms = int((time.time() - start) * 1000)
576
+ return LayerResult(layer_id=self.id, tag=tag, content=content, tokens_est=len(content.split()), compute_ms=ms, cached=False)
@@ -0,0 +1,25 @@
1
+ """ArkaOS Sync Engine — Hybrid sync for /arka update."""
2
+
3
+ from core.sync.engine import run_sync
4
+ from core.sync.schema import (
5
+ ChangeManifest,
6
+ DescriptorSyncResult,
7
+ FeatureSpec,
8
+ McpSyncResult,
9
+ Project,
10
+ SettingsSyncResult,
11
+ SkillSyncResult,
12
+ SyncReport,
13
+ )
14
+
15
+ __all__ = [
16
+ "run_sync",
17
+ "ChangeManifest",
18
+ "DescriptorSyncResult",
19
+ "FeatureSpec",
20
+ "McpSyncResult",
21
+ "Project",
22
+ "SettingsSyncResult",
23
+ "SkillSyncResult",
24
+ "SyncReport",
25
+ ]
@@ -0,0 +1,166 @@
1
+ """Descriptor syncer for the ArkaOS Sync Engine.
2
+
3
+ Syncs project descriptor YAML frontmatter: auto-pauses inactive projects,
4
+ archives missing paths, and updates detected stacks.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+
13
+ import yaml
14
+
15
+ from core.sync.schema import DescriptorSyncResult, Project
16
+
17
+ _PAUSE_THRESHOLD_DAYS = 30
18
+ _REACTIVATE_THRESHOLD_DAYS = 7
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Public API
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ def sync_descriptor(project: Project) -> DescriptorSyncResult:
27
+ """Sync a single project's descriptor file.
28
+
29
+ Reads the descriptor, checks if the project path exists, compares the
30
+ detected stack, checks git activity, and writes any updates back.
31
+ """
32
+ if not project.descriptor_path:
33
+ return DescriptorSyncResult(path=project.path, status="unchanged")
34
+
35
+ desc_path = Path(project.descriptor_path)
36
+ if not desc_path.exists():
37
+ return DescriptorSyncResult(path=project.path, status="unchanged")
38
+
39
+ try:
40
+ return _do_sync(project)
41
+ except Exception as exc: # noqa: BLE001
42
+ return DescriptorSyncResult(
43
+ path=project.path, status="error", error=str(exc)
44
+ )
45
+
46
+
47
+ def _do_sync(project: Project) -> DescriptorSyncResult:
48
+ """Execute the descriptor sync logic for a single project."""
49
+ desc_path = Path(project.descriptor_path) # type: ignore[arg-type]
50
+ text = desc_path.read_text()
51
+ frontmatter, body = _split_frontmatter(text)
52
+ changes: list[str] = []
53
+
54
+ if not Path(project.path).exists():
55
+ frontmatter["status"] = "archived"
56
+ changes.append("status: archived (path not found)")
57
+ _write_descriptor(desc_path, frontmatter, body)
58
+ return DescriptorSyncResult(
59
+ path=project.path, status="updated", changes=changes
60
+ )
61
+
62
+ _check_stack(frontmatter, project.stack, changes)
63
+ _check_activity(frontmatter, project.path, changes)
64
+
65
+ if not changes:
66
+ return DescriptorSyncResult(path=project.path, status="unchanged")
67
+
68
+ _write_descriptor(desc_path, frontmatter, body)
69
+ return DescriptorSyncResult(
70
+ path=project.path, status="updated", changes=changes
71
+ )
72
+
73
+
74
+ def sync_all_descriptors(projects: list[Project]) -> list[DescriptorSyncResult]:
75
+ """Sync descriptor files for all projects."""
76
+ return [sync_descriptor(p) for p in projects]
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Private helpers
81
+ # ---------------------------------------------------------------------------
82
+
83
+
84
+ def _split_frontmatter(text: str) -> tuple[dict, str]:
85
+ """Split a markdown file into its YAML frontmatter dict and body string.
86
+
87
+ Expects the file to start with '---' and have a closing '---' marker.
88
+ Returns ({}, full_text) if frontmatter markers are not found.
89
+ """
90
+ if not text.startswith("---"):
91
+ return {}, text
92
+
93
+ parts = text.split("---", 2)
94
+ if len(parts) < 3:
95
+ return {}, text
96
+
97
+ raw_yaml = parts[1]
98
+ body = parts[2]
99
+ parsed = yaml.safe_load(raw_yaml) or {}
100
+ return parsed, body
101
+
102
+
103
+ def _normalize_stack_item(item: str) -> str:
104
+ """Normalize a stack item to lowercase first word for comparison."""
105
+ return item.strip().lower().split()[0]
106
+
107
+
108
+ def _check_stack(
109
+ frontmatter: dict, detected_stack: list[str], changes: list[str]
110
+ ) -> None:
111
+ """Compare frontmatter stack with detected stack and update if different."""
112
+ fm_stack: list[str] = frontmatter.get("stack") or []
113
+ fm_normalized = {_normalize_stack_item(s) for s in fm_stack}
114
+ detected_normalized = {_normalize_stack_item(s) for s in detected_stack}
115
+
116
+ if fm_normalized != detected_normalized and detected_stack:
117
+ frontmatter["stack"] = detected_stack
118
+ changes.append(f"stack updated: {fm_stack} -> {detected_stack}")
119
+
120
+
121
+ def _check_activity(
122
+ frontmatter: dict, project_path: str, changes: list[str]
123
+ ) -> None:
124
+ """Check git activity and auto-pause or auto-reactivate the project."""
125
+ days = _get_last_commit_days(project_path)
126
+ if days is None:
127
+ return
128
+
129
+ current_status = frontmatter.get("status", "active")
130
+
131
+ if days > _PAUSE_THRESHOLD_DAYS and current_status == "active":
132
+ frontmatter["status"] = "paused"
133
+ changes.append(f"status: active -> paused ({days}d since last commit)")
134
+ elif days < _REACTIVATE_THRESHOLD_DAYS and current_status == "paused":
135
+ frontmatter["status"] = "active"
136
+ changes.append(f"status: paused -> active ({days}d since last commit)")
137
+
138
+
139
+ def _get_last_commit_days(project_path: str) -> int | None:
140
+ """Return days since the last git commit in project_path, or None."""
141
+ try:
142
+ result = subprocess.run(
143
+ ["git", "log", "-1", "--format=%ci"],
144
+ cwd=project_path,
145
+ capture_output=True,
146
+ text=True,
147
+ timeout=10,
148
+ )
149
+ raw = result.stdout.strip()
150
+ if not raw:
151
+ return None
152
+
153
+ commit_dt = datetime.fromisoformat(raw)
154
+ if commit_dt.tzinfo is None:
155
+ commit_dt = commit_dt.replace(tzinfo=timezone.utc)
156
+
157
+ now = datetime.now(tz=timezone.utc)
158
+ return (now - commit_dt).days
159
+ except Exception: # noqa: BLE001
160
+ return None
161
+
162
+
163
+ def _write_descriptor(desc_path: Path, frontmatter: dict, body: str) -> None:
164
+ """Write updated frontmatter and preserved body back to the descriptor file."""
165
+ fm_text = yaml.dump(frontmatter, default_flow_style=False, allow_unicode=True)
166
+ desc_path.write_text(f"---\n{fm_text}---{body}")