nexarch 0.12.1 → 0.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init-agent.js +74 -37
- package/dist/commands/init-project.js +51 -13
- package/dist/commands/setup.js +43 -2
- package/dist/lib/skills.js +81 -0
- package/package.json +1 -1
|
@@ -337,29 +337,29 @@ function canonicalTargetKey(filePath) {
|
|
|
337
337
|
const abs = resolve(filePath);
|
|
338
338
|
return process.platform === "win32" || process.platform === "darwin" ? abs.toLowerCase() : abs;
|
|
339
339
|
}
|
|
340
|
-
function injectAgentConfigs(registry) {
|
|
340
|
+
function injectAgentConfigs(registry, runtimeCodes) {
|
|
341
341
|
const templateByCode = new Map(registry.instructionTemplates.map((t) => [t.code, t]));
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
342
|
+
const sortedTargets = [...registry.instructionTargets]
|
|
343
|
+
.filter((target) => target.matchMode === "exact")
|
|
344
|
+
.sort((a, b) => a.sortOrder - b.sortOrder || a.filePathPattern.localeCompare(b.filePathPattern));
|
|
345
|
+
const normalizedRuntimeCodes = Array.from(new Set((runtimeCodes ?? []).map((value) => value.trim()).filter(Boolean)));
|
|
346
|
+
const candidateTargets = normalizedRuntimeCodes.length > 0
|
|
347
|
+
? sortedTargets.filter((target) => normalizedRuntimeCodes.includes(target.runtimeCode))
|
|
348
|
+
: sortedTargets;
|
|
349
|
+
const applyToTarget = (target) => {
|
|
348
350
|
const template = templateByCode.get(target.templateCode);
|
|
349
351
|
if (!template)
|
|
350
|
-
|
|
352
|
+
return null;
|
|
351
353
|
const filePath = join(process.cwd(), target.filePathPattern);
|
|
352
|
-
const targetKey = canonicalTargetKey(filePath);
|
|
353
|
-
if (seenTargets.has(targetKey))
|
|
354
|
-
continue;
|
|
355
|
-
seenTargets.add(targetKey);
|
|
356
|
-
if (!existsSync(filePath))
|
|
357
|
-
continue;
|
|
358
|
-
const existing = readFileSync(filePath, "utf8");
|
|
359
354
|
const sectionBody = template.body.trim();
|
|
360
355
|
const sectionHeading = target.sectionHeading ?? "## Nexarch Agent Registration";
|
|
361
356
|
const sectionMarker = target.sectionMarker ?? sectionHeading;
|
|
362
357
|
const managedBody = wrapManagedSection("agent-registration", sectionBody);
|
|
358
|
+
if (!existsSync(filePath)) {
|
|
359
|
+
writeFileSync(filePath, `${managedBody}\n`, "utf8");
|
|
360
|
+
return { path: filePath, status: "injected" };
|
|
361
|
+
}
|
|
362
|
+
const existing = readFileSync(filePath, "utf8");
|
|
363
363
|
if (target.insertionMode === "replace_section") {
|
|
364
364
|
let replaced = replaceManagedSection(existing, "agent-registration", sectionBody);
|
|
365
365
|
if (replaced === existing) {
|
|
@@ -372,29 +372,61 @@ function injectAgentConfigs(registry) {
|
|
|
372
372
|
}
|
|
373
373
|
if (replaced !== existing) {
|
|
374
374
|
writeFileSync(filePath, replaced, "utf8");
|
|
375
|
-
|
|
375
|
+
return { path: filePath, status: "updated" };
|
|
376
376
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
382
|
-
writeFileSync(filePath, existing + separator + managedBody + "\n", "utf8");
|
|
383
|
-
results.push({ path: filePath, status: "injected" });
|
|
377
|
+
if (existing.includes(managedBody) || existing.includes(sectionBody)) {
|
|
378
|
+
return { path: filePath, status: "already_present" };
|
|
384
379
|
}
|
|
385
|
-
|
|
380
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
381
|
+
writeFileSync(filePath, existing + separator + managedBody + "\n", "utf8");
|
|
382
|
+
return { path: filePath, status: "injected" };
|
|
386
383
|
}
|
|
387
384
|
if (existing.includes(managedBody) || existing.includes(sectionBody)) {
|
|
388
|
-
|
|
385
|
+
return { path: filePath, status: "already_present" };
|
|
389
386
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
387
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
388
|
+
const next = existing + separator + managedBody + "\n";
|
|
389
|
+
writeFileSync(filePath, next, "utf8");
|
|
390
|
+
return { path: filePath, status: "injected" };
|
|
391
|
+
};
|
|
392
|
+
const seenTargets = new Set();
|
|
393
|
+
const existingMatches = [];
|
|
394
|
+
for (const target of candidateTargets) {
|
|
395
|
+
const filePath = join(process.cwd(), target.filePathPattern);
|
|
396
|
+
const targetKey = canonicalTargetKey(filePath);
|
|
397
|
+
if (seenTargets.has(targetKey))
|
|
398
|
+
continue;
|
|
399
|
+
seenTargets.add(targetKey);
|
|
400
|
+
if (existsSync(filePath))
|
|
401
|
+
existingMatches.push(target);
|
|
402
|
+
}
|
|
403
|
+
const resultsFromExisting = (targets, apply) => targets
|
|
404
|
+
.map((target) => apply(target))
|
|
405
|
+
.filter((result) => Boolean(result));
|
|
406
|
+
const existingResults = existingMatches.length > 0 ? resultsFromExisting(existingMatches, applyToTarget) : [];
|
|
407
|
+
if (existingResults.length > 0 && normalizedRuntimeCodes.length === 0)
|
|
408
|
+
return existingResults;
|
|
409
|
+
if (normalizedRuntimeCodes.length > 0) {
|
|
410
|
+
const perRuntimeResults = [];
|
|
411
|
+
const seenResultTargets = new Set(existingMatches.map((target) => canonicalTargetKey(join(process.cwd(), target.filePathPattern))));
|
|
412
|
+
for (const runtimeCode of normalizedRuntimeCodes) {
|
|
413
|
+
const runtimeTargets = sortedTargets.filter((target) => target.runtimeCode === runtimeCode);
|
|
414
|
+
if (runtimeTargets.length === 0)
|
|
415
|
+
continue;
|
|
416
|
+
const runtimeExisting = runtimeTargets.find((target) => existsSync(join(process.cwd(), target.filePathPattern)));
|
|
417
|
+
const targetToApply = runtimeExisting ?? runtimeTargets[0];
|
|
418
|
+
const targetKey = canonicalTargetKey(join(process.cwd(), targetToApply.filePathPattern));
|
|
419
|
+
if (seenResultTargets.has(targetKey))
|
|
420
|
+
continue;
|
|
421
|
+
seenResultTargets.add(targetKey);
|
|
422
|
+
const result = applyToTarget(targetToApply);
|
|
423
|
+
if (result)
|
|
424
|
+
perRuntimeResults.push(result);
|
|
395
425
|
}
|
|
426
|
+
if (perRuntimeResults.length > 0)
|
|
427
|
+
return [...existingResults, ...perRuntimeResults.filter((result, index, arr) => arr.findIndex((item) => canonicalTargetKey(item.path) === canonicalTargetKey(result.path)) === index)];
|
|
396
428
|
}
|
|
397
|
-
return
|
|
429
|
+
return [];
|
|
398
430
|
}
|
|
399
431
|
function injectTrustAttestationBlock(path, attestation) {
|
|
400
432
|
if (!attestation.token || !attestation.payload)
|
|
@@ -450,14 +482,13 @@ function injectInitProjectReportingContract(path) {
|
|
|
450
482
|
"When you run `npx nexarch@latest init-project`, treat it as skeleton registration only.",
|
|
451
483
|
"Do not claim full completion until enrichment commands are actually executed.",
|
|
452
484
|
"",
|
|
453
|
-
"
|
|
485
|
+
"Useful status fields to report after init-project:",
|
|
454
486
|
'- `registration_status`: `"skeleton_only"` or `"enriched"`',
|
|
455
487
|
'- `enrichment_completed`: `true` or `false`',
|
|
456
488
|
"",
|
|
457
|
-
"
|
|
458
|
-
'-
|
|
459
|
-
'-
|
|
460
|
-
'- For enrichment, use explicit per-entity `update-entity` runs (no bulk entity-update shortcut).',
|
|
489
|
+
"Notes:",
|
|
490
|
+
'- While `enrichment_completed=false`, the project is a skeleton; describing it as fully registered would overstate what is in the graph.',
|
|
491
|
+
'- Enrichment uses explicit per-entity `update-entity` runs rather than a bulk shortcut, so each entity gets evidence-based content.',
|
|
461
492
|
"",
|
|
462
493
|
"You may set `registration_status=\"enriched\"` only after executing enrichment actions (e.g. update-entity for project/sub-packages, aliases, and any chosen gap wiring), with evidence-based per-entity descriptions/subtypes.",
|
|
463
494
|
"",
|
|
@@ -550,6 +581,7 @@ export async function initAgent(args) {
|
|
|
550
581
|
const bindRelationshipType = parseOptionValue(args, "--bind-relationship-type") ?? "depends_on";
|
|
551
582
|
const allowInstructionWriteFlag = parseFlag(args, "--allow-instruction-write");
|
|
552
583
|
const denyInstructionWriteFlag = parseFlag(args, "--deny-instruction-write");
|
|
584
|
+
const instructionRuntimeTargetsArg = parseOptionValue(args, "--instruction-runtime-targets");
|
|
553
585
|
const providerArg = parseOptionValue(args, "--provider");
|
|
554
586
|
const modelArg = parseOptionValue(args, "--model");
|
|
555
587
|
const clientArg = parseOptionValue(args, "--client");
|
|
@@ -1017,6 +1049,8 @@ export async function initAgent(args) {
|
|
|
1017
1049
|
detail: "skipped",
|
|
1018
1050
|
missingRequired: [],
|
|
1019
1051
|
};
|
|
1052
|
+
let selectedClient = clientArg ?? null;
|
|
1053
|
+
const explicitInstructionRuntimeTargets = Array.from(new Set((instructionRuntimeTargetsArg ?? "").split(",").map((value) => value.trim()).filter(Boolean)));
|
|
1020
1054
|
if (registration.ok) {
|
|
1021
1055
|
let provider = providerArg;
|
|
1022
1056
|
let model = modelArg;
|
|
@@ -1026,6 +1060,7 @@ export async function initAgent(args) {
|
|
|
1026
1060
|
model = model ?? await promptForValue("Model id (e.g. claude-sonnet-4-6): ", true);
|
|
1027
1061
|
client = client ?? await promptForValue("Client (e.g. claude-code/cursor/codex-cli): ", true);
|
|
1028
1062
|
}
|
|
1063
|
+
selectedClient = client ?? null;
|
|
1029
1064
|
const missingRequired = [
|
|
1030
1065
|
!provider ? "provider" : null,
|
|
1031
1066
|
!model ? "model" : null,
|
|
@@ -1110,7 +1145,9 @@ export async function initAgent(args) {
|
|
|
1110
1145
|
catch {
|
|
1111
1146
|
// non-fatal
|
|
1112
1147
|
}
|
|
1113
|
-
let existingInstructionTargets = injectAgentConfigs(registry
|
|
1148
|
+
let existingInstructionTargets = injectAgentConfigs(registry, explicitInstructionRuntimeTargets.length > 0
|
|
1149
|
+
? explicitInstructionRuntimeTargets
|
|
1150
|
+
: (selectedClient ? [selectedClient] : []));
|
|
1114
1151
|
if (existingInstructionTargets.length === 0) {
|
|
1115
1152
|
existingInstructionTargets = injectGenericAgentConfig(registry);
|
|
1116
1153
|
}
|
|
@@ -494,6 +494,30 @@ function readRootPackage(pkgPath) {
|
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
// Guess entity type + subtype for a sub-package based on its path and package.json scripts.
|
|
497
|
+
/**
|
|
498
|
+
* Application subtypes accepted by the ontology, as offered to the agent in
|
|
499
|
+
* enrichment guidance.
|
|
500
|
+
*
|
|
501
|
+
* Previously two separate hardcoded strings advertised `app_cli` and
|
|
502
|
+
* `app_data_pipeline`, neither of which exists — so any agent following the
|
|
503
|
+
* guidance produced an INVALID_ENTITY_SUBTYPE failure, while four real subtypes
|
|
504
|
+
* were never offered. Kept as one constant so the two guidance sites cannot
|
|
505
|
+
* drift apart again.
|
|
506
|
+
*
|
|
507
|
+
* Source of truth is ontology_entity_type_subtype for entity type `application`;
|
|
508
|
+
* `npm run ontology:export` in web/ regenerates the reference this mirrors.
|
|
509
|
+
*/
|
|
510
|
+
const APPLICATION_SUBTYPES = [
|
|
511
|
+
"app_custom_built",
|
|
512
|
+
"app_web",
|
|
513
|
+
"app_saas",
|
|
514
|
+
"app_mobile",
|
|
515
|
+
"app_integration_service",
|
|
516
|
+
"app_agent_host",
|
|
517
|
+
"app_cots",
|
|
518
|
+
"app_legacy",
|
|
519
|
+
];
|
|
520
|
+
const APPLICATION_SUBTYPE_HINT = `Valid subtypes: ${APPLICATION_SUBTYPES.join(" ")}`;
|
|
497
521
|
function classifySubPackage(pkgPath, relativePath) {
|
|
498
522
|
const topDir = relativePath.split("/")[0] ?? "";
|
|
499
523
|
// packages/* → shared library/component (no server, not independently deployable)
|
|
@@ -518,8 +542,13 @@ function classifySubPackage(pkgPath, relativePath) {
|
|
|
518
542
|
const hasServerScript = scripts.some((s) => ["start", "dev", "serve"].includes(s));
|
|
519
543
|
const hasBuildScript = scripts.some((s) => s === "build");
|
|
520
544
|
// A bin entry means it is a CLI application regardless of path.
|
|
545
|
+
//
|
|
546
|
+
// Classified as app_custom_built, not app_cli: the ontology has no app_cli
|
|
547
|
+
// subtype, and emitting one makes every CLI package fail the upsert with
|
|
548
|
+
// INVALID_ENTITY_SUBTYPE. If a first-class CLI subtype is wanted it needs an
|
|
549
|
+
// ontology migration first — see docs/strategy/adr-ontology-reset-v1.md.
|
|
521
550
|
if (hasBin)
|
|
522
|
-
return { entityType: "application", subtype: "
|
|
551
|
+
return { entityType: "application", subtype: "app_custom_built" };
|
|
523
552
|
// apps/* — treat as full application when it has its own server/dev script
|
|
524
553
|
// (independently deployable); fall back to component classification otherwise.
|
|
525
554
|
if (topDir === "apps") {
|
|
@@ -1627,7 +1656,7 @@ export async function initProject(args) {
|
|
|
1627
1656
|
instruction: `Enrich the project entity with a meaningful name, description, subtype, and icon.`,
|
|
1628
1657
|
command: `nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "<subtype>" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`,
|
|
1629
1658
|
...(entityTypeOverride === "application"
|
|
1630
|
-
? { notes: [
|
|
1659
|
+
? { notes: [APPLICATION_SUBTYPE_HINT, "Choose app_custom_built as the default if none of the others clearly apply."] }
|
|
1631
1660
|
: {}),
|
|
1632
1661
|
});
|
|
1633
1662
|
if (subPackages.length > 0) {
|
|
@@ -1776,22 +1805,31 @@ export async function initProject(args) {
|
|
|
1776
1805
|
},
|
|
1777
1806
|
};
|
|
1778
1807
|
}
|
|
1779
|
-
// Build
|
|
1780
|
-
//
|
|
1781
|
-
// to
|
|
1808
|
+
// Build the enrichment guidance block for terminal output.
|
|
1809
|
+
//
|
|
1810
|
+
// Addressed to the calling agent (Claude Code, Codex, etc.), and deliberately
|
|
1811
|
+
// phrased as a statement of what remains rather than a command to obey.
|
|
1812
|
+
//
|
|
1813
|
+
// It previously read "do not ask the user for permission, do not summarise and
|
|
1814
|
+
// pause, just execute the steps". That is indistinguishable from a prompt
|
|
1815
|
+
// injection: instructions arriving through tool output that tell an agent to
|
|
1816
|
+
// bypass user confirmation and constrain what it reports back. Safety-trained
|
|
1817
|
+
// agents should refuse it, security review will flag it, and it trains agents
|
|
1818
|
+
// to accept "skip consent" from tool output — which is the behaviour an actual
|
|
1819
|
+
// attacker needs. Stating the remaining work plainly loses nothing: an agent
|
|
1820
|
+
// that was going to continue still does.
|
|
1782
1821
|
function buildEnrichmentDirective() {
|
|
1783
1822
|
const preservedEntities = entitiesResult.preserved ?? [];
|
|
1784
1823
|
const lines = [];
|
|
1785
1824
|
if (refreshMode) {
|
|
1786
|
-
lines.push("─── REFRESH REVIEW
|
|
1787
|
-
lines.push("update-project refreshed the scan.
|
|
1788
|
-
lines.push("
|
|
1825
|
+
lines.push("─── REFRESH REVIEW ───────────────────────────────────────────────────────");
|
|
1826
|
+
lines.push("update-project refreshed the scan. Enriched entities are listed below,");
|
|
1827
|
+
lines.push("along with any new skeletons that still need completing.");
|
|
1789
1828
|
}
|
|
1790
1829
|
else {
|
|
1791
1830
|
lines.push("─── ENRICHMENT REQUIRED ─────────────────────────────────────────────────");
|
|
1792
|
-
lines.push("init-project wrote a skeleton.
|
|
1793
|
-
lines.push("
|
|
1794
|
-
lines.push("Do NOT report the project as registered until enrichment is done.");
|
|
1831
|
+
lines.push("init-project wrote a skeleton. The steps below complete the registration.");
|
|
1832
|
+
lines.push("Until they are done, this project is registered as a skeleton only.");
|
|
1795
1833
|
}
|
|
1796
1834
|
lines.push("");
|
|
1797
1835
|
lines.push(`PROJECT : ${projectExternalKey}`);
|
|
@@ -1852,11 +1890,11 @@ export async function initProject(args) {
|
|
|
1852
1890
|
}
|
|
1853
1891
|
}
|
|
1854
1892
|
lines.push("");
|
|
1855
|
-
lines.push("
|
|
1893
|
+
lines.push("REMAINING STEPS:");
|
|
1856
1894
|
let step = 1;
|
|
1857
1895
|
lines.push(` ${step++}. nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "<subtype>" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`);
|
|
1858
1896
|
if (entityTypeOverride === "application") {
|
|
1859
|
-
lines.push(`
|
|
1897
|
+
lines.push(` ${APPLICATION_SUBTYPE_HINT}`);
|
|
1860
1898
|
lines.push(` (choose app_custom_built as the default if none of the others clearly apply)`);
|
|
1861
1899
|
}
|
|
1862
1900
|
if (subPackages.length > 0) {
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,7 +2,23 @@ import { requireCredentials } from "../lib/credentials.js";
|
|
|
2
2
|
import { detectClientsFromRegistry, writeClientConfig, nexarchServerBlockFromRegistry } from "../lib/clients.js";
|
|
3
3
|
import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
|
|
4
4
|
import { initAgent } from "./init-agent.js";
|
|
5
|
+
import { installClaudeCodeSkill } from "../lib/skills.js";
|
|
5
6
|
import { login } from "./login.js";
|
|
7
|
+
function instructionRuntimeCodeForClient(code) {
|
|
8
|
+
switch (code) {
|
|
9
|
+
case "continue-dev":
|
|
10
|
+
return "continue";
|
|
11
|
+
case "claude-code":
|
|
12
|
+
case "cursor":
|
|
13
|
+
case "codex-cli":
|
|
14
|
+
case "windsurf":
|
|
15
|
+
case "copilot":
|
|
16
|
+
case "generic":
|
|
17
|
+
return code;
|
|
18
|
+
default:
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
6
22
|
export async function setup(args) {
|
|
7
23
|
try {
|
|
8
24
|
requireCredentials();
|
|
@@ -63,13 +79,38 @@ export async function setup(args) {
|
|
|
63
79
|
initAgentArgs.push("--from-setup");
|
|
64
80
|
if (!initAgentArgs.includes("--allow-instruction-write"))
|
|
65
81
|
initAgentArgs.push("--allow-instruction-write");
|
|
82
|
+
const instructionRuntimeTargets = Array.from(new Set(clients.map((client) => instructionRuntimeCodeForClient(client.code)).filter((value) => Boolean(value))));
|
|
83
|
+
if (instructionRuntimeTargets.length > 0) {
|
|
84
|
+
initAgentArgs.push("--instruction-runtime-targets", instructionRuntimeTargets.join(","));
|
|
85
|
+
}
|
|
66
86
|
await initAgent(initAgentArgs);
|
|
87
|
+
// Claude Code supports Agent Skills: a trigger description that sits
|
|
88
|
+
// permanently in the agent's context and loads a playbook when a build-shaped
|
|
89
|
+
// moment matches. MCP alone makes the graph available; the skill is what
|
|
90
|
+
// makes an agent check it before building something new. Installed under the
|
|
91
|
+
// same consent as instruction writes — setup already opts into those above.
|
|
92
|
+
const hasClaudeCode = clients.some((client) => client.code === "claude-code");
|
|
93
|
+
if (hasClaudeCode) {
|
|
94
|
+
try {
|
|
95
|
+
const skill = installClaudeCodeSkill(registry);
|
|
96
|
+
const verb = skill.status === "installed" ? "installed" : skill.status === "updated" ? "updated" : "already current";
|
|
97
|
+
console.log(`\nClaude Code skill ${verb}: ${skill.path}`);
|
|
98
|
+
if (skill.status !== "already_current") {
|
|
99
|
+
console.log(" New Claude Code sessions will check the architecture graph before building something new.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
console.log(`\nClaude Code skill install failed — ${message}`);
|
|
105
|
+
console.log(" Setup is otherwise complete; re-run setup to retry the skill.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
67
108
|
if (clients.length > 0) {
|
|
68
109
|
const names = clients.map((c) => c.name);
|
|
69
110
|
const listed = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
70
|
-
console.log(`\nSetup complete. Open a new ${listed} session
|
|
111
|
+
console.log(`\nSetup complete. Open a new ${listed} session — your Nexarch registration is already in place. If you want the agent to look for pending work, ask it to check in.`);
|
|
71
112
|
}
|
|
72
113
|
else {
|
|
73
|
-
console.log("\nSetup complete. Once you have configured an MCP client (see above), open a new session
|
|
114
|
+
console.log("\nSetup complete. Once you have configured an MCP client (see above), open a new session — your Nexarch registration is already in place. If you want the agent to look for pending work, ask it to check in.");
|
|
74
115
|
}
|
|
75
116
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code skill installation.
|
|
6
|
+
*
|
|
7
|
+
* MCP makes the architecture graph *available* to an agent; nothing makes the
|
|
8
|
+
* agent *consult* it. Tool descriptions are skimmed once and forgotten, and the
|
|
9
|
+
* injected AGENTS.md block is repo-scoped. A skill closes that gap: its
|
|
10
|
+
* description sits permanently in the agent's context as a trigger, and when a
|
|
11
|
+
* build-shaped moment matches, the full playbook loads.
|
|
12
|
+
*
|
|
13
|
+
* The body is registry-managed (template nexarch_claude_code_skill_v1) so it
|
|
14
|
+
* can be updated by migration like the instruction templates; the baked-in
|
|
15
|
+
* fallback below keeps installs working when the registry predates the
|
|
16
|
+
* template. Installed to the user-level skills directory because agent setup is
|
|
17
|
+
* a per-machine action, like MCP client configuration.
|
|
18
|
+
*/
|
|
19
|
+
export const CLAUDE_SKILL_TEMPLATE_CODE = "nexarch_claude_code_skill_v1";
|
|
20
|
+
export const CLAUDE_SKILL_DIR_NAME = "nexarch-architecture-graph";
|
|
21
|
+
export const FALLBACK_SKILL_BODY = `---
|
|
22
|
+
name: nexarch-architecture-graph
|
|
23
|
+
description: Consult the organisation's Nexarch architecture graph before building anything new, and register what gets built. Use when creating a new service, application, module, integration, API endpoint, or scheduled job; when adding a significant dependency or choosing between libraries; when asked whether a capability, integration, or dataset already exists; or when asked what applications or systems the organisation has. Requires the Nexarch MCP tools (nexarch_*).
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Nexarch Architecture Graph
|
|
27
|
+
|
|
28
|
+
The workspace has a shared architecture graph, populated by every connected
|
|
29
|
+
engineer's agent. The question this skill exists to make you ask:
|
|
30
|
+
|
|
31
|
+
> **Does something already do this?**
|
|
32
|
+
|
|
33
|
+
Answer it from the graph, not from memory — you can only remember this
|
|
34
|
+
repository; the graph knows all of them.
|
|
35
|
+
|
|
36
|
+
## Before building something new
|
|
37
|
+
|
|
38
|
+
1. \`nexarch_resolve_reference\` — resolve the raw names involved (packages,
|
|
39
|
+
platforms, product words) to canonical entities.
|
|
40
|
+
2. \`nexarch_list_entities\` — search for the capability: applications,
|
|
41
|
+
application_functions, integrations, data_stores that sound related.
|
|
42
|
+
3. **Something similar exists** → tell the human what you found — name, owner,
|
|
43
|
+
description — before writing code. Reuse beats rebuild; let them decide.
|
|
44
|
+
4. **Nothing exists** → build it, then register it (below).
|
|
45
|
+
|
|
46
|
+
## After building
|
|
47
|
+
|
|
48
|
+
- \`nexarch_upsert_entities\` for what you created; \`nexarch_upsert_relationships\`
|
|
49
|
+
to wire dependencies (\`part_of\`, \`depends_on\`, \`runs_on\`).
|
|
50
|
+
- New applications arrive as **proposed** and wait for a human to activate them
|
|
51
|
+
in the workspace — say so in your summary rather than calling them registered.
|
|
52
|
+
- Unsure whether something belongs in the graph? \`nexarch_emit_observations\`
|
|
53
|
+
is non-blocking and carries no schema commitment.
|
|
54
|
+
|
|
55
|
+
## Ground rules
|
|
56
|
+
|
|
57
|
+
- Read before you write; resolve names before treating them as unknown.
|
|
58
|
+
- Batch writes; don't spam single-entity calls.
|
|
59
|
+
- \`nexarch_get_applied_policies\` before making architectural recommendations —
|
|
60
|
+
governance constraints live there.
|
|
61
|
+
- End architectural work with a one-line summary of what was recorded and what
|
|
62
|
+
remains unresolved.
|
|
63
|
+
`;
|
|
64
|
+
export function installClaudeCodeSkill(registry, options = {}) {
|
|
65
|
+
const template = registry.instructionTemplates.find((t) => t.code === CLAUDE_SKILL_TEMPLATE_CODE);
|
|
66
|
+
const body = template ? template.body.trim() + "\n" : FALLBACK_SKILL_BODY;
|
|
67
|
+
const source = template ? "registry" : "fallback";
|
|
68
|
+
const skillDir = join(options.homeDir ?? homedir(), ".claude", "skills", CLAUDE_SKILL_DIR_NAME);
|
|
69
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
70
|
+
if (existsSync(skillPath)) {
|
|
71
|
+
const existing = readFileSync(skillPath, "utf8");
|
|
72
|
+
if (existing === body) {
|
|
73
|
+
return { path: skillPath, status: "already_current", source };
|
|
74
|
+
}
|
|
75
|
+
writeFileSync(skillPath, body, "utf8");
|
|
76
|
+
return { path: skillPath, status: "updated", source };
|
|
77
|
+
}
|
|
78
|
+
mkdirSync(skillDir, { recursive: true });
|
|
79
|
+
writeFileSync(skillPath, body, "utf8");
|
|
80
|
+
return { path: skillPath, status: "installed", source };
|
|
81
|
+
}
|