@ryuenn3123/agentic-senior-core 1.9.1 → 1.9.3

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.
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Context Compiler — Rulebook compilation and state persistence.
3
+ * Depends on: constants.mjs, utils.mjs, skill-selector.mjs
4
+ */
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+
8
+ import {
9
+ CLI_VERSION,
10
+ POLICY_FILE_NAME,
11
+ SKILL_PLATFORM_INDEX_PATH,
12
+ } from './constants.mjs';
13
+
14
+ import {
15
+ pathExists,
16
+ collectFileNames,
17
+ } from './utils.mjs';
18
+
19
+ import {
20
+ inferSkillDomainNamesFromSelection,
21
+ buildSkillPackSection,
22
+ } from './skill-selector.mjs';
23
+
24
+ export async function writeSelectedPolicy(targetDirectoryPath, selectedProfileName) {
25
+ const policyFilePath = path.join(targetDirectoryPath, '.agent-context', 'policies', POLICY_FILE_NAME);
26
+ const parsedPolicy = JSON.parse(await fs.readFile(policyFilePath, 'utf8'));
27
+ parsedPolicy.selectedProfile = selectedProfileName;
28
+ await fs.writeFile(policyFilePath, JSON.stringify(parsedPolicy, null, 2) + '\n', 'utf8');
29
+ }
30
+
31
+ export async function writeOnboardingReport({
32
+ targetDirectoryPath,
33
+ selectedProfileName,
34
+ selectedProfilePack,
35
+ selectedPreset,
36
+ selectedStackFileName,
37
+ selectedBlueprintFileName,
38
+ includeCiGuardrails,
39
+ setupDurationMs,
40
+ projectDetection,
41
+ selectedSkillDomains = [],
42
+ operationMode = 'init',
43
+ }) {
44
+ const onboardingReportPath = path.join(targetDirectoryPath, '.agent-context', 'state', 'onboarding-report.json');
45
+ const onboardingReport = {
46
+ cliVersion: CLI_VERSION,
47
+ generatedAt: new Date().toISOString(),
48
+ operationMode,
49
+ selectedProfile: selectedProfileName,
50
+ selectedProfilePack: selectedProfilePack
51
+ ? {
52
+ name: selectedProfilePack.slug,
53
+ sourceFile: selectedProfilePack.fileName,
54
+ }
55
+ : null,
56
+ selectedPreset,
57
+ selectedStack: selectedStackFileName,
58
+ selectedBlueprint: selectedBlueprintFileName,
59
+ ciGuardrailsEnabled: includeCiGuardrails,
60
+ setupDurationMs,
61
+ selectedSkillDomains,
62
+ autoDetection: {
63
+ recommendedStack: projectDetection.recommendedStackFileName,
64
+ recommendedBlueprint: projectDetection.recommendedBlueprintFileName,
65
+ confidenceLabel: projectDetection.confidenceLabel,
66
+ confidenceScore: projectDetection.confidenceScore,
67
+ confidenceGap: projectDetection.confidenceGap,
68
+ detectionReasoning: projectDetection.detectionReasoning,
69
+ rankedCandidates: projectDetection.rankedCandidates,
70
+ evidence: projectDetection.evidence,
71
+ },
72
+ };
73
+
74
+ await fs.writeFile(onboardingReportPath, JSON.stringify(onboardingReport, null, 2) + '\n', 'utf8');
75
+ }
76
+
77
+ export async function loadOnboardingReportIfExists(targetDirectoryPath) {
78
+ const onboardingReportPath = path.join(targetDirectoryPath, '.agent-context', 'state', 'onboarding-report.json');
79
+ if (!(await pathExists(onboardingReportPath))) {
80
+ return null;
81
+ }
82
+
83
+ const onboardingReportContent = await fs.readFile(onboardingReportPath, 'utf8');
84
+ return JSON.parse(onboardingReportContent);
85
+ }
86
+
87
+ export async function buildCompiledRulesContent({
88
+ targetDirectoryPath,
89
+ selectedProfileName,
90
+ selectedStackFileName,
91
+ selectedBlueprintFileName,
92
+ includeCiGuardrails,
93
+ }) {
94
+ const resolvedTargetDirectoryPath = path.resolve(targetDirectoryPath);
95
+ const selectedRulesDirectoryPath = path.join(resolvedTargetDirectoryPath, '.agent-context', 'rules');
96
+ const selectedStacksDirectoryPath = path.join(resolvedTargetDirectoryPath, '.agent-context', 'stacks');
97
+ const selectedBlueprintsDirectoryPath = path.join(resolvedTargetDirectoryPath, '.agent-context', 'blueprints');
98
+ const selectedStateDirectoryPath = path.join(resolvedTargetDirectoryPath, '.agent-context', 'state');
99
+ const selectedReviewDirectoryPath = path.join(resolvedTargetDirectoryPath, '.agent-context', 'review-checklists');
100
+ const skillPlatformIndex = JSON.parse(await fs.readFile(SKILL_PLATFORM_INDEX_PATH, 'utf8'));
101
+ const selectedSkillDomainNames = inferSkillDomainNamesFromSelection(selectedStackFileName, selectedBlueprintFileName);
102
+
103
+ const universalRuleFileNames = await collectFileNames(selectedRulesDirectoryPath);
104
+ const contextBlocks = [];
105
+
106
+ for (const universalRuleFileName of universalRuleFileNames) {
107
+ const universalRuleFilePath = path.join(selectedRulesDirectoryPath, universalRuleFileName);
108
+ const universalRuleContent = await fs.readFile(universalRuleFilePath, 'utf8');
109
+
110
+ contextBlocks.push(
111
+ `## UNIVERSAL RULE: ${universalRuleFileName}\nSource: .agent-context/rules/${universalRuleFileName}\n\n${universalRuleContent.trim()}`
112
+ );
113
+ }
114
+
115
+ const stackFilePath = path.join(selectedStacksDirectoryPath, selectedStackFileName);
116
+ const stackContent = await fs.readFile(stackFilePath, 'utf8');
117
+ contextBlocks.push(
118
+ `## STACK PROFILE: ${selectedStackFileName}\nSource: .agent-context/stacks/${selectedStackFileName}\n\n${stackContent.trim()}`
119
+ );
120
+
121
+ const blueprintFilePath = path.join(selectedBlueprintsDirectoryPath, selectedBlueprintFileName);
122
+ const blueprintContent = await fs.readFile(blueprintFilePath, 'utf8');
123
+ contextBlocks.push(
124
+ `## BLUEPRINT PROFILE: ${selectedBlueprintFileName}\nSource: .agent-context/blueprints/${selectedBlueprintFileName}\n\n${blueprintContent.trim()}`
125
+ );
126
+
127
+ if (includeCiGuardrails) {
128
+ const githubCiBlueprintContent = await fs.readFile(path.join(selectedBlueprintsDirectoryPath, 'ci-github-actions.md'), 'utf8');
129
+ const gitlabCiBlueprintContent = await fs.readFile(path.join(selectedBlueprintsDirectoryPath, 'ci-gitlab.md'), 'utf8');
130
+
131
+ contextBlocks.push(
132
+ `## CI/CD GUARDRAILS: ci-github-actions.md\nSource: .agent-context/blueprints/ci-github-actions.md\n\n${githubCiBlueprintContent.trim()}`
133
+ );
134
+ contextBlocks.push(
135
+ `## CI/CD GUARDRAILS: ci-gitlab.md\nSource: .agent-context/blueprints/ci-gitlab.md\n\n${gitlabCiBlueprintContent.trim()}`
136
+ );
137
+ }
138
+
139
+ for (const selectedSkillDomainName of selectedSkillDomainNames) {
140
+ const skillDomainEntry = skillPlatformIndex.domains?.[selectedSkillDomainName];
141
+ if (!skillDomainEntry) {
142
+ continue;
143
+ }
144
+
145
+ contextBlocks.push(await buildSkillPackSection(skillDomainEntry, skillPlatformIndex.defaultTier || 'advance'));
146
+ }
147
+
148
+ const architectureMapContent = await fs.readFile(path.join(selectedStateDirectoryPath, 'architecture-map.md'), 'utf8');
149
+ const dependencyMapContent = await fs.readFile(path.join(selectedStateDirectoryPath, 'dependency-map.md'), 'utf8');
150
+ const prChecklistContent = await fs.readFile(path.join(selectedReviewDirectoryPath, 'pr-checklist.md'), 'utf8');
151
+
152
+ contextBlocks.push(
153
+ `## STATE MAP: architecture-map.md\nSource: .agent-context/state/architecture-map.md\n\n${architectureMapContent.trim()}`
154
+ );
155
+ contextBlocks.push(
156
+ `## STATE MAP: dependency-map.md\nSource: .agent-context/state/dependency-map.md\n\n${dependencyMapContent.trim()}`
157
+ );
158
+ contextBlocks.push(
159
+ `## REVIEW CHECKLIST: pr-checklist.md\nSource: .agent-context/review-checklists/pr-checklist.md\n\n${prChecklistContent.trim()}`
160
+ );
161
+
162
+ return [
163
+ '# AGENTIC-SENIOR-CORE DYNAMIC GOVERNANCE RULESET',
164
+ '',
165
+ `Generated by Agentic-Senior-Core CLI v${CLI_VERSION}`,
166
+ `Timestamp: ${new Date().toISOString()}`,
167
+ `Selected profile: ${selectedProfileName}`,
168
+ `Selected policy file: .agent-context/policies/${POLICY_FILE_NAME}`,
169
+ '',
170
+ '## GOVERNANCE PRECEDENCE',
171
+ '1. Follow this compiled rulebook as the primary source.',
172
+ '2. Resolve exceptions from .agent-override.md only when explicitly defined.',
173
+ '3. Use architecture-map.md and dependency-map.md as change safety boundaries.',
174
+ '4. Enforce pr-checklist.md before declaring completion.',
175
+ '',
176
+ '## OVERRIDE PROTOCOL',
177
+ '- Default: strict compliance with this file.',
178
+ '- Exception path: .agent-override.md may explicitly allow narrow deviations.',
179
+ '- Scope policy: every override must include module scope, rationale, and expiry date.',
180
+ '',
181
+ ...contextBlocks,
182
+ '',
183
+ ].join('\n');
184
+ }
185
+
186
+ export async function compileDynamicContext({
187
+ targetDirectoryPath,
188
+ selectedProfileName,
189
+ selectedStackFileName,
190
+ selectedBlueprintFileName,
191
+ includeCiGuardrails,
192
+ }) {
193
+ const resolvedTargetDirectoryPath = path.resolve(targetDirectoryPath);
194
+ const compiledRules = await buildCompiledRulesContent({
195
+ targetDirectoryPath: resolvedTargetDirectoryPath,
196
+ selectedProfileName,
197
+ selectedStackFileName,
198
+ selectedBlueprintFileName,
199
+ includeCiGuardrails,
200
+ });
201
+
202
+ await fs.writeFile(path.join(resolvedTargetDirectoryPath, '.cursorrules'), compiledRules, 'utf8');
203
+ await fs.writeFile(path.join(resolvedTargetDirectoryPath, '.windsurfrules'), compiledRules, 'utf8');
204
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * CLI Constants — All configuration values extracted from the monolith.
3
+ * Zero dependencies on other lib modules.
4
+ */
5
+ import { readFileSync } from 'node:fs';
6
+ import { resolve, join, dirname } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const currentFilePath = fileURLToPath(import.meta.url);
10
+ const currentDirectoryPath = dirname(currentFilePath);
11
+
12
+ export const REPO_ROOT = resolve(currentDirectoryPath, '..', '..');
13
+ export const PACKAGE_JSON_PATH = join(REPO_ROOT, 'package.json');
14
+ export const CLI_VERSION = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8')).version;
15
+ export const AGENT_CONTEXT_DIR = join(REPO_ROOT, '.agent-context');
16
+ export const SKILL_PLATFORM_DIRECTORY = join(AGENT_CONTEXT_DIR, 'skills');
17
+ export const SKILL_PLATFORM_INDEX_PATH = join(SKILL_PLATFORM_DIRECTORY, 'index.json');
18
+ export const POLICY_FILE_NAME = 'llm-judge-threshold.json';
19
+ export const PROFILE_PACKS_DIRECTORY_NAME = 'profiles';
20
+
21
+ export const PROFILE_PACK_REQUIRED_FIELDS = [
22
+ 'slug',
23
+ 'displayName',
24
+ 'description',
25
+ 'defaultProfile',
26
+ 'defaultStack',
27
+ 'defaultBlueprint',
28
+ 'ciGuardrails',
29
+ 'lockCi',
30
+ 'blockingSeverities',
31
+ ];
32
+
33
+ export const ALLOWED_SEVERITY_LEVELS = new Set(['critical', 'high', 'medium', 'low']);
34
+
35
+ export 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
+
46
+ export const INIT_PRESETS = {
47
+ 'frontend-web': {
48
+ profile: 'balanced',
49
+ stack: 'typescript.md',
50
+ blueprint: 'api-nextjs.md',
51
+ ci: true,
52
+ description: 'Frontend-first web app starter',
53
+ },
54
+ 'backend-api': {
55
+ profile: 'balanced',
56
+ stack: 'python.md',
57
+ blueprint: 'fastapi-service.md',
58
+ ci: true,
59
+ description: 'Backend API starter with safe defaults',
60
+ },
61
+ 'fullstack-product': {
62
+ profile: 'balanced',
63
+ stack: 'typescript.md',
64
+ blueprint: 'api-nextjs.md',
65
+ ci: true,
66
+ description: 'Product delivery starter with fullstack governance',
67
+ },
68
+ 'platform-governance': {
69
+ profile: 'strict',
70
+ stack: 'go.md',
71
+ blueprint: 'go-service.md',
72
+ ci: true,
73
+ description: 'Strict release and platform governance starter',
74
+ },
75
+ 'mobile-react-native': {
76
+ profile: 'balanced',
77
+ stack: 'react-native.md',
78
+ blueprint: 'mobile-app.md',
79
+ ci: true,
80
+ description: 'Mobile app starter for React Native',
81
+ },
82
+ 'mobile-flutter': {
83
+ profile: 'balanced',
84
+ stack: 'flutter.md',
85
+ blueprint: 'mobile-app.md',
86
+ ci: true,
87
+ description: 'Mobile app starter for Flutter',
88
+ },
89
+ 'observability-platform': {
90
+ profile: 'strict',
91
+ stack: 'go.md',
92
+ blueprint: 'observability.md',
93
+ ci: true,
94
+ description: 'Observability and platform starter',
95
+ },
96
+ };
97
+
98
+ export const PROFILE_PRESETS = {
99
+ beginner: {
100
+ displayName: 'Beginner',
101
+ description: 'Safest path. Minimal decisions, TypeScript defaults, and CI enabled.',
102
+ defaultStackFileName: 'typescript.md',
103
+ defaultBlueprintFileName: 'api-nextjs.md',
104
+ defaultCi: true,
105
+ lockCi: true,
106
+ blockingSeverities: ['critical'],
107
+ },
108
+ balanced: {
109
+ displayName: 'Balanced',
110
+ description: 'Recommended for most teams. Guided choices with strong default guardrails.',
111
+ defaultStackFileName: null,
112
+ defaultBlueprintFileName: null,
113
+ defaultCi: true,
114
+ lockCi: false,
115
+ blockingSeverities: ['critical', 'high'],
116
+ },
117
+ strict: {
118
+ displayName: 'Strict',
119
+ description: 'Tighter governance. CI stays on and medium-risk findings can block merges.',
120
+ defaultStackFileName: null,
121
+ defaultBlueprintFileName: null,
122
+ defaultCi: true,
123
+ lockCi: true,
124
+ blockingSeverities: ['critical', 'high', 'medium'],
125
+ },
126
+ };
127
+
128
+ export const entryPointFiles = [
129
+ '.cursorrules',
130
+ '.windsurfrules',
131
+ 'AGENTS.md',
132
+ '.agent-override.md',
133
+ 'mcp.json',
134
+ ];
135
+
136
+ export const directoryCopies = ['.agent-context', '.github', '.gemini', '.agents'];
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Stack Detector — Project context auto-detection.
3
+ * Depends on: constants.mjs, utils.mjs
4
+ */
5
+ import fs from 'node:fs/promises';
6
+
7
+ import { BLUEPRINT_RECOMMENDATIONS } from './constants.mjs';
8
+ import { toTitleCase } from './utils.mjs';
9
+
10
+ export async function collectProjectMarkers(targetDirectoryPath) {
11
+ const markerNames = new Set();
12
+ const directoryEntries = await fs.readdir(targetDirectoryPath, { withFileTypes: true });
13
+
14
+ for (const directoryEntry of directoryEntries) {
15
+ if (directoryEntry.name === '.git' || directoryEntry.name === 'node_modules') {
16
+ continue;
17
+ }
18
+
19
+ markerNames.add(directoryEntry.name);
20
+ }
21
+
22
+ return markerNames;
23
+ }
24
+
25
+ export async function detectProjectContext(targetDirectoryPath) {
26
+ const markerNames = await collectProjectMarkers(targetDirectoryPath);
27
+ const detectionCandidates = [];
28
+ const hasExistingProjectFiles = markerNames.size > 0;
29
+
30
+ if (markerNames.has('package.json') || markerNames.has('tsconfig.json') || markerNames.has('next.config.js') || markerNames.has('next.config.mjs')) {
31
+ const evidence = [];
32
+ let confidenceScore = 0.7;
33
+
34
+ if (markerNames.has('package.json')) {
35
+ evidence.push('package.json');
36
+ confidenceScore += 0.12;
37
+ }
38
+
39
+ if (markerNames.has('tsconfig.json')) {
40
+ evidence.push('tsconfig.json');
41
+ confidenceScore += 0.12;
42
+ }
43
+
44
+ if (markerNames.has('next.config.js') || markerNames.has('next.config.mjs')) {
45
+ evidence.push('Next.js config');
46
+ confidenceScore += 0.05;
47
+ }
48
+
49
+ detectionCandidates.push({
50
+ stackFileName: 'typescript.md',
51
+ confidenceScore: Math.min(confidenceScore, 0.97),
52
+ evidence,
53
+ });
54
+ }
55
+
56
+ if (markerNames.has('pyproject.toml') || markerNames.has('requirements.txt')) {
57
+ detectionCandidates.push({
58
+ stackFileName: 'python.md',
59
+ confidenceScore: markerNames.has('pyproject.toml') ? 0.96 : 0.78,
60
+ evidence: markerNames.has('pyproject.toml') ? ['pyproject.toml'] : ['requirements.txt'],
61
+ });
62
+ }
63
+
64
+ if (markerNames.has('pom.xml') || markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) {
65
+ const evidence = [];
66
+ if (markerNames.has('pom.xml')) evidence.push('pom.xml');
67
+ if (markerNames.has('build.gradle') || markerNames.has('build.gradle.kts')) evidence.push('Gradle build file');
68
+ detectionCandidates.push({
69
+ stackFileName: 'java.md',
70
+ confidenceScore: markerNames.has('pom.xml') ? 0.95 : 0.84,
71
+ evidence,
72
+ });
73
+ }
74
+
75
+ if (markerNames.has('composer.json')) {
76
+ detectionCandidates.push({
77
+ stackFileName: 'php.md',
78
+ confidenceScore: 0.95,
79
+ evidence: ['composer.json'],
80
+ });
81
+ }
82
+
83
+ if (markerNames.has('go.mod')) {
84
+ detectionCandidates.push({
85
+ stackFileName: 'go.md',
86
+ confidenceScore: 0.96,
87
+ evidence: ['go.mod'],
88
+ });
89
+ }
90
+
91
+ if (markerNames.has('Cargo.toml')) {
92
+ detectionCandidates.push({
93
+ stackFileName: 'rust.md',
94
+ confidenceScore: 0.96,
95
+ evidence: ['Cargo.toml'],
96
+ });
97
+ }
98
+
99
+ if (markerNames.has('Gemfile')) {
100
+ detectionCandidates.push({
101
+ stackFileName: 'ruby.md',
102
+ confidenceScore: 0.95,
103
+ evidence: ['Gemfile'],
104
+ });
105
+ }
106
+
107
+ const hasDotNetMarker = Array.from(markerNames).some((markerName) => markerName.endsWith('.sln') || markerName.endsWith('.csproj'));
108
+ if (hasDotNetMarker) {
109
+ detectionCandidates.push({
110
+ stackFileName: 'csharp.md',
111
+ confidenceScore: 0.95,
112
+ evidence: ['.sln or .csproj file'],
113
+ });
114
+ }
115
+
116
+ if (markerNames.has('package.json') && (markerNames.has('android') || markerNames.has('ios') || markerNames.has('react-native.config.js'))) {
117
+ detectionCandidates.push({
118
+ stackFileName: 'react-native.md',
119
+ confidenceScore: 0.9,
120
+ evidence: ['package.json', 'mobile runtime markers'],
121
+ });
122
+ }
123
+
124
+ if (markerNames.has('pubspec.yaml')) {
125
+ detectionCandidates.push({
126
+ stackFileName: 'flutter.md',
127
+ confidenceScore: 0.94,
128
+ evidence: ['pubspec.yaml'],
129
+ });
130
+ }
131
+
132
+ if (detectionCandidates.length === 0) {
133
+ return {
134
+ hasExistingProjectFiles,
135
+ recommendedStackFileName: null,
136
+ recommendedBlueprintFileName: null,
137
+ confidenceLabel: null,
138
+ confidenceScore: 0,
139
+ confidenceGap: 0,
140
+ detectionReasoning: 'No known project markers were detected.',
141
+ rankedCandidates: [],
142
+ evidence: [],
143
+ };
144
+ }
145
+
146
+ detectionCandidates.sort((leftCandidate, rightCandidate) => rightCandidate.confidenceScore - leftCandidate.confidenceScore);
147
+ const strongestCandidate = detectionCandidates[0];
148
+ const secondStrongestCandidate = detectionCandidates[1];
149
+ const confidenceGap = secondStrongestCandidate
150
+ ? Number((strongestCandidate.confidenceScore - secondStrongestCandidate.confidenceScore).toFixed(2))
151
+ : Number(strongestCandidate.confidenceScore.toFixed(2));
152
+ const isAmbiguous = secondStrongestCandidate
153
+ && confidenceGap < 0.08;
154
+ const confidenceLabel = strongestCandidate.confidenceScore >= 0.9
155
+ ? 'high'
156
+ : strongestCandidate.confidenceScore >= 0.78
157
+ ? 'medium'
158
+ : 'low';
159
+ const evidence = isAmbiguous
160
+ ? [...strongestCandidate.evidence, `multiple stack signals detected`]
161
+ : strongestCandidate.evidence;
162
+ const rankedCandidates = detectionCandidates.slice(0, 3).map((detectionCandidate) => ({
163
+ stackFileName: detectionCandidate.stackFileName,
164
+ confidenceScore: Number(detectionCandidate.confidenceScore.toFixed(2)),
165
+ evidence: detectionCandidate.evidence,
166
+ }));
167
+ const detectionReasoning = isAmbiguous
168
+ ? `Top signal ${toTitleCase(strongestCandidate.stackFileName)} is close to ${toTitleCase(secondStrongestCandidate.stackFileName)} (confidence gap ${confidenceGap}).`
169
+ : `Top signal ${toTitleCase(strongestCandidate.stackFileName)} won with confidence ${strongestCandidate.confidenceScore.toFixed(2)} from markers: ${strongestCandidate.evidence.join(', ') || 'none'}.`;
170
+
171
+ return {
172
+ hasExistingProjectFiles,
173
+ recommendedStackFileName: strongestCandidate.stackFileName,
174
+ recommendedBlueprintFileName: BLUEPRINT_RECOMMENDATIONS[strongestCandidate.stackFileName] || null,
175
+ confidenceLabel,
176
+ confidenceScore: strongestCandidate.confidenceScore,
177
+ confidenceGap,
178
+ detectionReasoning,
179
+ rankedCandidates,
180
+ evidence,
181
+ };
182
+ }
183
+
184
+ export function buildDetectionSummary(projectDetection) {
185
+ if (!projectDetection.recommendedStackFileName) {
186
+ return 'I did not find enough stack markers to auto-detect this project confidently.';
187
+ }
188
+
189
+ const readableEvidence = projectDetection.evidence.length > 0
190
+ ? projectDetection.evidence.join(', ')
191
+ : 'basic project markers';
192
+
193
+ const confidenceGapSummary = typeof projectDetection.confidenceGap === 'number'
194
+ ? ` Confidence gap: ${projectDetection.confidenceGap}.`
195
+ : '';
196
+
197
+ return `This folder looks like ${toTitleCase(projectDetection.recommendedStackFileName)} with ${projectDetection.confidenceLabel} confidence based on ${readableEvidence}.${confidenceGapSummary}`;
198
+ }
199
+
200
+ export function formatDetectionCandidates(rankedCandidates) {
201
+ if (!rankedCandidates?.length) {
202
+ return 'No ranked candidates available.';
203
+ }
204
+
205
+ return rankedCandidates
206
+ .map((candidate, candidateIndex) => {
207
+ const evidenceSummary = candidate.evidence?.length ? candidate.evidence.join(', ') : 'no direct markers';
208
+ return `${candidateIndex + 1}. ${toTitleCase(candidate.stackFileName)} (score ${candidate.confidenceScore}) via ${evidenceSummary}`;
209
+ })
210
+ .join('\n');
211
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Profile Pack Parser — Team profile pack loading and matching.
3
+ * Depends on: constants.mjs, utils.mjs
4
+ */
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+
8
+ import {
9
+ PROFILE_PACK_REQUIRED_FIELDS,
10
+ PROFILE_PACKS_DIRECTORY_NAME,
11
+ } from './constants.mjs';
12
+
13
+ import {
14
+ pathExists,
15
+ collectFileNames,
16
+ normalizeChoiceInput,
17
+ matchProfileNameFromInput,
18
+ parseBooleanSetting,
19
+ parseBlockingSeverities,
20
+ } from './utils.mjs';
21
+
22
+ export function parseProfilePackContent(fileName, profilePackContent) {
23
+ const parsedFields = {};
24
+ const profilePackLines = profilePackContent.split(/\r?\n/);
25
+
26
+ for (const profilePackLine of profilePackLines) {
27
+ const lineMatch = profilePackLine.match(/^([A-Za-z][A-Za-z0-9 ]+):\s*(.+)$/);
28
+ if (!lineMatch) {
29
+ continue;
30
+ }
31
+
32
+ const fieldName = lineMatch[1].trim();
33
+ const fieldValue = lineMatch[2].trim();
34
+ parsedFields[fieldName] = fieldValue;
35
+ }
36
+
37
+ for (const requiredFieldName of PROFILE_PACK_REQUIRED_FIELDS) {
38
+ if (!parsedFields[requiredFieldName]) {
39
+ throw new Error(`Profile pack ${fileName} is missing required field: ${requiredFieldName}`);
40
+ }
41
+ }
42
+
43
+ const defaultProfileName = matchProfileNameFromInput(parsedFields.defaultProfile);
44
+ if (!defaultProfileName) {
45
+ throw new Error(`Profile pack ${fileName} has invalid defaultProfile: ${parsedFields.defaultProfile}`);
46
+ }
47
+
48
+ return {
49
+ fileName,
50
+ slug: normalizeChoiceInput(parsedFields.slug),
51
+ displayName: parsedFields.displayName,
52
+ description: parsedFields.description,
53
+ defaultProfileName,
54
+ defaultStackFileName: parsedFields.defaultStack.trim(),
55
+ defaultBlueprintFileName: parsedFields.defaultBlueprint.trim(),
56
+ defaultCi: parseBooleanSetting(parsedFields.ciGuardrails, `${fileName} ciGuardrails`),
57
+ lockCi: parseBooleanSetting(parsedFields.lockCi, `${fileName} lockCi`),
58
+ blockingSeverities: parseBlockingSeverities(parsedFields.blockingSeverities, fileName),
59
+ owner: parsedFields.owner || null,
60
+ lastUpdated: parsedFields.lastUpdated || null,
61
+ };
62
+ }
63
+
64
+ export async function collectProfilePacks(targetDirectoryPath) {
65
+ const profilePackDirectoryPath = path.join(targetDirectoryPath, '.agent-context', PROFILE_PACKS_DIRECTORY_NAME);
66
+ if (!(await pathExists(profilePackDirectoryPath))) {
67
+ return [];
68
+ }
69
+
70
+ const profilePackFileNames = await collectFileNames(profilePackDirectoryPath);
71
+ const profilePackDefinitions = [];
72
+
73
+ for (const profilePackFileName of profilePackFileNames) {
74
+ const profilePackFilePath = path.join(profilePackDirectoryPath, profilePackFileName);
75
+ const profilePackContent = await fs.readFile(profilePackFilePath, 'utf8');
76
+ profilePackDefinitions.push(parseProfilePackContent(profilePackFileName, profilePackContent));
77
+ }
78
+
79
+ return profilePackDefinitions;
80
+ }
81
+
82
+ export function findProfilePackByInput(profilePackInput, profilePackDefinitions) {
83
+ const normalizedProfilePackInput = normalizeChoiceInput(profilePackInput);
84
+
85
+ return profilePackDefinitions.find((profilePackDefinition) => {
86
+ const normalizedFileName = normalizeChoiceInput(profilePackDefinition.fileName.replace(/\.md$/i, ''));
87
+ const normalizedSlug = normalizeChoiceInput(profilePackDefinition.slug);
88
+ const normalizedDisplayName = normalizeChoiceInput(profilePackDefinition.displayName);
89
+
90
+ return normalizedProfilePackInput === normalizedFileName
91
+ || normalizedProfilePackInput === normalizedSlug
92
+ || normalizedProfilePackInput === normalizedDisplayName;
93
+ }) || null;
94
+ }