prjct-cli 1.3.0 → 1.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,139 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.0] - 2026-02-06
4
+
5
+ ### Features
6
+
7
+ - add citation format for context sources in templates (PRJ-113) (#117)
8
+
9
+
10
+ ## [1.4.2] - 2026-02-06
11
+
12
+ ### Features
13
+
14
+ - **Source citations in context files (PRJ-113)**: All generated context files now include `<!-- source: file, type -->` HTML comments showing where each section's data was detected from
15
+
16
+ ### Implementation Details
17
+
18
+ - Added `SourceInfo` type and `ContextSources` interface with `cite()` helper (`core/utils/citations.ts`)
19
+ - Extended `ProjectContext` with optional `sources` field — backward compatible, falls back to `defaultSources()`
20
+ - Added `buildSources()` to `sync-service.ts` — maps detected ecosystem/commands data to their source files (package.json, lock files, Cargo.toml, etc.)
21
+ - Citations added to 4 markdown formatters: Claude, Cursor, Windsurf, Copilot. Continue.dev skipped (JSON has no comment syntax)
22
+ - Context generator CLAUDE.md also updated with citation support
23
+ - Source types: `detected` (from files), `user-defined` (from config), `inferred` (from heuristics)
24
+
25
+ ### Learnings
26
+
27
+ - `context-generator.ts` and `formatters.ts` both independently generate CLAUDE.md content — both must be updated for consistent citations
28
+ - Sources can be determined post-detection from data values rather than threading metadata through every detection method
29
+ - Optional fields with fallback defaults (`sources?`) maintain backward compatibility without breaking existing callers
30
+
31
+ ### Test Plan
32
+
33
+ #### For QA
34
+ 1. Run `prjct sync --yes` — verify generated context files contain `<!-- source: ... -->` comments
35
+ 2. Check CLAUDE.md citations before: THIS PROJECT, Commands, Code Conventions, PROJECT STATE
36
+ 3. Check `.cursor/rules/prjct.mdc` citations before Tech Stack and Commands
37
+ 4. Check `.windsurf/rules/prjct.md` citations before Stack and Commands
38
+ 5. Check `.github/copilot-instructions.md` citations before Project Info and Commands
39
+ 6. Verify `.continue/config.json` unchanged (JSON has no comments)
40
+ 7. Run `bun test` — all 416 tests pass
41
+
42
+ #### For Users
43
+ **What changed:** Context files now show where data came from via HTML comments
44
+ **How to use:** Run `p. sync` — citations appear automatically
45
+ **Breaking changes:** None
46
+
47
+ ## [1.4.1] - 2026-02-06
48
+
49
+ ### Improvements
50
+
51
+ - **Better error messages for invalid commands (PRJ-98)**: Consistent, helpful CLI errors with did-you-mean suggestions and required parameter validation
52
+
53
+ ### Implementation Details
54
+
55
+ - Added `UNKNOWN_COMMAND` and `MISSING_PARAM` error codes to centralized error catalog (`core/utils/error-messages.ts`)
56
+ - Added `validateCommandParams()` — parses `CommandMeta.params` convention (`<required>` vs `[optional]`) and validates against actual CLI args before command execution
57
+ - Added `findClosestCommand()` with Levenshtein edit distance (threshold ≤ 2) for did-you-mean suggestions on typos
58
+ - All error paths now use `out.failWithHint()` for consistent formatting with hints, docs links, and file references
59
+ - Deprecated and unimplemented command errors also upgraded to use `failWithHint()`
60
+
61
+ ### Learnings
62
+
63
+ - `bin/prjct.ts` intercepts many commands (start, context, hooks, doctor, etc.) before `core/index.ts` — changes to dispatch only affect commands that reach core
64
+ - Template-only commands (e.g. `task`) are defined in `command-data.ts` but not registered in the command registry — they don't get param validation via CLI
65
+ - Levenshtein edit distance is simple to implement (~15 lines) and effective for CLI typo suggestions
66
+
67
+ ### Test Plan
68
+
69
+ #### For QA
70
+ 1. `prjct xyzzy` → "Unknown command: xyzzy" with help hint
71
+ 2. `prjct snyc` → "Did you mean 'prjct sync'?"
72
+ 3. `prjct shp` → "Did you mean 'prjct ship'?"
73
+ 4. `prjct bug` (no args) → "Missing required parameter: description" with usage
74
+ 5. `prjct idea` (no args) → "Missing required parameter: description" with usage
75
+ 6. `prjct sync --yes` → works normally (no regression)
76
+ 7. `prjct dash compact` → works normally (no regression)
77
+
78
+ #### For Users
79
+ **What changed:** CLI now shows helpful error messages with suggestions when you mistype a command or forget a required argument.
80
+ **How to use:** Just use prjct normally — errors are now more helpful automatically.
81
+ **Breaking changes:** None
82
+
83
+ ## [1.4.0] - 2026-02-06
84
+
85
+ ### Features
86
+
87
+ - programmatic verification checks for sync workflow (PRJ-106) (#115)
88
+
89
+ ## [1.3.1] - 2026-02-06
90
+
91
+ ### Features
92
+
93
+ - **Programmatic verification checks for sync workflow (PRJ-106)**: Post-sync validation with built-in and custom checks
94
+
95
+ ### Implementation Details
96
+
97
+ New `SyncVerifier` service (`core/services/sync-verifier.ts`) that runs verification checks after every sync. Three built-in checks run automatically:
98
+ - **Context files exist** — verifies `context/CLAUDE.md` was generated
99
+ - **JSON files valid** — validates `storage/state.json` syntax
100
+ - **No sensitive data** — scans context files for leaked API keys, passwords, secrets
101
+
102
+ Custom checks configurable in `.prjct/prjct.config.json`:
103
+ ```json
104
+ {
105
+ "verification": {
106
+ "checks": [
107
+ { "name": "Lint CLAUDE.md", "command": "npx markdownlint CLAUDE.md" },
108
+ { "name": "Custom validator", "script": ".prjct/verify.sh" }
109
+ ],
110
+ "failFast": false
111
+ }
112
+ }
113
+ ```
114
+
115
+ Integration: wired into `sync-service.ts` after file generation (step 11), results returned in `SyncResult.verification`. Display in `showSyncResult()` shows pass/fail per check with timing.
116
+
117
+ ### Learnings
118
+
119
+ - Non-critical verification must be wrapped in try/catch so it never breaks the sync workflow
120
+ - Config types must match optional fields between `LocalConfig` and `VerificationConfig` (both `checks` must be optional)
121
+ - Built-in + custom extensibility pattern (always run built-ins, then user commands) provides good defaults with flexibility
122
+
123
+ ### Test Plan
124
+
125
+ #### For QA
126
+ 1. Run `prjct sync --yes` — verify "Verified" section with 3 checks passing
127
+ 2. Add custom check to `.prjct/prjct.config.json` — verify it runs after sync
128
+ 3. Add failing custom check (`command: "exit 1"`) — verify `✗` with error
129
+ 4. Set `failFast: true` with failing check — verify remaining checks skipped
130
+ 5. Run `bun run build && bun run typecheck` — zero errors
131
+
132
+ #### For Users
133
+ **What changed:** `prjct sync` now validates generated output with pass/fail checks
134
+ **How to use:** Built-in checks run automatically. Add custom checks in `.prjct/prjct.config.json`
135
+ **Breaking changes:** None
136
+
3
137
  ## [1.3.0] - 2026-02-06
4
138
 
5
139
  ### Features
@@ -2,6 +2,7 @@
2
2
  * Tests for AI Tools Formatters
3
3
  *
4
4
  * @see PRJ-122
5
+ * @see PRJ-113 (citation support)
5
6
  */
6
7
 
7
8
  import { describe, expect, test } from 'bun:test'
@@ -15,6 +16,7 @@ import {
15
16
  type ProjectContext,
16
17
  } from '../../ai-tools/formatters'
17
18
  import { AI_TOOLS, getAIToolConfig } from '../../ai-tools/registry'
19
+ import { type ContextSources, cite, defaultSources } from '../../utils/citations'
18
20
 
19
21
  // =============================================================================
20
22
  // Test Fixtures
@@ -356,3 +358,119 @@ describe('Formatter Edge Cases', () => {
356
358
  expect(claudeResult).toContain('@scope/my-project')
357
359
  })
358
360
  })
