arkaos 5.9.0 → 5.10.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/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/core/governance/evidence_checks.py +166 -22
- package/core/sync/content_merger.py +77 -26
- package/core/sync/content_syncer.py +67 -29
- package/core/sync/engine.py +28 -20
- package/core/sync/manifest.py +48 -8
- package/core/sync/reporter.py +49 -29
- package/core/sync/schema.py +2 -0
- package/departments/ops/skills/update/references/sync-engine.md +1 -1
- package/departments/ops/skills/update/references/workflows.md +4 -2
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/zed/.rules +1 -1
- package/knowledge/commands-registry.json +1 -1
- package/knowledge/skills-manifest.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/THE-ARKAOS-GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The ArkaOS Guide
|
|
2
2
|
|
|
3
|
-
> v5.
|
|
3
|
+
> v5.10.0 — 89 agents, 17 departments, 340 skills, 306 commands, 20 ADRs.
|
|
4
4
|
> One file, everything you need to start. Generated by `scripts/guide_gen.py` — never hand-edited.
|
|
5
5
|
|
|
6
6
|
## What it is
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.10.0
|
|
@@ -35,7 +35,8 @@ import subprocess
|
|
|
35
35
|
import sys
|
|
36
36
|
import time
|
|
37
37
|
from dataclasses import asdict, dataclass, field, replace
|
|
38
|
-
from pathlib import Path
|
|
38
|
+
from pathlib import Path, PurePosixPath
|
|
39
|
+
from xml.etree import ElementTree
|
|
39
40
|
|
|
40
41
|
from core.governance.qg_digest import evidence_digest
|
|
41
42
|
from core.shared.test_evidence import coverage_percent_from_xml
|
|
@@ -553,30 +554,172 @@ def _junit_result(junit: Path) -> CheckResult:
|
|
|
553
554
|
)
|
|
554
555
|
|
|
555
556
|
|
|
557
|
+
def _coverage_from_xml(
|
|
558
|
+
coverage_xml: Path, project_dir: Path, changed: list[str] | None,
|
|
559
|
+
) -> CheckResult:
|
|
560
|
+
"""Coverage verdict from an artefact, refusing one that cannot describe the diff."""
|
|
561
|
+
stale = _stale_coverage_reason(coverage_xml, project_dir, changed)
|
|
562
|
+
if stale is not None:
|
|
563
|
+
return CheckResult(
|
|
564
|
+
check="coverage", ran=True, passed=False,
|
|
565
|
+
command="parse:coverage.xml", exit_code=None,
|
|
566
|
+
summary=stale, details_path=str(coverage_xml),
|
|
567
|
+
)
|
|
568
|
+
percent = coverage_percent_from_xml(coverage_xml)
|
|
569
|
+
if percent is None:
|
|
570
|
+
return CheckResult(
|
|
571
|
+
check="coverage", ran=True, passed=None,
|
|
572
|
+
command="parse:coverage.xml", exit_code=None,
|
|
573
|
+
summary="coverage.xml present but unparseable",
|
|
574
|
+
details_path=str(coverage_xml),
|
|
575
|
+
)
|
|
576
|
+
return CheckResult(
|
|
577
|
+
check="coverage", ran=True,
|
|
578
|
+
passed=percent >= COVERAGE_THRESHOLD,
|
|
579
|
+
command="parse:coverage.xml", exit_code=None,
|
|
580
|
+
summary=f"coverage {percent:.1f}% (threshold {COVERAGE_THRESHOLD:.0f}%)",
|
|
581
|
+
details_path=str(coverage_xml),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _stale_coverage_reason(
|
|
586
|
+
coverage_xml: Path, project_dir: Path, changed: list[str] | None,
|
|
587
|
+
) -> str | None:
|
|
588
|
+
"""Reason the artefact cannot describe this diff, or None if it can.
|
|
589
|
+
|
|
590
|
+
A coverage.xml older than the newest changed source measured a different
|
|
591
|
+
codebase, and a green number then vouches for code it never executed.
|
|
592
|
+
"""
|
|
593
|
+
try:
|
|
594
|
+
artefact_mtime = coverage_xml.stat().st_mtime
|
|
595
|
+
except OSError:
|
|
596
|
+
return "coverage.xml unreadable"
|
|
597
|
+
|
|
598
|
+
newest_name = _newest_changed_after(project_dir, changed, artefact_mtime)
|
|
599
|
+
if newest_name is not None:
|
|
600
|
+
return (
|
|
601
|
+
f"coverage.xml predates changed source ({newest_name}) — "
|
|
602
|
+
"regenerate it; it cannot describe this diff"
|
|
603
|
+
)
|
|
604
|
+
return _missing_module_reason(coverage_xml, changed, project_dir)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _missing_module_reason(
|
|
608
|
+
coverage_xml: Path, changed: list[str] | None, project_dir: Path,
|
|
609
|
+
) -> str | None:
|
|
610
|
+
"""Reason a changed module is absent from the artefact, or None."""
|
|
611
|
+
covered = _covered_paths(coverage_xml, project_dir)
|
|
612
|
+
missing = [
|
|
613
|
+
rel for rel in (changed or [])
|
|
614
|
+
if rel.endswith(".py")
|
|
615
|
+
and not rel.startswith("tests/")
|
|
616
|
+
and not _is_covered(rel, covered)
|
|
617
|
+
]
|
|
618
|
+
if not missing:
|
|
619
|
+
return None
|
|
620
|
+
return (
|
|
621
|
+
f"coverage.xml has no entry for {len(missing)} changed module(s), "
|
|
622
|
+
f"first: {missing[0]}"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _newest_changed_after(
|
|
627
|
+
project_dir: Path, changed: list[str] | None, cutoff: float,
|
|
628
|
+
) -> str | None:
|
|
629
|
+
"""Name of the newest changed .py file modified after cutoff, else None.
|
|
630
|
+
|
|
631
|
+
Only executable source can invalidate a coverage artefact — treating
|
|
632
|
+
CHANGELOG.md as "changed source" forced a full regeneration for edits no
|
|
633
|
+
test could ever execute (same filter the module-presence check applies).
|
|
634
|
+
"""
|
|
635
|
+
newest = cutoff
|
|
636
|
+
newest_name: str | None = None
|
|
637
|
+
for rel in changed or []:
|
|
638
|
+
if not rel.endswith(".py"):
|
|
639
|
+
continue
|
|
640
|
+
try:
|
|
641
|
+
mtime = (project_dir / rel).stat().st_mtime
|
|
642
|
+
except OSError:
|
|
643
|
+
continue
|
|
644
|
+
if mtime > newest:
|
|
645
|
+
newest, newest_name = mtime, rel
|
|
646
|
+
return newest_name
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _covered_paths(coverage_xml: Path, project_dir: Path) -> set[str]:
|
|
650
|
+
"""Covered files as paths relative to project_dir, exactly.
|
|
651
|
+
|
|
652
|
+
A ``class/@filename`` is relative to one of the ``sources/source``
|
|
653
|
+
roots, which are usually absolute and which the changed-file list never
|
|
654
|
+
carries. Each candidate is therefore rebuilt against every source and
|
|
655
|
+
re-expressed relative to the project, so comparison can be equality.
|
|
656
|
+
|
|
657
|
+
Suffix or stem matching is not good enough here: `core/` alone carries
|
|
658
|
+
14 colliding stems, this repo's own artefact yields five bare basenames,
|
|
659
|
+
and a suffix rule cannot tell `core/sync/engine.py` from
|
|
660
|
+
`vendor/core/sync/engine.py`. Equality can.
|
|
661
|
+
"""
|
|
662
|
+
try:
|
|
663
|
+
root = ElementTree.parse(coverage_xml).getroot()
|
|
664
|
+
except (ElementTree.ParseError, OSError):
|
|
665
|
+
return set()
|
|
666
|
+
sources = [(el.text or "").strip() for el in root.iterfind("sources/source")]
|
|
667
|
+
try:
|
|
668
|
+
base = project_dir.resolve()
|
|
669
|
+
except OSError:
|
|
670
|
+
base = project_dir
|
|
671
|
+
return {
|
|
672
|
+
candidate
|
|
673
|
+
for cls in root.iter("class")
|
|
674
|
+
if cls.get("filename")
|
|
675
|
+
for candidate in _source_candidates(cls.get("filename", ""), sources, base)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _source_candidates(
|
|
680
|
+
filename: str, sources: list[str], base: Path,
|
|
681
|
+
) -> set[str]:
|
|
682
|
+
"""Project-relative spellings of one covered file, anchored.
|
|
683
|
+
|
|
684
|
+
With ``<source>`` roots declared, every candidate must resolve through
|
|
685
|
+
one of them into the project — the unanchored raw filename let another
|
|
686
|
+
checkout's artefact vouch for this project's files. The raw spelling is
|
|
687
|
+
a fallback only when no sources exist; a relative source resolves
|
|
688
|
+
against the project, never the CWD.
|
|
689
|
+
"""
|
|
690
|
+
if not sources:
|
|
691
|
+
raw = PurePosixPath(filename)
|
|
692
|
+
return set() if raw.is_absolute() else {raw.as_posix()}
|
|
693
|
+
|
|
694
|
+
# Candidates must exist on disk; existence under more than one source
|
|
695
|
+
# is ambiguous — vouch for neither (fail closed).
|
|
696
|
+
found: set[str] = set()
|
|
697
|
+
for source in sources:
|
|
698
|
+
src = Path(source)
|
|
699
|
+
if not src.is_absolute():
|
|
700
|
+
src = base / src
|
|
701
|
+
try:
|
|
702
|
+
resolved = (src / filename).resolve()
|
|
703
|
+
if not resolved.is_file():
|
|
704
|
+
continue
|
|
705
|
+
found.add(PurePosixPath(resolved.relative_to(base)).as_posix())
|
|
706
|
+
except (OSError, ValueError):
|
|
707
|
+
continue
|
|
708
|
+
return found if len(found) == 1 else set()
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _is_covered(rel: str, covered: set[str]) -> bool:
|
|
712
|
+
"""True only when the artefact names exactly this project-relative path."""
|
|
713
|
+
return PurePosixPath(rel).as_posix() in covered
|
|
714
|
+
|
|
715
|
+
|
|
556
716
|
def _check_coverage(
|
|
557
717
|
project_dir: Path, changed: list[str] | None,
|
|
558
718
|
test_command: str | None, timeout: int,
|
|
559
719
|
) -> CheckResult:
|
|
560
720
|
coverage_xml = project_dir / "coverage.xml"
|
|
561
721
|
if coverage_xml.is_file():
|
|
562
|
-
|
|
563
|
-
if percent is None:
|
|
564
|
-
return CheckResult(
|
|
565
|
-
check="coverage", ran=True, passed=None,
|
|
566
|
-
command="parse:coverage.xml", exit_code=None,
|
|
567
|
-
summary="coverage.xml present but unparseable",
|
|
568
|
-
details_path=str(coverage_xml),
|
|
569
|
-
)
|
|
570
|
-
return CheckResult(
|
|
571
|
-
check="coverage", ran=True,
|
|
572
|
-
passed=percent >= COVERAGE_THRESHOLD,
|
|
573
|
-
command="parse:coverage.xml", exit_code=None,
|
|
574
|
-
summary=(
|
|
575
|
-
f"coverage {percent:.1f}% "
|
|
576
|
-
f"(threshold {COVERAGE_THRESHOLD:.0f}%)"
|
|
577
|
-
),
|
|
578
|
-
details_path=str(coverage_xml),
|
|
579
|
-
)
|
|
722
|
+
return _coverage_from_xml(coverage_xml, project_dir, changed)
|
|
580
723
|
junit = project_dir / "junit.xml"
|
|
581
724
|
if junit.is_file():
|
|
582
725
|
return _junit_result(junit)
|
|
@@ -767,9 +910,10 @@ def _check_spellcheck(
|
|
|
767
910
|
def _spellcheck_inspected_count(project_dir: Path, md_files: list[str]) -> int:
|
|
768
911
|
"""How many of ``md_files`` codespell actually reads after `skip`.
|
|
769
912
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
913
|
+
Replays the config's skip globs with fnmatch rather than asking codespell
|
|
914
|
+
itself — per-file ``--count`` probing is too slow, and a skipped file
|
|
915
|
+
simply produces no output, so codespell cannot be asked which files it
|
|
916
|
+
ignored.
|
|
773
917
|
"""
|
|
774
918
|
patterns = _codespell_skip_globs(project_dir)
|
|
775
919
|
if not patterns:
|
|
@@ -23,7 +23,7 @@ _END_RE = re.compile(r"<!--\s*arkaos:managed:end\s*-->")
|
|
|
23
23
|
class MergeResult:
|
|
24
24
|
"""Outcome of a managed-region merge operation."""
|
|
25
25
|
|
|
26
|
-
status: Literal["updated", "unchanged", "error"]
|
|
26
|
+
status: Literal["updated", "restamped", "drifted", "unchanged", "error"]
|
|
27
27
|
new_text: str
|
|
28
28
|
error: str | None = None
|
|
29
29
|
|
|
@@ -38,52 +38,103 @@ def merge_managed_content(
|
|
|
38
38
|
) -> MergeResult:
|
|
39
39
|
"""Merge managed_content into target_text inside the managed region.
|
|
40
40
|
|
|
41
|
-
Returns
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
Returns "updated" when the managed content itself changes, "restamped"
|
|
42
|
+
when the verified-current content carries a stale stamp, "drifted" when
|
|
43
|
+
the block was edited in place (nothing is written), "unchanged" when
|
|
44
|
+
everything already matches, or "error" when markers are malformed.
|
|
44
45
|
"""
|
|
46
|
+
# Normalise once so the hash, the rendered block and the drift check all
|
|
47
|
+
# see the same bytes: hashing the unstripped input against a stripped
|
|
48
|
+
# body made the merger report `drifted` on output it wrote itself.
|
|
49
|
+
managed_content = managed_content.strip()
|
|
45
50
|
starts = list(_START_RE.finditer(target_text))
|
|
46
51
|
ends = list(_END_RE.finditer(target_text))
|
|
47
52
|
|
|
53
|
+
malformed = _validate_markers(starts, ends, target_text)
|
|
54
|
+
if malformed is not None:
|
|
55
|
+
return malformed
|
|
56
|
+
|
|
57
|
+
new_hash = compute_managed_hash(managed_content)
|
|
58
|
+
new_block = _render_block(managed_content, version, new_hash)
|
|
59
|
+
|
|
60
|
+
if not starts:
|
|
61
|
+
return _prepend_block(target_text, new_block)
|
|
62
|
+
|
|
63
|
+
start_match, end_match = starts[0], ends[0]
|
|
64
|
+
rewritten = (
|
|
65
|
+
target_text[: start_match.start()]
|
|
66
|
+
+ new_block
|
|
67
|
+
+ target_text[end_match.end() :]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
body = target_text[start_match.end() : end_match.start()].strip()
|
|
71
|
+
return _decide(
|
|
72
|
+
stamped_hash=start_match.group("hash"),
|
|
73
|
+
stamped_version=start_match.group("version"),
|
|
74
|
+
body=body,
|
|
75
|
+
new_hash=new_hash,
|
|
76
|
+
version=version,
|
|
77
|
+
target_text=target_text,
|
|
78
|
+
rewritten=rewritten,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _decide(
|
|
83
|
+
*,
|
|
84
|
+
stamped_hash: str | None,
|
|
85
|
+
stamped_version: str | None,
|
|
86
|
+
body: str,
|
|
87
|
+
new_hash: str,
|
|
88
|
+
version: str,
|
|
89
|
+
target_text: str,
|
|
90
|
+
rewritten: str,
|
|
91
|
+
) -> MergeResult:
|
|
92
|
+
"""Choose the outcome for a well-formed block."""
|
|
93
|
+
if stamped_hash != new_hash:
|
|
94
|
+
return MergeResult(status="updated", new_text=rewritten)
|
|
95
|
+
# The stamp says the content matches canonical — but the stamp is a
|
|
96
|
+
# claim, not a measurement. Hash what is ACTUALLY between the markers
|
|
97
|
+
# before rewriting: an operator who edited inside the block leaves the
|
|
98
|
+
# stamp untouched, and a rewrite would delete their work to correct a
|
|
99
|
+
# version number.
|
|
100
|
+
if compute_managed_hash(body) != new_hash:
|
|
101
|
+
return MergeResult(
|
|
102
|
+
status="drifted",
|
|
103
|
+
new_text=target_text,
|
|
104
|
+
error="managed block was edited in place; left untouched",
|
|
105
|
+
)
|
|
106
|
+
if stamped_version == version:
|
|
107
|
+
return MergeResult(status="unchanged", new_text=target_text)
|
|
108
|
+
# Content verified current; only the stamp lags. Rewriting it keeps
|
|
109
|
+
# `version=` an honest record of the last sync that verified this file.
|
|
110
|
+
return MergeResult(status="restamped", new_text=rewritten)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _validate_markers(
|
|
114
|
+
starts: list[re.Match[str]],
|
|
115
|
+
ends: list[re.Match[str]],
|
|
116
|
+
target_text: str,
|
|
117
|
+
) -> MergeResult | None:
|
|
118
|
+
"""Return an error MergeResult when the marker pair is malformed."""
|
|
48
119
|
if len(starts) != len(ends):
|
|
49
120
|
return MergeResult(
|
|
50
121
|
status="error",
|
|
51
122
|
new_text=target_text,
|
|
52
123
|
error=f"unbalanced markers: {len(starts)} starts, {len(ends)} ends",
|
|
53
124
|
)
|
|
54
|
-
|
|
55
125
|
if len(starts) > 1:
|
|
56
126
|
return MergeResult(
|
|
57
127
|
status="error",
|
|
58
128
|
new_text=target_text,
|
|
59
129
|
error="multiple managed blocks are not supported",
|
|
60
130
|
)
|
|
61
|
-
|
|
62
|
-
new_hash = compute_managed_hash(managed_content)
|
|
63
|
-
new_block = _render_block(managed_content, version, new_hash)
|
|
64
|
-
|
|
65
|
-
if not starts:
|
|
66
|
-
return _prepend_block(target_text, new_block)
|
|
67
|
-
|
|
68
|
-
start_match = starts[0]
|
|
69
|
-
end_match = ends[0]
|
|
70
|
-
if end_match.start() < start_match.end():
|
|
131
|
+
if starts and ends[0].start() < starts[0].end():
|
|
71
132
|
return MergeResult(
|
|
72
133
|
status="error",
|
|
73
134
|
new_text=target_text,
|
|
74
135
|
error="end marker appears before start marker",
|
|
75
136
|
)
|
|
76
|
-
|
|
77
|
-
existing_hash = start_match.group("hash")
|
|
78
|
-
if existing_hash == new_hash:
|
|
79
|
-
return MergeResult(status="unchanged", new_text=target_text)
|
|
80
|
-
|
|
81
|
-
new_text = (
|
|
82
|
-
target_text[: start_match.start()]
|
|
83
|
-
+ new_block
|
|
84
|
-
+ target_text[end_match.end() :]
|
|
85
|
-
)
|
|
86
|
-
return MergeResult(status="updated", new_text=new_text)
|
|
137
|
+
return None
|
|
87
138
|
|
|
88
139
|
|
|
89
140
|
def _render_block(content: str, version: str, content_hash: str) -> str:
|
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import os
|
|
21
21
|
import shutil
|
|
22
|
+
from dataclasses import dataclass, field
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
|
|
24
25
|
import yaml
|
|
@@ -51,39 +52,64 @@ def _do_sync(project: Project) -> ContentSyncResult:
|
|
|
51
52
|
project_claude = Path(project.path) / ".claude"
|
|
52
53
|
project_claude.mkdir(parents=True, exist_ok=True)
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if errored:
|
|
65
|
-
status = "error"
|
|
66
|
-
elif updated:
|
|
67
|
-
status = "updated"
|
|
68
|
-
else:
|
|
69
|
-
status = "unchanged"
|
|
55
|
+
out = _Artefacts()
|
|
56
|
+
_sync_claude_md(core, project, project_claude, version, out)
|
|
57
|
+
_sync_rules(core, project_claude, out.updated, out.unchanged, out.errored)
|
|
58
|
+
_sync_stack_rules(
|
|
59
|
+
core, project, project_claude, out.updated, out.unchanged, out.errored
|
|
60
|
+
)
|
|
61
|
+
_sync_hooks(core, project_claude, out.updated, out.unchanged, out.errored)
|
|
62
|
+
_sync_constitution(core, project_claude, out.updated, out.unchanged, out.errored)
|
|
63
|
+
|
|
70
64
|
return ContentSyncResult(
|
|
71
65
|
path=project.path,
|
|
72
|
-
status=status,
|
|
73
|
-
artefacts_updated=updated,
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
status=out.status(),
|
|
67
|
+
artefacts_updated=out.updated,
|
|
68
|
+
artefacts_restamped=out.restamped,
|
|
69
|
+
artefacts_drifted=out.drifted,
|
|
70
|
+
artefacts_unchanged=out.unchanged,
|
|
71
|
+
artefacts_errored=out.errored,
|
|
76
72
|
)
|
|
77
73
|
|
|
78
74
|
|
|
75
|
+
@dataclass
|
|
76
|
+
class _Artefacts:
|
|
77
|
+
"""Per-project artefact outcomes, one list per status.
|
|
78
|
+
|
|
79
|
+
Passed as a single object rather than four parallel out-parameters —
|
|
80
|
+
adding a fifth status would otherwise mean editing every signature that
|
|
81
|
+
threads them through.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
updated: list[str] = field(default_factory=list)
|
|
85
|
+
restamped: list[str] = field(default_factory=list)
|
|
86
|
+
drifted: list[str] = field(default_factory=list)
|
|
87
|
+
unchanged: list[str] = field(default_factory=list)
|
|
88
|
+
errored: list[str] = field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
def status(self) -> str:
|
|
91
|
+
"""Worst outcome wins: error > drift > real change > restamp > no-op.
|
|
92
|
+
|
|
93
|
+
Drift outranks a change because it needs the operator's judgement;
|
|
94
|
+
it is NOT an error — the merger deliberately preserved their edit.
|
|
95
|
+
"""
|
|
96
|
+
if self.errored:
|
|
97
|
+
return "error"
|
|
98
|
+
if self.drifted:
|
|
99
|
+
return "drifted"
|
|
100
|
+
if self.updated:
|
|
101
|
+
return "updated"
|
|
102
|
+
if self.restamped:
|
|
103
|
+
return "restamped"
|
|
104
|
+
return "unchanged"
|
|
105
|
+
|
|
106
|
+
|
|
79
107
|
def _sync_claude_md(
|
|
80
108
|
core: Path,
|
|
81
109
|
project: Project,
|
|
82
110
|
project_claude: Path,
|
|
83
111
|
version: str,
|
|
84
|
-
|
|
85
|
-
unchanged: list[str],
|
|
86
|
-
errored: list[str],
|
|
112
|
+
out: _Artefacts,
|
|
87
113
|
) -> None:
|
|
88
114
|
# Stack conventions live in path-scoped rule files (_sync_stack_rules);
|
|
89
115
|
# the managed block carries only the shared base.
|
|
@@ -92,18 +118,30 @@ def _sync_claude_md(
|
|
|
92
118
|
)
|
|
93
119
|
target_file = project_claude / "CLAUDE.md"
|
|
94
120
|
target_text = target_file.read_text(encoding="utf-8") if target_file.exists() else ""
|
|
95
|
-
|
|
96
121
|
result = merge_managed_content(target_text, managed_content, version)
|
|
122
|
+
_record_claude_md(result, target_file, managed_content, out)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _record_claude_md(result, target_file, managed_content: str, out) -> None:
|
|
126
|
+
"""Apply one merge outcome: write, skip, or record without writing."""
|
|
97
127
|
if result.status == "error":
|
|
98
|
-
errored.append(f"CLAUDE.md: {result.error}")
|
|
99
|
-
|
|
100
|
-
|
|
128
|
+
out.errored.append(f"CLAUDE.md: {result.error}")
|
|
129
|
+
target_file.with_suffix(".md.arkaos-new").write_text(
|
|
130
|
+
managed_content, encoding="utf-8"
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
if result.status == "drifted":
|
|
134
|
+
# The operator edited inside the managed block. Overwriting to fix a
|
|
135
|
+
# version number would delete their work; record it as drift — not as
|
|
136
|
+
# an error, because nothing failed.
|
|
137
|
+
out.drifted.append("CLAUDE.md")
|
|
101
138
|
return
|
|
102
139
|
if result.status == "unchanged":
|
|
103
|
-
unchanged.append("CLAUDE.md")
|
|
140
|
+
out.unchanged.append("CLAUDE.md")
|
|
104
141
|
return
|
|
105
142
|
target_file.write_text(result.new_text, encoding="utf-8")
|
|
106
|
-
|
|
143
|
+
bucket = out.restamped if result.status == "restamped" else out.updated
|
|
144
|
+
bucket.append("CLAUDE.md")
|
|
107
145
|
|
|
108
146
|
|
|
109
147
|
# Descriptor slug -> stack-rules basename (no .md). Slugs are case-folded first.
|
package/core/sync/engine.py
CHANGED
|
@@ -13,19 +13,20 @@ from pathlib import Path
|
|
|
13
13
|
|
|
14
14
|
from core.runtime.user_paths import (
|
|
15
15
|
ecosystems_file as resolve_ecosystems_file,
|
|
16
|
+
)
|
|
17
|
+
from core.runtime.user_paths import (
|
|
16
18
|
projects_dir as resolve_projects_dir,
|
|
17
19
|
)
|
|
18
|
-
from core.sync.
|
|
20
|
+
from core.sync.agent_provisioner import sync_all_agents
|
|
21
|
+
from core.sync.content_syncer import sync_all_content
|
|
22
|
+
from core.sync.descriptor_syncer import sync_all_descriptors
|
|
19
23
|
from core.sync.discovery import discover_all_projects
|
|
24
|
+
from core.sync.manifest import build_manifest
|
|
20
25
|
from core.sync.mcp_optimizer import optimize_all_mcps
|
|
21
26
|
from core.sync.mcp_syncer import sync_all_mcps
|
|
22
|
-
from core.sync.settings_syncer import sync_all_settings
|
|
23
|
-
from core.sync.descriptor_syncer import sync_all_descriptors
|
|
24
|
-
from core.sync.agent_provisioner import sync_all_agents
|
|
25
|
-
from core.sync.content_syncer import sync_all_content
|
|
26
27
|
from core.sync.reporter import build_report, format_report, write_sync_state
|
|
27
28
|
from core.sync.schema import SyncReport
|
|
28
|
-
|
|
29
|
+
from core.sync.settings_syncer import sync_all_settings
|
|
29
30
|
|
|
30
31
|
# ---------------------------------------------------------------------------
|
|
31
32
|
# Public API
|
|
@@ -42,20 +43,7 @@ def run_sync(arkaos_home: Path, skills_dir: Path, home_path: str) -> SyncReport:
|
|
|
42
43
|
|
|
43
44
|
projects = _discover_projects(arkaos_home, skills_dir)
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
mcp_results = sync_all_mcps(projects, registry_path, home_path)
|
|
47
|
-
|
|
48
|
-
policy_path = Path(__file__).resolve().parents[2] / "config" / "mcp-policy.yaml"
|
|
49
|
-
vault_path = Path.home() / ".arkaos" / "secrets.json"
|
|
50
|
-
cache_path = Path.home() / ".arkaos" / "mcp-decisions.cache.json"
|
|
51
|
-
if policy_path.exists():
|
|
52
|
-
mcp_results = optimize_all_mcps(
|
|
53
|
-
projects,
|
|
54
|
-
mcp_results,
|
|
55
|
-
policy_path,
|
|
56
|
-
vault_path if vault_path.exists() else None,
|
|
57
|
-
cache_path,
|
|
58
|
-
)
|
|
46
|
+
mcp_results = _run_mcp_phase(projects, skills_dir, home_path)
|
|
59
47
|
|
|
60
48
|
settings_results = sync_all_settings(mcp_results)
|
|
61
49
|
descriptor_results = sync_all_descriptors(projects)
|
|
@@ -111,6 +99,24 @@ def main() -> None:
|
|
|
111
99
|
# ---------------------------------------------------------------------------
|
|
112
100
|
|
|
113
101
|
|
|
102
|
+
def _run_mcp_phase(projects: list, skills_dir: Path, home_path: str) -> list:
|
|
103
|
+
"""Sync .mcp.json for every project, then apply the policy optimizer."""
|
|
104
|
+
registry_path = skills_dir / "arka" / "mcps" / "registry.json"
|
|
105
|
+
results = sync_all_mcps(projects, registry_path, home_path)
|
|
106
|
+
|
|
107
|
+
policy_path = Path(__file__).resolve().parents[2] / "config" / "mcp-policy.yaml"
|
|
108
|
+
if not policy_path.exists():
|
|
109
|
+
return results
|
|
110
|
+
vault_path = Path.home() / ".arkaos" / "secrets.json"
|
|
111
|
+
return optimize_all_mcps(
|
|
112
|
+
projects,
|
|
113
|
+
results,
|
|
114
|
+
policy_path,
|
|
115
|
+
vault_path if vault_path.exists() else None,
|
|
116
|
+
Path.home() / ".arkaos" / "mcp-decisions.cache.json",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
114
120
|
def _read_previous_version(arkaos_home: Path) -> str:
|
|
115
121
|
"""Read version field from sync-state.json, defaulting to pending-sync."""
|
|
116
122
|
state_file = arkaos_home / "sync-state.json"
|
|
@@ -161,6 +167,8 @@ def _resolve_features_dir(arkaos_home: Path) -> Path:
|
|
|
161
167
|
return fallback
|
|
162
168
|
|
|
163
169
|
|
|
170
|
+
|
|
171
|
+
|
|
164
172
|
def _parse_scan_dirs(projects_dir_str: str) -> list[Path]:
|
|
165
173
|
"""Parse a projectsDir string, extracting all paths starting with /."""
|
|
166
174
|
segments = re.split(r",\s*", projects_dir_str.strip())
|
package/core/sync/manifest.py
CHANGED
|
@@ -46,12 +46,47 @@ def build_manifest(
|
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def
|
|
50
|
-
"""
|
|
49
|
+
def compare_versions(version: str, baseline: str) -> bool | None:
|
|
50
|
+
"""True if version > baseline, False if not, None if unorderable.
|
|
51
|
+
|
|
52
|
+
``None`` is deliberately distinct from ``False``. A baseline of
|
|
53
|
+
``unknown`` — which ``engine._read_current_version`` emits on a degraded
|
|
54
|
+
run and ``write_sync_state`` then persists — is not the same fact as "no
|
|
55
|
+
feature is newer", and collapsing the two makes a broken install report
|
|
56
|
+
"nothing changed" forever, with no error anywhere.
|
|
57
|
+
"""
|
|
51
58
|
def parse(v: str) -> tuple[int, ...]:
|
|
52
59
|
return tuple(int(part) for part in v.split("."))
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
try:
|
|
62
|
+
return parse(version) > parse(baseline)
|
|
63
|
+
except ValueError:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_version_newer(version: str, baseline: str) -> bool:
|
|
68
|
+
"""True only when the pair is orderable and version is strictly newer.
|
|
69
|
+
|
|
70
|
+
The conservative reading, for callers where doing nothing is the safe
|
|
71
|
+
answer. Callers that must distinguish "unknown baseline" from "nothing
|
|
72
|
+
is new" call :func:`compare_versions` instead.
|
|
73
|
+
"""
|
|
74
|
+
return compare_versions(version, baseline) is True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Backwards-compatible private alias (used by existing call sites and tests).
|
|
78
|
+
_is_version_newer = is_version_newer
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _baseline_is_unorderable(previous_version: str) -> bool:
|
|
82
|
+
"""True when previous_version itself cannot be ordered.
|
|
83
|
+
|
|
84
|
+
Tested against a known-good literal, never against the feature versions:
|
|
85
|
+
comparing the *pair* meant one malformed ``added_in`` in the registry
|
|
86
|
+
made an otherwise fine baseline look unorderable, and deprecated
|
|
87
|
+
features then stopped being removed — silently.
|
|
88
|
+
"""
|
|
89
|
+
return compare_versions("0.0.0", previous_version) is None
|
|
55
90
|
|
|
56
91
|
|
|
57
92
|
def _find_new_features(
|
|
@@ -59,14 +94,19 @@ def _find_new_features(
|
|
|
59
94
|
previous_version: str,
|
|
60
95
|
is_first: bool,
|
|
61
96
|
) -> list[str]:
|
|
62
|
-
"""Return names of features that are new relative to previous_version.
|
|
63
|
-
|
|
97
|
+
"""Return names of features that are new relative to previous_version.
|
|
98
|
+
|
|
99
|
+
An unorderable baseline is treated as a first sync rather than as "nothing
|
|
100
|
+
is new": answering "nothing" would leave a degraded install permanently
|
|
101
|
+
convinced it is up to date.
|
|
102
|
+
"""
|
|
103
|
+
if is_first or _baseline_is_unorderable(previous_version):
|
|
64
104
|
return [f.name for f in features if f.deprecated_in is None]
|
|
65
105
|
|
|
66
106
|
return [
|
|
67
107
|
f.name
|
|
68
108
|
for f in features
|
|
69
|
-
if
|
|
109
|
+
if compare_versions(f.added_in, previous_version) is True
|
|
70
110
|
]
|
|
71
111
|
|
|
72
112
|
|
|
@@ -76,12 +116,12 @@ def _find_deprecated_features(
|
|
|
76
116
|
is_first: bool,
|
|
77
117
|
) -> list[str]:
|
|
78
118
|
"""Return names of features deprecated after previous_version."""
|
|
79
|
-
if is_first:
|
|
119
|
+
if is_first or _baseline_is_unorderable(previous_version):
|
|
80
120
|
return []
|
|
81
121
|
|
|
82
122
|
return [
|
|
83
123
|
f.name
|
|
84
124
|
for f in features
|
|
85
125
|
if f.deprecated_in is not None
|
|
86
|
-
and
|
|
126
|
+
and compare_versions(f.deprecated_in, previous_version) is True
|
|
87
127
|
]
|
package/core/sync/reporter.py
CHANGED
|
@@ -6,7 +6,7 @@ Builds the sync report, writes sync state to disk, and formats terminal output.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
-
from datetime import
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
from core.sync.schema import (
|
|
@@ -40,14 +40,7 @@ def build_report(
|
|
|
40
40
|
agent_results: list[AgentProvisionResult] | None = None,
|
|
41
41
|
) -> SyncReport:
|
|
42
42
|
"""Aggregate all sync results into a SyncReport."""
|
|
43
|
-
|
|
44
|
-
mcp_results,
|
|
45
|
-
settings_results,
|
|
46
|
-
descriptor_results,
|
|
47
|
-
skill_results,
|
|
48
|
-
content_results=content_results,
|
|
49
|
-
agent_results=agent_results,
|
|
50
|
-
)
|
|
43
|
+
phases = (mcp_results, settings_results, descriptor_results, skill_results)
|
|
51
44
|
return SyncReport(
|
|
52
45
|
previous_version=previous_version,
|
|
53
46
|
current_version=current_version,
|
|
@@ -59,7 +52,9 @@ def build_report(
|
|
|
59
52
|
skill_results=skill_results,
|
|
60
53
|
content_results=content_results or [],
|
|
61
54
|
agent_results=agent_results or [],
|
|
62
|
-
errors=
|
|
55
|
+
errors=_collect_errors(
|
|
56
|
+
*phases, content_results=content_results, agent_results=agent_results
|
|
57
|
+
),
|
|
63
58
|
)
|
|
64
59
|
|
|
65
60
|
|
|
@@ -69,7 +64,7 @@ def write_sync_state(state_file: Path, report: SyncReport) -> None:
|
|
|
69
64
|
unique_paths = {r.path for r in report.mcp_results}
|
|
70
65
|
state = {
|
|
71
66
|
"version": report.current_version,
|
|
72
|
-
"last_sync": datetime.now(
|
|
67
|
+
"last_sync": datetime.now(UTC).isoformat(),
|
|
73
68
|
"projects_synced": len(unique_paths),
|
|
74
69
|
"skills_synced": len(report.skill_results),
|
|
75
70
|
"errors": report.errors,
|
|
@@ -94,22 +89,22 @@ def format_report(report: SyncReport) -> str:
|
|
|
94
89
|
|
|
95
90
|
key_changes = _format_key_changes(report)
|
|
96
91
|
if key_changes:
|
|
97
|
-
lines += ["", " Key changes:"]
|
|
98
|
-
lines += [f" - {c}" for c in key_changes]
|
|
99
|
-
|
|
100
|
-
total_deferred = sum(len(r.mcps_deferred) for r in report.mcp_results)
|
|
101
|
-
projects_with_deferred = sum(1 for r in report.mcp_results if r.mcps_deferred)
|
|
102
|
-
if total_deferred > 0:
|
|
103
|
-
lines += ["", f" Deferred MCPs: {total_deferred} across {projects_with_deferred} projects."]
|
|
92
|
+
lines += ["", " Key changes:", *[f" - {c}" for c in key_changes]]
|
|
104
93
|
|
|
105
|
-
lines +=
|
|
106
|
-
|
|
107
|
-
f" Errors: {len(report.errors)}",
|
|
108
|
-
_SEPARATOR,
|
|
109
|
-
]
|
|
94
|
+
lines += _format_deferred_lines(report.mcp_results)
|
|
95
|
+
lines += ["", f" Errors: {len(report.errors)}", _SEPARATOR]
|
|
110
96
|
return "\n".join(lines)
|
|
111
97
|
|
|
112
98
|
|
|
99
|
+
def _format_deferred_lines(results: list[McpSyncResult]) -> list[str]:
|
|
100
|
+
"""Deferred MCPs, or nothing when none were deferred."""
|
|
101
|
+
total = sum(len(r.mcps_deferred) for r in results)
|
|
102
|
+
if total == 0:
|
|
103
|
+
return []
|
|
104
|
+
projects = sum(1 for r in results if r.mcps_deferred)
|
|
105
|
+
return ["", f" Deferred MCPs: {total} across {projects} projects."]
|
|
106
|
+
|
|
107
|
+
|
|
113
108
|
# ---------------------------------------------------------------------------
|
|
114
109
|
# Private helpers
|
|
115
110
|
# ---------------------------------------------------------------------------
|
|
@@ -138,16 +133,25 @@ def _collect_errors(
|
|
|
138
133
|
for r in skills:
|
|
139
134
|
if r.error:
|
|
140
135
|
errors.append(f"Skill({r.skill_name}): {r.error}")
|
|
136
|
+
errors += _content_and_agent_errors(content_results, agent_results)
|
|
137
|
+
return errors
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _content_and_agent_errors(
|
|
141
|
+
content_results: list[ContentSyncResult] | None,
|
|
142
|
+
agent_results: list[AgentProvisionResult] | None,
|
|
143
|
+
) -> list[str]:
|
|
144
|
+
errors: list[str] = []
|
|
141
145
|
for r in content_results or []:
|
|
142
146
|
if r.error:
|
|
143
147
|
errors.append(f"Content({r.path}): {r.error}")
|
|
144
|
-
for
|
|
145
|
-
errors.append(f"Content({r.path}): {artefact_error}")
|
|
148
|
+
errors += [f"Content({r.path}): {e}" for e in r.artefacts_errored]
|
|
146
149
|
for r in agent_results or []:
|
|
147
150
|
if r.error:
|
|
148
151
|
errors.append(f"Agents({r.path}): {r.error}")
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
errors += [
|
|
153
|
+
f"Agents({r.path}): missing core file for {a}" for a in r.agents_errored
|
|
154
|
+
]
|
|
151
155
|
return errors
|
|
152
156
|
|
|
153
157
|
|
|
@@ -169,8 +173,12 @@ def _format_phase_line(label: str, results: list) -> str:
|
|
|
169
173
|
def _format_skill_line(results: list[SkillSyncResult]) -> str:
|
|
170
174
|
total = len(results)
|
|
171
175
|
updated = _count_updated(results)
|
|
176
|
+
restamped = sum(1 for r in results if r.status == "restamped")
|
|
172
177
|
unchanged = _count_unchanged(results)
|
|
173
|
-
|
|
178
|
+
counts = f"{updated} updated, {unchanged} unchanged"
|
|
179
|
+
if restamped:
|
|
180
|
+
counts = f"{updated} updated, {restamped} restamped, {unchanged} unchanged"
|
|
181
|
+
return f" {'Skills:':<14}{total} ecosystems synced ({counts})"
|
|
174
182
|
|
|
175
183
|
|
|
176
184
|
def _format_key_changes(report: SyncReport) -> list[str]:
|
|
@@ -215,8 +223,20 @@ def _add_skill_changes(results: list[SkillSyncResult], changes: list[str]) -> No
|
|
|
215
223
|
def _format_content_line(results: list[ContentSyncResult]) -> str:
|
|
216
224
|
total = len(results)
|
|
217
225
|
updated = _count_updated(results)
|
|
226
|
+
restamped = sum(1 for r in results if r.status == "restamped")
|
|
218
227
|
unchanged = _count_unchanged(results)
|
|
219
|
-
|
|
228
|
+
drifted = sum(1 for r in results if r.status == "drifted")
|
|
229
|
+
errored = sum(1 for r in results if r.status == "error")
|
|
230
|
+
counts = f"{updated} updated, {unchanged} unchanged"
|
|
231
|
+
if restamped:
|
|
232
|
+
counts = f"{updated} updated, {restamped} restamped, {unchanged} unchanged"
|
|
233
|
+
if drifted:
|
|
234
|
+
# Drift is not failure: the merger deliberately preserved an edit the
|
|
235
|
+
# operator made inside a managed block. It gets its own word.
|
|
236
|
+
counts += f", {drifted} drifted"
|
|
237
|
+
if errored:
|
|
238
|
+
counts += f", {errored} errored"
|
|
239
|
+
return f" {'Content:':<14}{total} synced ({counts})"
|
|
220
240
|
|
|
221
241
|
|
|
222
242
|
def _format_agents_line(results: list[AgentProvisionResult]) -> str:
|
package/core/sync/schema.py
CHANGED
|
@@ -88,6 +88,8 @@ class ContentSyncResult(BaseModel):
|
|
|
88
88
|
path: str
|
|
89
89
|
status: str
|
|
90
90
|
artefacts_updated: list[str] = Field(default_factory=list)
|
|
91
|
+
artefacts_restamped: list[str] = Field(default_factory=list)
|
|
92
|
+
artefacts_drifted: list[str] = Field(default_factory=list)
|
|
91
93
|
artefacts_unchanged: list[str] = Field(default_factory=list)
|
|
92
94
|
artefacts_errored: list[str] = Field(default_factory=list)
|
|
93
95
|
error: str | None = None
|
|
@@ -77,7 +77,7 @@ Returns JSON report for downstream consumption.
|
|
|
77
77
|
YAML files under `core/sync/features/*.yaml` (or `~/.arkaos/config/sync/features/*.yaml`). Each feature has:
|
|
78
78
|
- `detection_pattern` — regex searched in ecosystem SKILL.md to decide if the feature is already present. Matches any of: the `arka:feature:<name>` marker, the bare `## <section_title>` heading (legacy/customized sections), or — only where a token is unique enough to never appear in unrelated prose (e.g. `arka-forge`) — a historical keyword
|
|
79
79
|
- `content` — the section to inject if missing, wrapped in `<!-- arka:feature:<name>:start -->` / `<!-- arka:feature:<name>:end -->` markers so future runs detect it and deprecation can remove it precisely
|
|
80
|
-
- `deprecated_in` — if set, the
|
|
80
|
+
- `deprecated_in` — if set, the section is removed ONLY when a single, well-formed marker pair exists. An unmarked `## <section_title>` section is never deleted; it is reported and left in place.
|
|
81
81
|
|
|
82
82
|
The registry is self-detecting by contract: `detection_pattern` MUST match the feature's own `content` (locked by `tests/python/test_sync_features_registry.py`), otherwise every naive sync re-injects a duplicate section.
|
|
83
83
|
|
|
@@ -34,7 +34,7 @@ AI-powered sync that updates ecosystem skills, project descriptors, MCP configs,
|
|
|
34
34
|
|
|
35
35
|
## Hybrid Orchestration
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
The Python engine (see `sync-engine.md`) runs every deterministic phase: manifest, discovery, MCP sync, settings sync, descriptors, content sync (CLAUDE.md managed block, rules, hooks, constitution excerpt), agent provisioning, and state. Phase 4 runs as ONE AI subagent to handle intelligent ecosystem-skill text updates.
|
|
38
38
|
|
|
39
39
|
### Phase 4 — Intelligent Sync (AI Subagent)
|
|
40
40
|
|
|
@@ -50,7 +50,9 @@ After the Python engine completes, dispatch ONE subagent.
|
|
|
50
50
|
- Apply the `detection_pattern` regex to the SKILL.md text. It matches the `arka:feature:<name>` marker, the bare `## <section_title>` heading, or a unique historical keyword — a customized section without markers still counts as present and MUST NOT be duplicated.
|
|
51
51
|
- If NOT found: inject `content` (already marker-wrapped) after the last existing feature section, or after the "Commands" table if no feature sections exist (before "Orchestration Workflows").
|
|
52
52
|
3. For each feature where `deprecated_in` is set:
|
|
53
|
-
- Remove the `<!-- arka:feature:<name>:start -->` … `:end -->` block when
|
|
53
|
+
- Remove the `<!-- arka:feature:<name>:start -->` … `:end -->` block ONLY when a single, well-formed marker pair exists.
|
|
54
|
+
- **Never delete an unmarked `## <section_title>` section.** Without markers there is no way to tell ArkaOS's own text from the project's, and a section the operator customized looks identical to one they did not. Leave it in place and report it instead. `~/.claude/skills/` is not a git repository — a wrong deletion there is unrecoverable.
|
|
55
|
+
- If the markers are unbalanced, duplicated or inverted, do not edit the file at all; report the fault.
|
|
54
56
|
4. PRESERVE all custom content: commands, architecture, tech stack, business descriptions, ecosystem-specific workflow details.
|
|
55
57
|
|
|
56
58
|
### Report
|
package/harness/codex/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ArkaOS — The Operating System for AI Agent Teams
|
|
2
2
|
|
|
3
|
-
> v5.
|
|
3
|
+
> v5.10.0 — 89 agents, 17 departments, 340 skills. Generated by `scripts/harness_gen.py`; do not edit.
|
|
4
4
|
|
|
5
5
|
You are operating within ArkaOS. Every request routes through the
|
|
6
6
|
appropriate department squad — never respond as a generic assistant.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ArkaOS — The Operating System for AI Agent Teams
|
|
2
2
|
|
|
3
|
-
> v5.
|
|
3
|
+
> v5.10.0 — 89 agents, 17 departments, 340 skills. Generated by `scripts/harness_gen.py`; do not edit.
|
|
4
4
|
|
|
5
5
|
You are operating within ArkaOS. Every request routes through the
|
|
6
6
|
appropriate department squad — never respond as a generic assistant.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: ArkaOS v5.
|
|
2
|
+
description: ArkaOS v5.10.0 agent-team contract
|
|
3
3
|
alwaysApply: true
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# ArkaOS — The Operating System for AI Agent Teams
|
|
7
7
|
|
|
8
|
-
> v5.
|
|
8
|
+
> v5.10.0 — 89 agents, 17 departments, 340 skills. Generated by `scripts/harness_gen.py`; do not edit.
|
|
9
9
|
|
|
10
10
|
You are operating within ArkaOS. Every request routes through the
|
|
11
11
|
appropriate department squad — never respond as a generic assistant.
|
package/harness/gemini/GEMINI.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ArkaOS — The Operating System for AI Agent Teams
|
|
2
2
|
|
|
3
|
-
> v5.
|
|
3
|
+
> v5.10.0 — 89 agents, 17 departments, 340 skills. Generated by `scripts/harness_gen.py`; do not edit.
|
|
4
4
|
|
|
5
5
|
You are operating within ArkaOS. Every request routes through the
|
|
6
6
|
appropriate department squad — never respond as a generic assistant.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ArkaOS — The Operating System for AI Agent Teams
|
|
2
2
|
|
|
3
|
-
> v5.
|
|
3
|
+
> v5.10.0 — 89 agents, 17 departments, 340 skills. Generated by `scripts/harness_gen.py`; do not edit.
|
|
4
4
|
|
|
5
5
|
You are operating within ArkaOS. Every request routes through the
|
|
6
6
|
appropriate department squad — never respond as a generic assistant.
|
|
@@ -3,7 +3,7 @@ description: "Software Architect — ArkaOS /dev department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Gabriel, Software Architect of the ArkaOS /dev department (v5.
|
|
6
|
+
You are Gabriel, Software Architect of the ArkaOS /dev department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: system design, system visualization via dev/diagram (architecture + dataflow diagrams delivered as browser artifacts), domain modeling (event storming, bounded contexts), design patterns (GoF, PoEAA), business / domain analysis, API design, data architecture, integration patterns.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Creative Director — ArkaOS /brand department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Valentina, Creative Director of the ArkaOS /brand department (v5.
|
|
6
|
+
You are Valentina, Creative Director of the ArkaOS /brand department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: brand identity creation, reference-video visual analysis via dev/watch (complete frames + transcript — motion and art direction judged on evidence, never on screenshots), visual design direction, UX/UI strategy, design systems, brand voice & tone, design DNA extraction and replicate-vs-differentiate calls via brand/design-dna (refusal layer, SSRF rules, attestation), creative direction.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Chief Financial Officer — ArkaOS /fin department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Helena, Chief Financial Officer of the ArkaOS /fin department (v5.
|
|
6
|
+
You are Helena, Chief Financial Officer of the ArkaOS /fin department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: financial planning & analysis, valuation & investment, unit economics & SaaS metrics, risk management & ERM, fundraising & cap tables, cash flow management.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Chief of Staff & Governance Lead — ArkaOS /org department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Afonso, Chief of Staff & Governance Lead of the ArkaOS /org department (v5.
|
|
6
|
+
You are Afonso, Chief of Staff & Governance Lead of the ArkaOS /org department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: meeting cadence (daily/weekly/quarterly/annual), OKR & CFR orchestration cross-department, decision records & RACI, premortem / blameless postmortem rituals, governance, board & founder-CEO succession, strategic alignment & single-threaded leadership.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Community Strategist — ArkaOS /community department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Beatriz, Community Strategist of the ArkaOS /community department (v5.
|
|
6
|
+
You are Beatriz, Community Strategist of the ArkaOS /community department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: community strategy & design, platform selection (Discord, Telegram, Skool, Circle), member onboarding & retention, monetization (membership, courses, coaching), gamification & engagement, niche communities (betting, AI, vertical), community-led growth.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Content Strategist — ArkaOS /content department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Rafael, Content Strategist of the ArkaOS /content department (v5.
|
|
6
|
+
You are Rafael, Content Strategist of the ArkaOS /content department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: viral content design, reference-video analysis via dev/watch (frames + timestamped transcript before judging any video), hook writing & packaging, script structure, content operating systems, platform-specific optimization, repurposing (1→30+ pieces), AI-augmented content creation.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Conversion Strategist — ArkaOS /landing department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Ines, Conversion Strategist of the ArkaOS /landing department (v5.
|
|
6
|
+
You are Ines, Conversion Strategist of the ArkaOS /landing department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: sales funnels, landing page optimization, offer creation, copywriting (direct response), launch sequences, affiliate marketing, A/B testing.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Chief Operations Officer — ArkaOS /org department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Sofia, Chief Operations Officer of the ArkaOS /org department (v5.
|
|
6
|
+
You are Sofia, Chief Operations Officer of the ArkaOS /org department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: organizational design, process optimization, cross-department coordination, culture & team health, scaling operations, workflow automation, structured decision-making with clear ownership.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Copy & Language Director — ArkaOS /quality department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Eduardo, Copy & Language Director of the ArkaOS /quality department (v5.
|
|
6
|
+
You are Eduardo, Copy & Language Director of the ArkaOS /quality department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: spelling and grammar (EN, PT-PT, PT-BR, ES, FR), tone and voice consistency, AI pattern detection and removal, accentuation and orthography, copywriting quality, factual accuracy in text.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Chief Quality Officer — ArkaOS /quality department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Marta, Chief Quality Officer of the ArkaOS /quality department (v5.
|
|
6
|
+
You are Marta, Chief Quality Officer of the ArkaOS /quality department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: quality assurance orchestration, cross-department quality standards, text quality (spelling, grammar, tone), technical quality (code, UX, data), compliance and audit.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Chief Technology Officer — ArkaOS /dev department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Marco, Chief Technology Officer of the ArkaOS /dev department (v5.
|
|
6
|
+
You are Marco, Chief Technology Officer of the ArkaOS /dev department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: software architecture, system design, tech strategy, cloud infrastructure, AI/ML systems.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Design Ops Lead — ArkaOS /brand department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Iris, Design Ops Lead of the ArkaOS /brand department (v5.
|
|
6
|
+
You are Iris, Design Ops Lead of the ArkaOS /brand department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: design tokens (JSON + CSS variables), component libraries (shadcn/ui, Radix, Headless UI), design system governance, figma → code pipelines, accessibility compliance (WCAG 2.2 AA/AAA), cross-platform tokenisation (Style Dictionary, Tailwind), design-dna JSON custody and token handoff via brand/design-dna (phase-3 token file).
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "E-Commerce Director — ArkaOS /ecom department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Ricardo, E-Commerce Director of the ArkaOS /ecom department (v5.
|
|
6
|
+
You are Ricardo, E-Commerce Director of the ArkaOS /ecom department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: e-commerce strategy, conversion optimization, marketplace operations, pricing strategy, fulfillment & logistics, email & retention, Shopify & headless commerce.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Knowledge Director — ArkaOS /kb department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Clara, Knowledge Director of the ArkaOS /kb department (v5.
|
|
6
|
+
You are Clara, Knowledge Director of the ArkaOS /kb department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: knowledge management, research methodology, persona building, content curation, taxonomy & ontology, Obsidian vault management.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Leadership & People Director — ArkaOS /lead department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Rodrigo, Leadership & People Director of the ArkaOS /lead department (v5.
|
|
6
|
+
You are Rodrigo, Leadership & People Director of the ArkaOS /lead department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: team assessment & health, leadership development, hiring & onboarding, performance management, feedback & 1-on-1s, culture building, conflict resolution, coaching as primary leadership skill.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Marketing Director — ArkaOS /mkt department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Luna, Marketing Director of the ArkaOS /mkt department (v5.
|
|
6
|
+
You are Luna, Marketing Director of the ArkaOS /mkt department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: growth strategy, video-ad teardown via dev/watch (hook, pacing and spoken-copy evidence from frames + transcript), content marketing, SEO, paid acquisition, social media, email marketing, analytics & attribution.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Operations Lead — ArkaOS /ops department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Daniel, Operations Lead of the ArkaOS /ops department (v5.
|
|
6
|
+
You are Daniel, Operations Lead of the ArkaOS /ops department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: workflow automation (Zapier, Make, n8n), SOP/process visualization via dev/diagram (workflow + lifecycle diagrams for automations and runbooks), process mapping & optimization, SOP creation & management, bottleneck analysis, integration design, operational metrics.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Product Manager — ArkaOS /pm department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Carolina, Product Manager of the ArkaOS /pm department (v5.
|
|
6
|
+
You are Carolina, Product Manager of the ArkaOS /pm department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: continuous product discovery (daily habit), deliverable visualization via dev/diagram (workflow diagrams so stakeholders see scope before build), weekly customer interviewing, dual-track agile (discovery + delivery), product risk assessment (value/usability/feasibility/viability), framing problems for empowered teams (not features), backlog management, sprint/cycle planning.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "RevOps Lead — ArkaOS /saas department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Vicente, RevOps Lead of the ArkaOS /saas department (v5.
|
|
6
|
+
You are Vicente, RevOps Lead of the ArkaOS /saas department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: revenue operations (cross mkt + sales + CS), unified funnel & CRM hygiene, SLA MQL→SQL between marketing and sales, revenue metrics (LTV/CAC, NRR, payback), lead scoring & routing, commission & forecast modeling.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "SaaS Strategist — ArkaOS /saas department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Tiago, SaaS Strategist of the ArkaOS /saas department (v5.
|
|
6
|
+
You are Tiago, SaaS Strategist of the ArkaOS /saas department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: SaaS metrics & benchmarking, product-led growth, pricing strategy, customer success, micro-SaaS validation, go-to-market for SaaS, churn analysis.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Sales Director — ArkaOS /sales department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Miguel, Sales Director of the ArkaOS /sales department (v5.
|
|
6
|
+
You are Miguel, Sales Director of the ArkaOS /sales department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: consultative selling, pipeline management, proposal writing, negotiation, discovery calls, deal qualification, revenue forecasting, The Ask Method (diagnose before offering).
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Chief Strategist — ArkaOS /strat department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Tomas, Chief Strategist of the ArkaOS /strat department (v5.
|
|
6
|
+
You are Tomas, Chief Strategist of the ArkaOS /strat department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: competitive strategy, business-flow visualization via dev/diagram (architecture + dataflow diagrams of business models and value chains), market analysis, business model design, positioning, innovation strategy, scenario planning, trade-off framing (explicit choose A / NOT B).
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Technical & UX Quality Director — ArkaOS /quality department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Francisca, Technical & UX Quality Director of the ArkaOS /quality department (v5.
|
|
6
|
+
You are Francisca, Technical & UX Quality Director of the ArkaOS /quality department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: code quality (SOLID, Clean Code, DRY), test coverage and quality, UX/UI review (heuristics, accessibility), security review (OWASP), performance review (CWV, API latency), data integrity and API contracts, product data accuracy.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Tech Lead — ArkaOS /dev department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Paulo, Tech Lead of the ArkaOS /dev department (v5.
|
|
6
|
+
You are Paulo, Tech Lead of the ArkaOS /dev department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: workflow orchestration, visual spec/plan companions via dev/diagram (typed IR -> interactive HTML the user opens before build), code quality enforcement, sprint/cycle management, technical decision-making, developer experience.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Video Producer & Production Lead — ArkaOS /content department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Simão, Video Producer & Production Lead of the ArkaOS /content department (v5.
|
|
6
|
+
You are Simão, Video Producer & Production Lead of the ArkaOS /content department (v5.10.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: video production pipelines (script → storyboard → assets → edit → render), cut review via dev/watch (frame + transcript evidence on own renders before the Quality Gate), Hyperframes video-as-code editing (HTML/CSS/JS + GSAP → MP4), Higgsfield generation orchestration (image, video, audio, motion control, upscale, reframe), shot lists and EDLs (scene/shot/VO/on-screen-text columns), transcription-synced cuts and word-level captions, multi-format delivery (16:9, 9:16, 1:1; YouTube, Reels, TikTok), backend degradation planning (full → server-side → edit-ready package).
|
|
9
9
|
|
package/harness/zed/.rules
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ArkaOS — The Operating System for AI Agent Teams
|
|
2
2
|
|
|
3
|
-
> v5.
|
|
3
|
+
> v5.10.0 — 89 agents, 17 departments, 340 skills. Generated by `scripts/harness_gen.py`; do not edit.
|
|
4
4
|
|
|
5
5
|
You are operating within ArkaOS. Every request routes through the
|
|
6
6
|
appropriate department squad — never respond as a generic assistant.
|
package/package.json
CHANGED