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.
Files changed (40) hide show
  1. package/dist/assets/.arcane/governance/universal-agent-rules.md +6 -0
  2. package/dist/assets/.claude/commands/spell-address-review.md +4 -0
  3. package/dist/assets/.claude/commands/spell-adopt-docs.md +4 -0
  4. package/dist/assets/.claude/commands/spell-arcane-version.md +4 -0
  5. package/dist/assets/.claude/commands/spell-architect.md +4 -0
  6. package/dist/assets/.claude/commands/spell-brainstorm.md +4 -0
  7. package/dist/assets/.claude/commands/spell-bug.md +4 -0
  8. package/dist/assets/.claude/commands/spell-bump.md +4 -0
  9. package/dist/assets/.claude/commands/spell-check-drift.md +4 -0
  10. package/dist/assets/.claude/commands/spell-close-session.md +4 -0
  11. package/dist/assets/.claude/commands/spell-commit-work.md +4 -0
  12. package/dist/assets/.claude/commands/spell-create-pull-request.md +4 -0
  13. package/dist/assets/.claude/commands/spell-document.md +4 -0
  14. package/dist/assets/.claude/commands/spell-dotnet-expert.md +4 -0
  15. package/dist/assets/.claude/commands/spell-enchant.md +4 -0
  16. package/dist/assets/.claude/commands/spell-explain-concept.md +4 -0
  17. package/dist/assets/.claude/commands/spell-feedback.md +4 -0
  18. package/dist/assets/.claude/commands/spell-full-cycle.md +4 -0
  19. package/dist/assets/.claude/commands/spell-generate-bot-icons.md +4 -0
  20. package/dist/assets/.claude/commands/spell-implement.md +4 -0
  21. package/dist/assets/.claude/commands/spell-make-discoverable.md +4 -0
  22. package/dist/assets/.claude/commands/spell-manifest.md +4 -0
  23. package/dist/assets/.claude/commands/spell-open-session.md +4 -0
  24. package/dist/assets/.claude/commands/spell-plan.md +4 -0
  25. package/dist/assets/.claude/commands/spell-present-arcane.md +4 -0
  26. package/dist/assets/.claude/commands/spell-product-review.md +4 -0
  27. package/dist/assets/.claude/commands/spell-review-batch.md +4 -0
  28. package/dist/assets/.claude/commands/spell-review.md +4 -0
  29. package/dist/assets/.claude/commands/spell-save-idea.md +4 -0
  30. package/dist/assets/.claude/commands/spell-scope.md +4 -0
  31. package/dist/assets/.claude/commands/spell-security-review.md +4 -0
  32. package/dist/assets/.claude/commands/spell-ship.md +4 -0
  33. package/dist/assets/.claude/commands/spell-status.md +4 -0
  34. package/dist/assets/.claude/commands/spell-suggest-feature.md +4 -0
  35. package/dist/assets/.claude/commands/spell-summon-venture.md +4 -0
  36. package/dist/assets/.claude/commands/spell-test.md +4 -0
  37. package/dist/assets/.claude/commands/spell-todo.md +4 -0
  38. package/dist/index.js +243 -11
  39. package/dist/index.js.map +1 -1
  40. 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]].
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY whenever responding to PR review comments — fetch threads, triage, fix or reply, resolve.
3
+ ---
4
+
1
5
  # Address Review
2
6
 
3
7
  Invoke the Arcane `spell-address-review` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when bringing an existing, ungoverned doc tree under Arcane governance.
3
+ ---
4
+
1
5
  # Adopt Docs
2
6
 
3
7
  Invoke the Arcane `spell-adopt-docs` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when asked about the installed Arcane version, profile, or whether an update is available.
3
+ ---
4
+
1
5
  # Arcane Version
2
6
 
3
7
  Invoke the Arcane `spell-arcane-version` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY once a PRD exists and needs architecture decisions and a story breakdown before implementation starts.
3
+ ---
4
+
1
5
  # Architect
2
6
 
