arcane-cli 0.22.12 → 0.22.14
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/universal-agent-rules.md +6 -0
- package/dist/assets/.claude/commands/spell-address-review.md +4 -0
- package/dist/assets/.claude/commands/spell-adopt-docs.md +4 -0
- package/dist/assets/.claude/commands/spell-arcane-version.md +4 -0
- package/dist/assets/.claude/commands/spell-architect.md +4 -0
- package/dist/assets/.claude/commands/spell-brainstorm.md +4 -0
- package/dist/assets/.claude/commands/spell-bug.md +4 -0
- package/dist/assets/.claude/commands/spell-bump.md +4 -0
- package/dist/assets/.claude/commands/spell-check-drift.md +4 -0
- package/dist/assets/.claude/commands/spell-close-session.md +4 -0
- package/dist/assets/.claude/commands/spell-commit-work.md +4 -0
- package/dist/assets/.claude/commands/spell-create-pull-request.md +4 -0
- package/dist/assets/.claude/commands/spell-document.md +4 -0
- package/dist/assets/.claude/commands/spell-dotnet-expert.md +4 -0
- package/dist/assets/.claude/commands/spell-enchant.md +4 -0
- package/dist/assets/.claude/commands/spell-explain-concept.md +4 -0
- package/dist/assets/.claude/commands/spell-feedback.md +4 -0
- package/dist/assets/.claude/commands/spell-full-cycle.md +4 -0
- package/dist/assets/.claude/commands/spell-generate-bot-icons.md +4 -0
- package/dist/assets/.claude/commands/spell-implement.md +4 -0
- package/dist/assets/.claude/commands/spell-make-discoverable.md +4 -0
- package/dist/assets/.claude/commands/spell-manifest.md +4 -0
- package/dist/assets/.claude/commands/spell-open-session.md +4 -0
- package/dist/assets/.claude/commands/spell-plan.md +4 -0
- package/dist/assets/.claude/commands/spell-present-arcane.md +4 -0
- package/dist/assets/.claude/commands/spell-product-review.md +4 -0
- package/dist/assets/.claude/commands/spell-review-batch.md +4 -0
- package/dist/assets/.claude/commands/spell-review.md +4 -0
- package/dist/assets/.claude/commands/spell-save-idea.md +4 -0
- package/dist/assets/.claude/commands/spell-scope.md +4 -0
- package/dist/assets/.claude/commands/spell-security-review.md +4 -0
- package/dist/assets/.claude/commands/spell-ship.md +4 -0
- package/dist/assets/.claude/commands/spell-status.md +4 -0
- package/dist/assets/.claude/commands/spell-suggest-feature.md +4 -0
- package/dist/assets/.claude/commands/spell-summon-venture.md +4 -0
- package/dist/assets/.claude/commands/spell-test.md +4 -0
- package/dist/assets/.claude/commands/spell-todo.md +4 -0
- package/dist/index.js +243 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -53,6 +53,12 @@ See also: [[README]], [[DECISIONS]], [[governance/git-conventions|Git Convention
|
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
|
+
## Spell Lifecycle
|
|
57
|
+
|
|
58
|
+
22. **Lifecycle operations run through their spell when one is installed.** Committing, opening a PR, opening or closing a session, reviewing code, and shipping each have a dedicated spell (`spell-commit-work`, `spell-create-pull-request`, `spell-open-session`/`spell-close-session`, `spell-review`, `spell-ship`). When the installed component set includes the matching spell, invoke it for that operation rather than improvising the workflow from general knowledge — even when the user does not name the spell explicitly. The intent→spell routing table injected into each client's L1 instruction file (`CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`) is normative for this mapping, not illustrative.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
56
62
|
## Recommendation Guardrails (ADR-034)
|
|
57
63
|
|
|
58
64
|
15. **Flag actionable recommendations.** Any recommendation that could lead to a purchase, subscription, account creation, or irreversible action must: (a) be flagged explicitly, (b) use verified current information — never assume or fabricate, (c) present free/no-cost alternatives when they exist, (d) state confidence level if based on general knowledge. For >$50 or contracts, require explicit confirmation. See [[agents/agent-policies#actionable-recommendation-policy-adr-034|Full Policy]].
|
package/dist/index.js
CHANGED
|
@@ -1444,6 +1444,22 @@ ${rules}
|
|
|
1444
1444
|
**Denied:** ${def.tools.denied.join(", ")}
|
|
1445
1445
|
`;
|
|
1446
1446
|
}
|
|
1447
|
+
function renderSpellRoutingSection() {
|
|
1448
|
+
return `## Spell Routing
|
|
1449
|
+
|
|
1450
|
+
| When you're about to... | Invoke |
|
|
1451
|
+
|---|---|
|
|
1452
|
+
| Commit work | \`spell-commit-work\` |
|
|
1453
|
+
| Open a session | \`spell-open-session\` |
|
|
1454
|
+
| Close a session | \`spell-close-session\` |
|
|
1455
|
+
| Open a pull request | \`spell-create-pull-request\` |
|
|
1456
|
+
| Ship a feature end-to-end | \`spell-full-cycle\` |
|
|
1457
|
+
| Fix a bug | \`spell-bug\` |
|
|
1458
|
+
| Review code or a PR | \`spell-review\` |
|
|
1459
|
+
|
|
1460
|
+
If a spell exists for the workflow you are about to perform, invoke it \u2014 do not improvise the workflow from general knowledge, even when the user doesn't name the spell.
|
|
1461
|
+
`;
|
|
1462
|
+
}
|
|
1447
1463
|
function renderAgentRosterSection(entries) {
|
|
1448
1464
|
const rows = entries.map(
|
|
1449
1465
|
({ def, displayName }) => `| **${displayName}** | ${def.role} | ${def.category} |`
|
|
@@ -1564,32 +1580,35 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
1564
1580
|
}
|
|
1565
1581
|
synced.push(`.github/agents/${fileName}`);
|
|
1566
1582
|
}
|
|
1567
|
-
const
|
|
1583
|
+
const copilotSection = `${renderSpellRoutingSection()}
|
|
1584
|
+
${renderAgentRosterSection(resolved)}`;
|
|
1568
1585
|
const copilotMerged = await mergeIntoFile(
|
|
1569
1586
|
targetDir,
|
|
1570
1587
|
".github/copilot-instructions.md",
|
|
1571
|
-
|
|
1588
|
+
copilotSection,
|
|
1572
1589
|
{ force: true, dryRun: options.dryRun }
|
|
1573
1590
|
);
|
|
1574
1591
|
if (copilotMerged)
|
|
1575
1592
|
synced.push(".github/copilot-instructions.md (agents section)");
|
|
1576
1593
|
}
|
|
1577
1594
|
if (options.claude !== false) {
|
|
1578
|
-
const
|
|
1595
|
+
const claudeSection = `${renderSpellRoutingSection()}
|
|
1596
|
+
${renderAgentRosterSection(resolved)}`;
|
|
1579
1597
|
const claudeMerged = await mergeIntoFile(
|
|
1580
1598
|
targetDir,
|
|
1581
1599
|
"CLAUDE.md",
|
|
1582
|
-
|
|
1600
|
+
claudeSection,
|
|
1583
1601
|
{ force: true, dryRun: options.dryRun }
|
|
1584
1602
|
);
|
|
1585
1603
|
if (claudeMerged) synced.push("CLAUDE.md (agents section)");
|
|
1586
1604
|
}
|
|
1587
1605
|
if (options.codex !== false) {
|
|
1588
|
-
const
|
|
1606
|
+
const codexSection = `${renderSpellRoutingSection()}
|
|
1607
|
+
${renderAgentRosterSection(resolved)}`;
|
|
1589
1608
|
const codexMerged = await mergeIntoFile(
|
|
1590
1609
|
targetDir,
|
|
1591
1610
|
"AGENTS.md",
|
|
1592
|
-
|
|
1611
|
+
codexSection,
|
|
1593
1612
|
{ force: true, dryRun: options.dryRun }
|
|
1594
1613
|
);
|
|
1595
1614
|
if (codexMerged) synced.push("AGENTS.md (agents section)");
|
|
@@ -3576,8 +3595,8 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
3576
3595
|
// src/commands/doctor.ts
|
|
3577
3596
|
import { access as access4, readFile as readFile6, mkdir as mkdir6, copyFile as fsCopyFile2 } from "fs/promises";
|
|
3578
3597
|
import { join as join11, dirname as dirname3 } from "path";
|
|
3579
|
-
import { execFile as
|
|
3580
|
-
import { promisify } from "util";
|
|
3598
|
+
import { execFile as execFile3 } from "child_process";
|
|
3599
|
+
import { promisify as promisify2 } from "util";
|
|
3581
3600
|
|
|
3582
3601
|
// src/config/incidents.ts
|
|
3583
3602
|
var INCIDENT_QUEUE = [
|
|
@@ -3644,8 +3663,177 @@ function evaluateIncidentGate(records) {
|
|
|
3644
3663
|
};
|
|
3645
3664
|
}
|
|
3646
3665
|
|
|
3647
|
-
// src/
|
|
3666
|
+
// src/modules/platform-policy.ts
|
|
3667
|
+
import { execFile as execFile2 } from "child_process";
|
|
3668
|
+
import { promisify } from "util";
|
|
3648
3669
|
var execFileAsync = promisify(execFile2);
|
|
3670
|
+
var SANCTIONED_MERGE_METHODS = ["merge", "rebase"];
|
|
3671
|
+
function compareToLadder(effective, declaredButBlocked, interactionNote) {
|
|
3672
|
+
const disallowedButPermitted = effective.filter(
|
|
3673
|
+
(m) => !SANCTIONED_MERGE_METHODS.includes(m)
|
|
3674
|
+
);
|
|
3675
|
+
const missingSanctioned = SANCTIONED_MERGE_METHODS.filter(
|
|
3676
|
+
(m) => !effective.includes(m)
|
|
3677
|
+
);
|
|
3678
|
+
const passed = disallowedButPermitted.length === 0 && missingSanctioned.length === 0;
|
|
3679
|
+
const messages = [];
|
|
3680
|
+
if (interactionNote) messages.push(interactionNote);
|
|
3681
|
+
if (disallowedButPermitted.length > 0) {
|
|
3682
|
+
messages.push(
|
|
3683
|
+
`disallowed method(s) effectively permitted: ${disallowedButPermitted.join(", ")} (never sanctioned per git-conventions.md)`
|
|
3684
|
+
);
|
|
3685
|
+
}
|
|
3686
|
+
if (missingSanctioned.length > 0) {
|
|
3687
|
+
messages.push(
|
|
3688
|
+
`sanctioned method(s) not effectively available: ${missingSanctioned.join(", ")}`
|
|
3689
|
+
);
|
|
3690
|
+
}
|
|
3691
|
+
return {
|
|
3692
|
+
passed,
|
|
3693
|
+
effectiveMergeMethods: effective,
|
|
3694
|
+
declaredButBlocked,
|
|
3695
|
+
disallowedButPermitted,
|
|
3696
|
+
message: passed ? `effective merge methods [${effective.join(", ")}] match the declared ladder` : messages.join("; ")
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
function evaluateGitHubMergePolicy(rulesets) {
|
|
3700
|
+
const active = rulesets.filter(
|
|
3701
|
+
(r) => r.enforcement === "active" && r.target === "branch"
|
|
3702
|
+
);
|
|
3703
|
+
if (active.length === 0) {
|
|
3704
|
+
return {
|
|
3705
|
+
passed: false,
|
|
3706
|
+
effectiveMergeMethods: null,
|
|
3707
|
+
declaredButBlocked: [],
|
|
3708
|
+
disallowedButPermitted: [],
|
|
3709
|
+
message: "No active branch ruleset covers the target branch (checked via Rulesets, not the classic protection endpoint, which false-negatives on Rulesets-only repos)."
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
let allowedMergeMethods = null;
|
|
3713
|
+
let hasRequiredLinearHistory = false;
|
|
3714
|
+
for (const ruleset of active) {
|
|
3715
|
+
for (const rule of ruleset.rules) {
|
|
3716
|
+
if (rule.type === "required_linear_history") {
|
|
3717
|
+
hasRequiredLinearHistory = true;
|
|
3718
|
+
}
|
|
3719
|
+
if (rule.type === "pull_request") {
|
|
3720
|
+
const methods = rule.parameters?.["allowed_merge_methods"];
|
|
3721
|
+
if (Array.isArray(methods)) {
|
|
3722
|
+
const typed = methods;
|
|
3723
|
+
allowedMergeMethods = allowedMergeMethods === null ? typed : allowedMergeMethods.filter((m) => typed.includes(m));
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
const nominal = allowedMergeMethods ?? ["merge", "squash", "rebase"];
|
|
3729
|
+
const effective = hasRequiredLinearHistory ? nominal.filter((m) => m !== "merge") : nominal;
|
|
3730
|
+
const declaredButBlocked = hasRequiredLinearHistory && nominal.includes("merge") ? ["merge"] : [];
|
|
3731
|
+
const interactionNote = declaredButBlocked.length > 0 ? `allowed_merge_methods declares [${nominal.join(", ")}] but a required_linear_history rule silently blocks merge commits regardless -- effective methods are [${effective.join(", ")}]` : void 0;
|
|
3732
|
+
return compareToLadder(effective, declaredButBlocked, interactionNote);
|
|
3733
|
+
}
|
|
3734
|
+
async function fetchGitHubRulesets(owner, repo) {
|
|
3735
|
+
try {
|
|
3736
|
+
const { stdout: listOut } = await execFileAsync("gh", [
|
|
3737
|
+
"api",
|
|
3738
|
+
`repos/${owner}/${repo}/rulesets`
|
|
3739
|
+
]);
|
|
3740
|
+
const summaries = JSON.parse(listOut);
|
|
3741
|
+
const detailed = await Promise.all(
|
|
3742
|
+
summaries.map(async (s) => {
|
|
3743
|
+
const { stdout } = await execFileAsync("gh", [
|
|
3744
|
+
"api",
|
|
3745
|
+
`repos/${owner}/${repo}/rulesets/${s.id}`
|
|
3746
|
+
]);
|
|
3747
|
+
return JSON.parse(stdout);
|
|
3748
|
+
})
|
|
3749
|
+
);
|
|
3750
|
+
return detailed;
|
|
3751
|
+
} catch {
|
|
3752
|
+
return null;
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
function evaluateAdoMergePolicy(policies) {
|
|
3756
|
+
const enabled = policies.filter((p) => p.isEnabled);
|
|
3757
|
+
if (enabled.length === 0) {
|
|
3758
|
+
return {
|
|
3759
|
+
passed: false,
|
|
3760
|
+
effectiveMergeMethods: null,
|
|
3761
|
+
declaredButBlocked: [],
|
|
3762
|
+
disallowedButPermitted: [],
|
|
3763
|
+
message: 'No enabled "Limit merge types" branch policy found for the target branch.'
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
const effective = /* @__PURE__ */ new Set();
|
|
3767
|
+
for (const policy of enabled) {
|
|
3768
|
+
const s = policy.settings ?? {};
|
|
3769
|
+
if (s.allowNoFastForward) effective.add("merge");
|
|
3770
|
+
if (s.allowRebase || s.allowRebaseMerge) effective.add("rebase");
|
|
3771
|
+
if (s.useSquashMerge) effective.add("squash");
|
|
3772
|
+
}
|
|
3773
|
+
return compareToLadder([...effective], []);
|
|
3774
|
+
}
|
|
3775
|
+
async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
|
|
3776
|
+
try {
|
|
3777
|
+
const orgUrl = `https://dev.azure.com/${org}`;
|
|
3778
|
+
const { stdout: repoOut } = await execFileAsync("az", [
|
|
3779
|
+
"repos",
|
|
3780
|
+
"show",
|
|
3781
|
+
"--repository",
|
|
3782
|
+
repoName,
|
|
3783
|
+
"--org",
|
|
3784
|
+
orgUrl,
|
|
3785
|
+
"--project",
|
|
3786
|
+
project,
|
|
3787
|
+
"--query",
|
|
3788
|
+
"id",
|
|
3789
|
+
"--output",
|
|
3790
|
+
"tsv"
|
|
3791
|
+
]);
|
|
3792
|
+
const repositoryId = repoOut.trim();
|
|
3793
|
+
if (!repositoryId) return null;
|
|
3794
|
+
const { stdout } = await execFileAsync("az", [
|
|
3795
|
+
"repos",
|
|
3796
|
+
"policy",
|
|
3797
|
+
"list",
|
|
3798
|
+
"--repository-id",
|
|
3799
|
+
repositoryId,
|
|
3800
|
+
"--branch",
|
|
3801
|
+
branch,
|
|
3802
|
+
"--org",
|
|
3803
|
+
orgUrl,
|
|
3804
|
+
"--project",
|
|
3805
|
+
project,
|
|
3806
|
+
"--output",
|
|
3807
|
+
"json"
|
|
3808
|
+
]);
|
|
3809
|
+
return JSON.parse(stdout);
|
|
3810
|
+
} catch {
|
|
3811
|
+
return null;
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
function parseGitHubRemote(url) {
|
|
3815
|
+
const match = /github\.com[/:]([^/]+)\/([^/.]+?)(?:\.git)?\/?$/.exec(url);
|
|
3816
|
+
if (!match) return null;
|
|
3817
|
+
const [, owner, repo] = match;
|
|
3818
|
+
if (!owner || !repo) return null;
|
|
3819
|
+
return { owner, repo };
|
|
3820
|
+
}
|
|
3821
|
+
function parseAdoRemote(url) {
|
|
3822
|
+
const modern = /dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/?]+)/.exec(url);
|
|
3823
|
+
if (modern) {
|
|
3824
|
+
const [, org, project, repo] = modern;
|
|
3825
|
+
if (org && project && repo) return { org, project, repo };
|
|
3826
|
+
}
|
|
3827
|
+
const legacy = /([^./]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/?]+)/.exec(url);
|
|
3828
|
+
if (legacy) {
|
|
3829
|
+
const [, org, project, repo] = legacy;
|
|
3830
|
+
if (org && project && repo) return { org, project, repo };
|
|
3831
|
+
}
|
|
3832
|
+
return null;
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
// src/commands/doctor.ts
|
|
3836
|
+
var execFileAsync2 = promisify2(execFile3);
|
|
3649
3837
|
async function checkNodeVersion() {
|
|
3650
3838
|
const name = "Node.js version";
|
|
3651
3839
|
const raw = process.version;
|
|
@@ -3683,7 +3871,7 @@ async function checkVSCodeExtension(id, label) {
|
|
|
3683
3871
|
let installBin = "code";
|
|
3684
3872
|
for (const bin of ["code", "code-insiders"]) {
|
|
3685
3873
|
try {
|
|
3686
|
-
await
|
|
3874
|
+
await execFileAsync2(bin, ["--version"]);
|
|
3687
3875
|
installBin = bin;
|
|
3688
3876
|
break;
|
|
3689
3877
|
} catch {
|
|
@@ -3875,6 +4063,49 @@ async function checkPushPolicy(targetDir) {
|
|
|
3875
4063
|
message: `declared "blocked" but not enforced \u2014 ${missing.join("; ")}. The manifest claims a protection this repository does not have.`
|
|
3876
4064
|
};
|
|
3877
4065
|
}
|
|
4066
|
+
async function checkPlatformBranchPolicy(targetDir) {
|
|
4067
|
+
const name = "Platform branch/merge policy (T11)";
|
|
4068
|
+
const targets = await pushTargets(targetDir);
|
|
4069
|
+
const origin = targets.find((t) => t.remote === "origin") ?? targets[0];
|
|
4070
|
+
const url = origin?.urls[0];
|
|
4071
|
+
if (!url) {
|
|
4072
|
+
return { name, passed: true, message: "no remote configured \u2014 nothing to check", blocking: false };
|
|
4073
|
+
}
|
|
4074
|
+
const github = parseGitHubRemote(url);
|
|
4075
|
+
if (github) {
|
|
4076
|
+
const rulesets = await fetchGitHubRulesets(github.owner, github.repo);
|
|
4077
|
+
if (rulesets === null) {
|
|
4078
|
+
return {
|
|
4079
|
+
name,
|
|
4080
|
+
passed: false,
|
|
4081
|
+
blocking: false,
|
|
4082
|
+
message: `could not query GitHub Rulesets for ${github.owner}/${github.repo} \u2014 is \`gh\` installed and authenticated? Verify manually: gh api repos/${github.owner}/${github.repo}/rulesets`
|
|
4083
|
+
};
|
|
4084
|
+
}
|
|
4085
|
+
const result = evaluateGitHubMergePolicy(rulesets);
|
|
4086
|
+
return { name, passed: result.passed, blocking: false, message: result.message };
|
|
4087
|
+
}
|
|
4088
|
+
const ado = parseAdoRemote(url);
|
|
4089
|
+
if (ado) {
|
|
4090
|
+
const policies = await fetchAdoMergeTypePolicies(ado.org, ado.project, ado.repo, "main");
|
|
4091
|
+
if (policies === null) {
|
|
4092
|
+
return {
|
|
4093
|
+
name,
|
|
4094
|
+
passed: false,
|
|
4095
|
+
blocking: false,
|
|
4096
|
+
message: `could not query the Azure DevOps "Limit merge types" branch policy for ${ado.org}/${ado.project}/${ado.repo} \u2014 is \`az\` installed and authenticated? Verify manually in the ADO web UI under Branch Policies.`
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
4099
|
+
const result = evaluateAdoMergePolicy(policies);
|
|
4100
|
+
return { name, passed: result.passed, blocking: false, message: result.message };
|
|
4101
|
+
}
|
|
4102
|
+
return {
|
|
4103
|
+
name,
|
|
4104
|
+
passed: true,
|
|
4105
|
+
blocking: false,
|
|
4106
|
+
message: "remote is neither GitHub nor Azure DevOps \u2014 platform policy verification not available for this provider"
|
|
4107
|
+
};
|
|
4108
|
+
}
|
|
3878
4109
|
async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
3879
4110
|
console.log("\nspell doctor \u2014 checking your Arcane environment\n");
|
|
3880
4111
|
const results = await Promise.all([
|
|
@@ -3883,7 +4114,8 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
3883
4114
|
checkArcaneManifest(targetDir),
|
|
3884
4115
|
Promise.resolve(checkIncidentReleaseGate()),
|
|
3885
4116
|
checkPullRebase(targetDir),
|
|
3886
|
-
checkPushPolicy(targetDir)
|
|
4117
|
+
checkPushPolicy(targetDir),
|
|
4118
|
+
checkPlatformBranchPolicy(targetDir)
|
|
3887
4119
|
]);
|
|
3888
4120
|
const sessionResults = await checkSessionContinuity(targetDir);
|
|
3889
4121
|
results.push(...sessionResults);
|