nexarch 0.12.3 → 0.12.5
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 +38 -19
- package/dist/commands/init-project.js +256 -100
- package/dist/commands/setup.js +41 -0
- package/dist/lib/skills.js +81 -0
- package/package.json +35 -35
|
@@ -337,13 +337,14 @@ 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
342
|
const sortedTargets = [...registry.instructionTargets]
|
|
343
343
|
.filter((target) => target.matchMode === "exact")
|
|
344
344
|
.sort((a, b) => a.sortOrder - b.sortOrder || a.filePathPattern.localeCompare(b.filePathPattern));
|
|
345
|
-
const
|
|
346
|
-
|
|
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))
|
|
347
348
|
: sortedTargets;
|
|
348
349
|
const applyToTarget = (target) => {
|
|
349
350
|
const template = templateByCode.get(target.templateCode);
|
|
@@ -399,16 +400,31 @@ function injectAgentConfigs(registry, runtimeCode) {
|
|
|
399
400
|
if (existsSync(filePath))
|
|
400
401
|
existingMatches.push(target);
|
|
401
402
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
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);
|
|
425
|
+
}
|
|
426
|
+
if (perRuntimeResults.length > 0)
|
|
427
|
+
return [...existingResults, ...perRuntimeResults.filter((result, index, arr) => arr.findIndex((item) => canonicalTargetKey(item.path) === canonicalTargetKey(result.path)) === index)];
|
|
412
428
|
}
|
|
413
429
|
return [];
|
|
414
430
|
}
|
|
@@ -466,14 +482,13 @@ function injectInitProjectReportingContract(path) {
|
|
|
466
482
|
"When you run `npx nexarch@latest init-project`, treat it as skeleton registration only.",
|
|
467
483
|
"Do not claim full completion until enrichment commands are actually executed.",
|
|
468
484
|
"",
|
|
469
|
-
"
|
|
485
|
+
"Useful status fields to report after init-project:",
|
|
470
486
|
'- `registration_status`: `"skeleton_only"` or `"enriched"`',
|
|
471
487
|
'- `enrichment_completed`: `true` or `false`',
|
|
472
488
|
"",
|
|
473
|
-
"
|
|
474
|
-
'-
|
|
475
|
-
'-
|
|
476
|
-
'- 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.',
|
|
477
492
|
"",
|
|
478
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.",
|
|
479
494
|
"",
|
|
@@ -566,6 +581,7 @@ export async function initAgent(args) {
|
|
|
566
581
|
const bindRelationshipType = parseOptionValue(args, "--bind-relationship-type") ?? "depends_on";
|
|
567
582
|
const allowInstructionWriteFlag = parseFlag(args, "--allow-instruction-write");
|
|
568
583
|
const denyInstructionWriteFlag = parseFlag(args, "--deny-instruction-write");
|
|
584
|
+
const instructionRuntimeTargetsArg = parseOptionValue(args, "--instruction-runtime-targets");
|
|
569
585
|
const providerArg = parseOptionValue(args, "--provider");
|
|
570
586
|
const modelArg = parseOptionValue(args, "--model");
|
|
571
587
|
const clientArg = parseOptionValue(args, "--client");
|
|
@@ -1034,6 +1050,7 @@ export async function initAgent(args) {
|
|
|
1034
1050
|
missingRequired: [],
|
|
1035
1051
|
};
|
|
1036
1052
|
let selectedClient = clientArg ?? null;
|
|
1053
|
+
const explicitInstructionRuntimeTargets = Array.from(new Set((instructionRuntimeTargetsArg ?? "").split(",").map((value) => value.trim()).filter(Boolean)));
|
|
1037
1054
|
if (registration.ok) {
|
|
1038
1055
|
let provider = providerArg;
|
|
1039
1056
|
let model = modelArg;
|
|
@@ -1128,7 +1145,9 @@ export async function initAgent(args) {
|
|
|
1128
1145
|
catch {
|
|
1129
1146
|
// non-fatal
|
|
1130
1147
|
}
|
|
1131
|
-
let existingInstructionTargets = injectAgentConfigs(registry,
|
|
1148
|
+
let existingInstructionTargets = injectAgentConfigs(registry, explicitInstructionRuntimeTargets.length > 0
|
|
1149
|
+
? explicitInstructionRuntimeTargets
|
|
1150
|
+
: (selectedClient ? [selectedClient] : []));
|
|
1132
1151
|
if (existingInstructionTargets.length === 0) {
|
|
1133
1152
|
existingInstructionTargets = injectGenericAgentConfig(registry);
|
|
1134
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") {
|
|
@@ -1133,20 +1162,32 @@ export async function initProject(args) {
|
|
|
1133
1162
|
const displayName = nameOverride ?? projectName;
|
|
1134
1163
|
const projectSlug = slugify(displayName);
|
|
1135
1164
|
let projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
|
|
1165
|
+
// ADR 8a (docs/strategy/adr-ontology-reset-v1.md): the repository is modelled
|
|
1166
|
+
// as a `project` entity — evidence, not architecture. Monorepos no longer get
|
|
1167
|
+
// a synthetic root application; deployable packages become applications
|
|
1168
|
+
// sourced_from the project. projectExternalKey remains the APPLICATION key,
|
|
1169
|
+
// used only when the repo is itself a single deployable.
|
|
1170
|
+
const projectConstruct = entityTypeOverride === "application";
|
|
1171
|
+
const isMonorepo = subPackages.length > 0;
|
|
1172
|
+
const projectDirName = nameOverride ?? basename(dir);
|
|
1173
|
+
const projectDirSlug = slugify(projectDirName);
|
|
1174
|
+
const projectEntityKey = `project:${projectDirSlug}`;
|
|
1136
1175
|
// Compute sub-package external keys now that projectSlug is known.
|
|
1137
1176
|
// Unscoped names (no "/") get prefixed with the project slug to avoid ambiguous keys
|
|
1138
1177
|
// like "application:crawler" — they become "application:whatsontap-crawler" instead,
|
|
1139
1178
|
// matching what the enrichment agent would naturally choose.
|
|
1179
|
+
// Keys derive from the directory basename, not package.json name: paths are
|
|
1180
|
+
// stable, unique within a repo, and human-meaningful, while npm names can
|
|
1181
|
+
// collide with the project itself (this repo's CLI package is literally named
|
|
1182
|
+
// "nexarch"). For existing layouts this reproduces the pre-8a keys
|
|
1183
|
+
// byte-for-byte (application:nexarch_web), keeping re-runs idempotent.
|
|
1184
|
+
const seenSubSlugs = new Set();
|
|
1140
1185
|
for (const sp of subPackages) {
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
const relSlug = slugify(sp.relativePath.replace(/\//g, "_"));
|
|
1147
|
-
keySlug = relSlug ? `${projectSlug}_${relSlug}` : `${projectSlug}_${nameSlug}_sub`;
|
|
1148
|
-
}
|
|
1149
|
-
sp.externalKey = `${sp.entityType}:${keySlug}`;
|
|
1186
|
+
let dirSlug = slugify(basename(sp.relativePath));
|
|
1187
|
+
if (seenSubSlugs.has(dirSlug))
|
|
1188
|
+
dirSlug = slugify(sp.relativePath.replace(/\//g, "_"));
|
|
1189
|
+
seenSubSlugs.add(dirSlug);
|
|
1190
|
+
sp.externalKey = `${sp.entityType}:${projectDirSlug}_${dirSlug}`;
|
|
1150
1191
|
}
|
|
1151
1192
|
if (!asJson) {
|
|
1152
1193
|
console.log(` Project : ${displayName} (${entityTypeOverride})`);
|
|
@@ -1185,9 +1226,10 @@ export async function initProject(args) {
|
|
|
1185
1226
|
const output = {
|
|
1186
1227
|
dryRun: true,
|
|
1187
1228
|
project: {
|
|
1188
|
-
name: displayName,
|
|
1189
|
-
externalKey: projectExternalKey,
|
|
1190
|
-
entityType: entityTypeOverride,
|
|
1229
|
+
name: projectConstruct ? projectDirName : displayName,
|
|
1230
|
+
externalKey: projectConstruct ? projectEntityKey : projectExternalKey,
|
|
1231
|
+
entityType: projectConstruct ? "project" : entityTypeOverride,
|
|
1232
|
+
...(projectConstruct ? { subtype: isMonorepo ? "project_monorepo" : "project_repository" } : {}),
|
|
1191
1233
|
sourceRepository: detectedRepo
|
|
1192
1234
|
? {
|
|
1193
1235
|
ref: detectedRepo.rawRef,
|
|
@@ -1241,7 +1283,10 @@ export async function initProject(args) {
|
|
|
1241
1283
|
const repoUrl = repoUrlOverride ?? detectedRepo?.url ?? null;
|
|
1242
1284
|
const sourceVcsType = detectedRepo?.vcsType ?? "unknown";
|
|
1243
1285
|
const sourceProvider = detectedRepo?.provider ?? "unknown";
|
|
1244
|
-
if (
|
|
1286
|
+
if (projectConstruct && isMonorepo && (applicationRefOverride || forceCreateApplication) && !asJson) {
|
|
1287
|
+
console.log("\nNote: monorepo detected — no root application is created (ADR 8a); --application-ref/--create-application apply only to single-package repositories.");
|
|
1288
|
+
}
|
|
1289
|
+
if (entityTypeOverride === "application" && !forceCreateApplication && !isMonorepo) {
|
|
1245
1290
|
if (applicationRefOverride) {
|
|
1246
1291
|
projectExternalKey = applicationRefOverride;
|
|
1247
1292
|
if (!asJson)
|
|
@@ -1314,7 +1359,7 @@ export async function initProject(args) {
|
|
|
1314
1359
|
}
|
|
1315
1360
|
}
|
|
1316
1361
|
}
|
|
1317
|
-
logProgress("application.target", projectExternalKey);
|
|
1362
|
+
logProgress(projectConstruct ? "project.target" : "application.target", projectConstruct ? (isMonorepo ? projectEntityKey : `${projectEntityKey} + ${projectExternalKey}`) : projectExternalKey);
|
|
1318
1363
|
// In refresh mode, snapshot the current graph state for this project before writing,
|
|
1319
1364
|
// so we can diff what changed and surface stale relationships to the agent.
|
|
1320
1365
|
let currentOutgoingRels = [];
|
|
@@ -1347,26 +1392,46 @@ export async function initProject(args) {
|
|
|
1347
1392
|
: undefined;
|
|
1348
1393
|
// Build entity list — project entity + all resolved reference entities
|
|
1349
1394
|
const entities = [];
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1395
|
+
const scanProvenanceAttributes = {
|
|
1396
|
+
source_dir: dir,
|
|
1397
|
+
scanned_at: nowIso,
|
|
1398
|
+
package_json_count: packageJsonCount,
|
|
1399
|
+
...(repoUrl ? { repository_url: repoUrl, source_repository_url: repoUrl } : {}),
|
|
1400
|
+
repository_ref: repoRef,
|
|
1401
|
+
source_repository_ref: repoRef,
|
|
1402
|
+
source_vcs_type: sourceVcsType,
|
|
1403
|
+
source_provider: sourceProvider,
|
|
1404
|
+
...(detectedRepo?.canonicalRepoRef ? { source_repository_component_ref: detectedRepo.canonicalRepoRef } : {}),
|
|
1405
|
+
};
|
|
1406
|
+
// The repository as evidence: a `project` entity (ADR 8a). It arrives active —
|
|
1407
|
+
// a repository is a fact needing no review — while the applications it sources
|
|
1408
|
+
// arrive proposed (ADR 8b, enforced at the gateway).
|
|
1409
|
+
if (projectConstruct) {
|
|
1410
|
+
entities.push({
|
|
1411
|
+
externalKey: projectEntityKey,
|
|
1412
|
+
entityTypeCode: "project",
|
|
1413
|
+
entitySubtypeCode: isMonorepo ? "project_monorepo" : "project_repository",
|
|
1414
|
+
name: projectDirName,
|
|
1415
|
+
confidence: 1,
|
|
1416
|
+
attributes: {
|
|
1417
|
+
...scanProvenanceAttributes,
|
|
1418
|
+
...(projectName !== projectDirName ? { npm_package_name: projectName } : {}),
|
|
1419
|
+
},
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
// The root application exists only when the repo IS a single deployable
|
|
1423
|
+
// (or in legacy non-application mode). A monorepo's container is the project.
|
|
1424
|
+
if (!projectConstruct || !isMonorepo) {
|
|
1425
|
+
const projectSubtype = entityTypeOverride === "application" ? "app_custom_built" : undefined;
|
|
1426
|
+
entities.push({
|
|
1427
|
+
externalKey: projectExternalKey,
|
|
1428
|
+
entityTypeCode: entityTypeOverride,
|
|
1429
|
+
...(projectSubtype ? { entitySubtypeCode: projectSubtype } : {}),
|
|
1430
|
+
name: displayName,
|
|
1431
|
+
confidence: 1,
|
|
1432
|
+
attributes: scanProvenanceAttributes,
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1370
1435
|
// Resolved reference entities (deduplicated by canonical external ref)
|
|
1371
1436
|
const seenRefs = new Set();
|
|
1372
1437
|
for (const r of resolvedItems) {
|
|
@@ -1383,7 +1448,9 @@ export async function initProject(args) {
|
|
|
1383
1448
|
});
|
|
1384
1449
|
}
|
|
1385
1450
|
// Ensure source repository component is represented when we can infer one.
|
|
1386
|
-
|
|
1451
|
+
// Under the project construct the project entity IS the repository, so the
|
|
1452
|
+
// separate technology_component stand-in is no longer created.
|
|
1453
|
+
if (!projectConstruct && detectedRepo?.canonicalRepoRef && !seenRefs.has(detectedRepo.canonicalRepoRef)) {
|
|
1387
1454
|
seenRefs.add(detectedRepo.canonicalRepoRef);
|
|
1388
1455
|
entities.push({
|
|
1389
1456
|
externalKey: detectedRepo.canonicalRepoRef,
|
|
@@ -1410,13 +1477,18 @@ export async function initProject(args) {
|
|
|
1410
1477
|
if (seenSubKeys.has(sp.externalKey))
|
|
1411
1478
|
continue;
|
|
1412
1479
|
seenSubKeys.add(sp.externalKey);
|
|
1480
|
+
const dirDisplayName = basename(sp.relativePath);
|
|
1413
1481
|
entities.push({
|
|
1414
1482
|
externalKey: sp.externalKey,
|
|
1415
1483
|
entityTypeCode: sp.entityType,
|
|
1416
1484
|
entitySubtypeCode: sp.subtype,
|
|
1417
|
-
name:
|
|
1485
|
+
name: dirDisplayName,
|
|
1418
1486
|
confidence: 0.8,
|
|
1419
|
-
attributes: {
|
|
1487
|
+
attributes: {
|
|
1488
|
+
source_dir: `${dir}/${sp.relativePath}`,
|
|
1489
|
+
scanned_at: nowIso,
|
|
1490
|
+
...(sp.name && sp.name !== dirDisplayName ? { npm_package_name: sp.name } : {}),
|
|
1491
|
+
},
|
|
1420
1492
|
});
|
|
1421
1493
|
}
|
|
1422
1494
|
// Build a lookup from input name to resolved result for enrichment task and sub-app wiring
|
|
@@ -1458,8 +1530,13 @@ export async function initProject(args) {
|
|
|
1458
1530
|
seenRelPairs.add(key);
|
|
1459
1531
|
relationships.push({ relationshipTypeCode: type, fromEntityExternalKey: from, toEntityExternalKey: to, confidence, attributes });
|
|
1460
1532
|
}
|
|
1461
|
-
// Root-level deps →
|
|
1462
|
-
|
|
1533
|
+
// Root-level deps → the root application (single-package/legacy only). A
|
|
1534
|
+
// project carries provenance, not dependencies — the ontology forbids
|
|
1535
|
+
// depends_on FROM project by design.
|
|
1536
|
+
if (projectConstruct && isMonorepo && rootDepNames.size > 0 && !asJson) {
|
|
1537
|
+
console.log(` Note: ${rootDepNames.size} root-level manifest dep(s) recorded as entities; not wired to the project (shared tooling).`);
|
|
1538
|
+
}
|
|
1539
|
+
for (const r of (projectConstruct && isMonorepo ? [] : resolvedItems)) {
|
|
1463
1540
|
if (!r.canonicalExternalRef || !r.entityTypeCode)
|
|
1464
1541
|
continue;
|
|
1465
1542
|
if (!rootDepNames.has(r.input) && !rootDepNames.has(r.normalised))
|
|
@@ -1476,11 +1553,16 @@ export async function initProject(args) {
|
|
|
1476
1553
|
if (!sp.externalKey || seenSubKeys.has(`rel:${sp.externalKey}`))
|
|
1477
1554
|
continue;
|
|
1478
1555
|
seenSubKeys.add(`rel:${sp.externalKey}`);
|
|
1479
|
-
//
|
|
1480
|
-
//
|
|
1481
|
-
//
|
|
1482
|
-
//
|
|
1483
|
-
if (
|
|
1556
|
+
// ADR 8a: provenance, not containment. Every application-like package is
|
|
1557
|
+
// sourced_from the project; the composes/part_of wiring to a synthetic root
|
|
1558
|
+
// is gone. Genuine product composition is a human assertion made through
|
|
1559
|
+
// the proposal flow, never inferred from directory layout.
|
|
1560
|
+
if (projectConstruct) {
|
|
1561
|
+
if (isApplicationLikeEntityType(sp.entityType)) {
|
|
1562
|
+
addRel("sourced_from", sp.externalKey, projectEntityKey, 1);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
else if (sp.entityType === "application_component") {
|
|
1484
1566
|
addRel("part_of", sp.externalKey, projectExternalKey);
|
|
1485
1567
|
}
|
|
1486
1568
|
else if (sp.entityType === "application") {
|
|
@@ -1521,15 +1603,21 @@ export async function initProject(args) {
|
|
|
1521
1603
|
catch {
|
|
1522
1604
|
// keep fallback
|
|
1523
1605
|
}
|
|
1524
|
-
|
|
1606
|
+
if (!projectConstruct || !isMonorepo) {
|
|
1607
|
+
addRel("accountable_for", orgExternalKey, projectExternalKey, 1);
|
|
1608
|
+
}
|
|
1525
1609
|
// Also accountable_for any sub-package applications
|
|
1526
1610
|
for (const sp of subPackages) {
|
|
1527
1611
|
if (sp.externalKey && isApplicationLikeEntityType(sp.entityType)) {
|
|
1528
1612
|
addRel("accountable_for", orgExternalKey, sp.externalKey, 1);
|
|
1529
1613
|
}
|
|
1530
1614
|
}
|
|
1531
|
-
//
|
|
1532
|
-
if (
|
|
1615
|
+
// Single-package repos: the one application is sourced_from the project.
|
|
1616
|
+
if (projectConstruct && !isMonorepo) {
|
|
1617
|
+
addRel("sourced_from", projectExternalKey, projectEntityKey, 1);
|
|
1618
|
+
}
|
|
1619
|
+
// Legacy only: the project entity now carries repository provenance itself.
|
|
1620
|
+
if (!projectConstruct && detectedRepo?.canonicalRepoRef) {
|
|
1533
1621
|
addRel("depends_on", projectExternalKey, detectedRepo.canonicalRepoRef, 0.95);
|
|
1534
1622
|
}
|
|
1535
1623
|
// In refresh mode the raw graph state (fetched above) is emitted alongside scan results
|
|
@@ -1621,29 +1709,50 @@ export async function initProject(args) {
|
|
|
1621
1709
|
// so agents using --json receive the same mandatory instructions as text-mode agents.
|
|
1622
1710
|
const pendingSteps = [];
|
|
1623
1711
|
let stepNum = 1;
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1712
|
+
if (projectConstruct) {
|
|
1713
|
+
pendingSteps.push({
|
|
1714
|
+
step: stepNum++,
|
|
1715
|
+
action: "describe_project",
|
|
1716
|
+
instruction: `Give the project (repository) entity a short description of what lives in this repo.`,
|
|
1717
|
+
command: `nexarch update-entity --key "${projectEntityKey}" --entity-type "project" --name "${projectDirName}" --description "..."`,
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
if (!projectConstruct || !isMonorepo) {
|
|
1721
|
+
pendingSteps.push({
|
|
1722
|
+
step: stepNum++,
|
|
1723
|
+
action: "enrich_entity",
|
|
1724
|
+
instruction: `Enrich the application entity with a meaningful name, description, subtype, and icon.`,
|
|
1725
|
+
command: `nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "<subtype>" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`,
|
|
1726
|
+
...(entityTypeOverride === "application"
|
|
1727
|
+
? { notes: [APPLICATION_SUBTYPE_HINT, "Choose app_custom_built as the default if none of the others clearly apply."] }
|
|
1728
|
+
: {}),
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1633
1731
|
if (subPackages.length > 0) {
|
|
1634
1732
|
pendingSteps.push({
|
|
1635
1733
|
step: stepNum++,
|
|
1636
1734
|
action: "classify_sub_packages",
|
|
1637
|
-
instruction:
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1735
|
+
instruction: projectConstruct
|
|
1736
|
+
? `For each sub-package in classifyPackages, run update-entity to confirm type/subtype/name/description. The sourced_from relationship to the project is wired automatically — no structural add-relationship is needed. The external key includes the entity type as a prefix — if you change the entity type, the key changes (e.g. application_component:foo → application:foo) and the old key's sourced_from should be retired.`
|
|
1737
|
+
: `For each sub-package in classifyPackages: (1) run update-entity to confirm type/subtype/name/description, then (2) immediately run add-relationship to wire the structural relationship. The external key includes the entity type as a prefix — if you change the entity type, the key changes (e.g. application_component:foo → application:foo). Always run update-entity before add-relationship for each package.`,
|
|
1738
|
+
commandTemplates: projectConstruct
|
|
1739
|
+
? {
|
|
1740
|
+
updateEntity: `nexarch update-entity --key "<subPackageExternalKey>" --entity-type "<entityType>" --subtype "<subtype>" --name "..." --description "..."`,
|
|
1741
|
+
}
|
|
1742
|
+
: {
|
|
1743
|
+
updateEntity: `nexarch update-entity --key "<subPackageExternalKey>" --entity-type "<entityType>" --subtype "<subtype>" --name "..." --description "..."`,
|
|
1744
|
+
wireRelationship: `nexarch add-relationship --from "<from>" --to "<to>" --type <composes|part_of> (see structuralRelationship on each classifyPackages entry)`,
|
|
1745
|
+
},
|
|
1746
|
+
notes: projectConstruct
|
|
1747
|
+
? [
|
|
1748
|
+
`Every package is already sourced_from ${projectEntityKey} — provenance is automatic.`,
|
|
1749
|
+
"New applications arrive as proposed and wait for human activation in the workspace — do not describe them as fully registered.",
|
|
1750
|
+
]
|
|
1751
|
+
: [
|
|
1752
|
+
"application sub-packages: parent composes sub-app → add-relationship --from parent --to sub-app --type composes",
|
|
1753
|
+
"application_component sub-packages: component part_of parent → add-relationship --from sub-pkg --to parent --type part_of",
|
|
1754
|
+
"Do NOT wire the relationship before the entity exists at the correct key.",
|
|
1755
|
+
],
|
|
1647
1756
|
});
|
|
1648
1757
|
}
|
|
1649
1758
|
if (entityTypeOverride === "application") {
|
|
@@ -1653,7 +1762,7 @@ export async function initProject(args) {
|
|
|
1653
1762
|
instruction: `Review the codebase to identify discrete application functions (what the application does). Examine named modules, route layout, service boundaries, and any architecture documentation. Register functions as application_function entities with subtype core_function (primary business function), supporting_function (auxiliary/enablement), integration_function (external connectivity), or data_function (data processing). Only register functions clearly evidenced by the codebase — do not invent them.`,
|
|
1654
1763
|
commandTemplates: {
|
|
1655
1764
|
updateEntity: `nexarch update-entity --key "application_function:${projectSlug}_<function_slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`,
|
|
1656
|
-
addRelationship: `nexarch add-relationship --from "application_function:${projectSlug}_<function_slug>" --to "${projectExternalKey}" --type part_of`,
|
|
1765
|
+
addRelationship: `nexarch add-relationship --from "application_function:${projectSlug}_<function_slug>" --to ${projectConstruct && isMonorepo ? '"<owning application key from classifyPackages>"' : `"${projectExternalKey}"`} --type part_of`,
|
|
1657
1766
|
},
|
|
1658
1767
|
});
|
|
1659
1768
|
}
|
|
@@ -1664,7 +1773,7 @@ export async function initProject(args) {
|
|
|
1664
1773
|
commandTemplates: {
|
|
1665
1774
|
resolve: `nexarch resolve-names --names "..." --json`,
|
|
1666
1775
|
updateEntity: `nexarch update-entity --key "<canonicalExternalRef>" --entity-type "<entityType>" --name "..."`,
|
|
1667
|
-
addRelationship: `nexarch add-relationship --from "${projectExternalKey}" --to "<canonicalExternalRef>" --type depends_on`,
|
|
1776
|
+
addRelationship: `nexarch add-relationship --from ${projectConstruct && isMonorepo ? '"<the application that uses it>"' : `"${projectExternalKey}"`} --to "<canonicalExternalRef>" --type depends_on`,
|
|
1668
1777
|
},
|
|
1669
1778
|
});
|
|
1670
1779
|
pendingSteps.push({
|
|
@@ -1673,7 +1782,7 @@ export async function initProject(args) {
|
|
|
1673
1782
|
instruction: `Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register each as a decision_record entity.`,
|
|
1674
1783
|
commandTemplates: {
|
|
1675
1784
|
updateEntity: `nexarch update-entity --key "decision_record:${projectSlug}_<adr_slug>" --entity-type decision_record --subtype decision_architecture --name "..." --attributes-json '{"decision":{"summary":"...","detail":"..."}}'`,
|
|
1676
|
-
addRelationship: `nexarch add-relationship --from "decision_record:${projectSlug}_<adr_slug>" --to "${projectExternalKey}" --type decides`,
|
|
1785
|
+
addRelationship: `nexarch add-relationship --from "decision_record:${projectSlug}_<adr_slug>" --to ${projectConstruct && isMonorepo ? '"<the relevant application key>"' : `"${projectExternalKey}"`} --type decides`,
|
|
1677
1786
|
},
|
|
1678
1787
|
});
|
|
1679
1788
|
const preservedEntities = entitiesResult.preserved ?? [];
|
|
@@ -1696,10 +1805,17 @@ export async function initProject(args) {
|
|
|
1696
1805
|
},
|
|
1697
1806
|
}
|
|
1698
1807
|
: {}),
|
|
1699
|
-
projectEntity:
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1808
|
+
projectEntity: projectConstruct
|
|
1809
|
+
? {
|
|
1810
|
+
externalKey: projectEntityKey,
|
|
1811
|
+
entityType: "project",
|
|
1812
|
+
subtype: isMonorepo ? "project_monorepo" : "project_repository",
|
|
1813
|
+
...(isMonorepo ? {} : { applicationExternalKey: projectExternalKey }),
|
|
1814
|
+
}
|
|
1815
|
+
: {
|
|
1816
|
+
externalKey: projectExternalKey,
|
|
1817
|
+
entityType: entityTypeOverride,
|
|
1818
|
+
},
|
|
1703
1819
|
pendingSteps,
|
|
1704
1820
|
readFiles: readmeHints,
|
|
1705
1821
|
...(refreshMode
|
|
@@ -1776,25 +1892,34 @@ export async function initProject(args) {
|
|
|
1776
1892
|
},
|
|
1777
1893
|
};
|
|
1778
1894
|
}
|
|
1779
|
-
// Build
|
|
1780
|
-
//
|
|
1781
|
-
// to
|
|
1895
|
+
// Build the enrichment guidance block for terminal output.
|
|
1896
|
+
//
|
|
1897
|
+
// Addressed to the calling agent (Claude Code, Codex, etc.), and deliberately
|
|
1898
|
+
// phrased as a statement of what remains rather than a command to obey.
|
|
1899
|
+
//
|
|
1900
|
+
// It previously read "do not ask the user for permission, do not summarise and
|
|
1901
|
+
// pause, just execute the steps". That is indistinguishable from a prompt
|
|
1902
|
+
// injection: instructions arriving through tool output that tell an agent to
|
|
1903
|
+
// bypass user confirmation and constrain what it reports back. Safety-trained
|
|
1904
|
+
// agents should refuse it, security review will flag it, and it trains agents
|
|
1905
|
+
// to accept "skip consent" from tool output — which is the behaviour an actual
|
|
1906
|
+
// attacker needs. Stating the remaining work plainly loses nothing: an agent
|
|
1907
|
+
// that was going to continue still does.
|
|
1782
1908
|
function buildEnrichmentDirective() {
|
|
1783
1909
|
const preservedEntities = entitiesResult.preserved ?? [];
|
|
1784
1910
|
const lines = [];
|
|
1785
1911
|
if (refreshMode) {
|
|
1786
|
-
lines.push("─── REFRESH REVIEW
|
|
1787
|
-
lines.push("update-project refreshed the scan.
|
|
1788
|
-
lines.push("
|
|
1912
|
+
lines.push("─── REFRESH REVIEW ───────────────────────────────────────────────────────");
|
|
1913
|
+
lines.push("update-project refreshed the scan. Enriched entities are listed below,");
|
|
1914
|
+
lines.push("along with any new skeletons that still need completing.");
|
|
1789
1915
|
}
|
|
1790
1916
|
else {
|
|
1791
1917
|
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.");
|
|
1918
|
+
lines.push("init-project wrote a skeleton. The steps below complete the registration.");
|
|
1919
|
+
lines.push("Until they are done, this project is registered as a skeleton only.");
|
|
1795
1920
|
}
|
|
1796
1921
|
lines.push("");
|
|
1797
|
-
lines.push(`PROJECT : ${projectExternalKey}`);
|
|
1922
|
+
lines.push(`PROJECT : ${projectConstruct ? `${projectEntityKey} (${isMonorepo ? "monorepo" : "repository"})` : projectExternalKey}`);
|
|
1798
1923
|
lines.push(`DIR : ${dir}`);
|
|
1799
1924
|
if (readmeHints.length > 0) {
|
|
1800
1925
|
lines.push("");
|
|
@@ -1823,10 +1948,15 @@ export async function initProject(args) {
|
|
|
1823
1948
|
lines.push(` unresolved deps (${unresolvedDeps.length}): ${unresolvedDeps.slice(0, 5).join(", ")}${unresolvedDeps.length > 5 ? ` … +${unresolvedDeps.length - 5} more` : ""}`);
|
|
1824
1949
|
}
|
|
1825
1950
|
lines.push(` 1) nexarch update-entity --key "${sp.externalKey}" --entity-type "${sp.entityType}" --subtype "${sp.subtype}" --name "..." --description "..."`);
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1951
|
+
if (projectConstruct) {
|
|
1952
|
+
lines.push(` (sourced_from ${projectEntityKey} is wired automatically — no structural relationship needed)`);
|
|
1953
|
+
}
|
|
1954
|
+
else {
|
|
1955
|
+
const rel = structuralRelForSubPackage(sp, projectExternalKey);
|
|
1956
|
+
if (rel) {
|
|
1957
|
+
lines.push(` 2) nexarch add-relationship --from "${rel.from}" --to "${rel.to}" --type ${rel.type}`);
|
|
1958
|
+
lines.push(` (adjust key prefix if you changed the entity type above)`);
|
|
1959
|
+
}
|
|
1830
1960
|
}
|
|
1831
1961
|
}
|
|
1832
1962
|
}
|
|
@@ -1852,12 +1982,18 @@ export async function initProject(args) {
|
|
|
1852
1982
|
}
|
|
1853
1983
|
}
|
|
1854
1984
|
lines.push("");
|
|
1855
|
-
lines.push("
|
|
1985
|
+
lines.push("REMAINING STEPS:");
|
|
1856
1986
|
let step = 1;
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
lines.push(`
|
|
1860
|
-
|
|
1987
|
+
if (projectConstruct) {
|
|
1988
|
+
lines.push(` ${step++}. nexarch update-entity --key "${projectEntityKey}" --entity-type "project" --name "${projectDirName}" --description "..."`);
|
|
1989
|
+
lines.push(` (short description of what lives in this repository)`);
|
|
1990
|
+
}
|
|
1991
|
+
if (!projectConstruct || !isMonorepo) {
|
|
1992
|
+
lines.push(` ${step++}. nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "<subtype>" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`);
|
|
1993
|
+
if (entityTypeOverride === "application") {
|
|
1994
|
+
lines.push(` ${APPLICATION_SUBTYPE_HINT}`);
|
|
1995
|
+
lines.push(` (choose app_custom_built as the default if none of the others clearly apply)`);
|
|
1996
|
+
}
|
|
1861
1997
|
}
|
|
1862
1998
|
if (subPackages.length > 0) {
|
|
1863
1999
|
lines.push(` ${step++}. Update each sub-package listed under CLASSIFY_THESE above.`);
|
|
@@ -1869,19 +2005,28 @@ export async function initProject(args) {
|
|
|
1869
2005
|
lines.push(` integration_function (external connectivity), or data_function (data processing).`);
|
|
1870
2006
|
lines.push(` Only register functions clearly evidenced by the codebase — do not invent them.`);
|
|
1871
2007
|
lines.push(` For each one found:`);
|
|
2008
|
+
const fnOwnerTarget = projectConstruct && isMonorepo ? '"<owning application key from CLASSIFY_THESE>"' : `"${projectExternalKey}"`;
|
|
1872
2009
|
lines.push(` nexarch update-entity --key "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`);
|
|
1873
|
-
lines.push(` nexarch add-relationship --from "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --to
|
|
2010
|
+
lines.push(` nexarch add-relationship --from "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --to ${fnOwnerTarget} --type part_of`);
|
|
1874
2011
|
}
|
|
1875
2012
|
lines.push(` ${step++}. Scan the READMEs for platforms/SaaS not auto-detected (Vercel, Neon, Stripe, etc.).`);
|
|
1876
2013
|
lines.push(` For each found: nexarch resolve-names --names "..." --json → nexarch update-entity → nexarch add-relationship`);
|
|
1877
2014
|
lines.push(` ${step++}. Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register decision_record entities.`);
|
|
1878
2015
|
lines.push(` nexarch update-entity --key "decision_record:${projectExternalKey.split(":")[1] ?? "project"}_<adr_slug>" --entity-type decision_record --subtype decision_architecture --name "..." --attributes-json '{"decision":{"summary":"...","detail":"..."}}'`);
|
|
1879
|
-
lines.push(` nexarch add-relationship --from "decision_record:..." --to "${projectExternalKey}" --type decides`);
|
|
2016
|
+
lines.push(` nexarch add-relationship --from "decision_record:..." --to ${projectConstruct && isMonorepo ? '"<the relevant application key>"' : `"${projectExternalKey}"`} --type decides`);
|
|
1880
2017
|
lines.push("");
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
2018
|
+
if (projectConstruct) {
|
|
2019
|
+
lines.push("RELATIONSHIP RULES:");
|
|
2020
|
+
lines.push(` Every package is sourced_from ${projectEntityKey} — wired automatically (provenance, not containment).`);
|
|
2021
|
+
lines.push(" Manifest deps are pre-wired per package; do not re-add unless a key changed.");
|
|
2022
|
+
lines.push(" Genuine product composition (one product spanning these apps) is a human judgement — propose it, never infer it from directory layout.");
|
|
2023
|
+
}
|
|
2024
|
+
else {
|
|
2025
|
+
lines.push("RELATIONSHIP DIRECTION RULES (for sub-packages):");
|
|
2026
|
+
lines.push(" apps/* (deployable components) → parent composes → sub-application");
|
|
2027
|
+
lines.push(" packages/* (shared libraries) → parent depends_on → library");
|
|
2028
|
+
lines.push(" Deps wired from manifests are already pre-wired; do not re-add unless key changed.");
|
|
2029
|
+
}
|
|
1885
2030
|
if (refreshMode && (currentOutgoingRels.length > 0 || currentPartOfRels.length > 0)) {
|
|
1886
2031
|
lines.push("");
|
|
1887
2032
|
lines.push(`GRAPH_STATE (${currentOutgoingRels.length + currentPartOfRels.length} current relationships in Nexarch — compare against what the scanner detected above):`);
|
|
@@ -1905,7 +2050,18 @@ export async function initProject(args) {
|
|
|
1905
2050
|
ok: Number(entitiesResult.summary?.failed ?? 0) === 0,
|
|
1906
2051
|
status: outputStatus,
|
|
1907
2052
|
mode: refreshMode ? "refresh" : "init",
|
|
1908
|
-
project:
|
|
2053
|
+
project: projectConstruct
|
|
2054
|
+
? {
|
|
2055
|
+
name: projectDirName,
|
|
2056
|
+
externalKey: projectEntityKey,
|
|
2057
|
+
entityType: "project",
|
|
2058
|
+
subtype: isMonorepo ? "project_monorepo" : "project_repository",
|
|
2059
|
+
detectedEcosystems,
|
|
2060
|
+
...(isMonorepo
|
|
2061
|
+
? { applications: subPackages.filter((sp) => sp.entityType === "application").map((sp) => sp.externalKey) }
|
|
2062
|
+
: { applicationExternalKey: projectExternalKey }),
|
|
2063
|
+
}
|
|
2064
|
+
: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride, detectedEcosystems },
|
|
1909
2065
|
entities: entitiesResult.summary ?? {},
|
|
1910
2066
|
relationships: relsResult?.summary ?? { requested: 0, succeeded: 0, failed: 0 },
|
|
1911
2067
|
metrics: {
|
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,7 +79,32 @@ 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]}`;
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nexarch",
|
|
3
|
-
"version": "0.12.
|
|
4
|
-
"description": "Your architecture workspace for AI delivery.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"nexarch",
|
|
7
|
-
"mcp",
|
|
8
|
-
"architecture",
|
|
9
|
-
"ai"
|
|
10
|
-
],
|
|
11
|
-
"license": "MIT",
|
|
12
|
-
"author": "Nexarch <hello@nexarch.ai>",
|
|
13
|
-
"homepage": "https://nexarch.ai",
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=18"
|
|
16
|
-
},
|
|
17
|
-
"type": "module",
|
|
18
|
-
"bin": {
|
|
19
|
-
"nexarch": "dist/index.js"
|
|
20
|
-
},
|
|
21
|
-
"files": [
|
|
22
|
-
"dist"
|
|
23
|
-
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"build": "tsc",
|
|
26
|
-
"prepublishOnly": "tsc",
|
|
27
|
-
"dev": "tsx src/index.ts",
|
|
28
|
-
"typecheck": "tsc --noEmit"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@types/node": "^22",
|
|
32
|
-
"tsx": "^4",
|
|
33
|
-
"typescript": "^5"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "nexarch",
|
|
3
|
+
"version": "0.12.5",
|
|
4
|
+
"description": "Your architecture workspace for AI delivery.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nexarch",
|
|
7
|
+
"mcp",
|
|
8
|
+
"architecture",
|
|
9
|
+
"ai"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Nexarch <hello@nexarch.ai>",
|
|
13
|
+
"homepage": "https://nexarch.ai",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"nexarch": "dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"prepublishOnly": "tsc",
|
|
27
|
+
"dev": "tsx src/index.ts",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22",
|
|
32
|
+
"tsx": "^4",
|
|
33
|
+
"typescript": "^5"
|
|
34
|
+
}
|
|
35
|
+
}
|