361
+
362
+ // =============================================================================
363
+ // Citation Tests (PRJ-113)
364
+ // =============================================================================
365
+
366
+ const mockSources: ContextSources = {
367
+ name: { file: 'package.json', type: 'detected' },
368
+ version: { file: 'package.json', type: 'detected' },
369
+ ecosystem: { file: 'package.json', type: 'detected' },
370
+ languages: { file: 'package.json', type: 'detected' },
371
+ frameworks: { file: 'package.json', type: 'detected' },
372
+ commands: { file: 'bun.lockb', type: 'detected' },
373
+ projectType: { file: 'file count + frameworks', type: 'inferred' },
374
+ git: { file: 'git', type: 'detected' },
375
+ }
376
+
377
+ const ctxWithSources: ProjectContext = {
378
+ ...mockContext,
379
+ sources: mockSources,
380
+ }
381
+
382
+ describe('cite() helper', () => {
383
+ test('generates HTML comment with file and type', () => {
384
+ const result = cite({ file: 'package.json', type: 'detected' })
385
+ expect(result).toBe('<!-- source: package.json, detected -->')
386
+ })
387
+
388
+ test('supports inferred type', () => {
389
+ const result = cite({ file: 'file count + frameworks', type: 'inferred' })
390
+ expect(result).toBe('<!-- source: file count + frameworks, inferred -->')
391
+ })
392
+
393
+ test('supports user-defined type', () => {
394
+ const result = cite({ file: 'prjct.yaml', type: 'user-defined' })
395
+ expect(result).toBe('<!-- source: prjct.yaml, user-defined -->')
396
+ })
397
+ })
398
+
399
+ describe('defaultSources()', () => {
400
+ test('returns all source fields', () => {
401
+ const sources = defaultSources()
402
+ expect(sources.name).toBeDefined()
403
+ expect(sources.version).toBeDefined()
404
+ expect(sources.ecosystem).toBeDefined()
405
+ expect(sources.languages).toBeDefined()
406
+ expect(sources.frameworks).toBeDefined()
407
+ expect(sources.commands).toBeDefined()
408
+ expect(sources.projectType).toBeDefined()
409
+ expect(sources.git).toBeDefined()
410
+ })
411
+
412
+ test('git source is always "git"', () => {
413
+ const sources = defaultSources()
414
+ expect(sources.git.file).toBe('git')
415
+ expect(sources.git.type).toBe('detected')
416
+ })
417
+ })
418
+
419
+ describe('Citation integration', () => {
420
+ test('Claude format includes source citations', () => {
421
+ const result = formatForClaude(ctxWithSources, AI_TOOLS.claude)
422
+
423
+ expect(result).toContain('<!-- source: package.json, detected -->')
424
+ expect(result).toContain('<!-- source: bun.lockb, detected -->')
425
+ })
426
+
427
+ test('Cursor format includes source citations', () => {
428
+ const result = formatForCursor(ctxWithSources, AI_TOOLS.cursor)
429
+
430
+ expect(result).toContain('<!-- source: package.json, detected -->')
431
+ expect(result).toContain('<!-- source: bun.lockb, detected -->')
432
+ })
433
+
434
+ test('Windsurf format includes source citations', () => {
435
+ const result = formatForWindsurf(ctxWithSources, AI_TOOLS.windsurf)
436
+
437
+ expect(result).toContain('<!-- source: package.json, detected -->')
438
+ expect(result).toContain('<!-- source: bun.lockb, detected -->')
439
+ })
440
+
441
+ test('Copilot format includes source citations', () => {
442
+ const result = formatForCopilot(ctxWithSources, AI_TOOLS.copilot)
443
+
444
+ expect(result).toContain('<!-- source: package.json, detected -->')
445
+ expect(result).toContain('<!-- source: bun.lockb, detected -->')
446
+ })
447
+
448
+ test('formatters work without sources (backward compatible)', () => {
449
+ const ctxNoSources = { ...mockContext }
450
+ delete ctxNoSources.sources
451
+
452
+ // Should not throw
453
+ expect(() => formatForClaude(ctxNoSources, AI_TOOLS.claude)).not.toThrow()
454
+ expect(() => formatForCursor(ctxNoSources, AI_TOOLS.cursor)).not.toThrow()
455
+ expect(() => formatForWindsurf(ctxNoSources, AI_TOOLS.windsurf)).not.toThrow()
456
+ expect(() => formatForCopilot(ctxNoSources, AI_TOOLS.copilot)).not.toThrow()
457
+
458
+ // Should still contain default citation comments
459
+ const result = formatForClaude(ctxNoSources, AI_TOOLS.claude)
460
+ expect(result).toContain('<!-- source:')
461
+ })
462
+
463
+ test('Claude format has citations before each major section', () => {
464
+ const result = formatForClaude(ctxWithSources, AI_TOOLS.claude)
465
+
466
+ // Ecosystem citation before project type
467
+ const ecosystemIdx = result.indexOf('<!-- source: package.json, detected -->')
468
+ const projectTypeIdx = result.indexOf('**Type:**')
469
+ expect(ecosystemIdx).toBeLessThan(projectTypeIdx)
470
+
471
+ // Commands citation before commands table
472
+ const commandsCiteIdx = result.indexOf('<!-- source: bun.lockb, detected -->')
473
+ const commandsTableIdx = result.indexOf('| Action | Command |')
474
+ expect(commandsCiteIdx).toBeLessThan(commandsTableIdx)
475
+ })
476
+ })
@@ -8,6 +8,7 @@
8
8
  * - Windsurf: Similar to Cursor
