@ryuenn3123/agentic-senior-core 1.9.1 → 1.9.2

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.
@@ -1,1556 +1,61 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Agentic-Senior-Core CLI (V1.7)
3
+ * Agentic-Senior-Core CLI — Thin entry point.
4
4
  *
5
- * Newbie-first delivery engine for bootstrapping governance files with
6
- * profile selection, stack auto-detection, and plain-language summaries.
5
+ * All logic lives in lib/cli/ modules. This file only handles
6
+ * argument dispatch and the top-level error boundary.
7
7
  */
8
+ import { exit } from 'node:process';
8
9
 
9
- const fs = require("node:fs/promises");
10
- const fsSync = require("node:fs");
11
- const path = require("node:path");
12
- const readline = require("node:readline/promises");
13
- const { stdin: input, stdout: output, exit } = require("node:process");
14
-
15
- const REPO_ROOT = path.resolve(__dirname, "..");
16
- const PACKAGE_JSON_PATH = path.join(REPO_ROOT, "package.json");
17
- const CLI_VERSION = JSON.parse(fsSync.readFileSync(PACKAGE_JSON_PATH, "utf8")).version;
18
- const AGENT_CONTEXT_DIR = path.join(REPO_ROOT, ".agent-context");
19
- const SKILL_PLATFORM_DIRECTORY = path.join(AGENT_CONTEXT_DIR, "skills");
20
- const SKILL_PLATFORM_INDEX_PATH = path.join(SKILL_PLATFORM_DIRECTORY, "index.json");
21
- const POLICY_FILE_NAME = "llm-judge-threshold.json";
22
- const PROFILE_PACKS_DIRECTORY_NAME = "profiles";
23
- const PROFILE_PACK_REQUIRED_FIELDS = [
24
- "slug",
25
- "displayName",
26
- "description",
27
- "defaultProfile",
28
- "defaultStack",
29
- "defaultBlueprint",
30
- "ciGuardrails",
31
- "lockCi",
32
- "blockingSeverities",
33
- ];
34
- const ALLOWED_SEVERITY_LEVELS = new Set(["critical", "high", "medium", "low"]);
35
- const BLUEPRINT_RECOMMENDATIONS = {
36
- "typescript.md": "api-nextjs.md",
37
- "python.md": "fastapi-service.md",
38
- "java.md": "spring-boot-api.md",
39
- "php.md": "laravel-api.md",
40
- "go.md": "go-service.md",
41
- "csharp.md": "aspnet-api.md",
42
- "react-native.md": "mobile-app.md",
43
- "flutter.md": "mobile-app.md",
44
- };
45
- const INIT_PRESETS = {
46
- "frontend-web": {
47
- profile: "balanced",
48
- stack: "typescript.md",
49
- blueprint: "api-nextjs.md",
50
- ci: true,
51
- description: "Frontend-first web app starter",
52
- },
53
- "backend-api": {
54
- profile: "balanced",
55
- stack: "python.md",
56
- blueprint: "fastapi-service.md",
57
- ci: true,
58
- description: "Backend API starter with safe defaults",
59
- },
60
- "fullstack-product": {
61
- profile: "balanced",
62
- stack: "typescript.md",
63
- blueprint: "api-nextjs.md",
64
- ci: true,
65
- description: "Product delivery starter with fullstack governance",
66
- },
67
- "platform-governance": {
68
- profile: "strict",
69
- stack: "go.md",
70
- blueprint: "go-service.md",
71
- ci: true,
72
- description: "Strict release and platform governance starter",
73
- },
74
- "mobile-react-native": {
75
- profile: "balanced",
76
- stack: "react-native.md",
77
- blueprint: "mobile-app.md",
78
- ci: true,
79
- description: "Mobile app starter for React Native",
80
- },
81
- "mobile-flutter": {
82
- profile: "balanced",
83
- stack: "flutter.md",
84
- blueprint: "mobile-app.md",
85
- ci: true,
86
- description: "Mobile app starter for Flutter",
87
- },
88
- "observability-platform": {
89
- profile: "strict",
90
- stack: "go.md",
91
- blueprint: "observability.md",
92
- ci: true,
93
- description: "Observability and platform starter",
94
- },
95
- };
96
- const PROFILE_PRESETS = {
97
- beginner: {
98
- displayName: "Beginner",
99
- description: "Safest path. Minimal decisions, TypeScript defaults, and CI enabled.",
100
- defaultStackFileName: "typescript.md",
101
- defaultBlueprintFileName: "api-nextjs.md",
102
- defaultCi: true,
103
- lockCi: true,
104
- blockingSeverities: ["critical"],
105
- },
106
- balanced: {
107
- displayName: "Balanced",
108
- description: "Recommended for most teams. Guided choices with strong default guardrails.",
109
- defaultStackFileName: null,
110
- defaultBlueprintFileName: null,
111
- defaultCi: true,
112
- lockCi: false,
113
- blockingSeverities: ["critical", "high"],
114
- },
115
- strict: {
116
- displayName: "Strict",
117
- description: "Tighter governance. CI stays on and medium-risk findings can block merges.",
118
- defaultStackFileName: null,
119
- defaultBlueprintFileName: null,
120
- defaultCi: true,
121
- lockCi: true,
122
- blockingSeverities: ["critical", "high", "medium"],
123
- },
124
- };
125
-
126
- const entryPointFiles = [
127
- ".cursorrules",
128
- ".windsurfrules",
129
- "AGENTS.md",
130
- ".agent-override.md",
131
- "mcp.json",
132
- ];
133
-
134
- const directoryCopies = [".agent-context", ".github", ".gemini", ".agents"];
135
-
136
- function printUsage() {
137
- console.log("Agentic-Senior-Core CLI");
138
- console.log("");
139
- console.log("Local runtime:");
140
- console.log(" npm exec --yes @ryuenn3123/agentic-senior-core init");
141
- console.log(" npx @ryuenn3123/agentic-senior-core init");
142
- console.log(" npm install -g @ryuenn3123/agentic-senior-core && agentic-senior-core init");
143
- console.log(" bunx @ryuenn3123/agentic-senior-core init # optional Bun path");
144
- console.log(" open GitHub template: https://github.com/fatidaprilian/Agentic-Senior-Core/generate");
145
- console.log("");
146
- console.log("Usage:");
147
- console.log(" agentic-senior-core launch");
148
- console.log(" agentic-senior-core init [target-directory] [--preset <name>] [--profile <beginner|balanced|strict>] [--profile-pack <name>] [--stack <name>] [--blueprint <name>] [--ci <true|false>] [--newbie]");
149
- console.log(" agentic-senior-core upgrade [target-directory] [--dry-run] [--yes]");
150
- console.log(" agentic-senior-core skill [domain] [--tier <standard|advance|expert|above>] [--json]");
151
- console.log(" agentic-senior-core --version");
152
- console.log("");
153
- console.log("Options:");
154
- console.log(" --help Show help");
155
- console.log(" --version Show CLI version");
156
- console.log(" --profile Choose beginner, balanced, or strict");
157
- console.log(" --preset Use a plug-and-play starter preset (frontend-web, backend-api, fullstack-product, platform-governance, mobile-react-native, mobile-flutter, observability-platform)");
158
- console.log(" --profile-pack Apply a team profile pack (startup, regulated, platform)");
159
- console.log(" --newbie Alias for --profile beginner");
160
- console.log(" --stack Override stack selection");
161
- console.log(" --blueprint Override blueprint selection");
162
- console.log(" --ci Override CI/CD guardrails (true|false)");
163
- console.log(" --dry-run Preview upgrade without writing files");
164
- console.log(" --yes Skip confirmation prompts for upgrade");
165
- console.log(" --tier Choose a skill tier for the skill selector");
166
- console.log(" --json Emit machine-readable skill selection output");
167
- }
168
-
169
- async function pathExists(targetPath) {
170
- try {
171
- await fs.stat(targetPath);
172
- return true;
173
- } catch {
174
- return false;
175
- }
176
- }
177
-
178
- async function ensureDirectory(directoryPath) {
179
- await fs.mkdir(directoryPath, { recursive: true });
180
- }
181
-
182
- async function copyDirectory(sourceDirectoryPath, targetDirectoryPath) {
183
- if (path.resolve(sourceDirectoryPath) === path.resolve(targetDirectoryPath)) {
184
- return;
185
- }
186
-
187
- await ensureDirectory(targetDirectoryPath);
188
- const directoryEntries = await fs.readdir(sourceDirectoryPath, { withFileTypes: true });
189
-
190
- for (const directoryEntry of directoryEntries) {
191
- const sourceEntryPath = path.join(sourceDirectoryPath, directoryEntry.name);
192
- const targetEntryPath = path.join(targetDirectoryPath, directoryEntry.name);
193
-
194
- if (directoryEntry.isDirectory()) {
195
- await copyDirectory(sourceEntryPath, targetEntryPath);
196
- continue;
197
- }
198
-
199
- if (path.resolve(sourceEntryPath) === path.resolve(targetEntryPath)) {
200
- continue;
201
- }
202
-
203
- await fs.copyFile(sourceEntryPath, targetEntryPath);
204
- }
205
- }
206
-
207
- async function copyGovernanceAssetsToTarget(resolvedTargetDirectoryPath) {
208
- for (const sourceDirectoryName of directoryCopies) {
209
- const sourceDirectoryPath = path.join(REPO_ROOT, sourceDirectoryName);
210
- if (!(await pathExists(sourceDirectoryPath))) {
211
- continue;
212
- }
213
-
214
- await copyDirectory(sourceDirectoryPath, path.join(resolvedTargetDirectoryPath, sourceDirectoryName));
215
- }
216
-
217
- for (const entryPointFileName of entryPointFiles) {
218
- const sourceFilePath = path.join(REPO_ROOT, entryPointFileName);
219
- const targetFilePath = path.join(resolvedTargetDirectoryPath, entryPointFileName);
220
-
221
- if (!(await pathExists(sourceFilePath))) {
222
- continue;
223
- }
224
-
225
- if (path.resolve(sourceFilePath) === path.resolve(targetFilePath)) {
226
- continue;
227
- }
228
-
229
- await ensureDirectory(path.dirname(targetFilePath));
230
- await fs.copyFile(sourceFilePath, targetFilePath);
231
- }
232
- }
233
-
234
- async function askChoice(promptMessage, options, userInterface) {
235
- console.log(`\n${promptMessage}`);
236
- options.forEach((choiceLabel, choiceIndex) => {
237
- console.log(` ${choiceIndex + 1}. ${choiceLabel}`);
238
- });
239
-
240
- while (true) {
241
- const selectedRawInput = await userInterface.question("Choose a number: ");
242
- const selectedIndex = Number.parseInt(selectedRawInput.trim(), 10) - 1;
243
-
244
- if (Number.isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= options.length) {
245
- console.log("Invalid choice. Please select a valid number.");
246
- continue;
247
- }
248
-
249
- return options[selectedIndex];
250
- }
251
- }
252
-
253
- async function askYesNo(promptMessage, userInterface, defaultValue) {
254
- const suffix = typeof defaultValue === "boolean"
255
- ? defaultValue ? " (Y/n): " : " (y/N): "
256
- : " (y/n): ";
257
-
258
- while (true) {
259
- const answer = await userInterface.question(`\n${promptMessage}${suffix}`);
260
- const normalizedAnswer = answer.trim().toLowerCase();
261
-
262
- if (!normalizedAnswer && typeof defaultValue === "boolean") {
263
- return defaultValue;
264
- }
265
-
266
- if (normalizedAnswer === "y" || normalizedAnswer === "yes") return true;
267
- if (normalizedAnswer === "n" || normalizedAnswer === "no") return false;
268
-
269
- console.log("Please answer with 'y' or 'n'.");
270
- }
271
- }
272
-
273
- async function runLaunchCommand() {
274
- const userInterface = readline.createInterface({ input, output });
275
-
276
- try {
277
- console.log(`\nAgentic-Senior-Core CLI v${CLI_VERSION}`);
278
- console.log("Start with a numbered choice. You can still use commands later if you want direct control.");
279
-
280
- const launchChoice = await askChoice(
281
- "How do you want to start?",
282
- [
283
- "GitHub template (zero install)",
284
- "npm / npx path",
285
- "Bootstrap scripts",
286
- "Preset starter",
287
- "Interactive init wizard",
288
- "Skill selector",
289
- "Exit",
290
- ],
291
- userInterface
292
- );
293
-
294
- if (launchChoice === "GitHub template (zero install)") {
295
- console.log("\nOpen the GitHub template here:");
296
- console.log("https://github.com/fatidaprilian/Agentic-Senior-Core/generate");
297
- return;
298
- }
299
-
300
- if (launchChoice === "npm / npx path") {
301
- console.log("\nChoose one of these package paths:");
302
- console.log("npm exec --yes @ryuenn3123/agentic-senior-core init");
303
- console.log("npx @ryuenn3123/agentic-senior-core init");
304
- console.log("npm install -g @ryuenn3123/agentic-senior-core && agentic-senior-core init");
305
- return;
306
- }
307
-
308
- if (launchChoice === "Bootstrap scripts") {
309
- console.log("\nUse the repository bootstrap scripts:");
310
- console.log("Windows: powershell -ExecutionPolicy Bypass -File .\\scripts\\init-project.ps1 -TargetDirectory .");
311
- console.log("Linux/macOS: bash ./scripts/init-project.sh .");
312
- return;
313
- }
314
-
315
- if (launchChoice === "Preset starter") {
316
- const presetNames = Object.keys(INIT_PRESETS);
317
- const selectedPresetName = await askChoice(
318
- "Choose a starter preset:",
319
- presetNames.map((presetName) => `${presetName} - ${INIT_PRESETS[presetName].description}`),
320
- userInterface
321
- );
322
-
323
- await runInitCommand(".", { preset: normalizeChoiceInput(selectedPresetName.split(" - ")[0]) });
324
- return;
325
- }
326
-
327
- if (launchChoice === "Interactive init wizard") {
328
- await runInitCommand(".", {});
329
- return;
330
- }
331
-
332
- if (launchChoice === "Skill selector") {
333
- await runSkillCommand([]);
334
- return;
335
- }
336
-
337
- console.log("Exit selected.");
338
- } finally {
339
- userInterface.close();
340
- }
341
- }
342
-
343
- function toTitleCase(fileName) {
344
- return fileName
345
- .replace(/\.md$/i, "")
346
- .split(/[-_]/g)
347
- .map((wordPart) => wordPart.charAt(0).toUpperCase() + wordPart.slice(1))
348
- .join(" ");
349
- }
350
-
351
- function normalizeChoiceInput(rawInput) {
352
- return rawInput.trim().toLowerCase().replace(/\s+/g, "-");
353
- }
354
-
355
- function matchFileNameFromInput(rawInput, fileNames) {
356
- const normalizedInput = normalizeChoiceInput(rawInput);
357
-
358
- return fileNames.find((fileName) => {
359
- const normalizedFileName = normalizeChoiceInput(fileName.replace(/\.md$/i, ""));
360
- const normalizedTitle = normalizeChoiceInput(toTitleCase(fileName));
361
- return normalizedInput === normalizedFileName || normalizedInput === normalizedTitle;
362
- });
363
- }
364
-
365
- function matchProfileNameFromInput(rawInput) {
366
- const normalizedInput = normalizeChoiceInput(rawInput);
367
- return Object.keys(PROFILE_PRESETS).find((profileName) => profileName === normalizedInput) || null;
368
- }
369
-
370
- function parseBooleanSetting(rawBooleanValue, contextLabel) {
371
- const normalizedValue = normalizeChoiceInput(rawBooleanValue);
372
-
373
- if (normalizedValue === "true") {
374
- return true;
375
- }
376
-
377
- if (normalizedValue === "false") {
378
- return false;
379
- }
380
-
381
- throw new Error(`Invalid boolean value for ${contextLabel}: ${rawBooleanValue}`);
382
- }
383
-
384
- function parseBlockingSeverities(rawSeverityValues, fileName) {
385
- const parsedSeverities = rawSeverityValues
386
- .split(",")
387
- .map((severityValue) => normalizeChoiceInput(severityValue))
388
- .filter(Boolean);
389
-
390
- if (parsedSeverities.length === 0) {
391
- throw new Error(`Profile pack ${fileName} must define at least one blocking severity.`);
392
- }
393
-
394
- const invalidSeverity = parsedSeverities.find((severityValue) => !ALLOWED_SEVERITY_LEVELS.has(severityValue));
395
- if (invalidSeverity) {
396
- throw new Error(`Profile pack ${fileName} uses unsupported severity: ${invalidSeverity}`);
397
- }
398
-
399
- return parsedSeverities;
400
- }
401
-
402
- function parseProfilePackContent(fileName, profilePackContent) {
403
- const parsedFields = {};
404
- const profilePackLines = profilePackContent.split(/\r?\n/);
405
-
406
- for (const profilePackLine of profilePackLines) {
407
- const lineMatch = profilePackLine.match(/^([A-Za-z][A-Za-z0-9 ]+):\s*(.+)$/);
408
- if (!lineMatch) {
409
- continue;
410
- }
411
-
412
- const fieldName = lineMatch[1].trim();
413
- const fieldValue = lineMatch[2].trim();
414
- parsedFields[fieldName] = fieldValue;
415
- }
416
-
417
- for (const requiredFieldName of PROFILE_PACK_REQUIRED_FIELDS) {
418
- if (!parsedFields[requiredFieldName]) {
419
- throw new Error(`Profile pack ${fileName} is missing required field: ${requiredFieldName}`);
420
- }
421
- }
422
-
423
- const defaultProfileName = matchProfileNameFromInput(parsedFields.defaultProfile);
424
- if (!defaultProfileName) {
425
- throw new Error(`Profile pack ${fileName} has invalid defaultProfile: ${parsedFields.defaultProfile}`);
426
- }
427
-
428
- return {
429
- fileName,
430
- slug: normalizeChoiceInput(parsedFields.slug),
431
- displayName: parsedFields.displayName,
432
- description: parsedFields.description,
433
- defaultProfileName,
434
- defaultStackFileName: parsedFields.defaultStack.trim(),
435
- defaultBlueprintFileName: parsedFields.defaultBlueprint.trim(),
436
- defaultCi: parseBooleanSetting(parsedFields.ciGuardrails, `${fileName} ciGuardrails`),
437
- lockCi: parseBooleanSetting(parsedFields.lockCi, `${fileName} lockCi`),
438
- blockingSeverities: parseBlockingSeverities(parsedFields.blockingSeverities, fileName),
439
- owner: parsedFields.owner || null,
440
- lastUpdated: parsedFields.lastUpdated || null,
441
- };
442
- }
443
-
444
- async function collectProfilePacks(targetDirectoryPath) {
445
- const profilePackDirectoryPath = path.join(targetDirectoryPath, ".agent-context", PROFILE_PACKS_DIRECTORY_NAME);
446
- if (!(await pathExists(profilePackDirectoryPath))) {
447
- return [];
448
- }
449
-
450
- const profilePackFileNames = await collectFileNames(profilePackDirectoryPath);
451
- const profilePackDefinitions = [];
452
-
453
- for (const profilePackFileName of profilePackFileNames) {
454
- const profilePackFilePath = path.join(profilePackDirectoryPath, profilePackFileName);
455
- const profilePackContent = await fs.readFile(profilePackFilePath, "utf8");
456
- profilePackDefinitions.push(parseProfilePackContent(profilePackFileName, profilePackContent));
457
- }
458
-
459
- return profilePackDefinitions;
460
- }
461
-
462
- function findProfilePackByInput(profilePackInput, profilePackDefinitions) {
463
- const normalizedProfilePackInput = normalizeChoiceInput(profilePackInput);
464
-
465
- return profilePackDefinitions.find((profilePackDefinition) => {
466
- const normalizedFileName = normalizeChoiceInput(profilePackDefinition.fileName.replace(/\.md$/i, ""));
467
- const normalizedSlug = normalizeChoiceInput(profilePackDefinition.slug);
468
- const normalizedDisplayName = normalizeChoiceInput(profilePackDefinition.displayName);
469
-
470
- return normalizedProfilePackInput === normalizedFileName
471
- || normalizedProfilePackInput === normalizedSlug
472
- || normalizedProfilePackInput === normalizedDisplayName;
473
- }) || null;
474
- }
475
-
476
- async function loadSkillPlatformIndex() {
477
- const skillPlatformIndexContent = await fs.readFile(SKILL_PLATFORM_INDEX_PATH, "utf8");
478
- return JSON.parse(skillPlatformIndexContent);
479
- }
480
-
481
- function normalizeSkillTierInput(rawTierInput) {
482
- const normalizedTierInput = normalizeChoiceInput(rawTierInput);
483
- const allowedTierNames = new Set(["standard", "advance", "expert", "above"]);
484
-
485
- if (!allowedTierNames.has(normalizedTierInput)) {
486
- return null;
487
- }
488
-
489
- return normalizedTierInput;
490
- }
491
-
492
- function findSkillDomainByInput(skillDomainInput, skillDomainEntries) {
493
- const normalizedSkillDomainInput = normalizeChoiceInput(skillDomainInput);
494
-
495
- return skillDomainEntries.find((skillDomainEntry) => {
496
- const normalizedDomainName = normalizeChoiceInput(skillDomainEntry.name);
497
- const normalizedDisplayName = normalizeChoiceInput(skillDomainEntry.displayName);
498
-
499
- return normalizedSkillDomainInput === normalizedDomainName || normalizedSkillDomainInput === normalizedDisplayName;
500
- }) || null;
501
- }
502
-
503
- function formatSkillTierList(skillPlatformIndex) {
504
- return skillPlatformIndex.tiers.map((tierDefinition) => `${tierDefinition.name} (${tierDefinition.description})`).join("\n");
505
- }
506
-
507
- function inferSkillDomainNamesFromSelection(selectedStackFileName, selectedBlueprintFileName) {
508
- const inferredDomainNames = new Set();
509
-
510
- if (selectedBlueprintFileName === "api-nextjs.md" || selectedBlueprintFileName === "fastapi-service.md") {
511
- inferredDomainNames.add("frontend");
512
- inferredDomainNames.add("fullstack");
513
- inferredDomainNames.add("cli");
514
- }
515
-
516
- if (selectedBlueprintFileName === "go-service.md"
517
- || selectedBlueprintFileName === "spring-boot-api.md"
518
- || selectedBlueprintFileName === "laravel-api.md"
519
- || selectedBlueprintFileName === "aspnet-api.md") {
520
- inferredDomainNames.add("backend");
521
- inferredDomainNames.add("fullstack");
522
- inferredDomainNames.add("cli");
523
- }
524
-
525
- if (selectedStackFileName === "typescript.md") {
526
- inferredDomainNames.add("frontend");
527
- inferredDomainNames.add("cli");
528
- }
529
-
530
- if (selectedStackFileName === "go.md"
531
- || selectedStackFileName === "java.md"
532
- || selectedStackFileName === "php.md"
533
- || selectedStackFileName === "csharp.md"
534
- || selectedStackFileName === "python.md"
535
- || selectedStackFileName === "ruby.md"
536
- || selectedStackFileName === "rust.md") {
537
- inferredDomainNames.add("backend");
538
- }
539
-
540
- if (selectedStackFileName === "react-native.md" || selectedStackFileName === "flutter.md") {
541
- inferredDomainNames.add("frontend");
542
- inferredDomainNames.add("fullstack");
543
- inferredDomainNames.add("cli");
544
- }
545
-
546
- if (selectedBlueprintFileName === "mobile-app.md") {
547
- inferredDomainNames.add("frontend");
548
- inferredDomainNames.add("fullstack");
549
- inferredDomainNames.add("cli");
550
- }
551
-
552
- if (selectedBlueprintFileName === "observability.md") {
553
- inferredDomainNames.add("backend");
554
- inferredDomainNames.add("fullstack");
555
- inferredDomainNames.add("cli");
556
- }
557
-
558
- if (inferredDomainNames.size === 0) {
559
- inferredDomainNames.add("fullstack");
560
- inferredDomainNames.add("cli");
561
- }
562
-
563
- return Array.from(inferredDomainNames);
564
- }
565
-
566
- async function buildSkillPackSection(skillDomainEntry, selectedTierName) {
567
- const resolvedPackFileName = skillDomainEntry.tierToPackFileNames?.[selectedTierName]
568
- || skillDomainEntry.tierToPackFileNames?.[skillDomainEntry.defaultTier]
569
- || skillDomainEntry.defaultPackFileName;
570
- const skillPackFilePath = path.join(SKILL_PLATFORM_DIRECTORY, resolvedPackFileName);
571
- const skillPackContent = await fs.readFile(skillPackFilePath, "utf8");
572
-
573
- return [
574
- `## SKILL PACK: ${skillDomainEntry.displayName}`,
575
- `Source: .agent-context/skills/${resolvedPackFileName}`,
576
- `Default tier: ${skillDomainEntry.defaultTier}`,
577
- `Selected tier: ${selectedTierName}`,
578
- `Evidence: ${skillDomainEntry.evidence}`,
579
- "",
580
- skillPackContent.trim(),
581
- "",
582
- ].join("\n");
583
- }
584
-
585
- async function runSkillCommand(commandArguments) {
586
- const parsedSkillOptions = {
587
- domain: null,
588
- tier: null,
589
- tierProvided: false,
590
- json: false,
591
- };
592
-
593
- for (let argumentIndex = 0; argumentIndex < commandArguments.length; argumentIndex++) {
594
- const currentArgument = commandArguments[argumentIndex];
595
-
596
- if (!currentArgument.startsWith("--")) {
597
- parsedSkillOptions.domain = currentArgument;
598
- continue;
599
- }
600
-
601
- if (currentArgument === "--tier") {
602
- parsedSkillOptions.tier = normalizeSkillTierInput(commandArguments[argumentIndex + 1] || "");
603
- parsedSkillOptions.tierProvided = true;
604
- argumentIndex += 1;
605
- continue;
606
- }
607
-
608
- if (currentArgument.startsWith("--tier=")) {
609
- parsedSkillOptions.tier = normalizeSkillTierInput(currentArgument.split("=")[1]);
610
- parsedSkillOptions.tierProvided = true;
611
- continue;
612
- }
613
-
614
- if (currentArgument === "--json") {
615
- parsedSkillOptions.json = true;
616
- continue;
617
- }
618
-
619
- throw new Error(`Unknown option: ${currentArgument}`);
620
- }
621
-
622
- const skillPlatformIndex = await loadSkillPlatformIndex();
623
- const skillDomainEntries = Object.values(skillPlatformIndex.domains || {});
624
- const selectedSkillDomain = parsedSkillOptions.domain
625
- ? findSkillDomainByInput(parsedSkillOptions.domain, skillDomainEntries)
626
- : null;
627
-
628
- if (parsedSkillOptions.domain && !selectedSkillDomain) {
629
- throw new Error(`Unknown skill domain: ${parsedSkillOptions.domain}`);
630
- }
631
-
632
- if (parsedSkillOptions.tierProvided && !parsedSkillOptions.tier) {
633
- throw new Error(`Unknown skill tier: ${commandArguments.join(" ")}`);
634
- }
635
-
636
- const selectedTierName = parsedSkillOptions.tier || skillPlatformIndex.defaultTier || "advance";
637
- const recommendedPackFileName = selectedSkillDomain
638
- ? selectedSkillDomain.tierToPackFileNames?.[selectedTierName]
639
- || selectedSkillDomain.tierToPackFileNames?.[selectedSkillDomain.defaultTier]
640
- || selectedSkillDomain.defaultPackFileName
641
- || null
642
- : null;
643
-
644
- if (parsedSkillOptions.json) {
645
- console.log(JSON.stringify({
646
- defaultTier: skillPlatformIndex.defaultTier,
647
- selectedTier: selectedTierName,
648
- selectedDomain: selectedSkillDomain,
649
- recommendedPackFileName,
650
- }, null, 2));
651
- return;
652
- }
653
-
654
- console.log("Skill platform selector");
655
- console.log(`Default tier: ${skillPlatformIndex.defaultTier}`);
656
- console.log(`Available tiers:\n${formatSkillTierList(skillPlatformIndex)}`);
657
-
658
- if (!selectedSkillDomain) {
659
- console.log("\nAvailable domains:");
660
- for (const skillDomainEntry of skillDomainEntries) {
661
- console.log(`- ${skillDomainEntry.name}: ${skillDomainEntry.description}`);
662
- }
663
- return;
664
- }
665
-
666
- console.log(`\nSelected domain: ${selectedSkillDomain.displayName}`);
667
- console.log(`Selected tier: ${selectedTierName}`);
668
- console.log(`Recommended pack: ${recommendedPackFileName}`);
669
- console.log(`Purpose: ${selectedSkillDomain.description}`);
670
- console.log(`Evidence: ${selectedSkillDomain.evidence}`);
671
- }
672
-
673
- async function collectFileNames(folderPath) {
674
- const fileNames = await fs.readdir(folderPath, { withFileTypes: true });
675
- return fileNames
676
- .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
677
- .map((entry) => entry.name)
678
- .sort((leftName, rightName) => leftName.localeCompare(rightName));
679
- }
680
-
681
- async function collectProjectMarkers(targetDirectoryPath) {
682
- const markerNames = new Set();
683
- const directoryEntries = await fs.readdir(targetDirectoryPath, { withFileTypes: true });
684
-
685
- for (const directoryEntry of directoryEntries) {
686
- if (directoryEntry.name === ".git" || directoryEntry.name === "node_modules") {
687
- continue;
688
- }
689
-
690
- markerNames.add(directoryEntry.name);
691
- }
692
-
693
- return markerNames;
694
- }
695
-
696
- async function detectProjectContext(targetDirectoryPath) {
697
- const markerNames = await collectProjectMarkers(targetDirectoryPath);
698
- const detectionCandidates = [];
699
- const hasExistingProjectFiles = markerNames.size > 0;
700
-
701
- if (markerNames.has("package.json") || markerNames.has("tsconfig.json") || markerNames.has("next.config.js") || markerNames.has("next.config.mjs")) {
702
- const evidence = [];
703
- let confidenceScore = 0.7;
704
-
705
- if (markerNames.has("package.json")) {
706
- evidence.push("package.json");
707
- confidenceScore += 0.12;
708
- }
709
-
710
- if (markerNames.has("tsconfig.json")) {
711
- evidence.push("tsconfig.json");
712
- confidenceScore += 0.12;
713
- }
714
-
715
- if (markerNames.has("next.config.js") || markerNames.has("next.config.mjs")) {
716
- evidence.push("Next.js config");
717
- confidenceScore += 0.05;
718
- }
719
-
720
- detectionCandidates.push({
721
- stackFileName: "typescript.md",
722
- confidenceScore: Math.min(confidenceScore, 0.97),
723
- evidence,
724
- });
725
- }
726
-
727
- if (markerNames.has("pyproject.toml") || markerNames.has("requirements.txt")) {
728
- detectionCandidates.push({
729
- stackFileName: "python.md",
730
- confidenceScore: markerNames.has("pyproject.toml") ? 0.96 : 0.78,
731
- evidence: markerNames.has("pyproject.toml") ? ["pyproject.toml"] : ["requirements.txt"],
732
- });
733
- }
734
-
735
- if (markerNames.has("pom.xml") || markerNames.has("build.gradle") || markerNames.has("build.gradle.kts")) {
736
- const evidence = [];
737
- if (markerNames.has("pom.xml")) evidence.push("pom.xml");
738
- if (markerNames.has("build.gradle") || markerNames.has("build.gradle.kts")) evidence.push("Gradle build file");
739
- detectionCandidates.push({
740
- stackFileName: "java.md",
741
- confidenceScore: markerNames.has("pom.xml") ? 0.95 : 0.84,
742
- evidence,
743
- });
744
- }
745
-
746
- if (markerNames.has("composer.json")) {
747
- detectionCandidates.push({
748
- stackFileName: "php.md",
749
- confidenceScore: 0.95,
750
- evidence: ["composer.json"],
751
- });
752
- }
753
-
754
- if (markerNames.has("go.mod")) {
755
- detectionCandidates.push({
756
- stackFileName: "go.md",
757
- confidenceScore: 0.96,
758
- evidence: ["go.mod"],
759
- });
760
- }
761
-
762
- if (markerNames.has("Cargo.toml")) {
763
- detectionCandidates.push({
764
- stackFileName: "rust.md",
765
- confidenceScore: 0.96,
766
- evidence: ["Cargo.toml"],
767
- });
768
- }
769
-
770
- if (markerNames.has("Gemfile")) {
771
- detectionCandidates.push({
772
- stackFileName: "ruby.md",
773
- confidenceScore: 0.95,
774
- evidence: ["Gemfile"],
775
- });
776
- }
777
-
778
- const hasDotNetMarker = Array.from(markerNames).some((markerName) => markerName.endsWith(".sln") || markerName.endsWith(".csproj"));
779
- if (hasDotNetMarker) {
780
- detectionCandidates.push({
781
- stackFileName: "csharp.md",
782
- confidenceScore: 0.95,
783
- evidence: [".sln or .csproj file"],
784
- });
785
- }
786
-
787
- if (markerNames.has("package.json") && (markerNames.has("android") || markerNames.has("ios") || markerNames.has("react-native.config.js"))) {
788
- detectionCandidates.push({
789
- stackFileName: "react-native.md",
790
- confidenceScore: 0.9,
791
- evidence: ["package.json", "mobile runtime markers"],
792
- });
793
- }
794
-
795
- if (markerNames.has("pubspec.yaml")) {
796
- detectionCandidates.push({
797
- stackFileName: "flutter.md",
798
- confidenceScore: 0.94,
799
- evidence: ["pubspec.yaml"],
800
- });
801
- }
802
-
803
- if (detectionCandidates.length === 0) {
804
- return {
805
- hasExistingProjectFiles,
806
- recommendedStackFileName: null,
807
- recommendedBlueprintFileName: null,
808
- confidenceLabel: null,
809
- confidenceScore: 0,
810
- confidenceGap: 0,
811
- detectionReasoning: "No known project markers were detected.",
812
- rankedCandidates: [],
813
- evidence: [],
814
- };
815
- }
816
-
817
- detectionCandidates.sort((leftCandidate, rightCandidate) => rightCandidate.confidenceScore - leftCandidate.confidenceScore);
818
- const strongestCandidate = detectionCandidates[0];
819
- const secondStrongestCandidate = detectionCandidates[1];
820
- const confidenceGap = secondStrongestCandidate
821
- ? Number((strongestCandidate.confidenceScore - secondStrongestCandidate.confidenceScore).toFixed(2))
822
- : Number(strongestCandidate.confidenceScore.toFixed(2));
823
- const isAmbiguous = secondStrongestCandidate
824
- && confidenceGap < 0.08;
825
- const confidenceLabel = strongestCandidate.confidenceScore >= 0.9
826
- ? "high"
827
- : strongestCandidate.confidenceScore >= 0.78
828
- ? "medium"
829
- : "low";
830
- const evidence = isAmbiguous
831
- ? [...strongestCandidate.evidence, `multiple stack signals detected`]
832
- : strongestCandidate.evidence;
833
- const rankedCandidates = detectionCandidates.slice(0, 3).map((detectionCandidate) => ({
834
- stackFileName: detectionCandidate.stackFileName,
835
- confidenceScore: Number(detectionCandidate.confidenceScore.toFixed(2)),
836
- evidence: detectionCandidate.evidence,
837
- }));
838
- const detectionReasoning = isAmbiguous
839
- ? `Top signal ${toTitleCase(strongestCandidate.stackFileName)} is close to ${toTitleCase(secondStrongestCandidate.stackFileName)} (confidence gap ${confidenceGap}).`
840
- : `Top signal ${toTitleCase(strongestCandidate.stackFileName)} won with confidence ${strongestCandidate.confidenceScore.toFixed(2)} from markers: ${strongestCandidate.evidence.join(", ") || "none"}.`;
841
-
842
- return {
843
- hasExistingProjectFiles,
844
- recommendedStackFileName: strongestCandidate.stackFileName,
845
- recommendedBlueprintFileName: BLUEPRINT_RECOMMENDATIONS[strongestCandidate.stackFileName] || null,
846
- confidenceLabel,
847
- confidenceScore: strongestCandidate.confidenceScore,
848
- confidenceGap,
849
- detectionReasoning,
850
- rankedCandidates,
851
- evidence,
852
- };
853
- }
854
-
855
- function formatBlockingSeverities(blockingSeverities) {
856
- return blockingSeverities.join(", ");
857
- }
858
-
859
- function formatDuration(durationMs) {
860
- const durationInSeconds = (durationMs / 1000).toFixed(1);
861
- return `${durationInSeconds}s`;
862
- }
863
-
864
- function buildDetectionSummary(projectDetection) {
865
- if (!projectDetection.recommendedStackFileName) {
866
- return "I did not find enough stack markers to auto-detect this project confidently.";
867
- }
868
-
869
- const readableEvidence = projectDetection.evidence.length > 0
870
- ? projectDetection.evidence.join(", ")
871
- : "basic project markers";
872
-
873
- const confidenceGapSummary = typeof projectDetection.confidenceGap === "number"
874
- ? ` Confidence gap: ${projectDetection.confidenceGap}.`
875
- : "";
876
-
877
- return `This folder looks like ${toTitleCase(projectDetection.recommendedStackFileName)} with ${projectDetection.confidenceLabel} confidence based on ${readableEvidence}.${confidenceGapSummary}`;
878
- }
879
-
880
- function formatDetectionCandidates(rankedCandidates) {
881
- if (!rankedCandidates?.length) {
882
- return "No ranked candidates available.";
883
- }
884
-
885
- return rankedCandidates
886
- .map((candidate, candidateIndex) => {
887
- const evidenceSummary = candidate.evidence?.length ? candidate.evidence.join(", ") : "no direct markers";
888
- return `${candidateIndex + 1}. ${toTitleCase(candidate.stackFileName)} (score ${candidate.confidenceScore}) via ${evidenceSummary}`;
889
- })
890
- .join("\n");
891
- }
892
-
893
- async function writeSelectedPolicy(targetDirectoryPath, selectedProfileName) {
894
- const policyFilePath = path.join(targetDirectoryPath, ".agent-context", "policies", POLICY_FILE_NAME);
895
- const parsedPolicy = JSON.parse(await fs.readFile(policyFilePath, "utf8"));
896
- parsedPolicy.selectedProfile = selectedProfileName;
897
- await fs.writeFile(policyFilePath, JSON.stringify(parsedPolicy, null, 2) + "\n", "utf8");
898
- }
899
-
900
- async function writeOnboardingReport({
901
- targetDirectoryPath,
902
- selectedProfileName,
903
- selectedProfilePack,
904
- selectedPreset,
905
- selectedStackFileName,
906
- selectedBlueprintFileName,
907
- includeCiGuardrails,
908
- setupDurationMs,
909
- projectDetection,
910
- selectedSkillDomains = [],
911
- operationMode = "init",
912
- }) {
913
- const onboardingReportPath = path.join(targetDirectoryPath, ".agent-context", "state", "onboarding-report.json");
914
- const onboardingReport = {
915
- cliVersion: CLI_VERSION,
916
- generatedAt: new Date().toISOString(),
917
- operationMode,
918
- selectedProfile: selectedProfileName,
919
- selectedProfilePack: selectedProfilePack
920
- ? {
921
- name: selectedProfilePack.slug,
922
- sourceFile: selectedProfilePack.fileName,
923
- }
924
- : null,
925
- selectedPreset,
926
- selectedStack: selectedStackFileName,
927
- selectedBlueprint: selectedBlueprintFileName,
928
- ciGuardrailsEnabled: includeCiGuardrails,
929
- setupDurationMs,
930
- selectedSkillDomains,
931
- autoDetection: {
932
- recommendedStack: projectDetection.recommendedStackFileName,
933
- recommendedBlueprint: projectDetection.recommendedBlueprintFileName,
934
- confidenceLabel: projectDetection.confidenceLabel,
935
- confidenceScore: projectDetection.confidenceScore,
936
- confidenceGap: projectDetection.confidenceGap,
937
- detectionReasoning: projectDetection.detectionReasoning,
938
- rankedCandidates: projectDetection.rankedCandidates,
939
- evidence: projectDetection.evidence,
940
- },
941
- };
942
-
943
- await fs.writeFile(onboardingReportPath, JSON.stringify(onboardingReport, null, 2) + "\n", "utf8");
944
- }
945
-
946
- async function loadOnboardingReportIfExists(targetDirectoryPath) {
947
- const onboardingReportPath = path.join(targetDirectoryPath, ".agent-context", "state", "onboarding-report.json");
948
- if (!(await pathExists(onboardingReportPath))) {
949
- return null;
950
- }
951
-
952
- const onboardingReportContent = await fs.readFile(onboardingReportPath, "utf8");
953
- return JSON.parse(onboardingReportContent);
954
- }
955
-
956
- async function buildCompiledRulesContent({
957
- targetDirectoryPath,
958
- selectedProfileName,
959
- selectedStackFileName,
960
- selectedBlueprintFileName,
961
- includeCiGuardrails,
962
- }) {
963
- const resolvedTargetDirectoryPath = path.resolve(targetDirectoryPath);
964
- const selectedRulesDirectoryPath = path.join(resolvedTargetDirectoryPath, ".agent-context", "rules");
965
- const selectedStacksDirectoryPath = path.join(resolvedTargetDirectoryPath, ".agent-context", "stacks");
966
- const selectedBlueprintsDirectoryPath = path.join(resolvedTargetDirectoryPath, ".agent-context", "blueprints");
967
- const selectedStateDirectoryPath = path.join(resolvedTargetDirectoryPath, ".agent-context", "state");
968
- const selectedReviewDirectoryPath = path.join(resolvedTargetDirectoryPath, ".agent-context", "review-checklists");
969
- const skillPlatformIndex = JSON.parse(await fs.readFile(SKILL_PLATFORM_INDEX_PATH, "utf8"));
970
- const selectedSkillDomainNames = inferSkillDomainNamesFromSelection(selectedStackFileName, selectedBlueprintFileName);
971
-
972
- const universalRuleFileNames = await collectFileNames(selectedRulesDirectoryPath);
973
- const contextBlocks = [];
974
-
975
- for (const universalRuleFileName of universalRuleFileNames) {
976
- const universalRuleFilePath = path.join(selectedRulesDirectoryPath, universalRuleFileName);
977
- const universalRuleContent = await fs.readFile(universalRuleFilePath, "utf8");
978
-
979
- contextBlocks.push(
980
- `## UNIVERSAL RULE: ${universalRuleFileName}\nSource: .agent-context/rules/${universalRuleFileName}\n\n${universalRuleContent.trim()}`
981
- );
982
- }
983
-
984
- const stackFilePath = path.join(selectedStacksDirectoryPath, selectedStackFileName);
985
- const stackContent = await fs.readFile(stackFilePath, "utf8");
986
- contextBlocks.push(
987
- `## STACK PROFILE: ${selectedStackFileName}\nSource: .agent-context/stacks/${selectedStackFileName}\n\n${stackContent.trim()}`
988
- );
989
-
990
- const blueprintFilePath = path.join(selectedBlueprintsDirectoryPath, selectedBlueprintFileName);
991
- const blueprintContent = await fs.readFile(blueprintFilePath, "utf8");
992
- contextBlocks.push(
993
- `## BLUEPRINT PROFILE: ${selectedBlueprintFileName}\nSource: .agent-context/blueprints/${selectedBlueprintFileName}\n\n${blueprintContent.trim()}`
994
- );
995
-
996
- if (includeCiGuardrails) {
997
- const githubCiBlueprintContent = await fs.readFile(path.join(selectedBlueprintsDirectoryPath, "ci-github-actions.md"), "utf8");
998
- const gitlabCiBlueprintContent = await fs.readFile(path.join(selectedBlueprintsDirectoryPath, "ci-gitlab.md"), "utf8");
999
-
1000
- contextBlocks.push(
1001
- `## CI/CD GUARDRAILS: ci-github-actions.md\nSource: .agent-context/blueprints/ci-github-actions.md\n\n${githubCiBlueprintContent.trim()}`
1002
- );
1003
- contextBlocks.push(
1004
- `## CI/CD GUARDRAILS: ci-gitlab.md\nSource: .agent-context/blueprints/ci-gitlab.md\n\n${gitlabCiBlueprintContent.trim()}`
1005
- );
1006
- }
1007
-
1008
- for (const selectedSkillDomainName of selectedSkillDomainNames) {
1009
- const skillDomainEntry = skillPlatformIndex.domains?.[selectedSkillDomainName];
1010
- if (!skillDomainEntry) {
1011
- continue;
1012
- }
1013
-
1014
- contextBlocks.push(await buildSkillPackSection(skillDomainEntry, skillPlatformIndex.defaultTier || "advance"));
1015
- }
1016
-
1017
- const architectureMapContent = await fs.readFile(path.join(selectedStateDirectoryPath, "architecture-map.md"), "utf8");
1018
- const dependencyMapContent = await fs.readFile(path.join(selectedStateDirectoryPath, "dependency-map.md"), "utf8");
1019
- const prChecklistContent = await fs.readFile(path.join(selectedReviewDirectoryPath, "pr-checklist.md"), "utf8");
1020
-
1021
- contextBlocks.push(
1022
- `## STATE MAP: architecture-map.md\nSource: .agent-context/state/architecture-map.md\n\n${architectureMapContent.trim()}`
1023
- );
1024
- contextBlocks.push(
1025
- `## STATE MAP: dependency-map.md\nSource: .agent-context/state/dependency-map.md\n\n${dependencyMapContent.trim()}`
1026
- );
1027
- contextBlocks.push(
1028
- `## REVIEW CHECKLIST: pr-checklist.md\nSource: .agent-context/review-checklists/pr-checklist.md\n\n${prChecklistContent.trim()}`
1029
- );
1030
-
1031
- return [
1032
- "# AGENTIC-SENIOR-CORE DYNAMIC GOVERNANCE RULESET",
1033
- "",
1034
- `Generated by Agentic-Senior-Core CLI v${CLI_VERSION}`,
1035
- `Timestamp: ${new Date().toISOString()}`,
1036
- `Selected profile: ${selectedProfileName}`,
1037
- `Selected policy file: .agent-context/policies/${POLICY_FILE_NAME}`,
1038
- "",
1039
- "## GOVERNANCE PRECEDENCE",
1040
- "1. Follow this compiled rulebook as the primary source.",
1041
- "2. Resolve exceptions from .agent-override.md only when explicitly defined.",
1042
- "3. Use architecture-map.md and dependency-map.md as change safety boundaries.",
1043
- "4. Enforce pr-checklist.md before declaring completion.",
1044
- "",
1045
- "## OVERRIDE PROTOCOL",
1046
- "- Default: strict compliance with this file.",
1047
- "- Exception path: .agent-override.md may explicitly allow narrow deviations.",
1048
- "- Scope policy: every override must include module scope, rationale, and expiry date.",
1049
- "",
1050
- ...contextBlocks,
1051
- "",
1052
- ].join("\n");
1053
- }
1054
-
1055
- async function compileDynamicContext({
1056
- targetDirectoryPath,
1057
- selectedProfileName,
1058
- selectedStackFileName,
1059
- selectedBlueprintFileName,
1060
- includeCiGuardrails,
1061
- }) {
1062
- const resolvedTargetDirectoryPath = path.resolve(targetDirectoryPath);
1063
- const compiledRules = await buildCompiledRulesContent({
1064
- targetDirectoryPath: resolvedTargetDirectoryPath,
1065
- selectedProfileName,
1066
- selectedStackFileName,
1067
- selectedBlueprintFileName,
1068
- includeCiGuardrails,
1069
- });
1070
-
1071
- await fs.writeFile(path.join(resolvedTargetDirectoryPath, ".cursorrules"), compiledRules, "utf8");
1072
- await fs.writeFile(path.join(resolvedTargetDirectoryPath, ".windsurfrules"), compiledRules, "utf8");
1073
- }
1074
-
1075
- async function runInitCommand(targetDirectoryArgument, initOptions = {}) {
1076
- const resolvedTargetDirectoryPath = path.resolve(targetDirectoryArgument || ".");
1077
- const setupStartedAt = Date.now();
1078
- await ensureDirectory(resolvedTargetDirectoryPath);
1079
-
1080
- const userInterface = readline.createInterface({ input, output });
1081
-
1082
- try {
1083
- const stackFileNames = await collectFileNames(path.join(AGENT_CONTEXT_DIR, "stacks"));
1084
- const blueprintFileNames = await collectFileNames(path.join(AGENT_CONTEXT_DIR, "blueprints"));
1085
- const profilePackDefinitions = await collectProfilePacks(REPO_ROOT);
1086
- const selectedPreset = initOptions.preset ? INIT_PRESETS[initOptions.preset] || null : null;
1087
-
1088
- const selectedStackFileNameFromOption = initOptions.stack
1089
- ? matchFileNameFromInput(initOptions.stack, stackFileNames)
1090
- : null;
1091
- const selectedBlueprintFileNameFromOption = initOptions.blueprint
1092
- ? matchFileNameFromInput(initOptions.blueprint, blueprintFileNames)
1093
- : null;
1094
- const selectedProfilePack = initOptions.profilePack
1095
- ? findProfilePackByInput(initOptions.profilePack, profilePackDefinitions)
1096
- : null;
1097
-
1098
- if (initOptions.stack && !selectedStackFileNameFromOption) {
1099
- throw new Error(`Unknown stack: ${initOptions.stack}`);
1100
- }
1101
-
1102
- if (initOptions.blueprint && !selectedBlueprintFileNameFromOption) {
1103
- throw new Error(`Unknown blueprint: ${initOptions.blueprint}`);
1104
- }
1105
-
1106
- if (initOptions.profilePack && !selectedProfilePack) {
1107
- throw new Error(`Unknown profile pack: ${initOptions.profilePack}`);
1108
- }
1109
-
1110
- if (initOptions.preset && !selectedPreset) {
1111
- throw new Error(`Unknown preset: ${initOptions.preset}`);
1112
- }
1113
-
1114
- if (selectedProfilePack && !stackFileNames.includes(selectedProfilePack.defaultStackFileName)) {
1115
- throw new Error(
1116
- `Profile pack ${selectedProfilePack.fileName} references unknown stack file: ${selectedProfilePack.defaultStackFileName}`
1117
- );
1118
- }
1119
-
1120
- if (selectedProfilePack && !blueprintFileNames.includes(selectedProfilePack.defaultBlueprintFileName)) {
1121
- throw new Error(
1122
- `Profile pack ${selectedProfilePack.fileName} references unknown blueprint file: ${selectedProfilePack.defaultBlueprintFileName}`
1123
- );
1124
- }
1125
-
1126
- console.log(`\nAgentic-Senior-Core CLI v${CLI_VERSION}`);
1127
- console.log("I will copy governance files into your target folder and compile a single rulebook for your AI tools.");
1128
-
1129
- if (selectedPreset) {
1130
- console.log(`Using preset: ${initOptions.preset} (${selectedPreset.description}).`);
1131
- }
1132
-
1133
- const projectDetection = await detectProjectContext(resolvedTargetDirectoryPath);
1134
- if (projectDetection.hasExistingProjectFiles) {
1135
- console.log("I found files in the target directory, so I checked whether this already looks like an existing project.");
1136
- console.log(buildDetectionSummary(projectDetection));
1137
- console.log("Detection reasoning:");
1138
- console.log(projectDetection.detectionReasoning);
1139
- console.log("Top candidates:");
1140
- console.log(formatDetectionCandidates(projectDetection.rankedCandidates));
1141
- } else {
1142
- console.log("The target directory is empty, so I will guide you through a fresh setup.");
1143
- }
1144
-
1145
- const selectedProfileName = initOptions.profile
1146
- ? initOptions.profile
1147
- : selectedPreset?.profile
1148
- ? selectedPreset.profile
1149
- : initOptions.newbie
1150
- ? "beginner"
1151
- : selectedProfilePack?.defaultProfileName
1152
- ? selectedProfilePack.defaultProfileName
1153
- : normalizeChoiceInput(await askChoice(
1154
- "How much guidance do you want?",
1155
- Object.values(PROFILE_PRESETS).map((profilePreset) => `${profilePreset.displayName} — ${profilePreset.description}`),
1156
- userInterface
1157
- )).split("-")[0];
1158
-
1159
- const selectedProfile = PROFILE_PRESETS[selectedProfileName];
1160
- if (!selectedProfile) {
1161
- throw new Error(`Unknown profile: ${selectedProfileName}`);
1162
- }
1163
-
1164
- console.log(`\nSelected profile: ${selectedProfile.displayName}`);
1165
- console.log(`This profile will block these review severities in CI: ${formatBlockingSeverities(selectedProfile.blockingSeverities)}.`);
1166
-
1167
- if (selectedProfilePack) {
1168
- console.log(`Applying team profile pack: ${selectedProfilePack.displayName}.`);
1169
- console.log(`Pack defaults: stack ${toTitleCase(selectedProfilePack.defaultStackFileName)}, blueprint ${toTitleCase(selectedProfilePack.defaultBlueprintFileName)}.`);
1170
- }
1171
-
1172
- const shouldApplyDetectedStack = projectDetection.recommendedStackFileName && !selectedStackFileNameFromOption
1173
- ? await askYesNo(
1174
- `Use the detected stack recommendation (${toTitleCase(projectDetection.recommendedStackFileName)})?`,
1175
- userInterface,
1176
- projectDetection.confidenceLabel === "high"
1177
- )
1178
- : false;
1179
-
1180
- const stackDisplayChoices = stackFileNames.map((stackFileName) => toTitleCase(stackFileName));
1181
- const blueprintDisplayChoices = blueprintFileNames.map((blueprintFileName) => toTitleCase(blueprintFileName));
1182
-
1183
- const selectedResolvedStackFileName = selectedStackFileNameFromOption
1184
- || selectedPreset?.stack
1185
- || (shouldApplyDetectedStack ? projectDetection.recommendedStackFileName : null)
1186
- || selectedProfilePack?.defaultStackFileName
1187
- || selectedProfile.defaultStackFileName
1188
- || stackFileNames[
1189
- stackDisplayChoices.indexOf(
1190
- await askChoice("Which stack should this governance pack target?", stackDisplayChoices, userInterface)
1191
- )
1192
- ];
1193
-
1194
- const recommendedBlueprintFileName = shouldApplyDetectedStack
1195
- ? projectDetection.recommendedBlueprintFileName
1196
- : BLUEPRINT_RECOMMENDATIONS[selectedResolvedStackFileName] || null;
1197
-
1198
- if (!recommendedBlueprintFileName && !selectedBlueprintFileNameFromOption && !selectedProfile.defaultBlueprintFileName) {
1199
- console.log("\nI could not map that stack to a first-party blueprint automatically, so I will ask you to choose one.");
1200
- }
1201
-
1202
- const selectedResolvedBlueprintFileName = selectedBlueprintFileNameFromOption
1203
- || selectedPreset?.blueprint
1204
- || recommendedBlueprintFileName
1205
- || selectedProfilePack?.defaultBlueprintFileName
1206
- || selectedProfile.defaultBlueprintFileName
1207
- || blueprintFileNames[
1208
- blueprintDisplayChoices.indexOf(
1209
- await askChoice("Which blueprint should I scaffold into the compiled rulebook?", blueprintDisplayChoices, userInterface)
1210
- )
1211
- ];
1212
-
1213
- const includeCiGuardrails = typeof initOptions.ci === "boolean"
1214
- ? initOptions.ci
1215
- : typeof selectedPreset?.ci === "boolean"
1216
- ? selectedPreset.ci
1217
- : selectedProfilePack?.lockCi
1218
- ? selectedProfilePack.defaultCi
1219
- : typeof selectedProfilePack?.defaultCi === "boolean"
1220
- ? selectedProfilePack.defaultCi
1221
- : selectedProfile.lockCi
1222
- ? selectedProfile.defaultCi
1223
- : await askYesNo("Enable CI/CD guardrails and the LLM Judge policy?", userInterface, selectedProfile.defaultCi);
1224
-
1225
- await copyGovernanceAssetsToTarget(resolvedTargetDirectoryPath);
1226
-
1227
- await compileDynamicContext({
1228
- targetDirectoryPath: resolvedTargetDirectoryPath,
1229
- selectedProfileName,
1230
- selectedProfilePack,
1231
- selectedStackFileName: selectedResolvedStackFileName,
1232
- selectedBlueprintFileName: selectedResolvedBlueprintFileName,
1233
- includeCiGuardrails,
1234
- });
1235
-
1236
- await writeSelectedPolicy(resolvedTargetDirectoryPath, selectedProfileName);
1237
-
1238
- const setupDurationMs = Date.now() - setupStartedAt;
1239
- await writeOnboardingReport({
1240
- targetDirectoryPath: resolvedTargetDirectoryPath,
1241
- selectedProfileName,
1242
- selectedProfilePack,
1243
- selectedPreset: initOptions.preset || null,
1244
- selectedStackFileName: selectedResolvedStackFileName,
1245
- selectedBlueprintFileName: selectedResolvedBlueprintFileName,
1246
- includeCiGuardrails,
1247
- setupDurationMs,
1248
- projectDetection,
1249
- selectedSkillDomains: inferSkillDomainNamesFromSelection(selectedResolvedStackFileName, selectedResolvedBlueprintFileName),
1250
- operationMode: "init",
1251
- });
1252
-
1253
- console.log("\nInitialization complete.");
1254
- console.log(`- Target directory: ${resolvedTargetDirectoryPath}`);
1255
- console.log(`- Profile: ${selectedProfile.displayName}`);
1256
- if (initOptions.preset) {
1257
- console.log(`- Preset: ${initOptions.preset}`);
1258
- }
1259
- if (selectedProfilePack) {
1260
- console.log(`- Team profile pack: ${selectedProfilePack.displayName}`);
1261
- }
1262
- console.log(`- Stack: ${toTitleCase(selectedResolvedStackFileName)}`);
1263
- console.log(`- Blueprint: ${toTitleCase(selectedResolvedBlueprintFileName)}`);
1264
- console.log(`- CI/CD guardrails: ${includeCiGuardrails ? "enabled" : "disabled"}`);
1265
- console.log(`- Blocking severities: ${formatBlockingSeverities(selectedProfile.blockingSeverities)}`);
1266
- console.log(`- Setup time: ${formatDuration(setupDurationMs)}`);
1267
- console.log("- Generated files: .cursorrules, .windsurfrules, and .agent-context/state/onboarding-report.json");
1268
- console.log("\nPlain-language summary:");
1269
- console.log(`I prepared a ${selectedProfile.displayName.toLowerCase()} governance pack for a ${toTitleCase(selectedResolvedStackFileName)} project using the ${toTitleCase(selectedResolvedBlueprintFileName)} blueprint.`);
1270
- console.log("Your AI tools will now receive one compiled rulebook plus the original source rules, and your review threshold is stored in .agent-context/policies/llm-judge-threshold.json.");
1271
- } finally {
1272
- userInterface.close();
1273
- }
1274
- }
1275
-
1276
- async function runUpgradeCommand(targetDirectoryArgument, upgradeOptions = {}) {
1277
- const resolvedTargetDirectoryPath = path.resolve(targetDirectoryArgument || ".");
1278
- const setupStartedAt = Date.now();
1279
- await ensureDirectory(resolvedTargetDirectoryPath);
1280
-
1281
- const userInterface = readline.createInterface({ input, output });
1282
-
1283
- try {
1284
- console.log(`\nAgentic-Senior-Core CLI v${CLI_VERSION}`);
1285
- console.log("Running upgrade assistant for an existing repository.");
1286
-
1287
- await copyGovernanceAssetsToTarget(resolvedTargetDirectoryPath);
1288
-
1289
- const stackFileNames = await collectFileNames(path.join(AGENT_CONTEXT_DIR, "stacks"));
1290
- const blueprintFileNames = await collectFileNames(path.join(AGENT_CONTEXT_DIR, "blueprints"));
1291
- const existingOnboardingReport = await loadOnboardingReportIfExists(resolvedTargetDirectoryPath);
1292
- const projectDetection = await detectProjectContext(resolvedTargetDirectoryPath);
1293
-
1294
- const selectedProfileName = PROFILE_PRESETS[existingOnboardingReport?.selectedProfile]
1295
- ? existingOnboardingReport.selectedProfile
1296
- : "balanced";
1297
-
1298
- const selectedStackFileName = stackFileNames.includes(existingOnboardingReport?.selectedStack)
1299
- ? existingOnboardingReport.selectedStack
1300
- : projectDetection.recommendedStackFileName || "typescript.md";
1301
-
1302
- const selectedBlueprintFileName = blueprintFileNames.includes(existingOnboardingReport?.selectedBlueprint)
1303
- ? existingOnboardingReport.selectedBlueprint
1304
- : BLUEPRINT_RECOMMENDATIONS[selectedStackFileName] || "api-nextjs.md";
1305
-
1306
- const includeCiGuardrails = typeof existingOnboardingReport?.ciGuardrailsEnabled === "boolean"
1307
- ? existingOnboardingReport.ciGuardrailsEnabled
1308
- : true;
1309
-
1310
- const currentRulesPath = path.join(resolvedTargetDirectoryPath, ".cursorrules");
1311
- const currentRulesContent = await pathExists(currentRulesPath)
1312
- ? await fs.readFile(currentRulesPath, "utf8")
1313
- : "";
1314
-
1315
- const plannedRulesContent = await buildCompiledRulesContent({
1316
- targetDirectoryPath: resolvedTargetDirectoryPath,
1317
- selectedProfileName,
1318
- selectedStackFileName,
1319
- selectedBlueprintFileName,
1320
- includeCiGuardrails,
1321
- });
1322
-
1323
- const isRulesContentChanged = currentRulesContent !== plannedRulesContent;
1324
- const currentRuleLineCount = currentRulesContent ? currentRulesContent.split(/\r?\n/).length : 0;
1325
- const plannedRuleLineCount = plannedRulesContent.split(/\r?\n/).length;
1326
-
1327
- console.log("\nUpgrade preview:");
1328
- console.log(`- Target directory: ${resolvedTargetDirectoryPath}`);
1329
- console.log(`- Profile: ${toTitleCase(selectedProfileName)}`);
1330
- console.log(`- Stack: ${toTitleCase(selectedStackFileName)}`);
1331
- console.log(`- Blueprint: ${toTitleCase(selectedBlueprintFileName)}`);
1332
- console.log(`- CI/CD guardrails: ${includeCiGuardrails ? "enabled" : "disabled"}`);
1333
- console.log(`- Existing rules lines: ${currentRuleLineCount}`);
1334
- console.log(`- Planned rules lines: ${plannedRuleLineCount}`);
1335
- console.log(`- Rules changed: ${isRulesContentChanged ? "yes" : "no"}`);
1336
-
1337
- if (upgradeOptions.dryRun) {
1338
- console.log("\nDry run enabled. No files were modified.");
1339
- return;
1340
- }
1341
-
1342
- const shouldApplyUpgrade = upgradeOptions.skipConfirmation
1343
- ? true
1344
- : await askYesNo("Apply upgrade and write migrated files?", userInterface, true);
1345
-
1346
- if (!shouldApplyUpgrade) {
1347
- console.log("Upgrade cancelled by user.");
1348
- return;
1349
- }
1350
-
1351
- await fs.writeFile(currentRulesPath, plannedRulesContent, "utf8");
1352
- await fs.writeFile(path.join(resolvedTargetDirectoryPath, ".windsurfrules"), plannedRulesContent, "utf8");
1353
- await writeSelectedPolicy(resolvedTargetDirectoryPath, selectedProfileName);
1354
-
1355
- const setupDurationMs = Date.now() - setupStartedAt;
1356
- await writeOnboardingReport({
1357
- targetDirectoryPath: resolvedTargetDirectoryPath,
1358
- selectedProfileName,
1359
- selectedProfilePack: existingOnboardingReport?.selectedProfilePack || null,
1360
- selectedStackFileName,
1361
- selectedBlueprintFileName,
1362
- includeCiGuardrails,
1363
- setupDurationMs,
1364
- projectDetection,
1365
- operationMode: "upgrade",
1366
- });
1367
-
1368
- console.log("\nUpgrade complete.");
1369
- console.log(`- Rules rewritten: ${isRulesContentChanged ? "yes" : "no (metadata refreshed)"}`);
1370
- console.log(`- Setup time: ${formatDuration(setupDurationMs)}`);
1371
- console.log("- Updated files: .cursorrules, .windsurfrules, .agent-context/state/onboarding-report.json");
1372
- } finally {
1373
- userInterface.close();
1374
- }
1375
- }
10
+ import { CLI_VERSION } from '../lib/cli/constants.mjs';
11
+ import { printUsage } from '../lib/cli/utils.mjs';
12
+ import { runLaunchCommand } from '../lib/cli/commands/launch.mjs';
13
+ import { runInitCommand, parseInitArguments } from '../lib/cli/commands/init.mjs';
14
+ import { runUpgradeCommand, parseUpgradeArguments } from '../lib/cli/commands/upgrade.mjs';
15
+ import { runSkillCommand } from '../lib/cli/skill-selector.mjs';
1376
16
 
