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.
- package/LICENSE +21 -21
- package/README.md +253 -253
- package/dist/assets/.arcane/agents/agent-policies-template.md +414 -414
- package/dist/assets/.arcane/governance/agent-policies.md +414 -414
- package/dist/assets/.arcane/governance/agent-work-queue-model.md +61 -61
- package/dist/assets/.arcane/governance/cicd-standards.md +227 -227
- package/dist/assets/.arcane/governance/development-methodology.md +384 -384
- package/dist/assets/.arcane/governance/git-conventions.md +759 -759
- package/dist/assets/.arcane/governance/naming-conventions.md +142 -142
- package/dist/assets/.arcane/governance/new-business-setup.md +182 -182
- package/dist/assets/.arcane/governance/portable-bootstrap.md +101 -101
- package/dist/assets/.arcane/governance/product-excellence-standards.md +211 -211
- package/dist/assets/.arcane/governance/testing-standards.md +158 -158
- package/dist/assets/.arcane/governance/universal-agent-rules.md +108 -108
- package/dist/assets/.arcane/image-prompts/adelaide-image-prompt.md +140 -140
- package/dist/assets/.arcane/image-prompts/alexander-image-prompt.md +138 -138
- package/dist/assets/.arcane/image-prompts/bess-image-prompt.md +139 -139
- package/dist/assets/.arcane/image-prompts/circe-image-prompt.md +143 -143
- package/dist/assets/.arcane/image-prompts/custodio-image-prompt.md +140 -140
- package/dist/assets/.arcane/image-prompts/iris-image-prompt.md +139 -139
- package/dist/assets/.arcane/image-prompts/kellar-image-prompt.md +141 -141
- package/dist/assets/.arcane/image-prompts/lafayette-image-prompt.md +140 -140
- package/dist/assets/.arcane/image-prompts/lince-image-prompt.md +142 -142
- package/dist/assets/.arcane/image-prompts/mercurio-image-prompt.md +140 -140
- package/dist/assets/.arcane/image-prompts/merlin-image-prompt.md +139 -139
- package/dist/assets/.arcane/image-prompts/prospero-image-prompt.md +141 -141
- package/dist/assets/.arcane.json +5 -0
- package/dist/assets/.claude/commands/spell-enchant.md +9 -9
- package/dist/assets/.github/agents/adelaide.agent.md +44 -44
- package/dist/assets/.github/agents/alexander.agent.md +43 -43
- package/dist/assets/.github/agents/bess.agent.md +42 -42
- package/dist/assets/.github/agents/circe.agent.md +43 -43
- package/dist/assets/.github/agents/custodio.agent.md +44 -44
- package/dist/assets/.github/agents/iris.agent.md +43 -43
- package/dist/assets/.github/agents/kellar.agent.md +43 -43
- package/dist/assets/.github/agents/lafayette.agent.md +42 -42
- package/dist/assets/.github/agents/lince.agent.md +43 -43
- package/dist/assets/.github/agents/mercurio.agent.md +43 -43
- package/dist/assets/.github/agents/merlin.agent.md +44 -44
- package/dist/assets/.github/agents/prospero.agent.md +43 -43
- package/dist/assets/.github/prompts/spell-close-session.prompt.md +17 -5
- package/dist/assets/.github/prompts/spell-commit-work.prompt.md +24 -6
- package/dist/assets/.github/prompts/spell-create-pull-request.prompt.md +12 -3
- package/dist/assets/.github/prompts/spell-enchant.prompt.md +385 -385
- package/dist/assets/.github/prompts/spell-feedback.prompt.md +113 -113
- package/dist/assets/.github/prompts/spell-full-cycle.prompt.md +202 -201
- package/dist/assets/.github/prompts/spell-implement.prompt.md +101 -101
- package/dist/assets/.github/prompts/spell-open-session.prompt.md +20 -5
- package/dist/assets/.github/prompts/spell-scope.prompt.md +263 -263
- package/dist/assets/agents/architecture-lead.yaml +61 -61
- package/dist/assets/agents/collaborator.yaml +59 -59
- package/dist/assets/agents/devops.yaml +60 -60
- package/dist/assets/agents/frontend-dev.yaml +60 -60
- package/dist/assets/agents/fullstack-dev.yaml +59 -59
- package/dist/assets/agents/marketing-strategist.yaml +60 -60
- package/dist/assets/agents/mobile-dev.yaml +59 -59
- package/dist/assets/agents/operations-comms.yaml +58 -58
- package/dist/assets/agents/orchestrator.yaml +60 -60
- package/dist/assets/agents/qa-lead.yaml +60 -60
- package/dist/assets/agents/research-analyst.yaml +59 -59
- package/dist/assets/agents/security-ops.yaml +61 -61
- package/dist/index.js +346 -9
- package/dist/index.js.map +1 -1
- package/package.json +72 -72
- package/dist/assets/.claude/commands/spell-eas-ios-deploy.md +0 -9
- 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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
2118
|
-
|
|
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);
|