9
9
  */
10
10
 
11
+ import { type ContextSources, cite, defaultSources } from '../utils/citations'
11
12
  import type { AIToolConfig } from './registry'
12
13
 
13
14
  export interface ProjectContext {
@@ -35,6 +36,7 @@ export interface ProjectContext {
35
36
  workflow: string[]
36
37
  domain: string[]
37
38
  }
39
+ sources?: ContextSources
38
40
  }
39
41
 
40
42
  /**
@@ -42,6 +44,8 @@ export interface ProjectContext {
42
44
  * Detailed markdown with full context
43
45
  */
44
46
  export function formatForClaude(ctx: ProjectContext, _config: AIToolConfig): string {
47
+ const s = ctx.sources || defaultSources()
48
+
45
49
  return `# ${ctx.name} - Project Rules
46
50
  <!-- projectId: ${ctx.projectId} -->
47
51
  <!-- Generated: ${new Date().toISOString()} -->
@@ -49,11 +53,13 @@ export function formatForClaude(ctx: ProjectContext, _config: AIToolConfig): str
49
53
 
50
54
  ## THIS PROJECT (${ctx.ecosystem})
51
55
 
56
+ ${cite(s.ecosystem)}
52
57
  **Type:** ${ctx.projectType}
53
58
  **Path:** ${ctx.repoPath}
54
59
 
55
60
  ### Commands (USE THESE, NOT OTHERS)
56
61
 
62
+ ${cite(s.commands)}
57
63
  | Action | Command |
58
64
  |--------|---------|
59
65
  | Install dependencies | \`${ctx.commands.install}\` |
@@ -65,7 +71,9 @@ export function formatForClaude(ctx: ProjectContext, _config: AIToolConfig): str
65
71
 
66
72
  ### Code Conventions
67
73
 
74
+ ${cite(s.languages)}
68
75
  - **Languages**: ${ctx.languages.join(', ') || 'Not detected'}
76
+ ${cite(s.frameworks)}
69
77
  - **Frameworks**: ${ctx.frameworks.join(', ') || 'Not detected'}
70
78
 
71
79
  ---
@@ -93,6 +101,7 @@ p. sync → p. task "desc" → [work] → p. done → p. ship
93
101
 
94
102
  ## PROJECT STATE
95
103
 
104
+ ${cite(s.name)}
96
105
  | Field | Value |
97
106
  |-------|-------|
98
107
  | Name | ${ctx.name} |
@@ -120,6 +129,7 @@ Load from \`~/.prjct-cli/projects/${ctx.projectId}/agents/\`:
120
129
  * @see https://cursor.com/docs/context/rules
121
130
  */
122
131
  export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): string {
132
+ const s = ctx.sources || defaultSources()
123
133
  const lines: string[] = []
124
134
 
125
135
  // MDC format with YAML frontmatter
@@ -135,6 +145,7 @@ export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): str
135
145
  lines.push('')
136
146
 
137
147
  // Tech stack
148
+ lines.push(cite(s.languages))
138
149
  lines.push('## Tech Stack')
139
150
  if (ctx.languages.length > 0) {
140
151
  lines.push(`- Languages: ${ctx.languages.join(', ')}`)
@@ -145,6 +156,7 @@ export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): str
145
156
  lines.push('')
146
157
 
147
158
  // Commands
159
+ lines.push(cite(s.commands))
148
160
  lines.push('## Commands')
149
161
  lines.push(`- Install: \`${ctx.commands.install}\``)