1377
17
  async function main() {
1378
18
  const commandArgument = process.argv[2];
1379
19
  const commandArguments = process.argv.slice(3);
1380
20
 
1381
- if (!commandArgument) {
21
+ if (!commandArgument || commandArgument === 'launch') {
1382
22
  await runLaunchCommand();
1383
23
  return;
1384
24
  }
1385
25
 
1386
- if (commandArgument === "--help" || commandArgument === "-h") {
26
+ if (commandArgument === '--help' || commandArgument === '-h') {
1387
27
  printUsage();
1388
28
  return;
1389
29
  }
1390
30
 
1391
- if (commandArgument === "--version" || commandArgument === "-v") {
31
+ if (commandArgument === '--version' || commandArgument === '-v') {
1392
32
  console.log(CLI_VERSION);
1393
33
  return;
1394
34
  }
1395
35
 
1396
- if (commandArgument !== "init" && commandArgument !== "upgrade" && commandArgument !== "skill" && commandArgument !== "launch") {
1397
- console.error(`Unknown command: ${commandArgument}`);
1398
- printUsage();
1399
- exit(1);
1400
- }
1401
-
1402
- if (commandArgument === "launch") {
1403
- await runLaunchCommand();
1404
- return;
1405
- }
1406
-
1407
- if (commandArgument === "skill") {
36
+ if (commandArgument === 'skill') {
1408
37
  await runSkillCommand(commandArguments);
1409
38
  return;
1410
39
  }
1411
40
 
1412
- if (commandArgument === "upgrade") {
1413
- const parsedUpgradeOptions = {
1414
- targetDirectory: ".",
1415
- dryRun: false,
1416
- skipConfirmation: false,
1417
- };
1418
-
1419
- for (let argumentIndex = 0; argumentIndex < commandArguments.length; argumentIndex++) {
1420
- const currentArgument = commandArguments[argumentIndex];
1421
-
1422
- if (!currentArgument.startsWith("--")) {
1423
- parsedUpgradeOptions.targetDirectory = currentArgument;
1424
- continue;
1425
- }
1426
-
1427
- if (currentArgument === "--dry-run") {
1428
- parsedUpgradeOptions.dryRun = true;
1429
- continue;
1430
- }
1431
-
1432
- if (currentArgument === "--yes") {
1433
- parsedUpgradeOptions.skipConfirmation = true;
1434
- continue;
1435
- }
1436
-
1437
- throw new Error(`Unknown option: ${currentArgument}`);
1438
- }
1439
-
1440
- await runUpgradeCommand(parsedUpgradeOptions.targetDirectory, parsedUpgradeOptions);
41
+ if (commandArgument === 'upgrade') {
42
+ const upgradeOptions = parseUpgradeArguments(commandArguments);
43
+ await runUpgradeCommand(upgradeOptions.targetDirectory, upgradeOptions);
1441
44
  return;
1442
45
  }
1443
46
 
1444
- const parsedInitOptions = {
1445
- targetDirectory: ".",
1446
- preset: undefined,
1447
- profile: undefined,
1448
- profilePack: undefined,
1449
- stack: undefined,
1450
- blueprint: undefined,
1451
- ci: undefined,
1452
- newbie: false,
1453
- };
1454
-
1455
- for (let argumentIndex = 0; argumentIndex < commandArguments.length; argumentIndex++) {
1456
- const currentArgument = commandArguments[argumentIndex];
1457
-
1458
- if (!currentArgument.startsWith("--")) {
1459
- parsedInitOptions.targetDirectory = currentArgument;
1460
- continue;
1461
- }
1462
-
1463
- if (currentArgument === "--profile") {
1464
- parsedInitOptions.profile = matchProfileNameFromInput(commandArguments[argumentIndex + 1] || "");
1465
- argumentIndex += 1;
1466
- continue;
1467
- }
1468
-
1469
- if (currentArgument === "--preset") {
1470
- parsedInitOptions.preset = normalizeChoiceInput(commandArguments[argumentIndex + 1] || "");
1471
- argumentIndex += 1;
1472
- continue;
1473
- }
1474
-
1475
- if (currentArgument.startsWith("--preset=")) {
1476
- parsedInitOptions.preset = normalizeChoiceInput(currentArgument.split("=")[1]);
1477
- continue;
1478
- }
1479
-
1480
- if (currentArgument.startsWith("--profile=")) {
1481
- parsedInitOptions.profile = matchProfileNameFromInput(currentArgument.split("=")[1]);
1482
- continue;
1483
- }
1484
-
1485
- if (currentArgument === "--profile-pack") {
1486
- parsedInitOptions.profilePack = commandArguments[argumentIndex + 1];
1487
- argumentIndex += 1;
1488
- continue;
1489
- }
1490
-
1491
- if (currentArgument.startsWith("--profile-pack=")) {
1492
- parsedInitOptions.profilePack = currentArgument.split("=")[1];
1493
- continue;
1494
- }
1495
-
1496
- if (currentArgument === "--stack") {
1497
- parsedInitOptions.stack = commandArguments[argumentIndex + 1];
1498
- argumentIndex += 1;
1499
- continue;
1500
- }
1501
-
1502
- if (currentArgument.startsWith("--stack=")) {
1503
- parsedInitOptions.stack = currentArgument.split("=")[1];
1504
- continue;
1505
- }
1506
-
1507
- if (currentArgument === "--blueprint") {
1508
- parsedInitOptions.blueprint = commandArguments[argumentIndex + 1];
1509
- argumentIndex += 1;
1510
- continue;
1511
- }
1512
-
1513
- if (currentArgument.startsWith("--blueprint=")) {
1514
- parsedInitOptions.blueprint = currentArgument.split("=")[1];
1515
- continue;
1516
- }
1517
-
1518
- if (currentArgument === "--ci") {
1519
- const ciRawValue = commandArguments[argumentIndex + 1];
1520
- parsedInitOptions.ci = ciRawValue?.toLowerCase() === "true";
1521
- argumentIndex += 1;
1522
- continue;
1523
- }
1524
-
1525
- if (currentArgument.startsWith("--ci=")) {
1526
- parsedInitOptions.ci = currentArgument.split("=")[1]?.toLowerCase() === "true";
1527
- continue;
1528
- }
1529
-
1530
- if (currentArgument === "--newbie") {
1531
- parsedInitOptions.newbie = true;
1532
- continue;
1533
- }
1534
-
1535
- throw new Error(`Unknown option: ${currentArgument}`);
1536
- }
1537
-
1538
- if (parsedInitOptions.newbie && parsedInitOptions.profile && parsedInitOptions.profile !== "beginner") {
1539
- throw new Error("--newbie can only be combined with --profile beginner");
47
+ if (commandArgument === 'init') {
48
+ const initOptions = parseInitArguments(commandArguments);
49
+ await runInitCommand(initOptions.targetDirectory, initOptions);
50
+ return;
1540
51
  }
1541
52
 
1542
- await runInitCommand(parsedInitOptions.targetDirectory, {
1543
- preset: parsedInitOptions.preset,
1544
- profile: parsedInitOptions.profile,
1545
- profilePack: parsedInitOptions.profilePack,
1546
- stack: parsedInitOptions.stack,
1547
- blueprint: parsedInitOptions.blueprint,
1548
- ci: parsedInitOptions.ci,
1549
- newbie: parsedInitOptions.newbie,
1550
- });
53
+ console.error(`Unknown command: ${commandArgument}`);
54
+ printUsage();
55
+ exit(1);
1551
56
  }
1552
57
 
1553
58
  main().catch((error) => {
1554
- console.error("CLI failed:", error);
59
+ console.error('CLI failed:', error);
1555
60
  exit(1);
1556
61
  });