3
7
  Invoke the Arcane `spell-architect` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when exploring multiple solutions to a problem before committing to one.
3
+ ---
4
+
1
5
  # Brainstorm
2
6
 
3
7
  Invoke the Arcane `spell-brainstorm` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY whenever diagnosing and fixing a bug, even if the user just describes broken behavior.
3
+ ---
4
+
1
5
  # Bug
2
6
 
3
7
  Invoke the Arcane `spell-bug` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY before shipping any change that touches distributable content, to determine and apply the correct version bump.
3
+ ---
4
+
1
5
  # Bump Version
2
6
 
3
7
  Invoke the Arcane `spell-bump` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY to detect contradictions and stale statements across core docs, periodically or after major changes.
3
+ ---
4
+
1
5
  # Check Drift
2
6
 
3
7
  Invoke the Arcane `spell-check-drift` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY at the end of any significant work session, even if the user just says 'wrap up' or 'done for now'.
3
+ ---
4
+
1
5
  # Close Session
2
6
 
3
7
  Invoke the Arcane `spell-close-session` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY whenever committing work during a session, even if the user just says 'commit this'.
3
+ ---
4
+
1
5
  # Commit Work
2
6
 
3
7
  Invoke the Arcane `spell-commit-work` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY whenever opening a pull request for the current branch, even if the user just says 'open a PR'.
3
+ ---
4
+
1
5
  # Create Pull Request
2
6
 
3
7
  Invoke the Arcane `spell-create-pull-request` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when conversation content (an analysis, a decision, an investigation) should become a permanent reference doc.
3
+ ---
4
+
1
5
  # Document
2
6
 
3
7
  Invoke the Arcane `spell-document` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when working on a .NET project and best-practice guidance is needed.
3
+ ---
4
+
1
5
  # Dotnet Expert
2
6
 
3
7
  Invoke the Arcane `spell-dotnet-expert` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when a PRD should be enhanced — UX, accessibility, security, performance — before implementation.
3
+ ---
4
+
1
5
  # Enchant
2
6
 
3
7
  Invoke the Arcane `spell-enchant` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when the user needs a concept, architecture, or system behavior explained before deciding or building.
3
+ ---
4
+
1
5
  # Explain Concept
2
6
 
3
7
  Invoke the Arcane `spell-explain-concept` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when the user reports friction with AI assistance or the workflow itself, not the product being built.
3
+ ---
4
+
1
5
  # Feedback
2
6
 
3
7
  Invoke the Arcane `spell-feedback` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY for an end-to-end feature request that should chain plan, architect, implement, test, review, and ship.
3
+ ---
4
+
1
5
  # Full Cycle
2
6
 
3
7
  Invoke the Arcane `spell-full-cycle` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when a bot/agent needs a Teams-ready avatar and icon set generated.
3
+ ---
4
+
1
5
  # Generate Bot Icons
2
6
 
3
7
  Invoke the Arcane `spell-generate-bot-icons` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY to work through stories.json autonomously once architecture and stories exist.
3
+ ---
4
+
1
5
  # Implement
2
6
 
3
7
  Invoke the Arcane `spell-implement` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when auditing a web property's search/AI-assistant discoverability (metadata, robots, sitemap, indexing).
3
+ ---
4
+
1
5
  # Make Discoverable
2
6
 
3
7
  Invoke the Arcane `spell-make-discoverable` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY in a hub repo to triage idea-book/todo-book entries downstream.
3
+ ---
4
+
1
5
  # Manifest
2
6
 
3
7
  Invoke the Arcane `spell-manifest` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY at the start of any new session, even if the user just says 'let's start' or asks 'what's next'.
3
+ ---
4
+
1
5
  # Open Session
2
6
 
3
7
  Invoke the Arcane `spell-open-session` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when a feature description needs to become a PRD before architecture or implementation.
3
+ ---
4
+
1
5
  # Plan
2
6
 
