chati-dev 4.0.11 → 4.1.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.
Files changed (107) hide show
  1. package/README.md +5 -0
  2. package/bin/chati.js +16 -0
  3. package/framework/agents/build/dev.md +43 -10
  4. package/framework/agents/discover/brief.md +38 -8
  5. package/framework/agents/discover/brownfield-wu.md +44 -3
  6. package/framework/agents/discover/greenfield-wu.md +14 -0
  7. package/framework/agents/plan/architect.md +6 -0
  8. package/framework/agents/plan/detail.md +25 -0
  9. package/framework/agents/plan/tasks.md +29 -1
  10. package/framework/agents/plan/ux-brand-architect.md +215 -0
  11. package/framework/agents/plan/ux-component-engineer.md +289 -0
  12. package/framework/agents/plan/ux-researcher.md +166 -0
  13. package/framework/agents/plan/ux.md +126 -1008
  14. package/framework/agents/quality/qa-implementation.md +121 -22
  15. package/framework/agents/quality/qa-planning.md +18 -0
  16. package/framework/config.yaml +15 -4
  17. package/framework/constitution.md +8 -2
  18. package/framework/context/root.md +1 -1
  19. package/framework/data/entity-registry.yaml +59 -3
  20. package/framework/hooks/constitution-guard.js +67 -1
  21. package/framework/hooks/license-guard.js +4 -4
  22. package/framework/hooks/model-governance.js +2 -1
  23. package/framework/hooks/prism-engine.js +74 -6
  24. package/framework/hooks/read-protection.js +1 -1
  25. package/framework/hooks/session-digest.js +159 -7
  26. package/framework/hooks/settings.json +16 -4
  27. package/framework/hooks/style-guard.js +134 -0
  28. package/framework/hooks/undercover-guard.js +220 -0
  29. package/framework/intelligence/context-engine.md +21 -0
  30. package/framework/intelligence/hooks-performance.md +54 -0
  31. package/framework/intelligence/memory-layer.md +47 -0
  32. package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
  33. package/framework/orchestrator/chati.md +327 -1067
  34. package/framework/templates/brandbook-html-tmpl.md +107 -0
  35. package/framework/templates/session-memory-tmpl.yaml +68 -0
  36. package/package.json +1 -1
  37. package/src/autonomy/build-state.js +1 -1
  38. package/src/autonomy/worktree-manager.js +13 -13
  39. package/src/config/agent-customizer.js +1 -1
  40. package/src/config/gemini-hooks-generator.js +6 -6
  41. package/src/config/ide-configs.js +1 -1
  42. package/src/context/bracket-tracker.js +25 -4
  43. package/src/context/engine.js +37 -7
  44. package/src/context/formatter.js +45 -1
  45. package/src/context/index.js +3 -3
  46. package/src/dashboard/data-reader.js +7 -7
  47. package/src/decision/engine.js +2 -2
  48. package/src/decision/registry-healer.js +1 -1
  49. package/src/decision/registry-updater.js +2 -2
  50. package/src/extensions/loader.js +1 -1
  51. package/src/gates/g1-planning-complete.js +1 -1
  52. package/src/gates/g2-qa-planning.js +1 -1
  53. package/src/gates/g3-implementation.js +4 -4
  54. package/src/gates/g4-qa-implementation.js +2 -2
  55. package/src/health/auto-fix.js +3 -3
  56. package/src/health/engine.js +2 -2
  57. package/src/installer/brownfield-upgrader.js +3 -3
  58. package/src/installer/manifest.js +1 -1
  59. package/src/installer/transaction.js +1 -1
  60. package/src/installer/validator.js +2 -2
  61. package/src/intelligence/decision-engine.js +1 -1
  62. package/src/intelligence/memory-manager.js +1 -1
  63. package/src/intelligence/registry-manager.js +2 -2
  64. package/src/intelligence/timeline.js +1 -1
  65. package/src/license/client.js +1 -1
  66. package/src/license/commands.js +2 -2
  67. package/src/memory/agent-memory.js +3 -3
  68. package/src/memory/daily-digest.js +170 -0
  69. package/src/memory/dream.js +254 -0
  70. package/src/memory/gotchas.js +2 -2
  71. package/src/memory/index.js +18 -0
  72. package/src/memory/magic-docs.js +98 -0
  73. package/src/memory/memory-extractor.js +163 -0
  74. package/src/memory/session-digest.js +144 -6
  75. package/src/merger/yaml-merger.js +1 -1
  76. package/src/orchestrator/cli.js +980 -0
  77. package/src/orchestrator/handoff-engine.js +25 -4
  78. package/src/orchestrator/index.js +9 -0
  79. package/src/orchestrator/session-manager.js +1 -1
  80. package/src/preview/detector.js +3 -3
  81. package/src/preview/launcher.js +2 -2
  82. package/src/quality/metrics-collector.js +1 -1
  83. package/src/quality/test-runner.js +2 -4
  84. package/src/scanning/density-scanner.js +51 -0
  85. package/src/scanning/env-scanner.js +97 -0
  86. package/src/scanning/index.js +7 -0
  87. package/src/scanning/leakage-scanner.js +54 -0
  88. package/src/scanning/placeholder-scanner.js +40 -0
  89. package/src/scanning/security-scanner.js +94 -0
  90. package/src/security/bash-security.js +335 -0
  91. package/src/security/index.js +9 -0
  92. package/src/telemetry/config.js +3 -3
  93. package/src/telemetry/sender.js +1 -1
  94. package/src/terminal/cli-registry.js +1 -1
  95. package/src/terminal/cost-tracker.js +1 -2
  96. package/src/terminal/prompt-builder.js +11 -27
  97. package/src/terminal/rate-limiter.js +1 -2
  98. package/src/terminal/run-agent.js +3 -3
  99. package/src/terminal/spawner.js +41 -4
  100. package/src/upgrade/checker.js +2 -2
  101. package/src/upgrade/migrator.js +1 -1
  102. package/src/utils/config-parser.js +1 -1
  103. package/src/utils/file-lock.js +3 -3
  104. package/src/utils/provider-limits.js +35 -4
  105. package/src/wizard/i18n.js +2 -2
  106. package/src/wizard/index.js +2 -1
  107. package/src/wizard/questions.js +4 -2