150
162
  lines.push(`- Dev: \`${ctx.commands.dev}\``)
@@ -175,6 +187,7 @@ export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): str
175
187
  * Minimal bullet points
176
188
  */
177
189
  export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): string {
190
+ const s = ctx.sources || defaultSources()
178
191
  const lines: string[] = []
179
192
 
180
193
  lines.push('# Copilot Instructions')
@@ -183,6 +196,7 @@ export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): st
183
196
  lines.push('')
184
197
 
185
198
  // Key info
199
+ lines.push(cite(s.ecosystem))
186
200
  lines.push('## Project Info')
187
201
  lines.push(`- Type: ${ctx.projectType}`)
188
202
  lines.push(`- Stack: ${ctx.frameworks.join(', ') || ctx.ecosystem}`)
@@ -196,6 +210,7 @@ export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): st
196
210
  lines.push('')
197
211
 
198
212
  // Commands
213
+ lines.push(cite(s.commands))
199
214
  lines.push('## Commands')
200
215
  lines.push(`- Test: \`${ctx.commands.test}\``)
201
216
  lines.push(`- Build: \`${ctx.commands.build}\``)
@@ -210,6 +225,7 @@ export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): st
210
225
  * @see https://docs.windsurf.com/windsurf/cascade/memories
211
226
  */
