arcane-cli 0.22.3 → 0.22.4

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.
@@ -92,6 +92,19 @@ Arcane ships with twelve default agent personas, organized by functional cluster
92
92
 
93
93
  Twelve legendary agents — the Arcanos. Every namesake is public domain: golden-age stage magicians (Kellar, Lafayette, Adelaide, Alexander, Bess), myth and classic literature (Merlin, Circe, Iris, Prospero, Mercurio), and coinages from living vocabulary (Lince — *ojo de lince*; Custodio — *ángel custodio*). Lince is never anglicized.
94
94
 
95
+ ### Agent Definition Schema — Fields for Template/Renderer Authors
96
+
97
+ The table above is the human-readable roster; agent definitions (`agents/*.yaml`) and the generated roster (`.arcane/agents.yaml`) carry the same data as structured fields. This is where they live, for anyone building a third-party renderer (DMC is renderer #1):
98
+
99
+ | Field | Lives on | Populated by | Consumed by |
100
+ | --- | --- | --- | --- |
101
+ | `persona.description` / `behavioral_rules` | Agent definition (role-level, naming-strategy-agnostic) | Template author | Every client renderer (Copilot, Claude, Codex, OpenClaw) |
102
+ | `persona.personality` / `persona.voice` / `persona.catchphrases` | Agent definition | Template author | Renderers that want tone/flavor beyond the bare role |
103
+ | `persona.visual_description` | Agent definition | Template author | **Declared, not yet rendered by any client this repo ships.** Kept with the definition rather than a renderer, by design — data belongs with the thing it describes, not with any one consumer. A third-party image/avatar renderer reads this field directly from the YAML; no other wiring is required. |
104
+ | `epithet` (e.g. "the Archmage") | **Roster entry**, not the agent definition — see below | Naming strategy (`arcanos` only; `generic`/`random`/`custom` are deliberately epithet-less) | `.arcane/generated/openclaw-roster.json`'s `identity.epithet`, when present |
105
+
106
+ **Why `epithet` is schema v2's roster field, not a persona field:** an agent definition YAML (e.g. `architecture-lead.yaml`) is a role archetype reused across every naming strategy — "the Archmage" belongs to the persona *Merlin*, not to the "architecture-lead" role itself. Putting it in the definition would leak Arcanos-specific flavor into naming strategies designed to be epithet-neutral (`generic`'s plain role labels). It is populated during `spell agents init`'s naming step and flows through the roster entry (`.arcane/agents.yaml`) to `openclaw-roster.json` — never guessed or re-derived by a renderer.
107
+
95
108
  **Role consolidation (ADR-031):** Framework-specific dev roles (.NET, Blazor, React, Flutter, MAUI) are consolidated into generic roles (Full-Stack, Frontend, Mobile). The framework/stack is a prompt parameter, not an agent identity. This scales without needing a new agent for every technology.
96
109
 
97
110
  **Naming policy:** All agent names should be positive characters — beings, not objects — with a clear thematic connection to their role.
@@ -35,7 +35,7 @@ deliverables; every word in motion.
35
35
  - Follow platform guidelines (HIG for iOS, Material Design for Android)
36
36
  - App store submissions require a complete release checklist — no shortcuts
37
37
  - Performance profiling is required before marking any feature complete
38
- - [object Object]
38
+ - Backward compatibility: support at least the two previous major OS versions
39
39
 
40
40
  ## Tools
41
41
 
package/dist/index.js CHANGED
@@ -946,6 +946,20 @@ var ARCANOS_NAME_MAP = {
946
946
  "collaborator": "Iris",
947
947
  "security-ops": "Custodio"
948
948
  };
949
+ var ARCANOS_EPITHET_MAP = {
950
+ "orchestrator": "the Maestro",
951
+ "architecture-lead": "the Archmage",
952
+ "fullstack-dev": "the Conjuror",
953
+ "qa-lead": "the Unmasker",
954
+ "devops": "the Stormcaller",
955
+ "frontend-dev": "the Illusionist",
956
+ "mobile-dev": "the Swift",
957
+ "research-analyst": "the Man Who Knows",
958
+ "marketing-strategist": "the Charmweaver",
959
+ "operations-comms": "the Herald",
960
+ "collaborator": "the Emissary",
961
+ "security-ops": "the Warden"
962
+ };
949
963
  var RANDOM_NAME_POOL = [
950
964
  "Aiden",
951
965
  "Blake",
@@ -985,10 +999,11 @@ function applyGenericNames(roleIds) {
985
999
  }));
986
1000
  }
987
1001
  function applyArcanosNames(roleIds) {
988
- return roleIds.map((id) => ({
989
- definition: id,
990
- name: ARCANOS_NAME_MAP[id] ?? GENERIC_NAME_MAP[id] ?? id
991
- }));
1002
+ return roleIds.map((id) => {
1003
+ const name = ARCANOS_NAME_MAP[id] ?? GENERIC_NAME_MAP[id] ?? id;
1004
+ const epithet = ARCANOS_NAME_MAP[id] ? ARCANOS_EPITHET_MAP[id] : void 0;
1005
+ return epithet ? { definition: id, name, epithet } : { definition: id, name };
1006
+ });
992
1007
  }