@@ -0,0 +1,107 @@
1
+ # Brandbook HTML Template Reference
2
+
3
+ This document describes the mandatory structure for the `brandbook.html` visual style guide
4
+ produced by the UX agent in Phase 0. Each project gets a unique brandbook.html based on its
5
+ brand identity. This is NOT a literal HTML template to copy — it's a specification of what
6
+ sections must exist and what each must contain.
7
+
8
+ ## File Requirements
9
+
10
+ - **Format**: Standalone HTML5 document
11
+ - **CSS**: All styles in a single `<style>` tag (no external stylesheets)
12
+ - **Fonts**: Google Fonts via `<link>` tags (only external dependency allowed)
13
+ - **JavaScript**: Minimal (only for theme toggle and smooth scroll)
14
+ - **Output path**: `chati.dev/artifacts/4-UX/brandbook.html`
15
+
16
+ ## Mandatory Sections (14)
17
+
18
+ ### 1. Hero
19
+ - Project name as wordmark (using the primary display font)
20
+ - Brand tagline or description
21
+ - Metadata: version, date, author/team
22
+
23
+ ### 2. Navigation
24
+ - Sticky top bar with links to all sections
25
+ - Backdrop blur effect
26
+ - Active section highlighting on scroll
27
+
28
+ ### 3. Identity
29
+ - Brand principles (3-5 core values)
30
+ - Personality traits
31
+ - Design philosophy statement
32
+
33
+ ### 4. Colors
34
+ - Primary palette: visual swatches with hex, RGB, and CSS custom property name
35
+ - Semantic colors: success, warning, error, info with swatches
36
+ - Dark mode palette: show both themes side by side
37
+ - Contrast ratio badges (WCAG AA pass/fail)
38
+
39
+ ### 5. Typography
40
+ - Primary font: rendered specimen at all weights (300-900)
41
+ - Secondary font: same treatment
42
+ - Monospace font: same treatment
43
+ - Heading scale: H1 through H6 rendered at actual sizes
44
+ - Body text specimen
45
+ - Line height and letter spacing values
46
+
47
+ ### 6. Spacing
48
+ - Base unit and scale (4px, 8px, 12px, 16px, 24px, 32px, 48px, 64px, 96px)
49
+ - Visual blocks showing each spacing value
50
+ - Named tokens (--space-xs through --space-3xl)
51
+
52
+ ### 7. Border Radius
53
+ - Radius scale with visual examples on sample boxes
54
+ - Named tokens (--radius-sm, --radius-md, --radius-lg, --radius-full)
55
+
56
+ ### 8. Shadows
57
+ - Elevation levels with visual examples (shadow-sm, shadow-md, shadow-lg, shadow-xl)
58
+ - Each shadow shown on a card element
59
+
60
+ ### 9. Icons
61
+ - Selected icon library name and rationale
62
+ - Sample icons at each size (xs, sm, md, lg, xl)
63
+ - Usage guidelines (with text, standalone, touch targets)
64
+
65
+ ### 10. Components
66
+ - Buttons: primary, secondary, ghost, destructive in all states (default, hover, active, focus, disabled)
67
+ - Inputs: text, select, checkbox, radio in all states
68
+ - Cards: basic, interactive, featured
69
+ - Badges: status variants (success, warning, error, info)
70
+
71
+ ### 11. States
72
+ - Loading: skeleton, spinner, progress bar patterns
73
+ - Error: error message, error boundary, form validation
74
+ - Empty: empty state illustration guidelines
75
+ - Success: confirmation patterns
76
+
77
+ ### 12. Motion
78
+ - Timing scale table (fast, normal, slow durations)
79
+ - Easing curves (ease-in, ease-out, ease-in-out, spring)
80
+ - Micro-interaction notes (hover, focus, click feedback)
81
+ - Scroll animation personality description
82
+
83
+ ### 13. Accessibility
84
+ - Contrast ratio table for all color pairs
85
+ - WCAG AA compliance checklist
86
+ - Focus indicator specifications
87
+ - Reduced motion alternatives
88
+
89
+ ### 14. Brand Voice
90
+ - Tone spectrum (formal to casual, technical to friendly)
91
+ - Writing dos and donts
92
+ - Example phrases in brand voice
93
+ - Forbidden patterns (what the brand never says)
94
+
95
+ ## Visual Quality Standards
96
+
97
+ - Professional design quality (minimum 8.5/10 visual polish)
98
+ - No placeholder content (lorem ipsum, John Doe, example.com)
99
+ - No emojis (use icons from the approved library)
100
+ - No generic SaaS blue unless brand explicitly requires it
101
+ - Responsive: must work on viewport widths from 375px to 1920px
102
+ - Print: page breaks between major sections, simplified colors
103
+
104
+ ## Reference
105
+
106
+ See `/Users/ogabrielalonso/code/focus-ai/ping/docs/brand/brandbook.html` for a production
107
+ example with 14 sections, sticky nav, dark mode toggle, and complete token rendering.
@@ -0,0 +1,68 @@
1
+ # Session Memory Template — 8-Section Structured Digest
2
+ # Inspired by Claude Code's structured session memory (10 sections, 12K tokens).
3
+ # Total budget: ~10K tokens (40,000 chars at 4 chars/token).
4
+ #
5
+ # Used by session-digest hook (PreCompact) and buildStructuredDigest() to
6
+ # capture rich session context that survives context compaction.
7
+
8
+ version: "2.0"
9
+
10
+ sections:
11
+ - id: current_agent
12
+ description: "Currently active agent in the pipeline"
13
+ max_tokens: 100
14
+ priority: 1
15
+ required: true
16
+
17
+ - id: pipeline_position
18
+ description: "Current position in the DISCOVER→PLAN→BUILD→DEPLOY pipeline"
19
+ max_tokens: 100
20
+ priority: 2
21
+ required: true
22
+
23
+ - id: active_task
24
+ description: "Active task ID, title, and acceptance criteria (Given-When-Then)"
25
+ max_tokens: 500
26
+ priority: 3
27
+ required: false
28
+
29
+ - id: files_changed
30
+ description: "Files created, modified, or deleted during this session"
31
+ max_tokens: 1000
32
+ priority: 7
33
+ required: false
34
+
35
+ - id: blockers
36
+ description: "Active blockers preventing progress (C01-C15 code, G01-G08 general)"
37
+ max_tokens: 1000
38
+ priority: 4
39
+ required: false
40
+
41
+ - id: decisions_made
42
+ description: "Key decisions with rationale (what + why) made during session"
43
+ max_tokens: 3000
44
+ priority: 5
45
+ required: false
46
+
47
+ - id: gotchas_found
48
+ description: "Error patterns and gotchas discovered during session"
49
+ max_tokens: 2000
50
+ priority: 6
51
+ required: false
52
+
53
+ - id: next_steps
54
+ description: "Planned next steps and pending actions"
55
+ max_tokens: 2300
56
+ priority: 8
57
+ required: false
58
+
59
+ total_budget_tokens: 10000
60
+
61
+ truncation_order:
62
+ - next_steps
63
+ - gotchas_found
64
+ - files_changed
65
+ - blockers
66
+ - decisions_made
67
+ - active_task
68
+ # current_agent and pipeline_position are never truncated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.0.11",
3
+ "version": "4.1.0",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System — Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -131,7 +131,7 @@ export function loadBuildState(projectDir) {
131
131
  }
