arcane-cli 0.22.2 → 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.
- package/dist/assets/.arcane/governance/git-conventions.md +18 -2
- package/dist/assets/.arcane/governance/naming-conventions.md +13 -0
- package/dist/assets/.github/agents/mercurio.agent.md +1 -1
- package/dist/assets/.github/prompts/spell-close-session.prompt.md +1 -1
- package/dist/assets/.github/prompts/spell-open-session.prompt.md +1 -1
- package/dist/index.js +40 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -376,6 +376,8 @@ git branch -d type/old-topic # delete local branch
|
|
|
376
376
|
git push origin --delete type/old-topic # delete remote branch (safety net; ignore 'does not exist' errors if already auto-deleted)
|
|
377
377
|
```
|
|
378
378
|
|
|
379
|
+
**If `git branch -d` refuses with `error: the branch '<branch>' is not fully merged`, do not assume the branch is genuinely unmerged and do not reach for `-D` on that reading alone** — this repo's own sanctioned Rebase-and-fast-forward strategy (and any pre-ARC-009 squash merge) rewrites commit SHAs, so a fully-landed branch is permanently invisible to `-d`'s ancestry check. See **Content-Verified Branch Deletion** below for the actual procedure.
|
|
380
|
+
|
|
379
381
|
**From a linked worktree, do not run the block above** (ARC-028 R8). `git checkout main` fails with
|
|
380
382
|
`fatal: 'main' is already used by worktree at <path>` when the primary checkout holds it, and
|
|
381
383
|
`git branch -d` fails with `error: cannot delete branch '<branch>' used by worktree at <path>` for
|
|
@@ -397,6 +399,19 @@ refusing to delete a worktree-attached one, are the enforcement ARC-028 R3/R7 re
|
|
|
397
399
|
|
|
398
400
|
This applies to all actors — humans, interactive tools (Copilot, Claude), and autonomous agents. The `spell-commit-work` and `spell-close-session` prompts enforce this check.
|
|
399
401
|
|
|
402
|
+
### Content-Verified Branch Deletion (TODO.md merged-branch-cleanup finding)
|
|
403
|
+
|
|
404
|
+
Ancestry (`git branch --merged`, plain `git branch -d`) is not a reliable "is this landed?" test on this repo. Two of this repo's own sanctioned states defeat it identically: **Rebase-and-fast-forward** (ARC-009 §7) and any **pre-ARC-009 squash merge** both rewrite commit SHAs, so a fully-landed branch stays permanently invisible to `--merged` and refused by `-d` — not a rare edge case; every branch merged the sanctioned way hits this. Verify by content instead, for any local branch except `<trunk>` and any branch currently attached to another worktree (`git worktree list`):
|
|
405
|
+
|
|
406
|
+
1. **Fetch first.** `git fetch <remote> --prune` — a stale local `<trunk>` under-counts and produces a false "unmerged" reading.
|
|
407
|
+
2. **Prefer the provider's own record when a PR exists.** `gh pr list --head <branch> --state all` (or `az repos pr list --source-branch <branch> --status all`) — a confirmed `MERGED`/`completed` status is the fastest, most authoritative signal and needs no further check.
|
|
408
|
+
3. **Otherwise, check patch-id equivalence:** `git cherry <remote>/<trunk> <branch>`. Every `-`-prefixed line is patch-id-identical to a commit already on `<trunk>` — safe. A `+`-prefixed line means "no identical patch found," not "definitely unmerged": a squash merge, or independent re-authoring of the same change, produces a *different* patch-id for *identical resulting content*. Before concluding a `+` commit is genuinely unmerged, check whether its actual content already exists on `<trunk>` (search for the added text, or diff the specific file). This is not hypothetical — confirmed live 2026-08-31 (BC-03): two docs-only commits on one branch both showed `+` via `git cherry`, and both were byte-identical to content that had already landed on `<trunk>` through separate, differently-authored commits.
|
|
409
|
+
4. **Delete with `-D`, never plain `-d`**, only branches with **zero** real unmerged content (confirmed by step 2 or step 3) — `-d` will refuse on a rewritten SHA even when the branch is fully landed, which is the whole reason this section exists. Then `git push <remote> --delete <branch>` (ignore "does not exist" if already auto-deleted).
|
|
410
|
+
5. **A branch that fails verification is a finding to surface, not an obstacle.** Report it with a one-line summary of what its unmerged commit(s) actually contain, so a human can decide land vs. abandon. Do not delete it, and do not silently drop it from the report either — in a 2026-08-21 field test, content verification cleared 17 of 18 candidate branches as landed and caught the 1 genuinely unmerged branch a naive `-D` sweep would have destroyed.
|
|
411
|
+
6. **Idempotent by construction:** an already-deleted branch is simply absent from the next sweep's candidate list; a branch that failed verification is safely re-checked (and re-reported if still unresolved) on every run rather than being deleted or forgotten.
|
|
412
|
+
|
|
413
|
+
Apply the Same-Vantage-Point Check below before any deletion if this repository or its linked worktrees might be reached through more than one filesystem view. `spell-close-session` runs this as an idempotent sweep every session; `spell-open-session`'s stale-branch check uses the same procedure for its read-only candidate list.
|
|
414
|
+
|
|
400
415
|
### Same-Vantage-Point Check (EF-33 / ARC-028 R7)
|
|
401
416
|
|
|
402
417
|
`git worktree list` can **truthfully** report a live, healthy linked worktree as `prunable` when read through a cross-filesystem bridge — a Linux-side mount of a Windows host, a remote-mounted volume, a container bind-mount, any path-translation layer between the reading process and the repository's actual owning environment. The registered absolute path simply doesn't resolve from that vantage point, even though the directory exists and is valid from the machine that owns it. Nothing in Git's own output distinguishes "confirmed absent" from "not resolvable from this process's filesystem view" — both render identically as `prunable`. (A separate, unconditional hazard applies to `git branch --merged` regardless of vantage point — see the note at the end of this section; do not assume that one only matters when a bridge is in play.)
|
|
@@ -410,7 +425,7 @@ This applies to all actors — humans, interactive tools (Copilot, Claude), and
|
|
|
410
425
|
|
|
411
426
|
This is documented as a standing operational caution, not a code-level gate: EF-33's own intake report frames the underlying defect as cross-machine/cross-mount filesystem visibility, which no single CI runner can reproduce — the confirmation step above must be followed manually by whichever human or agent is about to run the destructive command. Confirmed as a live near-miss on 2026-08-03: a Linux-side read reported eight healthy linked worktrees `prunable`; the same repository read from Windows (their actual owning environment) showed zero prunable entries, and `git worktree prune -v` there removed nothing.
|
|
412
427
|
|
|
413
|
-
**Unconditional, vantage-point-independent note on branch deletion:** `git branch --merged` can also report a fully-landed branch as unmerged
|
|
428
|
+
**Unconditional, vantage-point-independent note on branch deletion:** `git branch --merged` can also report a fully-landed branch as unmerged, because Git's rebase-and-fast-forward and squash merge strategies (both sanctioned by this repo's own merge-strategy table above) rewrite commit SHAs, defeating ancestry-based detection. This has nothing to do with filesystem bridges — it fires identically on a single machine with no mount involved. See **Content-Verified Branch Deletion** above for the actual procedure; verify branch content before deleting regardless of what vantage-point confidence steps 1-4 above establish.
|
|
414
429
|
|
|
415
430
|
### ADO PR Lifecycle — Complete Command Reference
|
|
416
431
|
|
|
@@ -476,7 +491,8 @@ git push origin --delete <branch> # delete remote branch (safety net —
|
|
|
476
491
|
| `gh pr create` not found | `gh` CLI not installed | Use `az repos pr create` |
|
|
477
492
|
| `creatorVoteCounts` is false on branch policy | Vote registers but PR still blocked | A second human must approve via ADO web UI |
|
|
478
493
|
| `az repos pr update --status completed` returns `status: active` without error | PR silently not merged | Add `--squash false` to the command; the flag triggers the merge path correctly |
|
|
479
|
-
| `git branch -d <branch>` fails after merge
|
|
494
|
+
| `git branch -d <branch>` fails after merge, `error: cannot delete branch ... used by worktree` | Branch is attached to a worktree | Skip local delete; run remote delete + prune |
|
|
495
|
+
| `git branch -d <branch>` fails after merge, `error: ... is not fully merged` | Rebase/squash rewrote the branch's SHAs — see Content-Verified Branch Deletion above | Verify by content (`git cherry` or PR status), then `-D` only what passes |
|
|
480
496
|
| `git push origin --delete <branch>` fails with missing ref | Branch already deleted remotely | Treat as non-fatal and continue |
|
|
481
497
|
|
|
482
498
|
### Branch Lifecycle
|
|
@@ -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
|
-
-
|
|
38
|
+
- Backward compatibility: support at least the two previous major OS versions
|
|
39
39
|
|
|
40
40
|
## Tools
|
|
41
41
|
|
|
@@ -173,7 +173,7 @@ Rules:
|
|
|
173
173
|
- If this worktree was created with `--detach` there is no branch to name; report the path and the detached HEAD sha instead.
|
|
174
174
|
- Verify the merge landed with `git log --oneline -3 <remote>/<trunk>` (the remote-tracking ref), since local `<trunk>` is not checked out here to fast-forward.
|
|
175
175
|
|
|
176
|
-
- Common to both:
|
|
176
|
+
- **Common to both: run the idempotent, content-verified branch sweep** (TODO.md's merged-branch-cleanup finding) — not merely a list-and-suggest step. For every local branch except `<trunk>` and any branch attached to another worktree, apply [Content-Verified Branch Deletion](../../.arcane/governance/git-conventions.md#content-verified-branch-deletion-todomd-merged-branch-cleanup-finding) (git-conventions.md): fetch first, prefer a confirmed PR-merged status when one exists, otherwise verify by `git cherry` patch-id equivalence *and* a resulting-content check for `+`-flagged commits (a squash or independently re-authored commit can carry already-landed content under a different patch-id). Actually delete (`-D`, never `-d`) every branch that verifies as fully landed, local and remote. Report — do not delete — any branch with genuine unmerged content; a one-line summary of what it holds is enough for a human to decide land vs. abandon. Apply the Same-Vantage-Point Check (EF-33) before any deletion if the repository or its linked worktrees might be reached through more than one filesystem view. Safe to run every session: an already-clean branch list is simply a no-op.
|
|
177
177
|
- See [governance/git-conventions.md](../../.arcane/governance/git-conventions.md) Post-Merge Cleanup section.
|
|
178
178
|
|
|
179
179
|
11. **Return a concise closure report.**
|
|
@@ -83,7 +83,7 @@ Before anything else, check:
|
|
|
83
83
|
- Disallow random adjective-noun generator names (example: `ideal-disco`) as session defaults.
|
|
84
84
|
- If current branch is non-compliant and no active PR depends on it, record the required rename for the Mutation Guard. Do not rename during a read-only session.
|
|
85
85
|
- If an active PR uses the old branch name, do not force-delete the old remote branch; flag it and continue with a follow-up rename plan.
|
|
86
|
-
- **Stale local branches:** Run `git branch --merged main` to list branches already merged that should be deleted
|
|
86
|
+
- **Stale local branches:** Run `git branch --merged main` to list branches already merged that should be deleted — this session's own read-only candidate list (`--merged` also misses every branch landed via this repo's sanctioned rebase-and-fast-forward, so treat a *clean* result as "no ancestry-visible candidates," not "nothing to check"). Open-session only lists; it does not delete. Apply the [Content-Verified Branch Deletion](../../.arcane/governance/git-conventions.md#content-verified-branch-deletion-todomd-merged-branch-cleanup-finding) procedure (git-conventions.md) to any candidate before flagging it as safe, and leave the actual idempotent prune to `spell-close-session`'s sweep.
|
|
87
87
|
- **Tracker configuration check (early):** resolve active tracking settings before planning, in this order: root `.arcane.json` (if present) -> the committed self-hosted source manifest (`src/assets/.arcane.json`, read only when it declares `selfHosted: true` -- this is EF-14's recorded self-hosting resolution tier, distinct from treating the mere presence of a self-host marker as license to infer other, unrelated config) -> the current feature PRD frontmatter -> ask. Persisted once at `spell init`/`spell update` (EF-14); do not re-ask when any of the first three sources already sets it.
|
|
88
88
|
- `tracking_mode: internal | external`
|
|
89
89
|
- `external_provider: ado | jira | other`
|
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
|
-
|
|
990
|
-
|
|
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;
|