arcane-cli 0.22.3 โ†’ 0.22.5

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.
@@ -567,13 +567,15 @@ See also: [Agent Workflow โ€” Sync with main before opening a PR](#agent-workflo
567
567
  | **Description** | Must include: summary of changes, rationale, verification steps, and a linked work item ID if applicable |
568
568
  | **Pre-PR sync** | **Mandatory rebase on latest `origin/<target>` before pushing.** See [๐Ÿ›‘ Agent-mandatory pre-PR guard](#-agent-mandatory-pre-pr-guard) above โ€” this applies even when opening PRs via raw `az repos pr create` / `gh pr create` / MCP tools. |
569
569
  | **Branch cleanup** | After merge, delete the source branch (local and remote). |
570
- | **PR link format** | All PR references in agent output should be clickable markdown links to the PR, not a bare `PR #NNN`. |
570
+ | **PR link format** | All PR references in agent output **must** be clickable markdown links to the PR, not a bare `PR #NNN`. |
571
+ | **Doc-ID link format** | The same rule extends to `ADR-NNN`/`ARC-NNN`/`EF-NN`/journal entries/named governance docs โ€” never a bare ID. See `.github/instructions/agent-output.instructions.md` โ†’ Doc-ID Link Format for the full rule and required formats per context. |
571
572
 
572
573
  ### Agent PR Workflow
573
574
 
574
575
  1. **Create a work item** (if one doesn't exist) using `spell-suggest-feature` or `spell-bug`.
575
576
  2. **Open PR** with proper title, description, and work-item link.
576
- 3. **Notify the human** through your configured channel with: `[PR #{number}] {repo} โ€” {title} (#{workItemId})`.
577
+ 3. **Notify the human** through your configured channel with a clickable link, not a bare number:
578
+ `[PR #{number} โ€” {title}]({url}) โ€” {repo} (#{workItemId})`.
577
579
  4. **Address review feedback** if a reviewer requests changes.
578
580
  5. **After merge**, delete the branch (local + remote) and confirm cleanup.
579
581
 
@@ -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.
@@ -113,12 +113,12 @@ See [[agents/installation|Runtime Installation]].
113
113
 
114
114
  ### Step 3.3 โ€” Agent Git Identity Setup
115
115
 
116
- Every agent that will produce commits for this business must have a registered Git identity **before making its first commit**. See [[governance/git-conventions#agent-attribution-model|Git Conventions โ€” Agent Attribution]].
116
+ Every agent that will produce commits for this business must have a registered Git identity **before making its first commit**. See [[git-conventions#agent-attribution-model|Git Conventions โ€” Agent Attribution]].
117
117
 
118
118
  For each agent assigned to this business:
119
119
 
120
120
  1. **Confirm persona name** is registered in [[naming-conventions|Naming Conventions]] agent roster.
121
- 2. **Confirm Git identity** exists in the registered identities table in [[governance/git-conventions#agent-email-convention|Git Conventions]].
121
+ 2. **Confirm Git identity** exists in the registered identities table in [[git-conventions#agent-email-convention|Git Conventions]].
122
122
  3. **Add agent to `.mailmap`** in every repo the agent will commit to:
123
123
  ```
124
124
  {AGENT_NAME} <{AGENT_EMAIL}>
@@ -15,7 +15,13 @@ against a consistent bar. This is the spell-prompt analogue of [[product-excelle
15
15
  grades PRDs, not prompts). It is an **authoring-time standard** โ€” a checklist for contributors writing
16
16
  or reviewing spells. It is **not** meant to be read at runtime by the spells it grades.
17
17
 
18
- See [ARC-014](../../../DECISIONS.md#arc-014) for the decision that established this standard.
18
+ See [ARC-014](https://github.com/codemagicianhq/arcane/blob/main/DECISIONS.md#arc-014--spell-authoring-standards-a-quality-rubric-for-spell-prompts)
19
+ for the decision that established this standard. **Full canonical URL, not a same-repo
20
+ wiki-link** (corrected 2026-08-31, BC-06) โ€” this file ships to consumer repos, and
21
+ `DECISIONS.md` does not: `src/assets/DECISIONS.md` (what consumers actually receive via
22
+ `spell init`) is an empty starter template for the *consumer's own* decisions, so a
23
+ same-repo wiki-link to a specific ARC id would resolve to the wrong document once
24
+ installed, not simply fail to resolve.
19
25
 
20
26
  ## The Spell Quality Rubric
21
27
 
@@ -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
 
@@ -4,7 +4,7 @@ applyTo: "**"
4
4
 
5
5
  # Agent Output Formatting Rules
6
6
 
7
- Source of truth: `.arcane/governance/git-conventions.md` โ†’ PR Standards section.
7
+ Source of truth: `.arcane/governance/git-conventions.md` โ†’ PR Standards โ†’ PR Requirements.
8
8
 
9
9
  ## PR Link Format
10
10
 
@@ -20,9 +20,60 @@ All PR references in agent output **must** be clickable markdown links. Never wr
20
20
  [PR #270 โ€” fix: remove Unix-only preinstall script and approve esbuild builds](https://dev.azure.com/{org}/{project}/_git/{repo}/pullrequest/270)
21
21
  ```
22
22
 
23
+ ## Doc-ID Link Format
24
+
25
+ The same rule extends to every other identifier class this framework uses to reference
26
+ a decision or record: `ADR-NNN`, `ARC-NNN`, `EF-NN`, venture decision IDs, journal
27
+ entries, and named governance docs. A bare decision ID in agent output is the same
28
+ defect as a bare `PR #NNN` โ€” the reader has to go find it themselves, and a typo or
29
+ stale number is invisible until someone tries.
30
+
31
+ **Required format, by context:**
32
+ - **Inside a repo's own local markdown** (e.g. this repo's `journal/`, `PLAN.md`, or
33
+ `DECISIONS.md` cross-referencing itself): a wiki-link (`[[DECISIONS#ARC-NNN|ARC-NNN]]`)
34
+ or a relative markdown link that actually resolves from that file's own location.
35
+ - **Chat or PR output:** a clickable markdown link to the canonical file/anchor.
36
+ - **Governance docs this framework ships** (`.arcane/governance/*.md`): the full
37
+ canonical-repo URL. A file here lives in *this* repo today but ships into every
38
+ consumer repo's own tree, where a wiki-link or relative path to Arcane's own
39
+ `DECISIONS.md` either fails to resolve, or worse, silently resolves to the
40
+ *consumer's own* `DECISIONS.md` (an empty per-consumer starter template, not
41
+ Arcane's real decision record) โ€” wrong content, not just a dead link.
42
+ - **Spell prompts and instructions this framework ships**
43
+ (`.github/prompts/*.prompt.md`, `.github/instructions/*.md`): **plain text only โ€”
44
+ cite the bare ID, no link at all** (`ARC-035`, not `[ARC-035](...)`). A full
45
+ canonical URL is *also* unsafe here, not just a same-repo link: it bakes
46
+ `github.com/codemagicianhq/arcane` โ€” this project's own org and repo name โ€” into
47
+ content the org-token portability gate (`scripts/org-token-lint.ts`, D2 in
48
+ `spell-authoring-standards.md`) exists specifically to keep out of distributed
49
+ spells. **Confirmed live 2026-08-31 (BC-06):** the build's `Org-token lint` failed
50
+ on exactly this pattern in three prompt files, including two this same correction
51
+ pass had introduced minutes earlier while fixing the *other* link defect. Known
52
+ gap: the portability scan today only walks `.github/prompts/`, so a full URL in a
53
+ `.github/instructions/*.md` file โ€” like this one's own Merge Strategy section, two
54
+ paragraphs down โ€” ships uncaught; tracked in `TODO.md`.
55
+ - **Cross-repo references from anywhere else** (chat, PR output, another repo
56
+ entirely): the full canonical-repo URL.
57
+
58
+ **Never fabricate an unresolvable link.** If you cannot confirm the ID exists and where
59
+ it resolves to, say so and cite the ID as plain text rather than inventing a link that
60
+ looks real but goes nowhere.
61
+
23
62
  ## Merge Strategy
24
63
 
25
- All PRs must use **Rebase and fast-forward**. Never squash. Never merge commit. See ADR-048.
64
+ All PRs must use **Rebase and fast-forward**. Never squash. Never merge commit. See
65
+ ARC-009 ยง7 (Arcane's own framework decision โ€” cited as plain text, not linked, per
66
+ Doc-ID Link Format above: this file ships). **Corrected 2026-08-31 (BC-06), three
67
+ times over:** this file previously cited `ADR-048`, which does exist in the shipped
68
+ `framework-decisions.md` reference โ€” but its actual topic ("Code Versus Docs Branch
69
+ Policy") has nothing to do with merge strategy, a topical miscitation rather than a
70
+ missing one. The first fix replaced it with a same-repo wiki-link to this repo's own
71
+ `DECISIONS.md` โ€” itself the exact cross-repo defect this section warns about
72
+ (`DECISIONS.md` doesn't ship). The second fix replaced that with a full canonical
73
+ URL, which held up locally but failed the build's org-token portability gate: a
74
+ `github.com/codemagicianhq/arcane` URL bakes this project's own org into a file the
75
+ gate treats as distributed content (see Doc-ID Link Format above). Settled on the
76
+ bare ID โ€” the gate's own documented safe form.
26
77
 
27
78
  When completing a PR via `az repos pr update`, do **not** pass `--squash true`. The correct command:
28
79
 
@@ -9,7 +9,10 @@ agent: agent
9
9
 
10
10
  - This spell determines whether a version bump is required and applies the correct semver type.
11
11
  - Use it whenever you've changed `src/assets/`, `registry.ts`, or `profiles.ts` in the arcane repo.
12
- - It enforces the rule from CLAUDE.md: "Version bump required for any change to `src/assets/`."
12
+ - It enforces the rule from `project.md` โ†’ Constraints โ†’ Self-hosting: "Anything touching
13
+ `src/assets/` requires a version bump." (Corrected 2026-08-31, BC-06 โ€” this previously cited
14
+ CLAUDE.md, which now carries only the working protocol; the rule's live prose home moved to
15
+ `project.md:56-58`.)
13
16
 
14
17
  ---
15
18
 
@@ -51,7 +51,7 @@ For each PR in the batch, independently:
51
51
  1. Fetch the PR diff and metadata (`gh pr diff <n>` / `az repos pr show --id <n>` + diff). If a PR can't be fetched (deleted, no access, network error), **skip it, record it as `SKIPPED` with the reason, and continue** โ€” never abort the whole batch for one bad PR.
52
52
  2. Run the `spell-review` workflow against that diff โ€” the full dimension-coverage sweep (correctness, security, performance, tests, naming/clarity, architecture), severity-ranked findings (`HIGH` / `MEDIUM` / `LOW`, matching the `spell-review` ladder) with stable IDs, and the PASS/WARN/FAIL coverage table.
53
53
  3. Record a per-PR result: verdict (**approve** / **request-changes**), finding counts by severity, and the coverage table.
54
- 4. **Post the formal round state per PR** โ€” apply `spell-review`'s step 10 ([ARC-035](../../DECISIONS.md#arc-035--auto-merge-requires-a-clear-review-round) decision 3) to *this* PR's number, not just this workflow's own single-run PR: a **request-changes** verdict posts `gh pr review <n> --request-changes` (or `az repos pr set-vote --id <n> --vote reject`); a re-review that clears a round this batch previously blocked dismisses/supersedes that prior review. An **approve** verdict posts nothing. Never let a posting failure (unauthenticated CLI, PR closed mid-batch) skip or abort the remaining PRs in the batch โ€” record the result already computed and continue.
54
+ 4. **Post the formal round state per PR** โ€” apply `spell-review`'s step 10 (ARC-035 decision 3 โ€” cited as plain text, not linked; see `agent-output.instructions.md` โ†’ Doc-ID Link Format) to *this* PR's number, not just this workflow's own single-run PR: a **request-changes** verdict posts `gh pr review <n> --request-changes` (or `az repos pr set-vote --id <n> --vote reject`); a re-review that clears a round this batch previously blocked dismisses/supersedes that prior review. An **approve** verdict posts nothing. Never let a posting failure (unauthenticated CLI, PR closed mid-batch) skip or abort the remaining PRs in the batch โ€” record the result already computed and continue.
55
55
 
56
56
  Reviews are adversarial โ€” actively look for problems, do not just confirm quality. Apply the same no-fabrication coverage mandate as `spell-review`: a clean lens is reported as "no issues," never padded.
57
57
 
@@ -68,11 +68,11 @@ Build one report:
68
68
 
69
69
  | PR | Repo | Title | Verdict | High | Med | Low |
70
70
  | -- | ---- | ----- | ------- | ---- | --- | --- |
71
- | #n | repo | โ€ฆ | approve / request-changes | 0 | 1 | 2 |
72
- | #n | repo | โ€ฆ | SKIPPED โ€” {reason} | โ€” | โ€” | โ€” |
71
+ | [#n]({url}) | repo | โ€ฆ | approve / request-changes | 0 | 1 | 2 |
72
+ | [#n]({url}) | repo | โ€ฆ | SKIPPED โ€” {reason} | โ€” | โ€” | โ€” |
73
73
 
74
74
  ## Per-PR detail
75
- ### PR #n โ€” {title}
75
+ ### [PR #n]({url}) โ€” {title}
76
76
  - Verdict: โ€ฆ
77
77
  - Coverage: {PASS/WARN/FAIL per lens}
78
78
  - Findings: {R1 โ€ฆ}
@@ -122,7 +122,7 @@ Workflow:
122
122
 
123
123
  9. **Write back to the pull request** โ€” when a PR exists for these changes, sync a short verdict plus a findings summary (verdict + finding counts/IDs + a pointer to the saved report) into the PR body/description. Use provider-agnostic phrasing that works for either GitHub or Azure DevOps. Mark it clearly as a snapshot at review time, and fall back to a top-level PR comment if the description can't be updated.
124
124
 
125
- 10. **Post the formal round state** ([ARC-035](../../DECISIONS.md#arc-035--auto-merge-requires-a-clear-review-round) decision 3) โ€” step 9's write-back is a human-readable summary; this is the separate, machine-readable signal the `Review round clear` required check actually reads. Only a confirmed blocker posts a formal review โ€” an `APPROVE` or `DEFER` verdict posts nothing here, preserving the zero-friction path for PRs nobody formally blocks:
125
+ 10. **Post the formal round state** (ARC-035 decision 3 โ€” Arcane's own framework decision, cited as plain text, not linked, since this file ships; see `agent-output.instructions.md` โ†’ Doc-ID Link Format) โ€” step 9's write-back is a human-readable summary; this is the separate, machine-readable signal the `Review round clear` required check actually reads. Only a confirmed blocker posts a formal review โ€” an `APPROVE` or `DEFER` verdict posts nothing here, preserving the zero-friction path for PRs nobody formally blocks:
126
126
  - **REQUEST CHANGES verdict:**
127
127
  - GitHub: `gh pr review <n> --request-changes --body "<one-line blocker summary + pointer to the saved report>"`.
128
128
  - Azure DevOps: `az repos pr set-vote --id <n> --vote reject` โ€” the same primitive `spell-commit-work.prompt.md`'s author-side self-approval step already uses (`az repos pr set-vote --vote approve`), applied to the reviewer side.
@@ -11,7 +11,7 @@ agent: agent
11
11
  - **Hub-gated:** requires `role: "hub"` in `.arcane.json`. Refuses cleanly in a consumer repo, pointing at the right place to capture the idea instead.
12
12
  - Use this when starting a new venture (business, product, or personal project) that the hub should track.
13
13
  - Input format is: `Venture Name | Venture Type | Description` โ€” prompt will ask for missing fields.
14
- - Renamed from `spell-bootstrap-business` โ€” venture books and the registry didn't exist when that spell was named. Clean rename, no compatibility alias, per [[DECISIONS#ARC-008|ARC-008]]'s precedent.
14
+ - Renamed from `spell-bootstrap-business` โ€” venture books and the registry didn't exist when that spell was named. Clean rename, no compatibility alias, per ARC-008's precedent (cited as plain text, not linked โ€” this file ships; see `agent-output.instructions.md` โ†’ Doc-ID Link Format).
15
15
 
16
16
  ---
17
17
 
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;