132
132
 
133
133
  return state;
134
- } catch {
134
+ } catch { /* expected: operation may fail gracefully */
135
135
  return null;
136
136
  }
137
137
  }
@@ -64,7 +64,7 @@ export function isGitRepo(dir) {
64
64
  try {
65
65
  git('rev-parse --is-inside-work-tree', dir);
66
66
  return true;
67
- } catch {
67
+ } catch { /* expected: content may be malformed */
68
68
  return false;
69
69
  }
70
70
  }
@@ -100,7 +100,7 @@ export function createWorktree(projectDir, taskId, attempt) {
100
100
  if (existsSync(worktreePath)) {
101
101
  try {
102
102
  git(`worktree remove "${worktreePath}" --force`, absProject);
103
- } catch {
103
+ } catch { /* expected: operation may fail gracefully */
104
104
  rmSync(worktreePath, { recursive: true, force: true });
105
105
  }
106
106
  }
@@ -108,7 +108,7 @@ export function createWorktree(projectDir, taskId, attempt) {
108
108
  // Delete branch if it exists from a previous run
109
109
  try {
110
110
  git(`branch -D "${branch}"`, absProject);
111
- } catch {
111
+ } catch { /* expected: operation may fail gracefully */
112
112
  // Branch doesn't exist — fine
113
113
  }
114
114
 
@@ -118,12 +118,12 @@ export function createWorktree(projectDir, taskId, attempt) {
118
118
  const cleanup = () => {
119
119
  try {
120
120
  git(`worktree remove "${worktreePath}" --force`, absProject);
121
- } catch {
121
+ } catch { /* expected: operation may fail gracefully */
122
122
  // Worktree already removed — ignore
123
123
  }
124
124
  try {
125
125
  git(`branch -D "${branch}"`, absProject);
126
- } catch {
126
+ } catch { /* expected: operation may fail gracefully */
127
127
  // Branch already deleted — ignore
128
128
  }
129
129
  };
@@ -146,7 +146,7 @@ export function mergeWorktree(projectDir, worktreeBranch, mainBranch) {
146
146
  if (!mainBranch) {
147
147
  try {
148
148
  mainBranch = git('rev-parse --abbrev-ref HEAD', absProject);
149
- } catch {
149
+ } catch { /* expected: content may be malformed */
150
150
  mainBranch = 'main';
151
151
  }
152
152
  }
@@ -154,7 +154,7 @@ export function mergeWorktree(projectDir, worktreeBranch, mainBranch) {
154
154
  // Ensure we're on the main branch
155
155
  try {
156
156
  git(`checkout "${mainBranch}"`, absProject);
157
- } catch {
157
+ } catch { /* expected: operation may fail gracefully */
158
158
  return { success: false, conflicts: [`Failed to checkout ${mainBranch}`] };
159
159
  }
160
160
 
@@ -162,7 +162,7 @@ export function mergeWorktree(projectDir, worktreeBranch, mainBranch) {
162
162
  try {
163
163
  git(`merge "${worktreeBranch}" --no-edit`, absProject);
164
164
  return { success: true, conflicts: [] };
165
- } catch {
165
+ } catch { /* expected: operation may fail gracefully */
166
166
  // Parse conflicts from error output
167
167
  const conflicts = [];
168
168
  try {
@@ -170,14 +170,14 @@ export function mergeWorktree(projectDir, worktreeBranch, mainBranch) {
170
170
  if (status) {
171
171
  conflicts.push(...status.split('\n').filter(Boolean));
172
172
  }
173
- } catch {
173
+ } catch { /* expected: operation may fail gracefully */
174
174
  conflicts.push('Unknown conflict — check git status');
175
175
  }
176
176
 
177
177
  // Abort the failed merge
178
178
  try {
179
179
  git('merge --abort', absProject);
180
- } catch {
180
+ } catch { /* expected: operation may fail gracefully */
181
181
  // Already aborted or not in merging state
182
182
  }
183
183
 
@@ -207,7 +207,7 @@ export function cleanupWorktrees(projectDir, options = {}) {
207
207
  let entries;
208
208
  try {
209
209
  entries = readdirSync(worktreeBase);
210
- } catch {
210
+ } catch { /* expected: directory may not exist */
211
211
  return { cleaned, errors: ['Failed to read worktree directory'] };
212
212
  }
213
213
 
@@ -222,7 +222,7 @@ export function cleanupWorktrees(projectDir, options = {}) {
222
222
  if (age > maxAge) {
223
223
  try {
224
224
  git(`worktree remove "${entryPath}" --force`, absProject);
225
- } catch {
225
+ } catch { /* expected: operation may fail gracefully */
226
226
  rmSync(entryPath, { recursive: true, force: true });
227
227
  }
228
228
 
@@ -230,7 +230,7 @@ export function cleanupWorktrees(projectDir, options = {}) {
230
230
  const branch = `chati/${entry}`;
231
231
  try {
232
232
  git(`branch -D "${branch}"`, absProject);
233
- } catch {
233
+ } catch { /* expected: operation may fail gracefully */
234
234
  // Branch already deleted
235
235
  }
236
236
 
@@ -80,7 +80,7 @@ export function loadCustomization(projectDir, agentName) {
80
80
  }
81
81
 
82
82
  return { found: true, overrides };
83
- } catch {
83
+ } catch { /* expected: content may be malformed */
84
84
  return { found: false, overrides: {} };
85
85
  }
86
86
  }
@@ -93,7 +93,7 @@ async function main() {
93
93
  }
94
94
 
95
95
  console.log(JSON.stringify({}));
96
- } catch {
96
+ } catch { /* expected: operation may fail gracefully */
97
97
  console.log(JSON.stringify({}));
98
98
  }
99
99
  }
@@ -149,7 +149,7 @@ async function main() {
149
149
  }
150
150
 
151
151
  console.log(JSON.stringify({}));
152
- } catch {
152
+ } catch { /* expected: operation may fail gracefully */
153
153
  console.log(JSON.stringify({}));
154
154
  }
155
155
  }
@@ -236,7 +236,7 @@ async function main() {
236
236
  error: \`[Article XI] Cannot write "\${filePath}" in \${mode} mode. Allowed: \${MODE_SCOPES[mode].allowed.join(', ')}\`
237
237
  }));
238
238
  }
239
- } catch {
239
+ } catch { /* expected: operation may fail gracefully */
240
240
  console.log(JSON.stringify({}));
241
241
  }
242
242
  }
@@ -331,7 +331,7 @@ async function main() {
331
331
  }
332
332
 
333
333
  console.log(JSON.stringify({}));
334
- } catch {
334
+ } catch { /* expected: operation may fail gracefully */
335
335
  console.log(JSON.stringify({}));
336
336
  }
337
337
  }
@@ -411,7 +411,7 @@ async function main() {
411
411
  }
412
412
 
413
413
  console.log(JSON.stringify({}));
414
- } catch {
414
+ } catch { /* expected: operation may fail gracefully */
415
415
  console.log(JSON.stringify({}));
416
416
  }
417
417
  }
@@ -450,7 +450,7 @@ async function main() {
450
450
  }
451
451
 
452
452
  console.log(JSON.stringify({}));
453
- } catch {
453
+ } catch { /* expected: operation may fail gracefully */
454
454
  console.log(JSON.stringify({}));
455
455
  }
456
456
  }
@@ -39,7 +39,7 @@ export const IDE_CONFIGS = {
39
39
  },
40
40
  'vscode': {
41
41
  name: 'VS Code',
42
- description: 'Extensions: Continue, Copilot Chat, etc',
42
+ description: 'Extensions: Continue, AI Chat, etc',
43
43
  group: 'editor',
44
44
  recommended: false,
45
45
  configPath: '.vscode/chati/',
@@ -18,7 +18,7 @@
18
18
  * CRITICAL = 5.0% → maximum reinforcement (last interactions before handoff)
19
19
  */
20
20
 
21
- import { PROVIDER_LIMITS } from '../utils/provider-limits.js';
21
+ import { resolveContextLimit } from '../utils/provider-limits.js';
22
22
  import { BRACKETS } from '../utils/brackets.js';
23
23
 
24
24
  /**
@@ -40,9 +40,10 @@ const MEMORY_LEVELS = {
40
40
  * Calculate bracket from remaining context percentage.
41
41
  * @param {number} remainingPercent - 0 to 100
42
42
  * @param {string} [provider='claude'] - Provider name for context window scaling
43
+ * @param {string} [model] - Model tier (opus, sonnet, haiku) for precise context limits
43
44
  * @returns {{ bracket: string, activeLayers: string[], tokenBudget: number, budgetRatio: number, memoryLevel: string, handoffRequired: boolean, provider: string }}
44
45
  */
45
- export function calculateBracket(remainingPercent, provider = 'claude') {
46
+ export function calculateBracket(remainingPercent, provider = 'claude', model) {
46
47
  const pct = Math.max(0, Math.min(100, remainingPercent));
47
48
 
48
49
  let name = 'CRITICAL';
@@ -51,8 +52,8 @@ export function calculateBracket(remainingPercent, provider = 'claude') {
51
52
  else if (pct >= 25) name = 'DEPLETED';
52
53
 
53
54
  const def = BRACKETS[name];
54
- const providerLimit = PROVIDER_LIMITS[provider] || PROVIDER_LIMITS.claude;
55
- const tokenBudget = Math.round(def.budgetRatio * providerLimit);
55
+ const contextLimit = resolveContextLimit(model, provider);
56
+ const tokenBudget = Math.round(def.budgetRatio * contextLimit);
56
57
 
57
58
  return {
58
59
  bracket: name,
@@ -90,6 +91,26 @@ export function isLayerActive(bracket, layer) {
90
91
  return def.layers.includes(layer);
91
92
  }
92
93
 
94
+ /**
95
+ * Estimate remaining context percentage from actual prompt content size.
96
+ * Uses the heuristic that 1 token ≈ 4 characters.
97
+ *
98
+ * This is more accurate than turn-count estimation because it measures
99
+ * actual content rather than guessing from conversation length.
100
+ *
101
+ * @param {string} promptText - The full prompt text to estimate
102
+ * @param {string} [provider='claude'] - Provider name for context window scaling
103
+ * @param {string} [model] - Model tier (opus, sonnet, haiku) for precise limits
104
+ * @returns {number} Estimated remaining percentage (0-100)
105
+ */
106
+ export function estimateRemainingFromTokens(promptText, provider = 'claude', model) {
107
+ if (!promptText) return 100;
108
+ const estimatedTokens = Math.ceil(promptText.length / 4);
109
+ const limit = resolveContextLimit(model, provider);
110
+ const used = Math.min(estimatedTokens, limit) / limit;
111
+ return Math.round((1 - used) * 100);
112
+ }
113
+
93
114
  /**
94
115
  * Get all bracket definitions (for display/dashboard).
95
116
  * @returns {Record<string, { min: number, max: number, layers: string[], tokenBudget: number }>}
@@ -18,6 +18,14 @@ import { formatContext } from './formatter.js';
18
18
 
19
19
  const LAYER_TIMEOUT_MS = 100;
20
20
 
21
+ /**
22
+ * Static cache for L0+L1 layers.
23
+ * These layers never change mid-session (constitution and global rules are stable),
24
+ * so we cache them to improve prompt cache hit rates.
25
+ * Inspired by Claude Code's SYSTEM_PROMPT_DYNAMIC_BOUNDARY pattern.
26
+ */
27
+ let _staticCache = { key: null, l0: null, l1: null };
28
+
21
29
  /**
22
30
  * Run the PRISM context engine pipeline.
23
31
  *
@@ -61,14 +69,29 @@ export function runPrism(input) {
61
69
  // 3. Process each active layer with timeout protection
62
70
  const layerResults = {};
63
71
 
64
- // L0 — Always active
65
- const l0 = safeProcess('L0', () => processL0(ctx), errors);
66
- if (l0) { layers.push(l0); layerResults.l0 = l0; }
72
+ // L0 + L1 Static layers (cacheable, never change mid-session)
73
+ const cacheKey = `${input.domainsDir || ''}:${ctx.mode}`;
74
+ if (_staticCache.key === cacheKey && _staticCache.l0) {
75
+ layerResults.l0 = _staticCache.l0;
76
+ layers.push(_staticCache.l0);
77
+ if (_staticCache.l1) {
78
+ layerResults.l1 = _staticCache.l1;
79
+ layers.push(_staticCache.l1);
80
+ }
81
+ } else {
82
+ // L0 — Always active
83
+ const l0 = safeProcess('L0', () => processL0(ctx), errors);
84
+ if (l0) { layers.push(l0); layerResults.l0 = l0; }
85
+
86
+ // L1 — Always active
87
+ let l1 = null;
88
+ if (isLayerActive(bracket.bracket, 'L1')) {
89
+ l1 = safeProcess('L1', () => processL1(ctx), errors);
90
+ if (l1) { layers.push(l1); layerResults.l1 = l1; }
91
+ }
67
92
 
68
- // L1 Always active
69
- if (isLayerActive(bracket.bracket, 'L1')) {
70
- const l1 = safeProcess('L1', () => processL1(ctx), errors);
71
- if (l1) { layers.push(l1); layerResults.l1 = l1; }
93
+ // Cache static layers
94
+ _staticCache = { key: cacheKey, l0: layerResults.l0 || null, l1: l1 };
72
95
  }
73
96
 
74
97
  // L2 — Agent layer
@@ -132,6 +155,13 @@ function safeProcess(layerName, processFn, errors) {
132
155
  }
133
156
  }
134
157
 
158
+ /**
159
+ * Clear the static L0+L1 cache. Used when mode changes or for testing.
160
+ */
161
+ export function clearStaticCache() {
162
+ _staticCache = { key: null, l0: null, l1: null };
163
+ }
164
+
135
165
  /**
136
166
  * Get a summary of PRISM capabilities for display.
137
167
  * @returns {{ layers: number, brackets: string[], features: string[] }}
@@ -52,7 +52,19 @@ export function formatContext(options) {
52
52
  sections.push(formatKeywords(l5));
53
53
  }
54
54
 
55
- let body = sections.join('\n\n');
55
+ // Insert STATIC_BOUNDARY between static (L0+L1) and dynamic (L2-L5) sections.
56
+ // This enables prompt cache reuse for the stable prefix (Claude Code pattern).
57
+ const staticSections = sections.slice(0, l1 ? 2 : (l0 ? 1 : 0));
58
+ const dynamicSections = sections.slice(staticSections.length);
59
+
60
+ let body;
61
+ if (dynamicSections.length > 0) {
62
+ body = staticSections.join('\n\n') +
63
+ '\n\n <!-- STATIC_BOUNDARY -->\n\n' +
64
+ dynamicSections.join('\n\n');
65
+ } else {
66
+ body = staticSections.join('\n\n');
67
+ }
56
68
 
57
69
  // Truncate if over budget (rough estimate: 1 token ≈ 4 chars)
58
70
  const maxChars = tokenBudget * 4;
@@ -193,6 +205,38 @@ function truncateByPriority(sections, maxChars) {
193
205
  return result;
194
206
  }
195
207
 
208
+ /**
209
+ * Format only the static portion (L0 + L1) of the context.
210
+ * Useful for caching the stable prefix separately.
211
+ * @param {object} l0 - L0 Constitution result
212
+ * @param {object} [l1] - L1 Global result
213
+ * @returns {string} Static XML block
214
+ */
215
+ export function formatStaticBlock(l0, l1) {
216
+ const parts = [];
217
+ if (l0) parts.push(formatConstitution(l0));
218
+ if (l1) parts.push(formatGlobal(l1));
219
+ return parts.join('\n\n');
220
+ }
221
+
222
+ /**
223
+ * Format only the dynamic portion (L2-L5) of the context.
224
+ * Changes every turn based on agent, workflow, task, and keywords.
225
+ * @param {object} [l2] - L2 Agent result
226
+ * @param {object} [l3] - L3 Workflow result
227
+ * @param {object} [l4] - L4 Task result
228
+ * @param {object} [l5] - L5 Keywords result
229
+ * @returns {string} Dynamic XML block
230
+ */
231
+ export function formatDynamicBlock(l2, l3, l4, l5) {
232
+ const parts = [];
233
+ if (l2 && l2.agent) parts.push(formatAgent(l2));
234
+ if (l3 && l3.workflow) parts.push(formatWorkflow(l3));
235
+ if (l4 && l4.taskId) parts.push(formatTask(l4));
236
+ if (l5 && l5.matchCount > 0) parts.push(formatKeywords(l5));
237
+ return parts.join('\n\n');
238
+ }
239
+
196
240
  /** Escape XML special characters */
197
241
  function esc(str) {
198
242
  if (!str) return '';
@@ -1,4 +1,4 @@
1
- export { runPrism, getPrismInfo } from './engine.js';
2
- export { calculateBracket, estimateRemaining, isLayerActive, getBracketDefinitions } from './bracket-tracker.js';
3
- export { formatContext } from './formatter.js';
1
+ export { runPrism, getPrismInfo, clearStaticCache } from './engine.js';
2
+ export { calculateBracket, estimateRemaining, estimateRemainingFromTokens, isLayerActive, getBracketDefinitions } from './bracket-tracker.js';
3
+ export { formatContext, formatStaticBlock, formatDynamicBlock } from './formatter.js';
4
4
  export { loadDomainFile, loadAgentDomains, loadWorkflowDomains, extractRules } from './domain-loader.js';
@@ -25,7 +25,7 @@ export async function readDashboardData(targetDir) {
25
25
  if (existsSync(sessionPath)) {
26
26
  try {
27
27
  data.session = yaml.load(readFileSync(sessionPath, 'utf-8'));
28
- } catch {
28
+ } catch { /* expected: file may not exist */
29
29
  data.session = null;
30
30
  }
31
31
  }
@@ -35,7 +35,7 @@ export async function readDashboardData(targetDir) {
35
35
  if (existsSync(configPath)) {
36
36
  try {
37
37
  data.config = yaml.load(readFileSync(configPath, 'utf-8'));
38
- } catch {
38
+ } catch { /* expected: file may not exist */
39
39
  data.config = null;
40
40
  }
41
41
  }
@@ -64,7 +64,7 @@ export async function readDashboardData(targetDir) {
64
64
  try {
65
65
  const files = readdirSync(tasksDir).filter(f => f.endsWith('.yaml') || f.endsWith('.md'));
66
66
  data.taskProgress = { totalFiles: files.length };
67
- } catch {
67
+ } catch { /* expected: directory may not exist */
68
68
  // Ignore
69
69
  }
70
70
  }
@@ -75,7 +75,7 @@ export async function readDashboardData(targetDir) {
75
75
  try {
76
76
  const gotchasData = yaml.load(readFileSync(gotchasPath, 'utf-8'));
77
77
  data.gotchas = gotchasData?.gotchas || [];
78
- } catch {
78
+ } catch { /* expected: file may not exist */
79
79
  // Ignore
80
80
  }
81
81
  }
@@ -102,21 +102,21 @@ export async function readDashboardData(targetDir) {
102
102
  try {
103
103
  const { getMemoryStats } = await import('../intelligence/memory-manager.js');
104
104
  data.memoryStats = getMemoryStats(targetDir);
105
- } catch {
105
+ } catch { /* expected: operation may fail gracefully */
106
106
  // Intelligence module not available
107
107
  }
108
108
 
109
109
  try {
110
110
  const { getContextStatus } = await import('../intelligence/context-status.js');
111
111
  data.contextStatus = getContextStatus(targetDir);
112
- } catch {
112
+ } catch { /* expected: operation may fail gracefully */
113
113
  // Intelligence module not available
114
114
  }
115
115
 
116
116
  try {
117
117
  const { getRegistryStats } = await import('../intelligence/registry-manager.js');
118
118
  data.registryStats = getRegistryStats(targetDir);
119
- } catch {
119
+ } catch { /* expected: operation may fail gracefully */
120
120
  // Intelligence module not available
121
121
  }
122
122
 
@@ -168,7 +168,7 @@ export function getDecisionHistory(projectDir) {
168
168
  const content = readFileSync(historyPath, 'utf8');
169
169
  const data = JSON.parse(content);
170
170
  return data.decisions || [];
171
- } catch {
171
+ } catch { /* expected: file may not exist */
172
172
  return [];
173
173
  }
174
174
  }
@@ -193,7 +193,7 @@ export function recordDecision(projectDir, decision) {
193
193
  try {
194
194
  const content = readFileSync(historyPath, 'utf8');
195
195
  history = JSON.parse(content);
196
- } catch {
196
+ } catch { /* expected: file may not exist */
197
197
  // Keep empty history if parse fails
198
198
  }
199
199
  }
@@ -439,7 +439,7 @@ export function applyFixes(projectDir, fixes) {
439
439
  applied++;
440
440
  break;
441
441
  }
442
- } catch {
442
+ } catch { /* expected: operation may fail gracefully */
443
443
  // Skip failed fixes
444
444
  }
445
445
  });
@@ -290,7 +290,7 @@ export function generateEntityMeta(projectDir, filePath) {
290
290
  .slice(0, 5)
291
291
  .map(([word]) => word);
292
292
  meta.keywords.push(...topWords);
293
- } catch {
293
+ } catch { /* expected: operation may fail gracefully */
294
294
  // Ignore read errors
295
295
  }
296
296
  }
@@ -322,7 +322,7 @@ function scanDirectory(dirPath) {
322
322
  files.push(fullPath);
323
323
  }
324
324
  });
325
- } catch {
325
+ } catch { /* expected: operation may fail gracefully */
326
326
  // Ignore read errors
327
327
  }
328
328