arcane-cli 0.14.0 → 0.15.1

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 (66) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +253 -253
  3. package/dist/assets/.arcane/agents/agent-policies-template.md +414 -414
  4. package/dist/assets/.arcane/governance/agent-policies.md +414 -414
  5. package/dist/assets/.arcane/governance/agent-work-queue-model.md +61 -61
  6. package/dist/assets/.arcane/governance/cicd-standards.md +227 -227
  7. package/dist/assets/.arcane/governance/development-methodology.md +384 -384
  8. package/dist/assets/.arcane/governance/git-conventions.md +759 -759
  9. package/dist/assets/.arcane/governance/naming-conventions.md +142 -142
  10. package/dist/assets/.arcane/governance/new-business-setup.md +182 -182
  11. package/dist/assets/.arcane/governance/portable-bootstrap.md +101 -101
  12. package/dist/assets/.arcane/governance/product-excellence-standards.md +211 -211
  13. package/dist/assets/.arcane/governance/testing-standards.md +158 -158
  14. package/dist/assets/.arcane/governance/universal-agent-rules.md +108 -108
  15. package/dist/assets/.arcane/image-prompts/adelaide-image-prompt.md +140 -140
  16. package/dist/assets/.arcane/image-prompts/alexander-image-prompt.md +138 -138
  17. package/dist/assets/.arcane/image-prompts/bess-image-prompt.md +139 -139
  18. package/dist/assets/.arcane/image-prompts/circe-image-prompt.md +143 -143
  19. package/dist/assets/.arcane/image-prompts/custodio-image-prompt.md +140 -140
  20. package/dist/assets/.arcane/image-prompts/iris-image-prompt.md +139 -139
  21. package/dist/assets/.arcane/image-prompts/kellar-image-prompt.md +141 -141
  22. package/dist/assets/.arcane/image-prompts/lafayette-image-prompt.md +140 -140
  23. package/dist/assets/.arcane/image-prompts/lince-image-prompt.md +142 -142
  24. package/dist/assets/.arcane/image-prompts/mercurio-image-prompt.md +140 -140
  25. package/dist/assets/.arcane/image-prompts/merlin-image-prompt.md +139 -139
  26. package/dist/assets/.arcane/image-prompts/prospero-image-prompt.md +141 -141
  27. package/dist/assets/.arcane.json +5 -0
  28. package/dist/assets/.claude/commands/spell-enchant.md +9 -9
  29. package/dist/assets/.github/agents/adelaide.agent.md +44 -44
  30. package/dist/assets/.github/agents/alexander.agent.md +43 -43
  31. package/dist/assets/.github/agents/bess.agent.md +42 -42
  32. package/dist/assets/.github/agents/circe.agent.md +43 -43
  33. package/dist/assets/.github/agents/custodio.agent.md +44 -44
  34. package/dist/assets/.github/agents/iris.agent.md +43 -43
  35. package/dist/assets/.github/agents/kellar.agent.md +43 -43
  36. package/dist/assets/.github/agents/lafayette.agent.md +42 -42
  37. package/dist/assets/.github/agents/lince.agent.md +43 -43
  38. package/dist/assets/.github/agents/mercurio.agent.md +43 -43
  39. package/dist/assets/.github/agents/merlin.agent.md +44 -44
  40. package/dist/assets/.github/agents/prospero.agent.md +43 -43
  41. package/dist/assets/.github/prompts/spell-close-session.prompt.md +17 -5
  42. package/dist/assets/.github/prompts/spell-commit-work.prompt.md +24 -6
  43. package/dist/assets/.github/prompts/spell-create-pull-request.prompt.md +12 -3
  44. package/dist/assets/.github/prompts/spell-enchant.prompt.md +385 -385
  45. package/dist/assets/.github/prompts/spell-feedback.prompt.md +113 -113
  46. package/dist/assets/.github/prompts/spell-full-cycle.prompt.md +202 -201
  47. package/dist/assets/.github/prompts/spell-implement.prompt.md +101 -101
  48. package/dist/assets/.github/prompts/spell-open-session.prompt.md +20 -5
  49. package/dist/assets/.github/prompts/spell-scope.prompt.md +263 -263
  50. package/dist/assets/agents/architecture-lead.yaml +61 -61
  51. package/dist/assets/agents/collaborator.yaml +59 -59
  52. package/dist/assets/agents/devops.yaml +60 -60
  53. package/dist/assets/agents/frontend-dev.yaml +60 -60
  54. package/dist/assets/agents/fullstack-dev.yaml +59 -59
  55. package/dist/assets/agents/marketing-strategist.yaml +60 -60
  56. package/dist/assets/agents/mobile-dev.yaml +59 -59
  57. package/dist/assets/agents/operations-comms.yaml +58 -58
  58. package/dist/assets/agents/orchestrator.yaml +60 -60
  59. package/dist/assets/agents/qa-lead.yaml +60 -60
  60. package/dist/assets/agents/research-analyst.yaml +59 -59
  61. package/dist/assets/agents/security-ops.yaml +61 -61
  62. package/dist/index.js +346 -9
  63. package/dist/index.js.map +1 -1
  64. package/package.json +72 -72
  65. package/dist/assets/.claude/commands/spell-eas-ios-deploy.md +0 -9
  66. package/dist/assets/.github/prompts/spell-eas-ios-deploy.prompt.md +0 -183
