prjct-cli 1.8.0 → 1.9.0

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.
@@ -21,6 +21,7 @@ import { exec } from 'node:child_process'
21
21
  import os from 'node:os'
22
22
  import path from 'node:path'
23
23
  import { promisify } from 'node:util'
24
+ import { compareSemver } from '../schemas/model'
24
25
  import { fileExists } from '../utils/fs-helpers'
25
26
  import { readProviderCache, writeProviderCache } from '../utils/provider-cache'
26
27
 
@@ -58,6 +59,9 @@ export const ClaudeProvider: AIProviderConfig = {
58
59
  ignoreFile: '.claudeignore',
59
60
  websiteUrl: 'https://www.anthropic.com/claude',
60
61
  docsUrl: 'https://docs.anthropic.com/claude-code',
62
+ defaultModel: 'sonnet',
63
+ supportedModels: ['opus', 'sonnet', 'haiku'],
64
+ minCliVersion: '1.0.0',
61
65
  }
62
66
 
63
67
  /**
@@ -77,6 +81,9 @@ export const GeminiProvider: AIProviderConfig = {
77
81
  ignoreFile: '.geminiignore',
78
82
  websiteUrl: 'https://geminicli.com',
79
83
  docsUrl: 'https://geminicli.com/docs',
84
+ defaultModel: '2.5-flash',
85
+ supportedModels: ['2.5-pro', '2.5-flash', '2.0-flash'],
86
+ minCliVersion: '1.0.0',
80
87
  }
81
88
 
82
89
  /**
@@ -100,6 +107,9 @@ export const AntigravityProvider: AIProviderConfig = {
100
107
  ignoreFile: '.agentignore', // Assumed
101
108
  websiteUrl: 'https://gemini.google.com/app/antigravity',
102
109
  docsUrl: 'https://gemini.google.com/app/antigravity',
110
+ defaultModel: null, // Platform-managed
111
+ supportedModels: [],
112
+ minCliVersion: null,
103
113
  }
104
114
 
105
115
  /**
@@ -129,6 +139,9 @@ export const CursorProvider: AIProviderConfig = {
129
139
  isProjectLevel: true, // Config is project-level only
130
140
  websiteUrl: 'https://cursor.com',
131
141
  docsUrl: 'https://cursor.com/docs',
142
+ defaultModel: null, // Multi-model IDE, user selects
143
+ supportedModels: [],
144
+ minCliVersion: null,
132
145
  }
133
146
 
134
147
  /**
@@ -159,6 +172,9 @@ export const WindsurfProvider: AIProviderConfig = {
159
172
  isProjectLevel: true, // Config is project-level only
160
173
  websiteUrl: 'https://windsurf.com',
161
174
  docsUrl: 'https://docs.windsurf.com',
175
+ defaultModel: null, // Multi-model IDE, user selects
176
+ supportedModels: [],
177
+ minCliVersion: null,
162
178
  }
163
179
 
164
180
  /**
@@ -221,14 +237,33 @@ export async function detectProvider(provider: AIProviderName): Promise<Provider
221
237
  }
222
238
 
223
239
  const version = await getCliVersion(config.cliCommand)
240
+ const versionWarning = validateCliVersion(provider, version || undefined)
224
241
 
225
242
  return {
226
243
  installed: true,
227
244
  version: version || undefined,
228
245
  path: cliPath,
246
+ versionWarning: versionWarning || undefined,
229
247
  }
230
248
  }
231
249
 
250
+ /**
251
+ * Validate that a detected CLI version meets the provider's minimum requirement.
252
+ * Returns a warning message if the version is below minimum, or null if OK.
253
+ */
254
+ export function validateCliVersion(
255
+ provider: AIProviderName,
256
+ version: string | undefined
257
+ ): string | null {
258
+ const config = Providers[provider]
259
+ if (!config.minCliVersion || !version) return null
260
+
261
+ if (compareSemver(version, config.minCliVersion) < 0) {
262
+ return `⚠️ ${config.displayName} v${version} is below minimum v${config.minCliVersion}. Some features may not work correctly.`
263
+ }
264
+ return null
265
+ }
266
+
232
267
  /**
233
268
  * Detect all available CLI-based providers
234
269
  * Results are cached to disk with a 10-minute TTL to avoid redundant shell spawns.
@@ -4,6 +4,8 @@
4
4
  * Defines the structure for analysis.json - repository analysis.
5
5
  */
