arkaos 4.14.2 → 4.14.3
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/config/agent-roster.json +36 -0
- package/core/agents/roster_manifest.py +114 -0
- package/installer/skill-deploy.js +41 -6
- package/knowledge/skills-manifest.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.14.
|
|
1
|
+
4.14.3
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"generator": "core/agents/roster_manifest.py",
|
|
4
|
+
"purpose": "every specialist-gate owner resolves to a dispatchable agent source; installer/skill-deploy.js deploys these"
|
|
5
|
+
},
|
|
6
|
+
"gate_owners": {
|
|
7
|
+
"architect": {
|
|
8
|
+
"compiled": false,
|
|
9
|
+
"source": "departments/dev/agents/architect.md"
|
|
10
|
+
},
|
|
11
|
+
"backend-dev": {
|
|
12
|
+
"compiled": true,
|
|
13
|
+
"source": "config/claude-agents/backend-dev.md"
|
|
14
|
+
},
|
|
15
|
+
"dba": {
|
|
16
|
+
"compiled": true,
|
|
17
|
+
"source": "config/claude-agents/dba.md"
|
|
18
|
+
},
|
|
19
|
+
"devops-eng": {
|
|
20
|
+
"compiled": true,
|
|
21
|
+
"source": "config/claude-agents/devops-eng.md"
|
|
22
|
+
},
|
|
23
|
+
"frontend-dev": {
|
|
24
|
+
"compiled": false,
|
|
25
|
+
"source": "departments/dev/agents/frontend-dev.md"
|
|
26
|
+
},
|
|
27
|
+
"security-eng": {
|
|
28
|
+
"compiled": true,
|
|
29
|
+
"source": "config/claude-agents/security-eng.md"
|
|
30
|
+
},
|
|
31
|
+
"senior-dev": {
|
|
32
|
+
"compiled": false,
|
|
33
|
+
"source": "departments/dev/agents/senior-dev.md"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Agent roster manifest — every gate owner must be dispatchable.
|
|
2
|
+
|
|
3
|
+
Root cause this kills (operator incident, 2026-07-12): the specialist
|
|
4
|
+
gate blocked a lead from writing ``app/Http/Controllers/`` and told it
|
|
5
|
+
to dispatch ``senior-dev`` or ``backend-dev`` — but 4 of the 7 owners
|
|
6
|
+
named in ``config/agent-ownership.yaml`` (backend-dev, dba, devops-eng,
|
|
7
|
+
security-eng) were NOT dispatchable on real machines: they exist only
|
|
8
|
+
as department YAML, and the installer's agent deploy only ships ``.md``
|
|
9
|
+
files. A gate that demands an owner nobody can invoke is a dead end,
|
|
10
|
+
and dead ends teach sessions to rationalize bypasses.
|
|
11
|
+
|
|
12
|
+
This module is the single writer of ``config/agent-roster.json``:
|
|
13
|
+
|
|
14
|
+
python -m core.agents.roster_manifest
|
|
15
|
+
|
|
16
|
+
For every owner the ownership rules name, it resolves a dispatchable
|
|
17
|
+
source — a hand-authored ``departments/*/agents/<slug>.md`` (legacy,
|
|
18
|
+
wins because it may be hand-tuned) or the compiled
|
|
19
|
+
``config/claude-agents/<slug>.md`` (generated from the agent YAML by
|
|
20
|
+
the behavioral compiler) — and REFUSES to emit when neither exists:
|
|
21
|
+
a missing source is a build-time failure, never a runtime dead end.
|
|
22
|
+
``installer/skill-deploy.js`` reads the manifest and deploys every
|
|
23
|
+
gate owner; ``tests/python/test_agent_roster.py`` drift-gates it.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
34
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
35
|
+
OWNERSHIP_YAML = REPO_ROOT / "config" / "agent-ownership.yaml"
|
|
36
|
+
COMPILED_DIR = REPO_ROOT / "config" / "claude-agents"
|
|
37
|
+
DEPARTMENTS_DIR = REPO_ROOT / "departments"
|
|
38
|
+
ROSTER_JSON = REPO_ROOT / "config" / "agent-roster.json"
|
|
39
|
+
|
|
40
|
+
_NAME_RE = re.compile(r"^name:\s*(\S+)\s*$", re.MULTILINE)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def gate_owners() -> list[str]:
|
|
44
|
+
"""Every owner slug the ownership rules can demand (never ``*``)."""
|
|
45
|
+
config = yaml.safe_load(OWNERSHIP_YAML.read_text(encoding="utf-8"))
|
|
46
|
+
return sorted({
|
|
47
|
+
owner
|
|
48
|
+
for rule in config["ownership"]
|
|
49
|
+
for owner in (rule.get("owners") or [])
|
|
50
|
+
if owner != "*"
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _frontmatter_name(md_path: Path) -> str | None:
|
|
55
|
+
match = _NAME_RE.search(md_path.read_text(encoding="utf-8")[:2000])
|
|
56
|
+
return match.group(1) if match else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _resolve_source(slug: str) -> Path | None:
|
|
60
|
+
"""Dispatchable source for a slug: hand-authored dept .md wins,
|
|
61
|
+
compiled claude-agents .md is the generated fallback."""
|
|
62
|
+
for candidate in sorted(DEPARTMENTS_DIR.glob(f"*/agents/{slug}.md")):
|
|
63
|
+
if _frontmatter_name(candidate) == slug:
|
|
64
|
+
return candidate
|
|
65
|
+
compiled = COMPILED_DIR / f"{slug}.md"
|
|
66
|
+
if compiled.is_file() and _frontmatter_name(compiled) == slug:
|
|
67
|
+
return compiled
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_roster() -> dict:
|
|
72
|
+
owners = gate_owners()
|
|
73
|
+
entries: dict[str, dict] = {}
|
|
74
|
+
ghosts: list[str] = []
|
|
75
|
+
for slug in owners:
|
|
76
|
+
source = _resolve_source(slug)
|
|
77
|
+
if source is None:
|
|
78
|
+
ghosts.append(slug)
|
|
79
|
+
continue
|
|
80
|
+
entries[slug] = {
|
|
81
|
+
"source": str(source.relative_to(REPO_ROOT)),
|
|
82
|
+
"compiled": source.parent == COMPILED_DIR,
|
|
83
|
+
}
|
|
84
|
+
if ghosts:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"ownership rules demand owners with NO dispatchable source: "
|
|
87
|
+
f"{ghosts} — author the agent (YAML + compiler run) or fix "
|
|
88
|
+
f"config/agent-ownership.yaml; a gate must never demand an "
|
|
89
|
+
f"owner nobody can invoke"
|
|
90
|
+
)
|
|
91
|
+
return {
|
|
92
|
+
"_meta": {
|
|
93
|
+
"generator": "core/agents/roster_manifest.py",
|
|
94
|
+
"purpose": (
|
|
95
|
+
"every specialist-gate owner resolves to a dispatchable "
|
|
96
|
+
"agent source; installer/skill-deploy.js deploys these"
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
"gate_owners": entries,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def render() -> str:
|
|
104
|
+
return json.dumps(build_roster(), indent=2, sort_keys=True) + "\n"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main() -> int:
|
|
108
|
+
ROSTER_JSON.write_text(render(), encoding="utf-8")
|
|
109
|
+
print(f"agent roster written: {ROSTER_JSON}")
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
raise SystemExit(main())
|
|
@@ -135,13 +135,47 @@ export function deploySkills({
|
|
|
135
135
|
|
|
136
136
|
// ── Agent personas ──────────────────────────────────────────────────
|
|
137
137
|
if (agentsBase) {
|
|
138
|
-
|
|
138
|
+
const written = new Set();
|
|
139
|
+
counts.agents = deployAgents(deptRoot, agentsBase, written);
|
|
140
|
+
// Gate owners (F2 specialist-gate P0): every owner the ownership
|
|
141
|
+
// rules can demand MUST be dispatchable. 4 of 7 owners existed only
|
|
142
|
+
// as department YAML (never deployed — the agent loop above ships
|
|
143
|
+
// .md only), so the gate told sessions to dispatch agents nobody
|
|
144
|
+
// could invoke. The generated config/agent-roster.json maps each
|
|
145
|
+
// owner to a dispatchable source; hand-authored dept .md files win
|
|
146
|
+
// (already deployed above), compiled ones fill the gaps.
|
|
147
|
+
counts.agents += deployGateOwners(repoRoot, agentsBase, written);
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
log(counts);
|
|
142
151
|
return counts;
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
function deployGateOwners(repoRoot, agentsBase, written) {
|
|
155
|
+
let roster;
|
|
156
|
+
try {
|
|
157
|
+
roster = JSON.parse(readFileSync(
|
|
158
|
+
join(repoRoot, "config", "agent-roster.json"), "utf-8"));
|
|
159
|
+
} catch {
|
|
160
|
+
return 0; // older core without the roster — dept loop already ran
|
|
161
|
+
}
|
|
162
|
+
let deployed = 0;
|
|
163
|
+
for (const [slug, entry] of Object.entries(roster.gate_owners || {})) {
|
|
164
|
+
const target = `arka-${slug}.md`;
|
|
165
|
+
if (written.has(target)) continue; // dept .md won this run
|
|
166
|
+
const src = join(repoRoot, entry.source);
|
|
167
|
+
if (!existsSync(src)) continue;
|
|
168
|
+
try {
|
|
169
|
+
copyFileSync(src, join(agentsBase, target));
|
|
170
|
+
written.add(target);
|
|
171
|
+
deployed++;
|
|
172
|
+
} catch {
|
|
173
|
+
// One unreadable agent must not break the deploy.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return deployed;
|
|
177
|
+
}
|
|
178
|
+
|
|
145
179
|
function loadCuratedFilter(repoRoot) {
|
|
146
180
|
try {
|
|
147
181
|
const manifest = JSON.parse(readFileSync(
|
|
@@ -156,14 +190,14 @@ function loadCuratedFilter(repoRoot) {
|
|
|
156
190
|
}
|
|
157
191
|
}
|
|
158
192
|
|
|
159
|
-
function deployAgents(deptRoot, agentsBase) {
|
|
193
|
+
function deployAgents(deptRoot, agentsBase, written = new Set()) {
|
|
160
194
|
mkdirSync(agentsBase, { recursive: true });
|
|
161
195
|
let deployed = 0;
|
|
162
196
|
for (const dept of listSubdirs(deptRoot)) {
|
|
163
197
|
const agentsSrc = join(deptRoot, dept, "agents");
|
|
164
198
|
if (!existsSync(agentsSrc)) continue;
|
|
165
199
|
try {
|
|
166
|
-
deployed += deployDeptAgents(agentsSrc, agentsBase);
|
|
200
|
+
deployed += deployDeptAgents(agentsSrc, agentsBase, written);
|
|
167
201
|
} catch {
|
|
168
202
|
// Unreadable agents dir — skip the department, not the deploy.
|
|
169
203
|
}
|
|
@@ -171,7 +205,7 @@ function deployAgents(deptRoot, agentsBase) {
|
|
|
171
205
|
return deployed;
|
|
172
206
|
}
|
|
173
207
|
|
|
174
|
-
function deployDeptAgents(agentsSrc, agentsBase) {
|
|
208
|
+
function deployDeptAgents(agentsSrc, agentsBase, written) {
|
|
175
209
|
let deployed = 0;
|
|
176
210
|
for (const file of readdirSync(agentsSrc)) {
|
|
177
211
|
if (!file.endsWith(".md")) continue;
|
|
@@ -181,8 +215,9 @@ function deployDeptAgents(agentsSrc, agentsBase) {
|
|
|
181
215
|
} catch {
|
|
182
216
|
continue;
|
|
183
217
|
}
|
|
184
|
-
|
|
185
|
-
|
|
218
|
+
const target = `arka-${file.replace(/\.md$/, "")}.md`;
|
|
219
|
+
copyFileSync(srcFile, join(agentsBase, target));
|
|
220
|
+
written.add(target);
|
|
186
221
|
deployed++;
|
|
187
222
|
}
|
|
188
223
|
return deployed;
|
package/package.json
CHANGED