prjct-cli 1.9.0 → 1.10.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.
@@ -29,13 +29,47 @@ import type {
29
29
  import { getErrorMessage, isNotFoundError } from '../types/fs'
30
30
  import { fileExists } from '../utils/fs-helpers'
31
31
  import { PACKAGE_ROOT } from '../utils/version'
32
+ import { buildAntiHallucinationBlock, type ProjectGroundTruth } from './anti-hallucination'
32
33
  import { loadCommandContextConfig, resolveCommandContextFull } from './command-context'
34
+ import { buildEnvironmentBlock } from './environment-block'
33
35
  import {
36
+ budgetsFromCoordinator,
34
37
  DEFAULT_BUDGETS,
35
38
  filterSkillsByDomains,
36
39
  InjectionBudgetTracker,
37
40
  truncateToTokenBudget,
38
41
  } from './injection-validator'
42
+ import type { TokenBudgetCoordinator } from './token-budget'
43
+
44
+ // =============================================================================
45
+ // Section Priority (PRJ-301)
46
+ // =============================================================================
47
+
48
+ /**
49
+ * Prompt section priorities for budget trimming.
50
+ * When token budget is tight, optional sections are dropped first.
51
+ *
52
+ * @see PRJ-301
53
+ */
54
+ export type SectionPriority = 'critical' | 'important' | 'optional'
55
+
56
+ /**
57
+ * Canonical section ordering for prompt assembly.
58
+ * Based on research of 25+ system prompts from Claude Code, Gemini, ChatGPT.
59
+ *
60
+ * @see PRJ-301
61
+ */
62
+ export const PROMPT_SECTION_ORDER = [
63
+ 'identity', // Who the model is (agent + role)
64
+ 'environment', // Where: project, git, platform, model
65
+ 'ground_truth', // Sealed analysis: ecosystem, stack, patterns
66
+ 'capabilities', // Tools, agents, skills, plan mode
67
+ 'constraints', // Anti-hallucination rules (BEFORE task context)
68
+ 'task_context', // Files, state, memories, learned patterns
69
+ 'task', // Template content + subtasks (the actual instructions)
70
+ 'output_schema', // Structured response format
71
+ 'efficiency', // Token efficiency directive
72
+ ] as const
39
73
 
40
74
  // Re-export types for convenience
41
75
  export type {
@@ -81,6 +115,9 @@ class PromptBuilder {
81
115
  private _templateCache: Map<string, CachedTemplate> = new Map()
82
116
  private readonly TEMPLATE_CACHE_TTL_MS = 60_000 // 60 seconds
83
117
 
118
+ /** Active token budget coordinator (PRJ-266) */
119
+ private _coordinator: TokenBudgetCoordinator | null = null
120
+
84
121
  /**
85
122
  * Get a template with TTL caching.
86
123
  * Returns cached content if within TTL, otherwise loads from disk.
@@ -121,6 +158,34 @@ class PromptBuilder {
121
158
  this._checklistRoutingCacheTime = 0
122
159
  }
123
160
 
161
+ /**
162
+ * Set the token budget coordinator for model-aware budget management.
163
+ * When set, budget allocations flow from the coordinator instead of defaults.
164
+ *
165
+ * @see PRJ-266
166
+ */
167
+ setCoordinator(coordinator: TokenBudgetCoordinator | null): void {
168
+ this._coordinator = coordinator
169
+ }
170
+
171
+ /** Get the active coordinator (may be null) */
172
+ getCoordinator(): TokenBudgetCoordinator | null {
173
+ return this._coordinator
174
+ }
175
+
176
+ /**
177
+ * Get effective injection budgets.
178
+ * Uses coordinator allocation when available, falls back to DEFAULT_BUDGETS.
179
+ *
180
+ * @see PRJ-266
181
+ */
182
+ private getEffectiveBudgets() {
183
+ if (this._coordinator) {
184
+ return budgetsFromCoordinator(this._coordinator)
185
+ }
186
+ return DEFAULT_BUDGETS
187
+ }
188
+
124
189
  /**
125
190
  * Reset context (for testing)
126
191
  */
@@ -293,7 +358,7 @@ class PromptBuilder {
293
358
  parts.push('')
294
359
 
295
360
  const result = parts.join('\n')
296
- return truncateToTokenBudget(result, DEFAULT_BUDGETS.autoContext)
361
+ return truncateToTokenBudget(result, this.getEffectiveBudgets().autoContext)
297
362
  }
298
363
 
299
364
  /**
@@ -390,7 +455,12 @@ class PromptBuilder {
390
455
  }
391
456
 
392
457
  /**
393
- * Build a complete prompt for Claude from template, context, and enhancements
458
+ * Build a complete prompt for Claude from template, context, and enhancements.
459
+ *
460
+ * Section ordering follows research-backed pattern (PRJ-301):
461
+ * Identity → Environment → Ground Truth → Capabilities → Constraints →
462
+ * Task Context → Task → Output Schema → Efficiency
463
+ *
394
464
  * @deprecated Use buildWithInjection for auto-injected context
395
465
  */
396
466
  async build(
@@ -421,7 +491,11 @@ class PromptBuilder {
421
491
  commandContext = { agents: true, patterns: true, checklist: false, modules: [] }
422
492
  }
423
493
 
424
- // Agent assignment (config-driven)
494
+ // =========================================================================
495
+ // SECTION 1: IDENTITY (critical)
496
+ // Tell the LLM what it is before anything else.
497
+ // =========================================================================
498
+
425
499
  const needsAgent = commandContext.agents
426
500
 
427
501
  if (agent && needsAgent) {
@@ -431,53 +505,100 @@ class PromptBuilder {
431
505
  parts.push(`\nApply specialized expertise. Read agent file for details if needed.\n\n`)
432
506
  }
433
507
 
434
- // Core instruction (concise)
435
508
  parts.push(`TASK: ${template.frontmatter.description}\n`)
436
509
 
437
- // Tools (inline)
438
510
  if (template.frontmatter['allowed-tools']) {
439
511
  parts.push(`TOOLS: ${template.frontmatter['allowed-tools'].join(', ')}\n`)
440
512
  }
441
513
 
442
- // Critical parameters only
443
514
  const params = context as { params?: { task?: string; description?: string } }
444
515
  if (params.params?.task || params.params?.description) {
445
516
  parts.push(`INPUT: ${params.params.task || params.params.description}\n`)
446
517
  }
447
518
 
448
- parts.push('\n---\n')
519
+ // =========================================================================
520
+ // SECTION 2: ENVIRONMENT (important)
521
+ // Structured env block: project, git, platform, model.
522
+ // =========================================================================
449
523
 
450
- // Template content (include full template, frontmatter already stripped by loader)
451
- // This ensures Claude sees ALL instructions including critical rules at the top
452
- parts.push(template.content)
524
+ const projectPath = (context as { projectPath?: string }).projectPath
525
+ if (projectPath) {
526
+ const projectName = orchestratorContext?.project?.id
527
+ ? path.basename(projectPath)
528
+ : path.basename(projectPath)
529
+ const envBlock = buildEnvironmentBlock({
530
+ projectName,
531
+ projectPath,
532
+ isGitRepo: true,
533
+ gitBranch: orchestratorContext?.realContext?.gitBranch,
534
+ })
535
+ parts.push(`\n${envBlock}\n`)
536
+ }
537
+
538
+ // =========================================================================
539
+ // SECTION 3: GROUND TRUTH (important)
540
+ // Sealed analysis: ecosystem, domains, stack, code patterns.
541
+ // LLM knows the project reality before seeing task context.
542
+ // =========================================================================
453
543
 
454
- // ORCHESTRATOR CONTEXT: Inject loaded agents, skills, and subtasks
455
544
  if (orchestratorContext) {
456
- parts.push('\n## ORCHESTRATOR CONTEXT\n')
545
+ parts.push('\n## PROJECT ANALYSIS (Sealed)\n')
546
+ parts.push(`**Ecosystem**: ${orchestratorContext.project.ecosystem}\n`)
457
547
  parts.push(`**Primary Domain**: ${orchestratorContext.primaryDomain}\n`)
458
- parts.push(`**Domains**: ${orchestratorContext.detectedDomains.join(', ')}\n`)
459
- parts.push(`**Ecosystem**: ${orchestratorContext.project.ecosystem}\n\n`)
548
+ parts.push(`**Domains**: ${orchestratorContext.detectedDomains.join(', ')}\n\n`)
549
+ }
550
+
551
+ const needsPatterns = commandContext.patterns
552
+ const codePatternsContent = state?.codePatterns || ''
553
+ if (needsPatterns && codePatternsContent && codePatternsContent.trim()) {
554
+ const patternSummary = this.extractPatternSummary(codePatternsContent)
555
+ if (patternSummary) {
556
+ parts.push('## CODE PATTERNS\n')
557
+ parts.push(patternSummary)
558
+ parts.push('\nFull patterns: Read analysis/patterns.md\n')
559
+ }
560
+ }
460
561
 
461
- // Inject loaded agent content (truncated for context efficiency)
562
+ const analysisContent = state?.analysis || ''
563
+ if (needsPatterns && analysisContent && analysisContent.trim()) {
564
+ const stackMatch =
565
+ analysisContent.match(/Stack[:\s]+([^\n]+)/i) ||
566
+ analysisContent.match(/Technology[:\s]+([^\n]+)/i)
567
+ const stack = stackMatch ? stackMatch[1].trim() : 'detected'
568
+
569
+ parts.push(`\n## STACK\nStack: ${stack}\n`)
570
+ if (!codePatternsContent) {
571
+ parts.push(
572
+ 'Read analysis/repo-summary.md + similar files before coding. Match patterns exactly.\n'
573
+ )
574
+ }
575
+ }
576
+
577
+ // =========================================================================
578
+ // SECTION 4: CAPABILITIES (important)
579
+ // Available agents, skills, modules, plan mode.
580
+ // =========================================================================
581
+
582
+ if (orchestratorContext) {
583
+ // Loaded agents
462
584
  if (orchestratorContext.agents.length > 0) {
463
- parts.push('### LOADED AGENTS (Project-Specific Specialists)\n\n')
464
- for (const agent of orchestratorContext.agents) {
465
- parts.push(`#### Agent: ${agent.name} (${agent.domain})\n`)
466
- if (agent.effort) parts.push(`Effort: ${agent.effort}\n`)
467
- if (agent.model) parts.push(`Model: ${agent.model}\n`)
468
- if (agent.skills.length > 0) {
469
- parts.push(`Skills: ${agent.skills.join(', ')}\n`)
585
+ parts.push('\n### LOADED AGENTS (Project-Specific Specialists)\n\n')
586
+ for (const orcAgent of orchestratorContext.agents) {
587
+ parts.push(`#### Agent: ${orcAgent.name} (${orcAgent.domain})\n`)
588
+ if (orcAgent.effort) parts.push(`Effort: ${orcAgent.effort}\n`)
589
+ if (orcAgent.model) parts.push(`Model: ${orcAgent.model}\n`)
590
+ if (orcAgent.skills.length > 0) {
591
+ parts.push(`Skills: ${orcAgent.skills.join(', ')}\n`)
470
592
  }
471
- // Truncate agent content to token budget
472
593
  const truncatedContent = truncateToTokenBudget(
473
- agent.content,
474
- DEFAULT_BUDGETS.agentContent
594
+ orcAgent.content,
595
+ this.getEffectiveBudgets().agentContent
475
596
  )
476
597
  parts.push(`\`\`\`markdown\n${truncatedContent}\n\`\`\`\n\n`)
477
598
  }
478
599
  }
479
600
 
480
- // Filter skills by detected domains, then inject (truncated)
601
+ // Loaded skills (filtered by domain)
481
602
  const relevantSkills = filterSkillsByDomains(
482
603
  orchestratorContext.skills,
483
604
  orchestratorContext.detectedDomains
@@ -486,153 +607,121 @@ class PromptBuilder {
486
607
  parts.push('### LOADED SKILLS (From Agent Frontmatter)\n\n')
487
608
  for (const skill of relevantSkills) {
488
609
  parts.push(`#### Skill: ${skill.name}\n`)
489
- // Truncate skill content to token budget
490
610
  const truncatedContent = truncateToTokenBudget(
491
611
  skill.content,
492
- DEFAULT_BUDGETS.skillContent
612
+ this.getEffectiveBudgets().skillContent
493
613
  )
494
614
  parts.push(`\`\`\`markdown\n${truncatedContent}\n\`\`\`\n\n`)
495
615
  }
496
616
  }
617
+ }
497
618
 
498
- // Inject real codebase context (proactively gathered)
499
- if (orchestratorContext.realContext) {
500
- const rc = orchestratorContext.realContext
501
- parts.push('### CODEBASE CONTEXT (Real gathered proactively)\n\n')
502
-
503
- parts.push(`**Git State**: Branch \`${rc.gitBranch}\` | ${rc.gitStatus}\n\n`)
504
-
505
- if (rc.relevantFiles.length > 0) {
506
- parts.push('**Relevant Files** (scored by task relevance):\n')
507
- parts.push('| Score | File | Why |\n')
508
- parts.push('|-------|------|-----|\n')
509
- for (const f of rc.relevantFiles.slice(0, 8)) {
510
- parts.push(`| ${f.score} | ${f.path} | ${f.reason} |\n`)
511
- }
619
+ // Additional modules for SMART commands (PRJ-94/PRJ-298)
620
+ const additionalModules = this.getModulesForCommand(commandName, commandContext)
621
+ if (additionalModules.length > 0) {
622
+ for (const moduleName of additionalModules) {
623
+ const moduleContent = await this.loadModule(moduleName)
624
+ if (moduleContent) {
512
625
  parts.push('\n')
626
+ parts.push(moduleContent)
513
627
  }
628
+ }
629
+ }
514
630
 
515
- if (rc.signatures.length > 0) {
516
- parts.push('**Code Signatures** (top files):\n')
517
- for (const sig of rc.signatures) {
518
- parts.push(`\`\`\`typescript\n// ${sig.path}\n${sig.content}\n\`\`\`\n`)
519
- }
520
- parts.push('\n')
521
- }
631
+ // Plan mode / approval
632
+ if (planInfo?.isPlanning) {
633
+ parts.push(
634
+ `\n## PLAN MODE\nRead-only. Gather info → Analyze → Propose plan → Wait for approval.\n`
635
+ )
636
+ if (planInfo.allowedTools) parts.push(`Tools: ${planInfo.allowedTools.join(', ')}\n`)
637
+ }
638
+ if (planInfo?.requiresApproval) {
639
+ parts.push(
640
+ `\n## APPROVAL REQUIRED\nShow changes, list affected files, ask for confirmation.\n`
641
+ )
642
+ }
522
643
 
523
- if (rc.recentFiles.length > 0) {
524
- parts.push('**Recently Changed**: ')
525
- const recentSummary = rc.recentFiles
526
- .slice(0, 5)
527
- .map((f) => `${f.path} (${f.lastChanged})`)
528
- .join(', ')
529
- parts.push(`${recentSummary}\n\n`)
530
- }
644
+ // =========================================================================
645
+ // SECTION 5: CONSTRAINTS (critical)
646
+ // Anti-hallucination rules BEFORE task context.
647
+ // LLM has constraints loaded before processing code/files.
648
+ // =========================================================================
649
+
650
+ if (projectPath) {
651
+ const groundTruth: ProjectGroundTruth = {
652
+ projectPath,
653
+ language: orchestratorContext?.project?.ecosystem,
654
+ techStack: orchestratorContext?.project?.conventions || [],
655
+ domains: this.extractDomains(state),
656
+ fileCount: context.files?.length || context.filteredSize || 0,
657
+ availableAgents: orchestratorContext?.agents?.map((a) => a.name) || [],
531
658
  }
659
+ parts.push(`\n${buildAntiHallucinationBlock(groundTruth)}\n`)
660
+ } else {
661
+ // Fallback: compressed rules when no project context available
662
+ parts.push(this.buildCriticalRules())
663
+ }
532
664
 
533
- // Inject subtasks if fragmented
534
- if (orchestratorContext.requiresFragmentation && orchestratorContext.subtasks) {
535
- parts.push('### SUBTASKS (Execute in Order)\n\n')
536
- parts.push(
537
- '**IMPORTANT**: Focus on the CURRENT subtask. Use `p. done` when complete to advance.\n\n'
538
- )
539
- parts.push('| # | Domain | Description | Status |\n')
540
- parts.push('|---|--------|-------------|--------|\n')
541
-
542
- for (const subtask of orchestratorContext.subtasks) {
543
- const statusIcon =
544
- subtask.status === 'in_progress'
545
- ? '▶️ **CURRENT**'
546
- : subtask.status === 'completed'
547
- ? ' Done'
548
- : subtask.status === 'failed'
549
- ? '❌ Failed'
550
- : '⏳ Pending'
551
- parts.push(
552
- `| ${subtask.order} | ${subtask.domain} | ${subtask.description} | ${statusIcon} |\n`
553
- )
665
+ // =========================================================================
666
+ // SECTION 6: TASK CONTEXT (important)
667
+ // Files, codebase context, state, memories, patterns — all the data
668
+ // the LLM needs to work with, presented after it knows the rules.
669
+ // =========================================================================
670
+
671
+ // Codebase context (proactively gathered)
672
+ if (orchestratorContext?.realContext) {
673
+ const rc = orchestratorContext.realContext
674
+ parts.push('\n### CODEBASE CONTEXT\n\n')
675
+
676
+ parts.push(`**Git State**: Branch \`${rc.gitBranch}\` | ${rc.gitStatus}\n\n`)
677
+
678
+ if (rc.relevantFiles.length > 0) {
679
+ parts.push('**Relevant Files** (scored by task relevance):\n')
680
+ parts.push('| Score | File | Why |\n')
681
+ parts.push('|-------|------|-----|\n')
682
+ for (const f of rc.relevantFiles.slice(0, 8)) {
683
+ parts.push(`| ${f.score} | ${f.path} | ${f.reason} |\n`)
554
684
  }
685
+ parts.push('\n')
686
+ }
555
687
 
556
- // Find and highlight current subtask
557
- const currentSubtask = orchestratorContext.subtasks.find((s) => s.status === 'in_progress')
558
- if (currentSubtask) {
559
- parts.push(
560
- `\n**FOCUS ON SUBTASK #${currentSubtask.order}**: ${currentSubtask.description}\n`
561
- )
562
- parts.push(`Agent: ${currentSubtask.agent} | Domain: ${currentSubtask.domain}\n`)
563
- if (currentSubtask.dependsOn.length > 0) {
564
- parts.push(`Dependencies: ${currentSubtask.dependsOn.join(', ')}\n`)
565
- }
688
+ if (rc.signatures.length > 0) {
689
+ parts.push('**Code Signatures** (top files):\n')
690
+ for (const sig of rc.signatures) {
691
+ parts.push(`\`\`\`typescript\n// ${sig.path}\n${sig.content}\n\`\`\`\n`)
566
692
  }
567
693
  parts.push('\n')
568
694
  }
569
- }
570
695
 
571
- // Current state (only if exists and relevant)
572
- const relevantState = this.filterRelevantState(state)
573
- if (relevantState) {
574
- parts.push('\n## PRJCT STATE (Project Management Data)\n')
575
- parts.push(relevantState)
576
- parts.push('\n')
696
+ if (rc.recentFiles.length > 0) {
697
+ parts.push('**Recently Changed**: ')
698
+ const recentSummary = rc.recentFiles
699
+ .slice(0, 5)
700
+ .map((f) => `${f.path} (${f.lastChanged})`)
701
+ .join(', ')
702
+ parts.push(`${recentSummary}\n\n`)
703
+ }
577
704
  }
578
705
 
579
- // COMPRESSED: File list
706
+ // File list
580
707
  const files = context.files || []
581
708
  if (files.length > 0) {
582
709
  const top5 = files.slice(0, 5).join(', ')
583
710
  parts.push(`\n## FILES: ${files.length} available. Top: ${top5}\n`)
584
711
  parts.push('Read BEFORE modifying. Use Glob/Grep to find more.\n\n')
585
- } else if ((context as { projectPath?: string }).projectPath) {
586
- parts.push(
587
- `\n## PROJECT: ${(context as { projectPath: string }).projectPath}\nRead files before modifying.\n\n`
588
- )
589
- }
590
-
591
- // OPTIMIZED: Only include patterns for code-modifying commands (config-driven, PRJ-298)
592
- const needsPatterns = commandContext.patterns
593
-
594
- // Include code patterns analysis for code-modifying commands
595
- const codePatternsContent = state?.codePatterns || ''
596
- if (needsPatterns && codePatternsContent && codePatternsContent.trim()) {
597
- const patternSummary = this.extractPatternSummary(codePatternsContent)
598
- if (patternSummary) {
599
- parts.push('\n## CODE PATTERNS\n')
600
- parts.push(patternSummary)
601
- parts.push('\nFull patterns: Read analysis/patterns.md\n')
602
- }
603
- }
604
-
605
- const analysisContent = state?.analysis || ''
606
- if (needsPatterns && analysisContent && analysisContent.trim()) {
607
- const stackMatch =
608
- analysisContent.match(/Stack[:\s]+([^\n]+)/i) ||
609
- analysisContent.match(/Technology[:\s]+([^\n]+)/i)
610
- const stack = stackMatch ? stackMatch[1].trim() : 'detected'
611
-
612
- parts.push(`\n## STACK\nStack: ${stack}\n`)
613
- if (!codePatternsContent) {
614
- parts.push(
615
- 'Read analysis/repo-summary.md + similar files before coding. Match patterns exactly.\n'
616
- )
617
- }
712
+ } else if (projectPath) {
713
+ parts.push(`\n## PROJECT: ${projectPath}\nRead files before modifying.\n\n`)
618
714
  }
619
715
 
620
- // CRITICAL: Compressed rules
621
- parts.push(this.buildCriticalRules())
622
-
623
- // PRJ-94/PRJ-298: Inject additional modules for SMART commands (config-driven)
624
- const additionalModules = this.getModulesForCommand(commandName, commandContext)
625
- if (additionalModules.length > 0) {
626
- for (const moduleName of additionalModules) {
627
- const moduleContent = await this.loadModule(moduleName)
628
- if (moduleContent) {
629
- parts.push('\n')
630
- parts.push(moduleContent)
631
- }
632
- }
716
+ // Project state
717
+ const relevantState = this.filterRelevantState(state)
718
+ if (relevantState) {
719
+ parts.push('\n## PRJCT STATE (Project Management Data)\n')
720
+ parts.push(relevantState)
721
+ parts.push('\n')
633
722
  }
634
723
 
635
- // P1.1: Learned Patterns
724
+ // Learned patterns
636
725
  if (learnedPatterns && Object.keys(learnedPatterns).some((k) => learnedPatterns[k])) {
637
726
  parts.push('\n## PROJECT DEFAULTS (apply automatically)\n')
638
727
  for (const [key, value] of Object.entries(learnedPatterns)) {
@@ -642,7 +731,7 @@ class PromptBuilder {
642
731
  }
643
732
  }
644
733
 
645
- // P3.1: Think Block
734
+ // Think block
646
735
  if (thinkBlock?.plan && thinkBlock.plan.length > 0) {
647
736
  parts.push('\n## THINK FIRST (reasoning from analysis)\n')
648
737
  if (thinkBlock.conclusions && thinkBlock.conclusions.length > 0) {
@@ -658,7 +747,7 @@ class PromptBuilder {
658
747
  parts.push(`Confidence: ${Math.round((thinkBlock.confidence || 0.5) * 100)}%\n`)
659
748
  }
660
749
 
661
- // P3.3: Relevant Memories
750
+ // Relevant memories
662
751
  if (relevantMemories && relevantMemories.length > 0) {
663
752
  parts.push('\n## CONTEXT (apply these)\n')
664
753
  for (const memory of relevantMemories) {
@@ -669,20 +758,68 @@ class PromptBuilder {
669
758
  }
670
759
  }
671
760
 
672
- // P3.4: Plan Mode
673
- if (planInfo?.isPlanning) {
761
+ parts.push('\n---\n')
762
+
763
+ // =========================================================================
764
+ // SECTION 7: TASK (critical)
765
+ // Template content (actual instructions) + subtasks.
766
+ // LLM reads this AFTER knowing identity, env, rules, and context.
767
+ // =========================================================================
768
+
769
+ parts.push(template.content)
770
+
771
+ // Subtasks (if fragmented)
772
+ if (orchestratorContext?.requiresFragmentation && orchestratorContext.subtasks) {
773
+ parts.push('\n### SUBTASKS (Execute in Order)\n\n')
674
774
  parts.push(
675
- `\n## PLAN MODE\nRead-only. Gather info Analyze Propose plan Wait for approval.\n`
775
+ '**IMPORTANT**: Focus on the CURRENT subtask. Use `p. done` when complete to advance.\n\n'
676
776
  )
677
- if (planInfo.allowedTools) parts.push(`Tools: ${planInfo.allowedTools.join(', ')}\n`)
777
+ parts.push('| # | Domain | Description | Status |\n')
778
+ parts.push('|---|--------|-------------|--------|\n')
779
+
780
+ for (const subtask of orchestratorContext.subtasks) {
781
+ const statusIcon =
782
+ subtask.status === 'in_progress'
783
+ ? '▶️ **CURRENT**'
784
+ : subtask.status === 'completed'
785
+ ? '✅ Done'
786
+ : subtask.status === 'failed'
787
+ ? '❌ Failed'
788
+ : '⏳ Pending'
789
+ parts.push(
790
+ `| ${subtask.order} | ${subtask.domain} | ${subtask.description} | ${statusIcon} |\n`
791
+ )
792
+ }
793
+
794
+ const currentSubtask = orchestratorContext.subtasks.find((s) => s.status === 'in_progress')
795
+ if (currentSubtask) {
796
+ parts.push(
797
+ `\n**FOCUS ON SUBTASK #${currentSubtask.order}**: ${currentSubtask.description}\n`
798
+ )
799
+ parts.push(`Agent: ${currentSubtask.agent} | Domain: ${currentSubtask.domain}\n`)
800
+ if (currentSubtask.dependsOn.length > 0) {
801
+ parts.push(`Dependencies: ${currentSubtask.dependsOn.join(', ')}\n`)
802
+ }
803
+ }
804
+ parts.push('\n')
678
805
  }
679
- if (planInfo?.requiresApproval) {
680
- parts.push(
681
- `\n## APPROVAL REQUIRED\nShow changes, list affected files, ask for confirmation.\n`
682
- )
806
+
807
+ // =========================================================================
808
+ // SECTION 8: OUTPUT (important)
809
+ // Output schema and quality checklists.
810
+ // =========================================================================
811
+
812
+ // Output schema (PRJ-264)
813
+ const schemaType = this.getSchemaTypeForCommand(commandName)
814
+ if (schemaType) {
815
+ const { renderSchemaForPrompt } = await import('../schemas/llm-output')
816
+ const schemaBlock = renderSchemaForPrompt(schemaType)
817
+ if (schemaBlock) {
818
+ parts.push(`\n${schemaBlock}\n`)
819
+ }
683
820
  }
684
821
 
685
- // P4.1: Quality Checklists (config-driven, PRJ-298)
822
+ // Quality checklists (PRJ-298)
686
823
  if (commandContext.checklist) {
687
824
  const routing = await this.loadChecklistRouting()
688
825
  const checklists = await this.loadChecklists()
@@ -698,18 +835,12 @@ class PromptBuilder {
698
835
  }
699
836
  }
700
837
 
701
- // PRJ-264: Output schema injection for structured responses
702
- const schemaType = this.getSchemaTypeForCommand(commandName)
703
- if (schemaType) {
704
- const { renderSchemaForPrompt } = await import('../schemas/llm-output')
705
- const schemaBlock = renderSchemaForPrompt(schemaType)
706
- if (schemaBlock) {
707
- parts.push(`\n${schemaBlock}\n`)
708
- }
709
- }
838
+ // =========================================================================
839
+ // SECTION 9: EFFICIENCY (critical)
840
+ // Token efficiency directive — be concise, no preamble.
841
+ // =========================================================================
710
842
 
711
- // Simple execution directive
712
- parts.push('\nEXECUTE: Follow flow. Use tools. Decide.\n')
843
+ parts.push(this.buildEfficiencyDirective())
713
844
 
714
845
  return parts.join('')
715
846
  }
@@ -721,7 +852,8 @@ class PromptBuilder {
721
852
  filterRelevantState(state: State): string | null {
722
853
  if (!state || Object.keys(state).length === 0) return null
723
854
 
724
- const tracker = new InjectionBudgetTracker({ totalPrompt: DEFAULT_BUDGETS.stateData })
855
+ const budgets = this.getEffectiveBudgets()
856
+ const tracker = new InjectionBudgetTracker({ totalPrompt: budgets.stateData })
725
857
  const criticalFiles = ['now', 'next', 'context', 'analysis', 'codePatterns']
726
858
  const relevant: string[] = []
727
859
 
@@ -798,7 +930,9 @@ class PromptBuilder {
798
930
  }
799
931
 
800
932
  /**
801
- * Build critical anti-hallucination rules section
933
+ * Build critical anti-hallucination rules section.
934
+ * Used as fallback when full anti-hallucination block can't be built
935
+ * (e.g., no project path available).
802
936
  */
803
937
  buildCriticalRules(): string {
804
938
  const fileCount = this._currentContext?.files?.length || this._currentContext?.filteredSize || 0
@@ -812,6 +946,44 @@ class PromptBuilder {
812
946
  Context: ${fileCount} files available. Read what you need.
813
947
  `
814
948
  }
949
+
950
+ /**
951
+ * Build token efficiency directive (PRJ-301).
952
+ * Instructs the LLM to be concise and avoid wasting tokens on preamble.
953
+ */
954
+ buildEfficiencyDirective(): string {
955
+ return `
956
+ ## OUTPUT RULES
957
+ - Be concise. Maximum 4 lines of explanation unless asked for detail.
958
+ - No preamble ("Here is...", "I'll help you...", "Based on...").
959
+ - No postamble (summaries, next steps suggestions unless asked).
960
+ - When executing code: show the code, not the explanation.
961
+ - Prefer structured output (JSON) over free text when applicable.
962
+
963
+ EXECUTE: Follow flow. Use tools. Decide.
964
+ `
965
+ }
966
+
967
+ /**
968
+ * Extract domain flags from state data.
969
+ * Returns the domains object if available in the raw state.
970
+ */
971
+ private extractDomains(state: State): ProjectGroundTruth['domains'] | undefined {
972
+ if (!state) return undefined
973
+ // State may contain raw domains from state.json (loaded by context-builder)
974
+ const raw = state as Record<string, unknown>
975
+ if (raw.domains && typeof raw.domains === 'object') {
976
+ const d = raw.domains as Record<string, boolean>
977
+ return {
978
+ hasFrontend: d.hasFrontend ?? false,
979
+ hasBackend: d.hasBackend ?? false,
980
+ hasDatabase: d.hasDatabase ?? false,
981
+ hasTesting: d.hasTesting ?? false,
982
+ hasDocker: d.hasDocker ?? false,
983
+ }
984
+ }
985
+ return undefined
986
+ }
815
987
  }
816
988
 
817
989
  const promptBuilder = new PromptBuilder()