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.
- package/VERSION +1 -1
- package/arka/skills/forge/SKILL.md +649 -0
- package/config/constitution.yaml +8 -0
- package/config/hooks/post-tool-use.sh +43 -0
- package/config/hooks/session-start.sh +24 -0
- package/config/hooks/user-prompt-submit.sh +25 -1
- package/core/forge/__init__.py +104 -0
- package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/complexity.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/handoff.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/persistence.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/renderer.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/forge/complexity.py +125 -0
- package/core/forge/handoff.py +100 -0
- package/core/forge/persistence.py +308 -0
- package/core/forge/renderer.py +261 -0
- package/core/forge/schema.py +213 -0
- package/core/synapse/__init__.py +2 -2
- package/core/synapse/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/engine.py +4 -2
- package/core/synapse/layers.py +49 -0
- package/core/sync/__init__.py +25 -0
- package/core/sync/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/descriptor_syncer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/discovery.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/manifest.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/mcp_syncer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/reporter.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/settings_syncer.cpython-313.pyc +0 -0
- package/core/sync/descriptor_syncer.py +166 -0
- package/core/sync/discovery.py +256 -0
- package/core/sync/engine.py +177 -0
- package/core/sync/features/forge.yaml +16 -0
- package/core/sync/features/quality-gate.yaml +15 -0
- package/core/sync/features/spec-gate.yaml +15 -0
- package/core/sync/features/workflow-tiers.yaml +19 -0
- package/core/sync/manifest.py +87 -0
- package/core/sync/mcp_syncer.py +255 -0
- package/core/sync/reporter.py +178 -0
- package/core/sync/schema.py +94 -0
- package/core/sync/settings_syncer.py +121 -0
- package/core/workflow/state_reader.sh +25 -1
- package/departments/ops/skills/update/SKILL.md +69 -0
- package/installer/update.js +14 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Project discovery for the ArkaOS Sync Engine.
|
|
2
|
+
|
|
3
|
+
Discovers projects from 3 sources: descriptors, filesystem, and ecosystems.
|
|
4
|
+
Detects tech stacks and deduplicates across sources.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from core.sync.schema import Project
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --- Stack detection helpers ---
|
|
18
|
+
|
|
19
|
+
def _detect_from_composer(project_path: Path) -> list[str]:
|
|
20
|
+
composer = project_path / "composer.json"
|
|
21
|
+
if not composer.exists():
|
|
22
|
+
return []
|
|
23
|
+
try:
|
|
24
|
+
data = json.loads(composer.read_text())
|
|
25
|
+
require = data.get("require", {})
|
|
26
|
+
if "laravel/framework" in require:
|
|
27
|
+
return ["php", "laravel"]
|
|
28
|
+
return ["php"]
|
|
29
|
+
except (json.JSONDecodeError, OSError):
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _detect_from_package_json(project_path: Path) -> list[str]:
|
|
34
|
+
pkg = project_path / "package.json"
|
|
35
|
+
if not pkg.exists():
|
|
36
|
+
return []
|
|
37
|
+
try:
|
|
38
|
+
data = json.loads(pkg.read_text())
|
|
39
|
+
deps = {
|
|
40
|
+
**data.get("dependencies", {}),
|
|
41
|
+
**data.get("devDependencies", {}),
|
|
42
|
+
}
|
|
43
|
+
stack: list[str] = []
|
|
44
|
+
if "nuxt" in deps:
|
|
45
|
+
stack.extend(["javascript", "nuxt", "vue"])
|
|
46
|
+
elif "vue" in deps:
|
|
47
|
+
stack.extend(["javascript", "vue"])
|
|
48
|
+
elif "next" in deps:
|
|
49
|
+
stack.extend(["javascript", "next", "react"])
|
|
50
|
+
elif "react" in deps:
|
|
51
|
+
stack.extend(["javascript", "react"])
|
|
52
|
+
else:
|
|
53
|
+
stack.append("javascript")
|
|
54
|
+
return stack
|
|
55
|
+
except (json.JSONDecodeError, OSError):
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _detect_from_pyproject(project_path: Path) -> list[str]:
|
|
60
|
+
pyproject = project_path / "pyproject.toml"
|
|
61
|
+
if not pyproject.exists():
|
|
62
|
+
return []
|
|
63
|
+
return ["python"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def detect_stack(project_path: Path) -> list[str]:
|
|
67
|
+
"""Detect tech stack from project files.
|
|
68
|
+
|
|
69
|
+
Checks composer.json, package.json, and pyproject.toml in order.
|
|
70
|
+
Returns a deduplicated list of detected technologies.
|
|
71
|
+
"""
|
|
72
|
+
stack: list[str] = []
|
|
73
|
+
stack.extend(_detect_from_composer(project_path))
|
|
74
|
+
stack.extend(_detect_from_package_json(project_path))
|
|
75
|
+
stack.extend(_detect_from_pyproject(project_path))
|
|
76
|
+
seen: set[str] = set()
|
|
77
|
+
result: list[str] = []
|
|
78
|
+
for item in stack:
|
|
79
|
+
if item not in seen:
|
|
80
|
+
seen.add(item)
|
|
81
|
+
result.append(item)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# --- Descriptor discovery helpers ---
|
|
86
|
+
|
|
87
|
+
def _parse_descriptor_frontmatter(text: str) -> dict:
|
|
88
|
+
"""Extract YAML frontmatter from a markdown file."""
|
|
89
|
+
if not text.startswith("---"):
|
|
90
|
+
return {}
|
|
91
|
+
parts = text.split("---", 2)
|
|
92
|
+
if len(parts) < 3:
|
|
93
|
+
return {}
|
|
94
|
+
try:
|
|
95
|
+
return yaml.safe_load(parts[1]) or {}
|
|
96
|
+
except yaml.YAMLError:
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _read_descriptor_item(item: Path) -> dict:
|
|
101
|
+
"""Read a descriptor file and return its frontmatter as a dict."""
|
|
102
|
+
try:
|
|
103
|
+
return _parse_descriptor_frontmatter(item.read_text())
|
|
104
|
+
except OSError:
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _process_descriptor_item(item: Path, descriptor_dir: Path) -> "Project | None":
|
|
109
|
+
"""Parse a single descriptor file and return a Project or None."""
|
|
110
|
+
fm = _read_descriptor_item(item)
|
|
111
|
+
raw_path = fm.get("path", "")
|
|
112
|
+
if not raw_path:
|
|
113
|
+
return None
|
|
114
|
+
project_path = Path(raw_path)
|
|
115
|
+
if not project_path.exists():
|
|
116
|
+
return None
|
|
117
|
+
name = fm.get("name", project_path.name)
|
|
118
|
+
stack = detect_stack(project_path)
|
|
119
|
+
return Project(
|
|
120
|
+
path=str(project_path),
|
|
121
|
+
name=name,
|
|
122
|
+
ecosystem=fm.get("ecosystem") or None,
|
|
123
|
+
stack=stack,
|
|
124
|
+
descriptor_path=str(item),
|
|
125
|
+
has_mcp_json=(project_path / ".mcp.json").exists(),
|
|
126
|
+
has_settings=(project_path / ".claude").is_dir(),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def discover_from_descriptors(descriptor_dir: Path) -> list[Project]:
|
|
131
|
+
"""Discover projects from .md descriptor files with YAML frontmatter.
|
|
132
|
+
|
|
133
|
+
Reads .md files in descriptor_dir and PROJECT.md in subdirectories.
|
|
134
|
+
Skips entries whose paths don't exist on the filesystem.
|
|
135
|
+
"""
|
|
136
|
+
if not descriptor_dir.exists():
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
candidates: list[Path] = list(descriptor_dir.glob("*.md"))
|
|
140
|
+
for subdir in descriptor_dir.iterdir():
|
|
141
|
+
if subdir.is_dir():
|
|
142
|
+
project_md = subdir / "PROJECT.md"
|
|
143
|
+
if project_md.exists():
|
|
144
|
+
candidates.append(project_md)
|
|
145
|
+
|
|
146
|
+
projects: list[Project] = []
|
|
147
|
+
for item in candidates:
|
|
148
|
+
project = _process_descriptor_item(item, descriptor_dir)
|
|
149
|
+
if project is not None:
|
|
150
|
+
projects.append(project)
|
|
151
|
+
|
|
152
|
+
return projects
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# --- Filesystem discovery ---
|
|
156
|
+
|
|
157
|
+
def discover_from_filesystem(scan_dirs: list[Path]) -> list[Project]:
|
|
158
|
+
"""Discover projects by scanning directories for .mcp.json or .claude/ markers."""
|
|
159
|
+
projects: list[Project] = []
|
|
160
|
+
for scan_dir in scan_dirs:
|
|
161
|
+
if not scan_dir.exists():
|
|
162
|
+
continue
|
|
163
|
+
for subdir in scan_dir.iterdir():
|
|
164
|
+
if not subdir.is_dir():
|
|
165
|
+
continue
|
|
166
|
+
has_mcp = (subdir / ".mcp.json").is_file()
|
|
167
|
+
has_claude = (subdir / ".claude").is_dir()
|
|
168
|
+
has_claude_md = (subdir / "CLAUDE.md").is_file()
|
|
169
|
+
if not has_mcp and not has_claude and not has_claude_md:
|
|
170
|
+
continue
|
|
171
|
+
stack = detect_stack(subdir)
|
|
172
|
+
projects.append(Project(
|
|
173
|
+
path=str(subdir),
|
|
174
|
+
name=subdir.name,
|
|
175
|
+
stack=stack,
|
|
176
|
+
has_mcp_json=has_mcp,
|
|
177
|
+
has_settings=has_claude,
|
|
178
|
+
))
|
|
179
|
+
|
|
180
|
+
return projects
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# --- Ecosystem discovery ---
|
|
184
|
+
|
|
185
|
+
def discover_from_ecosystems(ecosystems_file: Path) -> list[Project]:
|
|
186
|
+
"""Discover projects from an ecosystems.json registry file."""
|
|
187
|
+
if not ecosystems_file.exists():
|
|
188
|
+
return []
|
|
189
|
+
try:
|
|
190
|
+
data = json.loads(ecosystems_file.read_text())
|
|
191
|
+
except (json.JSONDecodeError, OSError):
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
projects: list[Project] = []
|
|
195
|
+
for eco_key, eco_data in data.get("ecosystems", {}).items():
|
|
196
|
+
for proj_name, proj_path_str in eco_data.get("project_paths", {}).items():
|
|
197
|
+
proj_path = Path(proj_path_str)
|
|
198
|
+
if not proj_path.exists():
|
|
199
|
+
continue
|
|
200
|
+
stack = detect_stack(proj_path)
|
|
201
|
+
projects.append(Project(
|
|
202
|
+
path=str(proj_path),
|
|
203
|
+
name=proj_name,
|
|
204
|
+
ecosystem=eco_key,
|
|
205
|
+
stack=stack,
|
|
206
|
+
has_mcp_json=(proj_path / ".mcp.json").exists(),
|
|
207
|
+
has_settings=(proj_path / ".claude").is_dir(),
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
return projects
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# --- Merge and deduplication helpers ---
|
|
214
|
+
|
|
215
|
+
def _merge_project(primary: Project, secondary: Project) -> Project:
|
|
216
|
+
"""Merge two Project records — primary data wins over secondary."""
|
|
217
|
+
return Project(
|
|
218
|
+
path=primary.path,
|
|
219
|
+
name=primary.name,
|
|
220
|
+
ecosystem=primary.ecosystem or secondary.ecosystem,
|
|
221
|
+
stack=primary.stack if primary.stack else secondary.stack,
|
|
222
|
+
descriptor_path=primary.descriptor_path or secondary.descriptor_path,
|
|
223
|
+
has_mcp_json=primary.has_mcp_json or secondary.has_mcp_json,
|
|
224
|
+
has_settings=primary.has_settings or secondary.has_settings,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _deduplicate(projects: list[Project]) -> list[Project]:
|
|
229
|
+
"""Deduplicate projects by resolved absolute path. First entry wins."""
|
|
230
|
+
seen: dict[str, Project] = {}
|
|
231
|
+
for project in projects:
|
|
232
|
+
key = str(Path(project.path).resolve())
|
|
233
|
+
if key not in seen:
|
|
234
|
+
seen[key] = project
|
|
235
|
+
else:
|
|
236
|
+
seen[key] = _merge_project(seen[key], project)
|
|
237
|
+
return list(seen.values())
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def discover_all_projects(
|
|
241
|
+
descriptor_dir: Path,
|
|
242
|
+
scan_dirs: list[Path],
|
|
243
|
+
ecosystems_file: Path,
|
|
244
|
+
) -> list[Project]:
|
|
245
|
+
"""Discover all projects from all sources, deduplicate, and sort by name.
|
|
246
|
+
|
|
247
|
+
Descriptor data wins over filesystem/ecosystem data during merging.
|
|
248
|
+
"""
|
|
249
|
+
descriptor_projects = discover_from_descriptors(descriptor_dir)
|
|
250
|
+
filesystem_projects = discover_from_filesystem(scan_dirs)
|
|
251
|
+
ecosystem_projects = discover_from_ecosystems(ecosystems_file)
|
|
252
|
+
|
|
253
|
+
all_projects = descriptor_projects + ecosystem_projects + filesystem_projects
|
|
254
|
+
deduplicated = _deduplicate(all_projects)
|
|
255
|
+
|
|
256
|
+
return sorted(deduplicated, key=lambda p: p.name)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Engine Orchestrator for the ArkaOS Sync Engine.
|
|
2
|
+
|
|
3
|
+
Coordinates all sync phases and provides a CLI entry point for /arka update.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from core.sync.manifest import build_manifest
|
|
15
|
+
from core.sync.discovery import discover_all_projects
|
|
16
|
+
from core.sync.mcp_syncer import sync_all_mcps
|
|
17
|
+
from core.sync.settings_syncer import sync_all_settings
|
|
18
|
+
from core.sync.descriptor_syncer import sync_all_descriptors
|
|
19
|
+
from core.sync.reporter import build_report, format_report, write_sync_state
|
|
20
|
+
from core.sync.schema import SyncReport
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Public API
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_sync(arkaos_home: Path, skills_dir: Path, home_path: str) -> SyncReport:
|
|
29
|
+
"""Orchestrate all deterministic sync phases and return a SyncReport."""
|
|
30
|
+
previous_version = _read_previous_version(arkaos_home)
|
|
31
|
+
current_version = _read_current_version(arkaos_home)
|
|
32
|
+
features_dir = _resolve_features_dir(arkaos_home)
|
|
33
|
+
|
|
34
|
+
manifest = build_manifest(previous_version, current_version, features_dir)
|
|
35
|
+
|
|
36
|
+
projects = _discover_projects(arkaos_home, skills_dir)
|
|
37
|
+
|
|
38
|
+
registry_path = skills_dir / "arka" / "mcps" / "registry.json"
|
|
39
|
+
mcp_results = sync_all_mcps(projects, registry_path, home_path)
|
|
40
|
+
settings_results = sync_all_settings(mcp_results)
|
|
41
|
+
descriptor_results = sync_all_descriptors(projects)
|
|
42
|
+
|
|
43
|
+
report = build_report(
|
|
44
|
+
previous_version,
|
|
45
|
+
current_version,
|
|
46
|
+
mcp_results,
|
|
47
|
+
settings_results,
|
|
48
|
+
descriptor_results,
|
|
49
|
+
[],
|
|
50
|
+
new_features=manifest.new_features,
|
|
51
|
+
deprecated_features=manifest.deprecated_features,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
state_file = arkaos_home / "sync-state.json"
|
|
55
|
+
write_sync_state(state_file, report)
|
|
56
|
+
|
|
57
|
+
return report
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main() -> None:
|
|
61
|
+
"""CLI entry point for the sync engine."""
|
|
62
|
+
parser = argparse.ArgumentParser(description="ArkaOS Sync Engine")
|
|
63
|
+
parser.add_argument("--home", required=True, help="ArkaOS home directory")
|
|
64
|
+
parser.add_argument("--skills", required=True, help="Skills directory")
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--output",
|
|
67
|
+
choices=["text", "json"],
|
|
68
|
+
default="text",
|
|
69
|
+
help="Output format",
|
|
70
|
+
)
|
|
71
|
+
args = parser.parse_args()
|
|
72
|
+
|
|
73
|
+
report = run_sync(
|
|
74
|
+
arkaos_home=Path(args.home),
|
|
75
|
+
skills_dir=Path(args.skills),
|
|
76
|
+
home_path=str(Path.home()),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if args.output == "json":
|
|
80
|
+
print(report.model_dump_json(indent=2))
|
|
81
|
+
else:
|
|
82
|
+
print(format_report(report))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Private helpers
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _read_previous_version(arkaos_home: Path) -> str:
|
|
91
|
+
"""Read version field from sync-state.json, defaulting to pending-sync."""
|
|
92
|
+
state_file = arkaos_home / "sync-state.json"
|
|
93
|
+
if not state_file.exists():
|
|
94
|
+
return "pending-sync"
|
|
95
|
+
try:
|
|
96
|
+
data = json.loads(state_file.read_text())
|
|
97
|
+
return data.get("version", "pending-sync") or "pending-sync"
|
|
98
|
+
except (json.JSONDecodeError, OSError):
|
|
99
|
+
return "pending-sync"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _read_current_version(arkaos_home: Path) -> str:
|
|
103
|
+
"""Read version from VERSION file in the ArkaOS repo."""
|
|
104
|
+
repo_path = _read_repo_path(arkaos_home)
|
|
105
|
+
if repo_path is None:
|
|
106
|
+
return "unknown"
|
|
107
|
+
version_file = repo_path / "VERSION"
|
|
108
|
+
if not version_file.exists():
|
|
109
|
+
return "unknown"
|
|
110
|
+
try:
|
|
111
|
+
return version_file.read_text().strip()
|
|
112
|
+
except OSError:
|
|
113
|
+
return "unknown"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _read_repo_path(arkaos_home: Path) -> Path | None:
|
|
117
|
+
"""Read the absolute repo path from .repo-path file."""
|
|
118
|
+
repo_path_file = arkaos_home / ".repo-path"
|
|
119
|
+
if not repo_path_file.exists():
|
|
120
|
+
return None
|
|
121
|
+
try:
|
|
122
|
+
raw = repo_path_file.read_text().strip()
|
|
123
|
+
return Path(raw) if raw else None
|
|
124
|
+
except OSError:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _resolve_features_dir(arkaos_home: Path) -> Path:
|
|
129
|
+
"""Resolve the features directory from repo or fallback config."""
|
|
130
|
+
repo_path = _read_repo_path(arkaos_home)
|
|
131
|
+
if repo_path is not None:
|
|
132
|
+
repo_features = repo_path / "core" / "sync" / "features"
|
|
133
|
+
if repo_features.exists():
|
|
134
|
+
return repo_features
|
|
135
|
+
|
|
136
|
+
fallback = arkaos_home / "config" / "sync" / "features"
|
|
137
|
+
return fallback
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_scan_dirs(projects_dir_str: str) -> list[Path]:
|
|
141
|
+
"""Parse a projectsDir string, extracting all paths starting with /."""
|
|
142
|
+
segments = re.split(r",\s*", projects_dir_str.strip())
|
|
143
|
+
paths: list[Path] = []
|
|
144
|
+
for segment in segments:
|
|
145
|
+
match = re.match(r"(/[^\s]+)", segment.strip())
|
|
146
|
+
if match:
|
|
147
|
+
paths.append(Path(match.group(1)))
|
|
148
|
+
return paths
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _discover_projects(arkaos_home: Path, skills_dir: Path) -> list:
|
|
152
|
+
"""Combine profile.json dirs, descriptor dir, and ecosystems into projects."""
|
|
153
|
+
descriptor_dir = skills_dir / "arka" / "projects"
|
|
154
|
+
ecosystems_file = skills_dir / "arka" / "knowledge" / "ecosystems.json"
|
|
155
|
+
|
|
156
|
+
scan_dirs = _load_scan_dirs_from_profile(arkaos_home)
|
|
157
|
+
|
|
158
|
+
return discover_all_projects(descriptor_dir, scan_dirs, ecosystems_file)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _load_scan_dirs_from_profile(arkaos_home: Path) -> list[Path]:
|
|
162
|
+
"""Read projectsDir from profile.json and parse into scan directory paths."""
|
|
163
|
+
profile_file = arkaos_home / "profile.json"
|
|
164
|
+
if not profile_file.exists():
|
|
165
|
+
return []
|
|
166
|
+
try:
|
|
167
|
+
data = json.loads(profile_file.read_text())
|
|
168
|
+
projects_dir_str = data.get("projectsDir", "")
|
|
169
|
+
if not projects_dir_str:
|
|
170
|
+
return []
|
|
171
|
+
return _parse_scan_dirs(projects_dir_str)
|
|
172
|
+
except (json.JSONDecodeError, OSError):
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
sys.exit(main())
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: forge-integration
|
|
2
|
+
added_in: "2.14.0"
|
|
3
|
+
mandatory: true
|
|
4
|
+
section_title: "Forge Integration"
|
|
5
|
+
detection_pattern: "arka-forge"
|
|
6
|
+
deprecated_in: null
|
|
7
|
+
content: |
|
|
8
|
+
## Forge Integration
|
|
9
|
+
|
|
10
|
+
Complex requests (complexity score >= 5) are automatically routed to
|
|
11
|
+
The Forge for multi-agent planning before execution.
|
|
12
|
+
|
|
13
|
+
- Phase 0.5: Forge analysis (after spec creation, before squad planning)
|
|
14
|
+
- Complexity assessment: automatic via Synapse L8 (ForgeContextLayer)
|
|
15
|
+
- Manual invocation: `/forge` command
|
|
16
|
+
- Handoff: Forge outputs structured plan → squad executes phases
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: quality-gate
|
|
2
|
+
added_in: "2.10.0"
|
|
3
|
+
mandatory: true
|
|
4
|
+
section_title: "Quality Gate"
|
|
5
|
+
detection_pattern: "Marta.*CQO|Quality Gate"
|
|
6
|
+
deprecated_in: null
|
|
7
|
+
content: |
|
|
8
|
+
## Quality Gate
|
|
9
|
+
|
|
10
|
+
Mandatory on every workflow. Nothing ships without approval.
|
|
11
|
+
|
|
12
|
+
- **Marta (CQO):** Orchestrates review, absolute veto power
|
|
13
|
+
- **Eduardo (Copy Director):** Reviews all text output
|
|
14
|
+
- **Francisca (Tech Director):** Reviews all code and technical output
|
|
15
|
+
- Verdict: APPROVED or REJECTED (binary, no partial)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: spec-driven-gate
|
|
2
|
+
added_in: "2.13.0"
|
|
3
|
+
mandatory: true
|
|
4
|
+
section_title: "Spec-Driven Development"
|
|
5
|
+
detection_pattern: "arka-spec"
|
|
6
|
+
deprecated_in: null
|
|
7
|
+
content: |
|
|
8
|
+
## Spec-Driven Development
|
|
9
|
+
|
|
10
|
+
Phase 0 of all workflows. No implementation begins without a validated spec.
|
|
11
|
+
|
|
12
|
+
- Invocation: automatic before any feature/fix work
|
|
13
|
+
- Gate: spec must be approved before planning phase starts
|
|
14
|
+
- Storage: `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
|
|
15
|
+
- Review: user approval required on written spec
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: workflow-tiers
|
|
2
|
+
added_in: "2.12.0"
|
|
3
|
+
mandatory: true
|
|
4
|
+
section_title: "Workflow Tiers"
|
|
5
|
+
detection_pattern: "Enterprise.*phase|Focused.*phase|Specialist.*phase"
|
|
6
|
+
deprecated_in: null
|
|
7
|
+
content: |
|
|
8
|
+
## Workflow Tiers
|
|
9
|
+
|
|
10
|
+
Three workflow tiers based on task complexity:
|
|
11
|
+
|
|
12
|
+
| Tier | Phases | When |
|
|
13
|
+
|------|--------|------|
|
|
14
|
+
| Enterprise | 7-10 phases | Complex features, multi-file changes |
|
|
15
|
+
| Focused | 3-5 phases | Medium tasks, single-domain changes |
|
|
16
|
+
| Specialist | 1-2 phases | Simple tasks, quick fixes |
|
|
17
|
+
|
|
18
|
+
Tier selection is automatic based on complexity assessment.
|
|
19
|
+
Quality Gate phase is mandatory on ALL tiers.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Change Manifest Builder — loads feature specs and computes version diffs."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from core.sync.schema import ChangeManifest, FeatureSpec
|
|
8
|
+
|
|
9
|
+
_FIRST_SYNC_MARKERS = {"pending-sync", "none", ""}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_features(features_dir: Path) -> list[FeatureSpec]:
|
|
13
|
+
"""Load all FeatureSpec instances from YAML files in features_dir."""
|
|
14
|
+
if not features_dir.exists():
|
|
15
|
+
return []
|
|
16
|
+
|
|
17
|
+
features: list[FeatureSpec] = []
|
|
18
|
+
for path in sorted(features_dir.iterdir()):
|
|
19
|
+
if path.suffix != ".yaml":
|
|
20
|
+
continue
|
|
21
|
+
data = yaml.safe_load(path.read_text())
|
|
22
|
+
features.append(FeatureSpec(**data))
|
|
23
|
+
|
|
24
|
+
return features
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_manifest(
|
|
28
|
+
previous_version: str,
|
|
29
|
+
current_version: str,
|
|
30
|
+
features_dir: Path,
|
|
31
|
+
) -> ChangeManifest:
|
|
32
|
+
"""Build a ChangeManifest comparing previous_version to current_version."""
|
|
33
|
+
features = load_features(features_dir)
|
|
34
|
+
is_first = previous_version in _FIRST_SYNC_MARKERS
|
|
35
|
+
|
|
36
|
+
new_features = _find_new_features(features, previous_version, is_first)
|
|
37
|
+
deprecated_features = _find_deprecated_features(features, previous_version, is_first)
|
|
38
|
+
|
|
39
|
+
return ChangeManifest(
|
|
40
|
+
previous_version=previous_version,
|
|
41
|
+
current_version=current_version,
|
|
42
|
+
is_first_sync=is_first,
|
|
43
|
+
features=features,
|
|
44
|
+
new_features=new_features,
|
|
45
|
+
deprecated_features=deprecated_features,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_version_newer(version: str, baseline: str) -> bool:
|
|
50
|
+
"""Return True if version is strictly newer than baseline (semver int tuple)."""
|
|
51
|
+
def parse(v: str) -> tuple[int, ...]:
|
|
52
|
+
return tuple(int(part) for part in v.split("."))
|
|
53
|
+
|
|
54
|
+
return parse(version) > parse(baseline)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _find_new_features(
|
|
58
|
+
features: list[FeatureSpec],
|
|
59
|
+
previous_version: str,
|
|
60
|
+
is_first: bool,
|
|
61
|
+
) -> list[str]:
|
|
62
|
+
"""Return names of features that are new relative to previous_version."""
|
|
63
|
+
if is_first:
|
|
64
|
+
return [f.name for f in features if f.deprecated_in is None]
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
f.name
|
|
68
|
+
for f in features
|
|
69
|
+
if _is_version_newer(f.added_in, previous_version)
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _find_deprecated_features(
|
|
74
|
+
features: list[FeatureSpec],
|
|
75
|
+
previous_version: str,
|
|
76
|
+
is_first: bool,
|
|
77
|
+
) -> list[str]:
|
|
78
|
+
"""Return names of features deprecated after previous_version."""
|
|
79
|
+
if is_first:
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
f.name
|
|
84
|
+
for f in features
|
|
85
|
+
if f.deprecated_in is not None
|
|
86
|
+
and _is_version_newer(f.deprecated_in, previous_version)
|
|
87
|
+
]
|