3
7
  Invoke the Arcane `spell-plan` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when asked to present, demo, or explain Arcane itself to an audience.
3
+ ---
4
+
1
5
  # Present Arcane
2
6
 
3
7
  Invoke the Arcane `spell-present-arcane` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when evaluating a shipped feature's real-world performance and deciding next actions.
3
+ ---
4
+
1
5
  # Product Review
2
6
 
3
7
  Invoke the Arcane `spell-product-review` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when multiple open PRs need an adversarial review pass and a consolidated go/no-go verdict.
3
+ ---
4
+
1
5
  # Review Batch
2
6
 
3
7
  Invoke the Arcane `spell-review-batch` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY before merging any nontrivial change, even if the user doesn't explicitly ask for review.
3
+ ---
4
+
1
5
  # Review
2
6
 
3
7
  Invoke the Arcane `spell-review` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY to capture a fleeting idea immediately, even if the user just says 'note that' or 'don't forget'.
3
+ ---
4
+
1
5
  # Save Idea
2
6
 
3
7
  Invoke the Arcane `spell-save-idea` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when a PRD is too large for one Spell Loop cycle and needs epic-splitting before architecture.
3
+ ---
4
+
1
5
  # Scope
2
6
 
3
7
  Invoke the Arcane `spell-scope` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY before shipping any change that introduces new attack surface, handles sensitive data, or changes trust boundaries.
3
+ ---
4
+
1
5
  # Security Review
2
6
 
3
7
  Invoke the Arcane `spell-security-review` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY before any deploy, even if the user just says 'ship it'.
3
+ ---
4
+
1
5
  # Ship
2
6
 
3
7
  Invoke the Arcane `spell-ship` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when asked for a quick snapshot of session/repo state — branch, TODOs, decisions, last journal.
3
+ ---
4
+
1
5
  # Status
2
6
 
3
7
  Invoke the Arcane `spell-status` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY when smoke testing or usage surfaces a feature gap worth tracking.
3
+ ---
4
+
1
5
  # Suggest Feature
2
6
 
3
7
  Invoke the Arcane `spell-suggest-feature` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY in a hub repo when creating a new venture from scratch.
3
+ ---
4
+
1
5
  # Summon Venture
2
6
 
3
7
  Invoke the Arcane `spell-summon-venture` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY to validate a completed story or feature against coverage thresholds before marking it done.
3
+ ---
4
+
1
5
  # Test
2
6
 
3
7
  Invoke the Arcane `spell-test` spell workflow.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Use PROACTIVELY whenever a raw idea needs to become one or more well-scoped TODO items, even if the user just says 'add a todo'.
3
+ ---
4
+
1
5
  # Todo
2
6
 
3
7
  Invoke the Arcane `spell-todo` spell workflow.
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 rosterSection = renderAgentRosterSection(resolved);
1583
+ const copilotSection = `${renderSpellRoutingSection()}
1584
+ ${renderAgentRosterSection(resolved)}`;
1568
1585
  const copilotMerged = await mergeIntoFile(
1569
1586
  targetDir,
1570
1587
  ".github/copilot-instructions.md",
1571
- rosterSection,
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 rosterSection = renderAgentRosterSection(resolved);
1595
+ const claudeSection = `${renderSpellRoutingSection()}
1596
+ ${renderAgentRosterSection(resolved)}`;
1579
1597
  const claudeMerged = await mergeIntoFile(
1580
1598
  targetDir,
1581
1599
  "CLAUDE.md",
1582
- rosterSection,
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 rosterSection = renderAgentRosterSection(resolved);
1606
+ const codexSection = `${renderSpellRoutingSection()}
1607
+ ${renderAgentRosterSection(resolved)}`;
1589
1608
  const codexMerged = await mergeIntoFile(
1590
1609
  targetDir,
1591
1610
  "AGENTS.md",
1592
- rosterSection,
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 execFile2 } from "child_process";
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/commands/doctor.ts
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 execFileAsync(bin, ["--version"]);
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);