212
227
  export function formatForWindsurf(ctx: ProjectContext, _config: AIToolConfig): string {
228
+ const s = ctx.sources || defaultSources()
213
229
  const lines: string[] = []
214
230
 
215
231
  // YAML frontmatter (Windsurf uses trigger: always_on instead of alwaysApply)
@@ -226,6 +242,7 @@ export function formatForWindsurf(ctx: ProjectContext, _config: AIToolConfig): s
226
242
  lines.push('')
227
243
 
228
244
  // Tech stack (concise)
245
+ lines.push(cite(s.languages))
229
246
  lines.push('## Stack')
230
247
  lines.push(`- ${ctx.languages.join(', ')}`)
231
248
  if (ctx.frameworks.length > 0) {
@@ -234,6 +251,7 @@ export function formatForWindsurf(ctx: ProjectContext, _config: AIToolConfig): s
234
251
  lines.push('')
235
252
 
236
253
  // Commands (essential only)
254
+ lines.push(cite(s.commands))
237
255
  lines.push('## Commands')
238
256
  lines.push('```bash')
239
257
  lines.push(`# Install`)
@@ -529,6 +529,28 @@ export class AnalysisCommands extends PrjctCommandsBase {
529
529
  console.log('')
530
530
  }
531
531
 
532
+ // ═══════════════════════════════════════════════════════════════════════
533
+ // VERIFICATION - Post-sync validation checks
534
+ // ═══════════════════════════════════════════════════════════════════════
535
+ if (result.verification) {
536
+ const v = result.verification
537
+ if (v.passed) {
538
+ const items = v.checks.map((c) => `${c.name} (${c.durationMs}ms)`)
539
+ out.section('Verified')
540
+ out.list(items, { bullet: '✓' })
541
+ } else {
542
+ out.section('Verification')
543
+ const items = v.checks.map((c) =>
544
+ c.passed ? `✓ ${c.name}` : `✗ ${c.name}${c.error ? ` — ${c.error}` : ''}`
545
+ )
546
+ out.list(items)
547
+ if (v.skippedCount > 0) {
548
+ out.warn(`${v.skippedCount} check(s) skipped (fail-fast)`)
549
+ }
550
+ }
551
+ console.log('')
552
+ }
553
+
532
554
  // ═══════════════════════════════════════════════════════════════════════
533
555
  // NEXT STEPS - Clear call to action
534
556
  // ═══════════════════════════════════════════════════════════════════════
package/core/index.ts CHANGED
@@ -15,6 +15,7 @@ import type { CommandMeta } from './commands/registry'
15
15
  import { detectAllProviders, detectAntigravity } from './infrastructure/ai-provider'
16
16
  import configManager from './infrastructure/config-manager'
17
17
  import { sessionTracker } from './services/session-tracker'
18
+ import { getError } from './utils/error-messages'
18
19
  import out from './utils/output'
19
20
 
20
21
  interface ParsedCommandArgs {
@@ -53,27 +54,34 @@ async function main(): Promise<void> {
53
54
  const cmd = commandRegistry.getByName(commandName)
54
55
 
55
56
  if (!cmd) {
56
- console.error(`Unknown command: ${commandName}`)
57
- console.error(`\nUse 'prjct --help' to see available commands.`)
57
+ const suggestion = findClosestCommand(commandName)
58
+ const hint = suggestion
59
+ ? `Did you mean 'prjct ${suggestion}'? Run 'prjct --help' for all commands`
60
+ : "Run 'prjct --help' to see available commands"
61
+ out.failWithHint(
62
+ getError('UNKNOWN_COMMAND', { message: `Unknown command: ${commandName}`, hint })
63
+ )
58
64
  out.end()
59
65
  process.exit(1)
60
66
  }
61
67
 
62
68
  // 2. Check if deprecated
63
69
  if (cmd.deprecated) {
64
- console.error(`Command '${commandName}' is deprecated.`)
65
- if (cmd.replacedBy) {
66
- console.error(`Use 'prjct ${cmd.replacedBy}' instead.`)
67
- }
70
+ const hint = cmd.replacedBy
71
+ ? `Use 'prjct ${cmd.replacedBy}' instead`
72
+ : "Run 'prjct --help' to see available commands"
73
+ out.failWithHint({ message: `Command '${commandName}' is deprecated`, hint })
68
74
  out.end()
69
75
  process.exit(1)
70
76
  }
71
77
 
72
78
  // 3. Check if implemented
73
79
  if (!cmd.implemented) {
74
- console.error(`Command '${commandName}' exists but is not yet implemented.`)
75
- console.error(`Check the roadmap or contribute: https://github.com/jlopezlira/prjct-cli`)
76
- console.error(`\nUse 'prjct --help' to see available commands.`)
80
+ out.failWithHint({
81
+ message: `Command '${commandName}' is not yet implemented`,
82
+ hint: "Run 'prjct --help' to see available commands",
83
+ docs: 'https://github.com/jlopezlira/prjct-cli',
84
+ })
77
85
  out.end()
78
86
  process.exit(1)
79
87
  }
@@ -81,7 +89,15 @@ async function main(): Promise<void> {
81
89
  // 4. Parse arguments
82
90
  const { parsedArgs, options } = parseCommandArgs(cmd, rawArgs)
83
91
 
84
- // 4.5. Session tracking — touch/create session before command execution
92
+ // 4.5. Validate required params
93
+ const paramError = validateCommandParams(cmd, parsedArgs)
94
+ if (paramError) {
95
+ out.failWithHint(paramError)
96
+ out.end()
97
+ process.exit(1)
98
+ }
99
+
100
+ // 4.6. Session tracking — touch/create session before command execution
85
101
  let projectId: string | null = null
86
102
  const commandStartTime = Date.now()
87
103
  try {
@@ -193,6 +209,71 @@ async function main(): Promise<void> {
193
209
  }
194
210
  }
195
211
 
212
+ /**
213
+ * Validate that required params are provided
214
+ * Parses CommandMeta.params: <required> vs [optional]
215
+ */
216
+ function validateCommandParams(
217
+ cmd: CommandMeta,
218
+ parsedArgs: string[]
219
+ ): import('./utils/error-messages').ErrorWithHint | null {
220
+ if (!cmd.params) return null
221
+
222
+ // Extract required params: tokens wrapped in <angle brackets>
223
+ const requiredParams = cmd.params.match(/<[^>]+>/g)
224
+ if (!requiredParams || requiredParams.length === 0) return null
225
+
226
+ // Check if enough positional args provided
227
+ if (parsedArgs.length < requiredParams.length) {
228
+ const paramNames = requiredParams.map((p) => p.slice(1, -1)).join(', ')
229
+ const usage = cmd.usage.terminal || `prjct ${cmd.name} ${cmd.params}`
230
+ return getError('MISSING_PARAM', {
231
+ message: `Missing required parameter: ${paramNames}`,
232
+ hint: `Usage: ${usage}`,
233
+ })
234
+ }
235
+
236
+ return null
237
+ }
238
+
239
+ /**
240
+ * Find closest matching command name for did-you-mean suggestions
241
+ * Uses Levenshtein edit distance — suggests if distance <= 2
242
+ */
243
+ function findClosestCommand(input: string): string | null {
244
+ const allNames = commandRegistry.getAll().map((c) => c.name)
245
+ let best: string | null = null
246
+ let bestDist = Infinity
247
+
248
+ for (const name of allNames) {
249
+ const dist = editDistance(input.toLowerCase(), name.toLowerCase())
250
+ if (dist < bestDist) {
251
+ bestDist = dist
252
+ best = name
253
+ }
254
+ }
255
+
256
+ // Only suggest if edit distance is at most 2
257
+ return bestDist <= 2 ? best : null
258
+ }
259
+
260
+ function editDistance(a: string, b: string): number {
261
+ const m = a.length
262
+ const n = b.length
263
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
264
+ for (let i = 0; i <= m; i++) dp[i][0] = i
265
+ for (let j = 0; j <= n; j++) dp[0][j] = j
266
+ for (let i = 1; i <= m; i++) {
267
+ for (let j = 1; j <= n; j++) {
268
+ dp[i][j] =
269
+ a[i - 1] === b[j - 1]
270
+ ? dp[i - 1][j - 1]
271
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
272
+ }
273
+ }
274
+ return dp[m][n]
275
+ }
276
+
196
277
  /**
197
278
  * Parse command arguments dynamically
198
279
  */
@@ -12,6 +12,7 @@
12
12
  import fs from 'node:fs/promises'
13
13
  import path from 'node:path'
14
14
  import pathManager from '../infrastructure/path-manager'
15
+ import { type ContextSources, cite, defaultSources } from '../utils/citations'
15
16
  import dateHelper from '../utils/date-helper'
16
17
  import { mergePreservedSections, validatePreserveBlocks } from '../utils/preserve-sections'
17
18
  import { NestedContextResolver } from './nested-context-resolver'
@@ -103,13 +104,14 @@ export class ContextFileGenerator {
103
104
  git: GitData,
104
105
  stats: ProjectStats,
105
106
  commands: Commands,
106
- agents: AgentInfo[]
107
+ agents: AgentInfo[],
108
+ sources?: ContextSources
107
109
  ): Promise<string[]> {
108
110
  const contextPath = path.join(this.config.globalPath, 'context')
109
111
 
110
112
  // Generate all context files IN PARALLEL
111
113
  await Promise.all([
112
- this.generateClaudeMd(contextPath, git, stats, commands, agents),
114
+ this.generateClaudeMd(contextPath, git, stats, commands, agents, sources),
113
115
  this.generateNowMd(contextPath),
114
116
  this.generateNextMd(contextPath),
115
117
  this.generateIdeasMd(contextPath),
@@ -137,10 +139,12 @@ export class ContextFileGenerator {
137
139
  git: GitData,
138
140
  stats: ProjectStats,
139
141
  commands: Commands,
140
- agents: AgentInfo[]
142
+ agents: AgentInfo[],
143
+ sources?: ContextSources
141
144
  ): Promise<void> {
142
145
  const workflowAgents = agents.filter((a) => a.type === 'workflow').map((a) => a.name)
143
146
  const domainAgents = agents.filter((a) => a.type === 'domain').map((a) => a.name)
147
+ const s = sources || defaultSources()
144
148
 
145
149
  const content = `# ${stats.name} - Project Rules
146
150
  <!-- projectId: ${this.config.projectId} -->
@@ -149,11 +153,13 @@ export class ContextFileGenerator {
149
153
 
150
154
  ## THIS PROJECT (${stats.ecosystem})
151
155
 
156
+ ${cite(s.ecosystem)}
152
157
  **Type:** ${stats.projectType}
153
158
  **Path:** ${this.config.projectPath}
154
159
 
155
160
  ### Commands (USE THESE, NOT OTHERS)
156
161
 
162
+ ${cite(s.commands)}
157
163
  | Action | Command |
158
164
  |--------|---------|
159
165
  | Install dependencies | \`${commands.install}\` |
@@ -165,7 +171,9 @@ export class ContextFileGenerator {
165
171
 
166
172
  ### Code Conventions
167
173
 
174
+ ${cite(s.languages)}
168
175
  - **Languages**: ${stats.languages.join(', ') || 'Not detected'}
176
+ ${cite(s.frameworks)}
169
177
  - **Frameworks**: ${stats.frameworks.join(', ') || 'Not detected'}
170
178
 
171
179
  ---
@@ -193,6 +201,7 @@ p. sync → p. task "desc" → [work] → p. done → p. ship
193
201
 
194
202
  ## PROJECT STATE
195
203
 
204
+ ${cite(s.name)}
196
205
  | Field | Value |
197
206
  |-------|-------|
198
207
  | Name | ${stats.name} |