package/dist/index.js CHANGED
@@ -802,6 +802,167 @@ async function applyNamingStrategy(strategy, roleIds) {
802
802
  import { readFile as readFile2, readdir as readdir2, access as access2 } from "fs/promises";
803
803
  import { join } from "path";
804
804
  import { parse } from "yaml";
805
+
806
+ // src/modules/agent-schema.ts
807
+ var CURRENT_ROSTER_SCHEMA_VERSION = 2;
808
+ var POWER_LEVELS = /* @__PURE__ */ new Set([
809
+ "Spectator",
810
+ "Apprentice",
811
+ "Wizard",
812
+ "Sorcerer",
813
+ "Magus",
814
+ "Archmage"
815
+ ]);
816
+ var AGENT_CATEGORIES = /* @__PURE__ */ new Set([
817
+ "engineering",
818
+ "operations",
819
+ "quality",
820
+ "research"
821
+ ]);
822
+ var NAMING_STRATEGIES = /* @__PURE__ */ new Set(["arcanos", "generic", "random", "custom"]);
823
+ var AGENT_PROFILES = /* @__PURE__ */ new Set(["base", "full", "custom"]);
824
+ var AgentConfigValidationError = class extends Error {
825
+ constructor(source, issues) {
826
+ super(`Invalid agent configuration in ${source}:
827
+ - ${issues.join("\n- ")}`);
828
+ this.name = "AgentConfigValidationError";
829
+ }
830
+ };
831
+ function isRecord(value) {
832
+ return typeof value === "object" && value !== null && !Array.isArray(value);
833
+ }
834
+ function requireRecord(value, path4, issues) {
835
+ if (!isRecord(value)) {
836
+ issues.push(`${path4} must be an object`);
837
+ return void 0;
838
+ }
839
+ return value;
840
+ }
841
+ function requireString(value, path4, issues) {
842
+ if (typeof value !== "string" || value.trim().length === 0) {
843
+ issues.push(`${path4} must be a non-empty string`);
844
+ }
845
+ }
846
+ function requireBoolean(value, path4, issues) {
847
+ if (typeof value !== "boolean") issues.push(`${path4} must be a boolean`);
848
+ }
849
+ function requireStringArray(value, path4, issues) {
850
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
851
+ issues.push(`${path4} must be an array of strings`);
852
+ }
853
+ }
854
+ function requireEnum(value, allowed, path4, issues) {
855
+ if (typeof value !== "string" || !allowed.has(value)) {
856
+ issues.push(`${path4} must be one of: ${[...allowed].join(", ")}`);
857
+ }
858
+ }
859
+ function validateOptionalClient(clients, key, property, expected, issues) {
860
+ if (clients[key] === void 0) return;
861
+ const client = requireRecord(clients[key], `clients.${key}`, issues);
862
+ if (!client) return;
863
+ if (expected === "string") {
864
+ requireString(client[property], `clients.${key}.${property}`, issues);
865
+ } else {
866
+ requireBoolean(client[property], `clients.${key}.${property}`, issues);
867
+ }
868
+ }
869
+ function validateAgentDefinition(value, source) {
870
+ const issues = [];
871
+ const definition = requireRecord(value, "definition", issues);
872
+ if (!definition) throw new AgentConfigValidationError(source, issues);
873
+ requireString(definition.id, "id", issues);
874
+ requireString(definition.role, "role", issues);
875
+ requireEnum(definition.category, AGENT_CATEGORIES, "category", issues);
876
+ const persona = requireRecord(definition.persona, "persona", issues);
877
+ if (persona) {
878
+ requireString(persona.description, "persona.description", issues);
879
+ requireStringArray(persona.behavioral_rules, "persona.behavioral_rules", issues);
880
+ for (const key of ["personality", "voice", "visual_description"]) {
881
+ if (persona[key] !== void 0) requireString(persona[key], `persona.${key}`, issues);
882
+ }
883
+ if (persona.catchphrases !== void 0) {
884
+ requireStringArray(persona.catchphrases, "persona.catchphrases", issues);
885
+ }
886
+ }
887
+ const model = requireRecord(definition.model, "model", issues);
888
+ if (model) {
889
+ requireString(model.primary, "model.primary", issues);
890
+ requireStringArray(model.fallback, "model.fallback", issues);
891
+ }
892
+ const tools = requireRecord(definition.tools, "tools", issues);
893
+ if (tools) {
894
+ requireStringArray(tools.allowed, "tools.allowed", issues);
895
+ requireStringArray(tools.denied, "tools.denied", issues);
896
+ }
897
+ const autonomy = requireRecord(definition.autonomy, "autonomy", issues);
898
+ if (autonomy) {
899
+ requireEnum(
900
+ autonomy.default_power_level,
901
+ POWER_LEVELS,
902
+ "autonomy.default_power_level",
903
+ issues
904
+ );
905
+ requireBoolean(autonomy.exec_allowed, "autonomy.exec_allowed", issues);
906
+ }
907
+ const spawn = requireRecord(definition.spawn, "spawn", issues);
908
+ if (spawn) {
909
+ requireBoolean(spawn.can_spawn, "spawn.can_spawn", issues);
910
+ requireStringArray(spawn.spawnable_by, "spawn.spawnable_by", issues);
911
+ }
912
+ const clients = requireRecord(definition.clients, "clients", issues);
913
+ if (clients) {
914
+ validateOptionalClient(clients, "openclaw", "workspace_prefix", "string", issues);
915
+ validateOptionalClient(clients, "copilot", "agent_file", "boolean", issues);
916
+ validateOptionalClient(clients, "claude", "include_in_instructions", "boolean", issues);
917
+ validateOptionalClient(clients, "codex", "include_in_agents_md", "boolean", issues);
918
+ }
919
+ if (issues.length > 0) throw new AgentConfigValidationError(source, issues);
920
+ return definition;
921
+ }
922
+ function validateAgentRoster(value, source) {
923
+ const issues = [];
924
+ const parsed = requireRecord(value, "roster", issues);
925
+ if (!parsed) throw new AgentConfigValidationError(source, issues);
926
+ const version = parsed.schema_version;
927
+ if (!Number.isInteger(version)) {
928
+ throw new AgentConfigValidationError(source, [
929
+ 'schema_version must be an integer; run "spell agents init" with a compatible Arcane CLI'
930
+ ]);
931
+ }
932
+ if (version > CURRENT_ROSTER_SCHEMA_VERSION) {
933
+ throw new AgentConfigValidationError(source, [
934
+ `schema_version ${version} is newer than supported version ${CURRENT_ROSTER_SCHEMA_VERSION}; upgrade Arcane before reading this roster`
935
+ ]);
936
+ }
937
+ if (version < 1) {
938
+ throw new AgentConfigValidationError(source, [
939
+ `schema_version ${version} is unsupported; supported versions are 1 and ${CURRENT_ROSTER_SCHEMA_VERSION}`
940
+ ]);
941
+ }
942
+ const roster = version === 1 ? { ...parsed, schema_version: CURRENT_ROSTER_SCHEMA_VERSION } : parsed;
943
+ requireEnum(roster.naming_strategy, NAMING_STRATEGIES, "naming_strategy", issues);
944
+ requireEnum(roster.agent_profile, AGENT_PROFILES, "agent_profile", issues);
945
+ const openclaw = requireRecord(roster.openclaw, "openclaw", issues);
946
+ if (openclaw) {
947
+ requireBoolean(openclaw.enabled, "openclaw.enabled", issues);
948
+ requireString(openclaw.workspace_root, "openclaw.workspace_root", issues);
949
+ }
950
+ if (!Array.isArray(roster.roster)) {
951
+ issues.push("roster must be an array");
952
+ } else {
953
+ roster.roster.forEach((entry, index) => {
954
+ const item = requireRecord(entry, `roster[${index}]`, issues);
955
+ if (!item) return;
956
+ requireString(item.definition, `roster[${index}].definition`, issues);
957
+ if (item.name !== null) requireString(item.name, `roster[${index}].name`, issues);
958
+ requireString(item.id, `roster[${index}].id`, issues);
959
+ });
960
+ }
961
+ if (issues.length > 0) throw new AgentConfigValidationError(source, issues);
962
+ return roster;
963
+ }
964
+
965
+ // src/modules/agent-loader.ts
805
966
  var AgentDefinitionNotFoundError = class extends Error {
806
967
  constructor(id, searchDir) {
807
968
  super(
@@ -818,6 +979,14 @@ var AgentRosterNotFoundError = class extends Error {
818
979
  this.name = "AgentRosterNotFoundError";
819
980
  }
820
981
  };
982
+ function parseYaml(content, source) {
983
+ try {
984
+ return parse(content);
985
+ } catch (err) {
986
+ const detail = err instanceof Error ? err.message : "unknown YAML parse error";
987
+ throw new AgentConfigValidationError(source, [`malformed YAML: ${detail}`]);
988
+ }
989
+ }
821
990
  async function loadAgentDefinition(agentsDir, id) {
822
991
  const filePath = join(agentsDir, `${id}.yaml`);
823
992
  let content;
@@ -826,7 +995,7 @@ async function loadAgentDefinition(agentsDir, id) {
826
995
  } catch {
827
996
  throw new AgentDefinitionNotFoundError(id, agentsDir);
828
997
  }
829
- return parse(content);
998
+ return validateAgentDefinition(parseYaml(content, filePath), filePath);
830
999
  }
831
1000
  async function loadRoster(targetDir) {
832
1001
  const rosterPath = join(targetDir, ".arcane", "agents.yaml");
@@ -836,7 +1005,7 @@ async function loadRoster(targetDir) {
836
1005
  } catch {
837
1006
  throw new AgentRosterNotFoundError();
838
1007
  }
839
- return parse(content);
1008
+ return validateAgentRoster(parseYaml(content, rosterPath), rosterPath);
840
1009
  }
841
1010
  async function rosterExists(targetDir) {
842
1011
  try {
@@ -1332,9 +1501,10 @@ async function runAgentsSync(options, targetDir, assetsDir) {
1332
1501
  try {
1333
1502
  roster = await loadRoster(targetDir);
1334
1503
  } catch (err) {
1335
- if (err instanceof AgentRosterNotFoundError) {
1504
+ if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
1336
1505
  console.error(err.message);
1337
1506
  process.exit(1);
1507
+ return;
1338
1508
  }
1339
1509
  throw err;
1340
1510
  }
@@ -1373,9 +1543,10 @@ async function runAgentsList(targetDir) {
1373
1543
  try {
1374
1544
  roster = await loadRoster(targetDir);
1375
1545
  } catch (err) {
1376
- if (err instanceof AgentRosterNotFoundError) {
1546
+ if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
1377
1547
  console.error(err.message);
1378
1548
  process.exit(1);
1549
+ return;
1379
1550
  }
1380
1551
  throw err;
1381
1552
  }
@@ -1415,6 +1586,35 @@ async function runAgentsList(targetDir) {
1415
1586
  import { execFile } from "child_process";
1416
1587
  import { promisify } from "util";
1417
1588
  var execFileAsync = promisify(execFile);
1589
+ async function inspectGitRepository(cwd) {
1590
+ try {
1591
+ const { stdout: stdout2 } = await execFileAsync(
1592
+ "git",
1593
+ ["rev-parse", "--is-inside-work-tree"],
1594
+ { cwd }
1595
+ );
1596
+ if (stdout2.trim() !== "true") {
1597
+ return { status: "not-repository" };
1598
+ }
1599
+ } catch {
1600
+ return { status: "not-repository" };
1601
+ }
1602
+ try {
1603
+ await execFileAsync("git", ["rev-parse", "--verify", "HEAD"], { cwd });
1604
+ } catch {
1605
+ return { status: "no-commits" };
1606
+ }
1607
+ const { stdout } = await execFileAsync(
1608
+ "git",
1609
+ ["status", "--porcelain", "-uall"],
1610
+ { cwd }
1611
+ );
1612
+ const lines = stdout.trim();
1613
+ return {
1614
+ status: "ready",
1615
+ uncommittedChanges: lines.length === 0 ? 0 : lines.split("\n").length
1616
+ };
1617
+ }
1418
1618
  async function countUncommittedChanges(cwd) {
1419
1619
  try {
1420
1620
  const { stdout } = await execFileAsync("git", ["status", "--porcelain", "-uall"], { cwd });
@@ -1752,6 +1952,34 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
1752
1952
  }
1753
1953
  throw err;
1754
1954
  }
1955
+ console.warn(
1956
+ `WARNING: Arcane v${packageVersion} update safety notice: commit your work before updating.`
1957
+ );
1958
+ console.warn(
1959
+ "Updates can replace managed files. A clean committed baseline is required for recovery."
1960
+ );
1961
+ const gitState = await inspectGitRepository(targetDir);
1962
+ if (gitState.status === "not-repository") {
1963
+ console.error(
1964
+ "Update refused: this directory is not a Git repository or Git is unavailable."
1965
+ );
1966
+ process.exit(1);
1967
+ return;
1968
+ }
1969
+ if (gitState.status === "no-commits") {
1970
+ console.error(
1971
+ "Update refused: this repository has no commits. Commit the current baseline before updating."
1972
+ );
1973
+ process.exit(1);
1974
+ return;
1975
+ }
1976
+ if (gitState.uncommittedChanges > 0) {
1977
+ console.error(
1978
+ `Update refused: this repository has ${gitState.uncommittedChanges} uncommitted change${gitState.uncommittedChanges === 1 ? "" : "s"}. Commit or otherwise clean the working tree before updating.`
1979
+ );
1980
+ process.exit(1);
1981
+ return;
1982
+ }
1755
1983
  if (manifest.version === packageVersion && manifest.components.length > 0) {
1756
1984
  console.log("Already up to date.");
1757
1985
  return;
@@ -1777,13 +2005,17 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
1777
2005
  const updatedFiles = [];
1778
2006
  for (const file of component.files) {
1779
2007
  const srcPath = join6(assetsDir, file);
1780
- if (options.dryRun) {
2008
+ const preserveExisting = component.skipExisting && await fileExists(join6(targetDir, file));
2009
+ if (preserveExisting) {
2010
+ console.log(` ${options.dryRun ? "[dry-run] Would preserve" : "Preserved"}: ${file}`);
2011
+ } else if (options.dryRun) {
1781
2012
  console.log(` [dry-run] Would update: ${file}`);
2013
+ fileCount++;
1782
2014
  } else {
1783
2015
  await copyFile(srcPath, targetDir, file, { force: true });
2016
+ fileCount++;
1784
2017
  }
1785
2018
  updatedFiles.push(file);
1786
- fileCount++;
1787
2019
  }
1788
2020
  for (const dir of component.directories ?? []) {
1789
2021
  const srcDirPath = join6(assetsDir, dir);
@@ -2040,6 +2272,73 @@ import { access as access3, readFile as readFile5, mkdir as mkdir5, copyFile as
2040
2272
  import { join as join9, dirname as dirname2 } from "path";
2041
2273
  import { execFile as execFile2 } from "child_process";
2042
2274
  import { promisify as promisify2 } from "util";
2275
+
2276
+ // src/config/incidents.ts
2277
+ var INCIDENT_QUEUE = [
2278
+ { id: "EF-05", severity: "medium", impact: "other", confidence: "confirmed", state: "open", detectedOn: "2026-07-31" },
2279
+ { id: "EF-08", severity: "medium", impact: "other", confidence: "confirmed", state: "open", detectedOn: "2026-07-31" },
2280
+ { id: "EF-16", severity: "high", impact: "other", confidence: "confirmed", state: "open", detectedOn: "2026-07-31" },
2281
+ { id: "EF-17", severity: "high", impact: "other", confidence: "confirmed", state: "open", detectedOn: "2026-07-31" },
2282
+ { id: "EF-20", severity: "high", impact: "other", confidence: "research", state: "open", detectedOn: "2026-07-31" },
2283
+ { id: "EF-21", severity: "high", impact: "other", confidence: "confirmed", state: "open", detectedOn: "2026-07-31" },
2284
+ { id: "EF-23", severity: "medium", impact: "other", confidence: "confirmed", state: "open", detectedOn: "2026-07-31" },
2285
+ { id: "EF-25", severity: "high", impact: "data-integrity", confidence: "confirmed", state: "resolved", detectedOn: "2026-07-14", resolution: "PRs 13 and 14" },
2286
+ { id: "EF-26", severity: "high", impact: "security", confidence: "confirmed", state: "resolved", detectedOn: "2026-07-31", resolution: "PR 15" },
2287
+ { id: "EF-27", severity: "high", impact: "security", confidence: "confirmed", state: "resolved", detectedOn: "2026-07-31", resolution: "PR 16" },
2288
+ { id: "EF-28", severity: "high", impact: "security", confidence: "confirmed", state: "resolved", detectedOn: "2026-07-31", resolution: "PR 17" },
2289
+ { id: "EF-29", severity: "medium", impact: "other", confidence: "confirmed", state: "resolved", detectedOn: "2026-07-31", resolution: "PR 18" }
2290
+ ];
2291
+
2292
+ // src/modules/incident-gate.ts
2293
+ var DEFERRAL_PATTERN = /^Deferred \d{4}-\d{2}-\d{2} — known open, accepting the risk\.$/;
2294
+ var BLOCKING_SEVERITIES = /* @__PURE__ */ new Set(["critical", "high"]);
2295
+ var PROTECTED_IMPACTS = /* @__PURE__ */ new Set(["data-integrity", "security"]);
2296
+ function evaluateIncidentGate(records) {
2297
+ const blockers = [];
2298
+ const acceptedRisks = [];
2299
+ const seen = /* @__PURE__ */ new Set();
2300
+ for (const record of records) {
2301
+ if (seen.has(record.id)) {
2302
+ blockers.push(`${record.id}: duplicate incident record`);
2303
+ continue;
2304
+ }
2305
+ seen.add(record.id);
2306
+ if (record.confidence === "research") continue;
2307
+ if (!PROTECTED_IMPACTS.has(record.impact)) continue;
2308
+ if (!record.severity) {
2309
+ blockers.push(`${record.id}: confirmed ${record.impact} incident is missing severity`);
2310
+ continue;
2311
+ }
2312
+ if (!BLOCKING_SEVERITIES.has(record.severity)) continue;
2313
+ if (record.state === "resolved") {
2314
+ if (!record.resolution?.trim()) {
2315
+ blockers.push(`${record.id}: resolved incident is missing resolution evidence`);
2316
+ }
2317
+ continue;
2318
+ }
2319
+ if (record.state === "deferred") {
2320
+ if (!record.deferral || !DEFERRAL_PATTERN.test(record.deferral)) {
2321
+ blockers.push(
2322
+ `${record.id}: deferral must be exactly "Deferred <date> \u2014 known open, accepting the risk."`
2323
+ );
2324
+ } else {
2325
+ acceptedRisks.push(`${record.id}: ${record.deferral}`);
2326
+ }
2327
+ continue;
2328
+ }
2329
+ blockers.push(
2330
+ `${record.id}: unresolved ${record.severity} ${record.impact} incident blocks release`
2331
+ );
2332
+ }
2333
+ return {
2334
+ blocked: blockers.length > 0,
2335
+ blockers,
2336
+ acceptedRisks,
2337
+ checked: records.length
2338
+ };
2339
+ }
2340
+
2341
+ // src/commands/doctor.ts
2043
2342
  var execFileAsync2 = promisify2(execFile2);
2044
2343
  async function checkNodeVersion() {
2045
2344
  const name = "Node.js version";
@@ -2096,6 +2395,28 @@ async function checkArcaneManifest(targetDir) {
2096
2395
  const manifestPath2 = join9(targetDir, ".arcane.json");
2097
2396
  try {
2098
2397
  await access3(manifestPath2);
2398
+ } catch {
2399
+ const selfHostedManifestPath = join9(targetDir, "src", "assets", ".arcane.json");
2400
+ try {
2401
+ const raw = await readFile5(selfHostedManifestPath, "utf8");
2402
+ const manifest = JSON.parse(raw);
2403
+ if (manifest.selfHosted === true && manifest.tracking_mode === "internal") {
2404
+ return {
2405
+ name,
2406
+ passed: true,
2407
+ message: "self-hosted source tree \u2014 committed source manifest is authoritative"
2408
+ };
2409
+ }
2410
+ } catch {
2411
+ }
2412
+ return {
2413
+ name,
2414
+ passed: false,
2415
+ message: "No .arcane.json found. Run `spell init` to initialize Arcane in this project.",
2416
+ blocking: false
2417
+ };
2418
+ }
2419
+ try {
2099
2420
  const raw = await readFile5(manifestPath2, "utf8");
2100
2421
  const manifest = JSON.parse(raw);
2101
2422
  if (!manifest.version || !Array.isArray(manifest.components)) {
@@ -2114,10 +2435,25 @@ async function checkArcaneManifest(targetDir) {
2114
2435
  return {
2115
2436
  name,
2116
2437
  passed: false,
2117
- message: "No .arcane.json found. Run `spell init` to initialize Arcane in this project.",
2118
- blocking: false
2438
+ message: ".arcane.json is present but contains invalid JSON. Re-run `spell init`."
2439
+ };
2440
+ }
2441
+ }
2442
+ function checkIncidentReleaseGate() {
2443
+ const result = evaluateIncidentGate(INCIDENT_QUEUE);
2444
+ if (result.blocked) {
2445
+ return {
2446
+ name: "Incident release gate (ARC-024)",
2447
+ passed: false,
2448
+ message: result.blockers.join("; ")
2119
2449
  };
2120
2450
  }
2451
+ const accepted = result.acceptedRisks.length > 0 ? `; ${result.acceptedRisks.length} explicitly deferred` : "";
2452
+ return {
2453
+ name: "Incident release gate (ARC-024)",
2454
+ passed: true,
2455
+ message: `${result.checked} incidents classified${accepted}`
2456
+ };
2121
2457
  }
2122
2458
  var SESSION_CONTINUITY_FILES = [
2123
2459
  "TODO.md",
@@ -2164,7 +2500,8 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
2164
2500
  const results = await Promise.all([
2165
2501
  checkNodeVersion(),
2166
2502
  checkVSCodeExtension("GitHub.copilot-chat", "GitHub Copilot (Chat)"),
2167
- checkArcaneManifest(targetDir)
2503
+ checkArcaneManifest(targetDir),
2504
+ Promise.resolve(checkIncidentReleaseGate())
2168
2505
  ]);
2169
2506
  const sessionResults = await checkSessionContinuity(targetDir);
2170
2507
  results.push(...sessionResults);