6
6
 
7
+ import type { ModelMetadata } from './model'
8
+
7
9
  export interface CodePattern {
8
10
  name: string
9
11
  description: string
@@ -28,6 +30,8 @@ export interface AnalysisSchema {
28
30
  patterns: CodePattern[]
29
31
  antiPatterns: AntiPattern[]
30
32
  analyzedAt: string // ISO8601
33
+ /** Which AI model was used for this analysis (PRJ-265) */
34
+ modelMetadata?: ModelMetadata
31
35
  }
32
36
 
33
37
  export const DEFAULT_ANALYSIS: Omit<AnalysisSchema, 'projectId'> = {
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Task Classification Schema
3
+ *
4
+ * Defines the structure for LLM-based domain classification results.
5
+ * Replaces hardcoded keyword lists with structured classification output.
6
+ *
7
+ * @see PRJ-299
8
+ */
9
+
10
+ import { z } from 'zod'
11
+
12
+ // =============================================================================
13
+ // Classification Schemas
14
+ // =============================================================================
15
+
16
+ export const ClassificationDomainSchema = z.enum([
17
+ 'frontend',
18
+ 'backend',
19
+ 'database',
20
+ 'devops',
21
+ 'testing',
22
+ 'docs',
23
+ 'uxui',
24
+ 'general',
25
+ ])
26
+
27
+ export const TaskClassificationSchema = z.object({
28
+ /** Primary domain for this task */
29
+ primaryDomain: ClassificationDomainSchema,
30
+ /** Secondary domains that are also relevant */
31
+ secondaryDomains: z.array(ClassificationDomainSchema),
32
+ /** Confidence in the classification (0-1) */
33
+ confidence: z.number().min(0).max(1),
34
+ /** Glob patterns for relevant files */
35
+ filePatterns: z.array(z.string()),
36
+ /** Agent names that should handle this task */
37
+ relevantAgents: z.array(z.string()),
38
+ })
39
+
40
+ export const ClassificationCacheEntrySchema = z.object({
41
+ /** The classification result */
42
+ classification: TaskClassificationSchema,
43
+ /** When this was classified */
44
+ classifiedAt: z.string(),
45
+ /** How this was classified */
46
+ source: z.enum(['cache', 'history', 'llm', 'heuristic']),
47
+ /** Hash of the task description for cache lookup */
48
+ descriptionHash: z.string(),
49
+ /** Project ID this classification belongs to */
50
+ projectId: z.string(),
51
+ })
52
+
53
+ export const ClassificationCacheSchema = z.object({
54
+ /** Cached classifications keyed by descriptionHash */
55
+ entries: z.record(z.string(), ClassificationCacheEntrySchema),
56
+ /** Confirmed patterns from successful task completions */
57
+ confirmedPatterns: z.array(
58
+ z.object({
59
+ descriptionHash: z.string(),
60
+ classification: TaskClassificationSchema,
61
+ confirmedAt: z.string(),
62
+ taskDescription: z.string(),
63
+ })
64
+ ),
65
+ })
66
+
67
+ // =============================================================================
68
+ // Inferred Types
69
+ // =============================================================================
70
+
71
+ export type ClassificationDomain = z.infer<typeof ClassificationDomainSchema>
72
+ export type TaskClassification = z.infer<typeof TaskClassificationSchema>
73
+ export type ClassificationCacheEntry = z.infer<typeof ClassificationCacheEntrySchema>
74
+ export type ClassificationCache = z.infer<typeof ClassificationCacheSchema>
75
+
76
+ // =============================================================================
77
+ // Defaults
78
+ // =============================================================================
79
+
80
+ export const DEFAULT_CLASSIFICATION_CACHE: ClassificationCache = {
81
+ entries: {},
82
+ confirmedPatterns: [],
83
+ }
84
+
85
+ export const GENERAL_CLASSIFICATION: TaskClassification = {
86
+ primaryDomain: 'general',
87
+ secondaryDomains: [],
88
+ confidence: 0.3,
89
+ filePatterns: ['**/*.ts', '**/*.js'],
90
+ relevantAgents: [],
91
+ }
@@ -16,10 +16,16 @@
16
16
  export * from './agents'
17
17
  // Analysis
18
18
  export * from './analysis'
19
+ // Classification (LLM-based domain detection)
20
+ export * from './classification'
19
21
  // Ideas
20
22
  export * from './ideas'
21
23
  // Issues (local cache of issue tracker issues)
22
24
  export * from './issues'
25
+ // LLM output schemas (structured response validation)
26
+ export * from './llm-output'
27
+ // Model specification (AI provider model tracking)
28
+ export * from './model'
23
29
  // Outcomes
24
30
  export * from './outcomes'
25
31
  // Permissions
@@ -0,0 +1,170 @@
1
+ /**
2
+ * LLM Output Schemas
3
+ *
4
+ * Zod schemas for all LLM prompt response types.
5
+ * These schemas are:
6
+ * 1. Injected into prompts as explicit format instructions
7
+ * 2. Used to validate LLM responses before storage or use
8
+ * 3. Define the contract between prompts and response handling
9
+ *
10
+ * @see PRJ-264
11
+ */
12
+
13
+ import { z } from 'zod'
14
+ import { ClassificationDomainSchema, TaskClassificationSchema } from './classification'
15
+
16
+ // =============================================================================
17
+ // Re-export classification schema (it IS an LLM output schema)
18
+ // =============================================================================
19
+
20
+ export { TaskClassificationSchema } from './classification'
21
+
22
+ // =============================================================================
23
+ // Agent Assignment Schema
24
+ // =============================================================================
25
+
26
+ /** LLM response when selecting which agent should handle a task */
27
+ export const AgentAssignmentSchema = z.object({
28
+ /** Agent file name (e.g., "backend.md", "frontend.md") */
29
+ agentName: z.string(),
30
+ /** Why this agent was selected */
31
+ reasoning: z.string(),
32
+ /** Confidence in the assignment (0-1) */
33
+ confidence: z.number().min(0).max(1),
34
+ })
35
+
36
+ // =============================================================================
37
+ // Subtask Breakdown Schema
38
+ // =============================================================================
39
+
40
+ /** LLM response when breaking a task into subtasks */
41
+ export const SubtaskBreakdownSchema = z.object({
42
+ /** Subtasks in execution order */
43
+ subtasks: z.array(
44
+ z.object({
45
+ /** Short description of the subtask */
46
+ description: z.string(),
47
+ /** Domain this subtask belongs to */
48
+ domain: ClassificationDomainSchema,
49
+ /** Suggested agent for this subtask */
50
+ agent: z.string(),
51
+ /** IDs of subtasks this depends on (by index, 0-based) */
52
+ dependsOn: z.array(z.number()),
53
+ })
54
+ ),
55
+ /** Estimated total effort */
56
+ effort: z.enum(['low', 'medium', 'high']),
57
+ })
58
+
59
+ // =============================================================================
60
+ // Schema-to-Prompt Serializer
61
+ // =============================================================================
62
+
63
+ /**
64
+ * Registry of output schemas keyed by prompt type.
65
+ * Used by prompt-builder to inject the correct schema into each prompt.
66
+ */
67
+ export const OUTPUT_SCHEMAS: Record<string, { schema: z.ZodTypeAny; example: string }> = {
68
+ classification: {
69
+ schema: TaskClassificationSchema,
70
+ example: JSON.stringify(
71
+ {
72
+ primaryDomain: 'backend',
73
+ secondaryDomains: ['database'],
74
+ confidence: 0.9,
75
+ filePatterns: ['src/api/**'],
76
+ relevantAgents: ['backend.md'],
77
+ },
78
+ null,
79
+ 2
80
+ ),
81
+ },
82
+ agentAssignment: {
83
+ schema: AgentAssignmentSchema,
84
+ example: JSON.stringify(
85
+ {
86
+ agentName: 'backend.md',
87
+ reasoning: 'Task involves API endpoint creation',
88
+ confidence: 0.85,
89
+ },
90
+ null,
91
+ 2
92
+ ),
93
+ },
94
+ subtaskBreakdown: {
95
+ schema: SubtaskBreakdownSchema,
96
+ example: JSON.stringify(
97
+ {
98
+ subtasks: [
99
+ {
100
+ description: 'Add schema validation',
101
+ domain: 'backend',
102
+ agent: 'backend.md',
103
+ dependsOn: [],
104
+ },
105
+ {
106
+ description: 'Add unit tests',
107
+ domain: 'testing',
108
+ agent: 'testing.md',
109
+ dependsOn: [0],
110
+ },
111
+ ],
112
+ effort: 'medium',
113
+ },
114
+ null,
115
+ 2
116
+ ),
117
+ },
118
+ }
119
+
120
+ /**
121
+ * Render a schema as prompt instructions.
122
+ * Returns a markdown block that tells the LLM exactly what format to use.
123
+ */
124
+ export function renderSchemaForPrompt(schemaType: string): string | null {
125
+ const entry = OUTPUT_SCHEMAS[schemaType]
126
+ if (!entry) return null
127
+
128
+ return `## OUTPUT FORMAT
129
+
130
+ Return ONLY valid JSON matching this schema (no markdown, no explanation):
131
+
132
+ \`\`\`json
133
+ ${entry.example}
134
+ \`\`\`
135
+
136
+ Fields:
137
+ ${describeSchema(entry.schema)}`
138
+ }
139
+
140
+ /**
141
+ * Extract field descriptions from a Zod object schema.
142
+ */
143
+ function describeSchema(schema: z.ZodTypeAny): string {
144
+ if (schema instanceof z.ZodObject) {
145
+ const shape = schema.shape as Record<string, z.ZodTypeAny>
146
+ return Object.entries(shape)
147
+ .map(([key, field]) => `- \`${key}\`: ${describeField(field)}`)
148
+ .join('\n')
149
+ }
150
+ return '(see example above)'
151
+ }
152
+
153
+ /**
154
+ * Describe a single Zod field type for prompt injection.
155
+ */
156
+ function describeField(field: z.ZodTypeAny): string {
157
+ if (field instanceof z.ZodString) return 'string'
158
+ if (field instanceof z.ZodNumber) return 'number'
159
+ if (field instanceof z.ZodEnum) return `one of: ${(field.options as string[]).join(', ')}`
160
+ if (field instanceof z.ZodArray) return `array of ${describeField(field.element)}`
161
+ if (field instanceof z.ZodObject) return 'object'
162
+ return 'any'
163
+ }
164
+
165
+ // =============================================================================
166
+ // Inferred Types
167
+ // =============================================================================
168
+
169
+ export type AgentAssignment = z.infer<typeof AgentAssignmentSchema>
170
+ export type SubtaskBreakdown = z.infer<typeof SubtaskBreakdownSchema>
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Model Schema
3
+ *
4
+ * Defines model specification types for AI providers.
5
+ * Records which model was used for each analysis and task,
6
+ * enabling consistency tracking and mismatch warnings.
7
+ *
8
+ * @see PRJ-265
9
+ */
10
+
11
+ import { z } from 'zod'
12
+
13
+ // =============================================================================
14
+ // Provider-Specific Model Identifiers
15
+ // =============================================================================
16
+
17
+ /** Claude model identifiers (short names matching agent frontmatter convention) */
18
+ export const ClaudeModelSchema = z.enum(['opus', 'sonnet', 'haiku'])
19
+
20
+ /** Gemini model identifiers */
21
+ export const GeminiModelSchema = z.enum(['2.5-pro', '2.5-flash', '2.0-flash'])
22
+
23
+ /** Generic model identifier - allows any string for future providers */
24
+ export const AIModelSchema = z.string().min(1)
25
+
26
+ // =============================================================================
27
+ // Supported Models Per Provider
28
+ // =============================================================================
29
+
30
+ export const SUPPORTED_MODELS: Record<string, readonly string[]> = {
31
+ claude: ['opus', 'sonnet', 'haiku'],
32
+ gemini: ['2.5-pro', '2.5-flash', '2.0-flash'],
33
+ cursor: [], // Multi-model IDE, user selects model
34
+ windsurf: [], // Multi-model IDE, user selects model
35
+ antigravity: [], // Platform-managed
36
+ } as const
37
+
38
+ export const DEFAULT_MODELS: Record<string, string> = {
39
+ claude: 'sonnet',
40
+ gemini: '2.5-flash',
41
+ } as const
42
+
43
+ // =============================================================================
44
+ // Minimum CLI Versions
45
+ // =============================================================================
46
+
47
+ export const MIN_CLI_VERSIONS: Record<string, string> = {
48
+ claude: '1.0.0',
49
+ gemini: '1.0.0',
50
+ } as const
51
+
52
+ // =============================================================================
53
+ // Model Metadata - Recorded Per Operation
54
+ // =============================================================================
55
+
56
+ /** Model metadata recorded with each analysis or task */
57
+ export const ModelMetadataSchema = z.object({
58
+ /** Provider name (e.g., 'claude', 'gemini') */
59
+ provider: z.string(),
60
+ /** Model identifier (e.g., 'opus', 'sonnet', '2.5-pro') */
61
+ model: z.string(),
62
+ /** CLI version used */
63
+ cliVersion: z.string().optional(),
64
+ /** When this was recorded */
65
+ recordedAt: z.string(),
66
+ })
67
+
68
+ // =============================================================================
69
+ // Model Configuration - Per Project
70
+ // =============================================================================
71
+
72
+ /** Per-project model preference */
73
+ export const ModelPreferenceSchema = z.object({
74
+ /** Preferred model for this project */
75
+ preferredModel: z.string().optional(),
76
+ /** Model used for last analysis (for mismatch detection) */
77
+ lastAnalysisModel: ModelMetadataSchema.optional(),
78
+ })
79
+
80
+ // =============================================================================
81
+ // Inferred Types
82
+ // =============================================================================
83
+
84
+ export type ClaudeModel = z.infer<typeof ClaudeModelSchema>
85
+ export type GeminiModel = z.infer<typeof GeminiModelSchema>
86
+ export type ModelMetadata = z.infer<typeof ModelMetadataSchema>
87
+ export type ModelPreference = z.infer<typeof ModelPreferenceSchema>
88
+
89
+ // =============================================================================
90
+ // Validation Helpers
91
+ // =============================================================================
92
+
93
+ /** Check if a model is valid for a given provider */
94
+ export function isValidModelForProvider(provider: string, model: string): boolean {
95
+ const supported = SUPPORTED_MODELS[provider]
96
+ if (!supported || supported.length === 0) return true // No restriction for multi-model IDEs
97
+ return supported.includes(model)
98
+ }
99
+
100
+ /** Get the default model for a provider */
101
+ export function getDefaultModel(provider: string): string | null {
102
+ return DEFAULT_MODELS[provider] ?? null
103
+ }
104
+
105
+ /** Get supported models for a provider */
106
+ export function getSupportedModels(provider: string): readonly string[] {
107
+ return SUPPORTED_MODELS[provider] ?? []
108
+ }
109
+
110
+ /** Get minimum CLI version for a provider */
111
+ export function getMinCliVersion(provider: string): string | null {
112
+ return MIN_CLI_VERSIONS[provider] ?? null
113
+ }
114
+
115
+ /**
116
+ * Compare semver versions. Returns:
117
+ * -1 if a < b
118
+ * 0 if a == b
119
+ * 1 if a > b
120
+ */
121
+ export function compareSemver(a: string, b: string): -1 | 0 | 1 {
122
+ const pa = a.split('.').map(Number)
123
+ const pb = b.split('.').map(Number)
124
+ for (let i = 0; i < 3; i++) {
125
+ const va = pa[i] ?? 0
126
+ const vb = pb[i] ?? 0
127
+ if (va < vb) return -1
128
+ if (va > vb) return 1
129
+ }
130
+ return 0
131
+ }
132
+
133
+ /** Check if a CLI version meets minimum requirements */
134
+ export function meetsMinVersion(provider: string, version: string): boolean {
135
+ const min = MIN_CLI_VERSIONS[provider]
136
+ if (!min) return true // No minimum defined
137
+ return compareSemver(version, min) >= 0
138
+ }
139
+
140
+ /**
141
+ * Check for model mismatch between analysis and current task.
142
+ * Returns a warning message if the models differ, or null if they match.
143
+ */
144
+ export function checkModelMismatch(
145
+ analysisModel: ModelMetadata | undefined,
146
+ taskModel: ModelMetadata | undefined
147
+ ): string | null {
148
+ if (!analysisModel || !taskModel) return null
149
+ if (analysisModel.provider !== taskModel.provider || analysisModel.model !== taskModel.model) {
150
+ return `⚠️ Model mismatch: analysis used ${analysisModel.provider}/${analysisModel.model}, but task is using ${taskModel.provider}/${taskModel.model}. Results may differ.`
151
+ }
152
+ return null
153
+ }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { z } from 'zod'
12
+ import { ModelMetadataSchema } from './model'
12
13
 
13
14
  // =============================================================================
14
15
  // Zod Schemas - Source of Truth
@@ -90,6 +91,8 @@ export const CurrentTaskSchema = z.object({
90
91
  // Fibonacci estimation
91
92
  estimatedPoints: z.number().optional(), // Fibonacci: 1,2,3,5,8,13,21
92
93
  estimatedMinutes: z.number().optional(), // Derived from points
94
+ // Model specification - which AI model was used (PRJ-265)
95
+ modelMetadata: ModelMetadataSchema.optional(),
93
96
  })
94
97
 
95
98
  export const PreviousTaskSchema = z.object({
@@ -78,6 +78,8 @@ export interface ProjectSettings {
78
78
  autoCommit?: boolean
79
79
  commitFooter?: string
80
80
  branchNaming?: string
81
+ /** Preferred AI model for this project (e.g., 'opus', 'sonnet', '2.5-pro') */
82
+ preferredModel?: string
81
83
  }
82
84
 
83
85
  /**
@@ -78,6 +78,15 @@ export interface AIProviderConfig {
78
78
 
79
79
  /** URL for provider documentation */
80
80
  docsUrl: string
81
+
82
+ /** Default model for this provider (e.g., 'sonnet', '2.5-flash'). Null for multi-model IDEs */
83
+ defaultModel: string | null
84
+
85
+ /** Supported model identifiers. Empty array for multi-model IDEs (user selects model) */
86
+ supportedModels: readonly string[]
87
+
88
+ /** Minimum CLI version required. Null for non-CLI providers */
89
+ minCliVersion: string | null
81
90
  }
82
91
 
83
92
  /**
@@ -92,6 +101,9 @@ export interface ProviderDetectionResult {
92
101
 
93
102
  /** Path to the CLI executable */
94
103
  path?: string
104
+
105
+ /** Warning if CLI version is below minimum requirement */
106
+ versionWarning?: string
95
107
  }
96
108
 
97
109
  /**