993
1008
  function applyRandomNames(roleIds) {
994
1009
  const shuffled = [...RANDOM_NAME_POOL].sort(() => Math.random() - 0.5);
@@ -1182,6 +1197,7 @@ function validateAgentRoster(value, source) {
1182
1197
  requireString(item.definition, `roster[${index}].definition`, issues);
1183
1198
  if (item.name !== null) requireString(item.name, `roster[${index}].name`, issues);
1184
1199
  requireString(item.id, `roster[${index}].id`, issues);
1200
+ if (item.epithet !== void 0) requireString(item.epithet, `roster[${index}].epithet`, issues);
1185
1201
  });
1186
1202
  }
1187
1203
  if (issues.length > 0) throw new AgentConfigValidationError(source, issues);
@@ -1442,6 +1458,7 @@ ${rows}
1442
1458
  async function syncAgents(targetDir, assetsDir, roster, options = {}) {
1443
1459
  const synced = [];
1444
1460
  const skipped = [];
1461
+ let hasUnresolvedRoles = false;
1445
1462
  const projectDefs = projectAgentsDir(targetDir);
1446
1463
  const bundledDefs = join2(assetsDir, "agents");
1447
1464
  const resolved = [];
@@ -1454,6 +1471,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
1454
1471
  def = await loadAgentDefinition(bundledDefs, entry.definition);
1455
1472
  } catch {
1456
1473
  skipped.push(`${entry.definition} (definition not found)`);
1474
+ hasUnresolvedRoles = true;
1457
1475
  continue;
1458
1476
  }
1459
1477
  }
@@ -1506,6 +1524,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
1506
1524
  id: entry.id,
1507
1525
  identity: {
1508
1526
  name: entry.name ?? entry.definition,
1527
+ ...entry.epithet ? { epithet: entry.epithet } : {},
1509
1528
  role: def.role
1510
1529
  }
1511
1530
  }))
@@ -1575,7 +1594,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
1575
1594
  );
1576
1595
  if (codexMerged) synced.push("AGENTS.md (agents section)");
1577
1596
  }
1578
- return { synced, skipped };
1597
+ return { synced, skipped, hasUnresolvedRoles };
1579
1598
  }
1580
1599
 
1581
1600
  // src/modules/agents.ts
@@ -1660,9 +1679,9 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
1660
1679
  }
1661
1680
  printStep("Assigning names...");
1662
1681
  const named = await applyNamingStrategy(namingStrategy, selectedRoleIds);
1663
- const rosterEntries = named.map(({ definition, name }) => {
1682
+ const rosterEntries = named.map(({ definition, name, epithet }) => {
1664
1683
  const id = definition === "orchestrator" ? "main" : name.toLowerCase().replace(/\s+/g, "-");
1665
- return { definition, name, id };
1684
+ return epithet ? { definition, name, id, epithet } : { definition, name, id };
1666
1685
  });
1667
1686
  const roster = {
1668
1687
  schema_version: 2,
@@ -1706,7 +1725,7 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
1706
1725
  return;
1707
1726
  }
1708
1727
  printStep("Syncing agent definitions to all clients...");
1709
- const { synced, skipped } = await syncAgents(
1728
+ const { synced, skipped, hasUnresolvedRoles } = await syncAgents(
1710
1729
  targetDir,
1711
1730
  assetsDir,
1712
1731
  roster,
@@ -1721,6 +1740,12 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
1721
1740
  console.log();
1722
1741
  console.log(` \u{1F4A1} Customize: edit .arcane/agents.yaml \u2192 spell agents sync`);
1723
1742
  console.log();
1743
+ if (hasUnresolvedRoles) {
1744
+ console.error(
1745
+ ' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
1746
+ );
1747
+ process.exitCode = 1;
1748
+ }
1724
1749
  }
1725
1750
  async function runAgentsSync(options, targetDir, assetsDir) {
1726
1751
  let roster;
@@ -1739,7 +1764,7 @@ async function runAgentsSync(options, targetDir, assetsDir) {
1739
1764
  } else {
1740
1765
  console.log("\n Syncing agents...\n");
1741
1766
  }
1742
- const { synced, skipped } = await syncAgents(
1767
+ const { synced, skipped, hasUnresolvedRoles } = await syncAgents(
1743
1768
  targetDir,
1744
1769
  assetsDir,
1745
1770
  roster,
@@ -1763,6 +1788,12 @@ async function runAgentsSync(options, targetDir, assetsDir) {
1763
1788
  }
1764
1789
  }
1765
1790
  console.log();
1791
+ if (hasUnresolvedRoles) {
1792
+ console.error(
1793
+ ' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
1794
+ );
1795
+ process.exitCode = 1;
1796
+ }
1766
1797
  }
1767
1798
  async function runAgentsList(targetDir) {
1768
1799
  let roster;