chati-dev 3.3.2 → 4.0.1

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 (48) hide show
  1. package/README.md +4 -4
  2. package/framework/agents/plan/ux.md +41 -1
  3. package/framework/config.yaml +18 -3
  4. package/framework/constitution.md +18 -10
  5. package/framework/context/governance.md +2 -0
  6. package/framework/context/root.md +1 -1
  7. package/framework/data/entity-registry.yaml +43 -3
  8. package/framework/domains/constitution.yaml +1 -1
  9. package/framework/domains/global.yaml +7 -3
  10. package/framework/domains/workflows/standard-flow.yaml +33 -0
  11. package/framework/i18n/en.yaml +11 -2
  12. package/framework/i18n/es.yaml +11 -2
  13. package/framework/i18n/fr.yaml +11 -2
  14. package/framework/i18n/pt.yaml +11 -2
  15. package/framework/intelligence/context-engine.md +22 -17
  16. package/framework/presets/nextjs.yaml +41 -0
  17. package/framework/presets/node-express.yaml +39 -0
  18. package/framework/presets/react-vite.yaml +37 -0
  19. package/framework/presets/supabase-fullstack.yaml +37 -0
  20. package/framework/templates/brandbook-tmpl.yaml +113 -0
  21. package/framework/templates/component-spec-tmpl.yaml +74 -0
  22. package/framework/templates/design-token-tmpl.yaml +55 -0
  23. package/framework/templates/icon-system-tmpl.yaml +95 -0
  24. package/package.json +1 -1
  25. package/scripts/bundle-framework.js +1 -1
  26. package/scripts/doctor/checks/agents.js +77 -0
  27. package/scripts/doctor/checks/constitution.js +41 -0
  28. package/scripts/doctor/checks/domain-alignment.js +58 -0
  29. package/scripts/doctor/checks/prism-layers.js +84 -0
  30. package/scripts/doctor/checks/registry.js +55 -0
  31. package/scripts/doctor/checks/schemas.js +61 -0
  32. package/scripts/doctor/fixes/reference-fix.js +100 -0
  33. package/scripts/doctor/fixes/registry-fix.js +56 -0
  34. package/scripts/doctor/index.js +212 -0
  35. package/scripts/health-check.js +8 -8
  36. package/src/autonomy/surface-criteria.js +226 -0
  37. package/src/context/bracket-tracker.js +44 -13
  38. package/src/context/domain-loader.js +22 -0
  39. package/src/context/engine.js +18 -7
  40. package/src/context/formatter.js +21 -1
  41. package/src/context/layers/l5-keywords.js +53 -0
  42. package/src/intelligence/context-status.js +20 -9
  43. package/src/intelligence/decision-engine.js +253 -0
  44. package/src/terminal/prompt-builder.js +341 -1
  45. package/src/terminal/run-agent.js +15 -0
  46. package/src/terminal/run-parallel.js +77 -5
  47. package/src/terminal/spawner.js +13 -0
  48. package/src/utils/feature-flags.js +106 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Surface Criteria — 7 codified checkpoints for autonomous execution pause.
3
+ *
4
+ * Defines WHEN the system should surface a decision to the user instead
5
+ * of proceeding autonomously. Each criterion has a severity level:
6
+ * critical — always pause, cannot be batch-confirmed
7
+ * warning — pause unless already confirmed, batchable
8
+ *
9
+ * Short-circuit: critical criteria (C003, C006, C007) are checked first.
10
+ * Batch confirm: C001 + C002 can be grouped into a single user prompt.
11
+ * Session memory: once confirmed, a criterion can be skipped for the session.
12
+ */
13
+
14
+ /**
15
+ * The 7 surface criteria.
16
+ */
17
+ export const CRITERIA = {
18
+ C001: {
19
+ id: 'C001',
20
+ name: 'ambiguous_requirement',
21
+ severity: 'warning',
22
+ description: 'Requirement is ambiguous and can be interpreted multiple ways.',
23
+ },
24
+ C002: {
25
+ id: 'C002',
26
+ name: 'multiple_approaches',
27
+ severity: 'warning',
28
+ description: 'Multiple valid implementation approaches exist.',
29
+ },
30
+ C003: {
31
+ id: 'C003',
32
+ name: 'destructive_operation',
33
+ severity: 'critical',
34
+ description: 'Action would delete, overwrite, or irreversibly modify existing data or code.',
35
+ },
36
+ C004: {
37
+ id: 'C004',
38
+ name: 'external_interaction',
39
+ severity: 'warning',
40
+ description: 'Action involves external systems (APIs, deployments, notifications).',
41
+ },
42
+ C005: {
43
+ id: 'C005',
44
+ name: 'cost_threshold',
45
+ severity: 'warning',
46
+ description: 'Estimated cost exceeds defined threshold for the session.',
47
+ },
48
+ C006: {
49
+ id: 'C006',
50
+ name: 'scope_creep',
51
+ severity: 'critical',
52
+ description: 'Proposed changes extend beyond the original scope or task definition.',
53
+ },
54
+ C007: {
55
+ id: 'C007',
56
+ name: 'security_sensitive',
57
+ severity: 'critical',
58
+ description: 'Action involves credentials, secrets, permissions, or security-sensitive operations.',
59
+ },
60
+ };
61
+
62
+ /**
63
+ * Check if a criterion has already been confirmed in this session.
64
+ *
65
+ * @param {string} criterionId - e.g., 'C001'
66
+ * @param {Set<string>|string[]} sessionDecisions - Previously confirmed criteria
67
+ * @returns {boolean}
68
+ */
69
+ export function isAlreadyConfirmed(criterionId, sessionDecisions) {
70
+ if (!sessionDecisions) return false;
71
+ if (sessionDecisions instanceof Set) return sessionDecisions.has(criterionId);
72
+ if (Array.isArray(sessionDecisions)) return sessionDecisions.includes(criterionId);
73
+ return false;
74
+ }
75
+
76
+ /**
77
+ * Evaluate surface criteria for a given action context.
78
+ *
79
+ * @param {object} action - The action being evaluated
80
+ * @param {string} [action.type] - Action type (e.g., 'write', 'delete', 'deploy', 'create')
81
+ * @param {string} [action.target] - Target of the action (e.g., file path, resource name)
82
+ * @param {string} [action.description] - Human-readable action description
83
+ * @param {string[]} [action.tags] - Semantic tags (e.g., ['destructive', 'external', 'security'])
84
+ * @param {number} [action.estimatedCost] - Estimated cost in dollars
85
+ * @param {object} context - Evaluation context
86
+ * @param {Set<string>|string[]} [context.sessionDecisions] - Previously confirmed criteria
87
+ * @param {number} [context.costThreshold] - Cost threshold for C005 (default: 1.0)
88
+ * @param {string[]} [context.taskScope] - Original task scope keywords
89
+ * @returns {{ shouldPause: boolean, triggers: Array<{ id: string, name: string, severity: string, reason: string }>, batchable: boolean }}
90
+ */
91
+ export function evaluateSurfaceCriteria(action, context = {}) {
92
+ if (!action) {
93
+ return { shouldPause: false, triggers: [], batchable: false };
94
+ }
95
+
96
+ const triggers = [];
97
+ const sessionDecisions = context.sessionDecisions || new Set();
98
+ const tags = new Set((action.tags || []).map(t => t.toLowerCase()));
99
+ const actionType = (action.type || '').toLowerCase();
100
+
101
+ // --- Critical criteria first (short-circuit) ---
102
+
103
+ // C003: Destructive operation
104
+ if (isDestructive(actionType, tags) && !isAlreadyConfirmed('C003', sessionDecisions)) {
105
+ triggers.push({
106
+ id: 'C003',
107
+ name: CRITERIA.C003.name,
108
+ severity: 'critical',
109
+ reason: `Destructive operation: ${action.description || action.type || 'unknown'}`,
110
+ });
111
+ }
112
+
113
+ // C006: Scope creep
114
+ if (isScopeCreep(action, context) && !isAlreadyConfirmed('C006', sessionDecisions)) {
115
+ triggers.push({
116
+ id: 'C006',
117
+ name: CRITERIA.C006.name,
118
+ severity: 'critical',
119
+ reason: `Changes extend beyond original task scope.`,
120
+ });
121
+ }
122
+
123
+ // C007: Security sensitive
124
+ if (isSecuritySensitive(actionType, tags, action.target) && !isAlreadyConfirmed('C007', sessionDecisions)) {
125
+ triggers.push({
126
+ id: 'C007',
127
+ name: CRITERIA.C007.name,
128
+ severity: 'critical',
129
+ reason: `Security-sensitive operation: ${action.description || action.target || 'unknown'}`,
130
+ });
131
+ }
132
+
133
+ // --- Warning criteria ---
134
+
135
+ // C001: Ambiguous requirement
136
+ if (tags.has('ambiguous') && !isAlreadyConfirmed('C001', sessionDecisions)) {
137
+ triggers.push({
138
+ id: 'C001',
139
+ name: CRITERIA.C001.name,
140
+ severity: 'warning',
141
+ reason: 'Requirement is ambiguous.',
142
+ });
143
+ }
144
+
145
+ // C002: Multiple approaches
146
+ if (tags.has('multiple_approaches') && !isAlreadyConfirmed('C002', sessionDecisions)) {
147
+ triggers.push({
148
+ id: 'C002',
149
+ name: CRITERIA.C002.name,
150
+ severity: 'warning',
151
+ reason: 'Multiple valid approaches exist.',
152
+ });
153
+ }
154
+
155
+ // C004: External interaction
156
+ if (isExternal(actionType, tags) && !isAlreadyConfirmed('C004', sessionDecisions)) {
157
+ triggers.push({
158
+ id: 'C004',
159
+ name: CRITERIA.C004.name,
160
+ severity: 'warning',
161
+ reason: `External interaction: ${action.description || action.type || 'unknown'}`,
162
+ });
163
+ }
164
+
165
+ // C005: Cost threshold
166
+ const costThreshold = context.costThreshold ?? 1.0;
167
+ if (action.estimatedCost && action.estimatedCost > costThreshold && !isAlreadyConfirmed('C005', sessionDecisions)) {
168
+ triggers.push({
169
+ id: 'C005',
170
+ name: CRITERIA.C005.name,
171
+ severity: 'warning',
172
+ reason: `Estimated cost $${action.estimatedCost.toFixed(2)} exceeds threshold $${costThreshold.toFixed(2)}.`,
173
+ });
174
+ }
175
+
176
+ // Determine if batchable (only if ALL triggers are warnings, no critical)
177
+ const hasCritical = triggers.some(t => t.severity === 'critical');
178
+ const warningOnly = triggers.length > 0 && !hasCritical;
179
+ const batchable = warningOnly && triggers.length > 1;
180
+
181
+ return {
182
+ shouldPause: triggers.length > 0,
183
+ triggers,
184
+ batchable,
185
+ };
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Internal detection helpers
190
+ // ---------------------------------------------------------------------------
191
+
192
+ function isDestructive(actionType, tags) {
193
+ return (
194
+ tags.has('destructive') ||
195
+ ['delete', 'remove', 'drop', 'reset', 'force-push', 'overwrite'].includes(actionType)
196
+ );
197
+ }
198
+
199
+ function isSecuritySensitive(actionType, tags, target) {
200
+ if (tags.has('security') || tags.has('credentials') || tags.has('secrets')) return true;
201
+ if (target && /\.(env|pem|key|secret|credentials|password)/i.test(target)) return true;
202
+ if (['chmod', 'chown'].includes(actionType)) return true;
203
+ return false;
204
+ }
205
+
206
+ function isExternal(actionType, tags) {
207
+ return (
208
+ tags.has('external') ||
209
+ ['deploy', 'publish', 'push', 'send', 'notify'].includes(actionType)
210
+ );
211
+ }
212
+
213
+ function isScopeCreep(action, context) {
214
+ if (!context.taskScope || context.taskScope.length === 0) return false;
215
+ if (!action.tags || action.tags.length === 0) return false;
216
+
217
+ const scopeSet = new Set(context.taskScope.map(s => s.toLowerCase()));
218
+ const actionTags = action.tags.map(t => t.toLowerCase());
219
+
220
+ // If action has 'scope_creep' tag explicitly
221
+ if (actionTags.includes('scope_creep')) return true;
222
+
223
+ // If none of the action tags overlap with task scope, it may be scope creep
224
+ const hasOverlap = actionTags.some(t => scopeSet.has(t));
225
+ return !hasOverlap && actionTags.length > 0;
226
+ }
@@ -2,32 +2,58 @@
2
2
  * Bracket Tracker — Pure arithmetic for context window management.
3
3
  *
4
4
  * Brackets determine how much context to inject:
5
- * FRESH (60-100%) → All 5 layers active
6
- * MODERATE (40-60%) → L0 + L1 + L2 + L3 (skip L4 task detail)
5
+ * FRESH (60-100%) → All 6 layers active (L0-L5)
6
+ * MODERATE (40-60%) → L0 + L1 + L2 + L3 + L5 (skip L4 task detail)
7
7
  * DEPLETED (25-40%) → L0 + L1 + L2 only
8
8
  * CRITICAL (<25%) → L0 + L1 only (handoff mandatory)
9
+ *
10
+ * Progressive Reinforcement Model (v4.0):
11
+ * As context window depletes, the model forgets initial instructions
12
+ * and needs MORE reinforcement, not less. Budget expressed as percentage
13
+ * of the provider's total context window — proportional across providers.
14
+ *
15
+ * FRESH = 1.5% → minimal reinforcement (context is plentiful)
16
+ * MODERATE = 2.5% → growing reinforcement (model starting to forget)
17
+ * DEPLETED = 4.0% → heavy reinforcement (significant context loss)
18
+ * CRITICAL = 5.0% → maximum reinforcement (last interactions before handoff)
9
19
  */
10
20
 
21
+ /** Provider context window limits (tokens). */
22
+ const PROVIDER_LIMITS = {
23
+ claude: 200_000,
24
+ gemini: 1_000_000,
25
+ codex: 128_000,
26
+ };
27
+
11
28
  const BRACKETS = {
12
- FRESH: { min: 60, max: 100, layers: ['L0', 'L1', 'L2', 'L3', 'L4'], tokenBudget: 8000 },
13
- MODERATE: { min: 40, max: 60, layers: ['L0', 'L1', 'L2', 'L3'], tokenBudget: 5000 },
14
- DEPLETED: { min: 25, max: 40, layers: ['L0', 'L1', 'L2'], tokenBudget: 3000 },
15
- CRITICAL: { min: 0, max: 25, layers: ['L0', 'L1'], tokenBudget: 1500 },
29
+ FRESH: { min: 60, max: 100, layers: ['L0', 'L1', 'L2', 'L3', 'L4', 'L5'], budgetRatio: 0.015 },
30
+ MODERATE: { min: 40, max: 60, layers: ['L0', 'L1', 'L2', 'L3', 'L5'], budgetRatio: 0.025 },
31
+ DEPLETED: { min: 25, max: 40, layers: ['L0', 'L1', 'L2'], budgetRatio: 0.040 },
32
+ CRITICAL: { min: 0, max: 25, layers: ['L0', 'L1'], budgetRatio: 0.050 },
16
33
  };
17
34
 
35
+ /**
36
+ * Memory injection level per bracket.
37
+ * As context degrades, memory becomes MORE important:
38
+ * FRESH = none (context abundant, no memory needed)
39
+ * MODERATE = metadata (light reminders of relevant memories)
40
+ * DEPLETED = chunks (recovery via memory summaries)
41
+ * CRITICAL = full (full memory dump for handoff continuity)
42
+ */
18
43
  const MEMORY_LEVELS = {
19
- FRESH: 'full',
20
- MODERATE: 'chunks',
21
- DEPLETED: 'metadata',
22
- CRITICAL: 'none',
44
+ FRESH: 'none',
45
+ MODERATE: 'metadata',
46
+ DEPLETED: 'chunks',
47
+ CRITICAL: 'full',
23
48
  };
24
49
 
25
50
  /**
26
51
  * Calculate bracket from remaining context percentage.
27
52
  * @param {number} remainingPercent - 0 to 100
28
- * @returns {{ bracket: string, activeLayers: string[], tokenBudget: number, memoryLevel: string, handoffRequired: boolean }}
53
+ * @param {string} [provider='claude'] - Provider name for context window scaling
54
+ * @returns {{ bracket: string, activeLayers: string[], tokenBudget: number, budgetRatio: number, memoryLevel: string, handoffRequired: boolean, provider: string }}
29
55
  */
30
- export function calculateBracket(remainingPercent) {
56
+ export function calculateBracket(remainingPercent, provider = 'claude') {
31
57
  const pct = Math.max(0, Math.min(100, remainingPercent));
32
58
 
33
59
  let name = 'CRITICAL';
@@ -36,13 +62,18 @@ export function calculateBracket(remainingPercent) {
36
62
  else if (pct >= 25) name = 'DEPLETED';
37
63
 
38
64
  const def = BRACKETS[name];
65
+ const providerLimit = PROVIDER_LIMITS[provider] || PROVIDER_LIMITS.claude;
66
+ const tokenBudget = Math.round(def.budgetRatio * providerLimit);
67
+
39
68
  return {
40
69
  bracket: name,
41
70
  activeLayers: [...def.layers],
42
- tokenBudget: def.tokenBudget,
71
+ tokenBudget,
72
+ budgetRatio: def.budgetRatio,
43
73
  memoryLevel: MEMORY_LEVELS[name],
44
74
  handoffRequired: pct < 15,
45
75
  remainingPercent: pct,
76
+ provider,
46
77
  };
47
78
  }
48
79
 
@@ -91,6 +91,28 @@ export function loadGlobalDomain(domainsDir) {
91
91
  return result.loaded ? result.data : null;
92
92
  }
93
93
 
94
+ /**
95
+ * Load all keyword domain files from chati.dev/domains/keywords/.
96
+ * @param {string} domainsDir - Path to chati.dev/domains/
97
+ * @returns {Map<string, object>} Map of topicName → domain data (keywords + rules)
98
+ */
99
+ export function loadKeywordDomains(domainsDir) {
100
+ const keywordsDir = join(domainsDir, 'keywords');
101
+ const domains = new Map();
102
+
103
+ if (!existsSync(keywordsDir)) return domains;
104
+
105
+ const files = readdirSync(keywordsDir).filter(f => f.endsWith('.yaml'));
106
+ for (const file of files) {
107
+ const name = basename(file, '.yaml');
108
+ const result = loadDomainFile(join(keywordsDir, file));
109
+ if (result.loaded) {
110
+ domains.set(name, result.data);
111
+ }
112
+ }
113
+ return domains;
114
+ }
115
+
94
116
  /**
95
117
  * Extract rules array from a domain object.
96
118
  * Domain files have a `rules` key with an array of rule objects.
@@ -1,7 +1,7 @@
1
1
  /**
2
- * PRISM Context Engine — 5-layer context injection pipeline.
2
+ * PRISM Context Engine — 6-layer context injection pipeline.
3
3
  *
4
- * Orchestrates L0-L4 layers, respects bracket constraints,
4
+ * Orchestrates L0-L5 layers, respects bracket constraints,
5
5
  * and produces formatted XML context for agent prompts.
6
6
  *
7
7
  * Pipeline: bracket calculation → layer processing → formatting → output
@@ -13,6 +13,7 @@ import { processL1 } from './layers/l1-global.js';
13
13
  import { processL2 } from './layers/l2-agent.js';
14
14
  import { processL3 } from './layers/l3-workflow.js';
15
15
  import { processL4 } from './layers/l4-task.js';
16
+ import { processL5 } from './layers/l5-keywords.js';
16
17
  import { formatContext } from './formatter.js';
17
18
 
18
19
  const LAYER_TIMEOUT_MS = 100;
@@ -31,14 +32,16 @@ const LAYER_TIMEOUT_MS = 100;
31
32
  * @param {object} [input.handoff] - Handoff data from previous agent
32
33
  * @param {string[]} [input.artifacts] - Relevant artifact paths
33
34
  * @param {string[]} [input.taskCriteria] - Active task criteria
35
+ * @param {string} [input.userPrompt] - User prompt text for L5 keyword matching
36
+ * @param {string} [input.provider] - CLI provider for context window scaling (claude, gemini, codex)
34
37
  * @returns {{ xml: string, bracket: object, layers: object[], errors: string[] }}
35
38
  */
36
39
  export function runPrism(input) {
37
40
  const errors = [];
38
41
  const layers = [];
39
42
 
40
- // 1. Calculate bracket
41
- const bracket = calculateBracket(input.remainingPercent);
43
+ // 1. Calculate bracket (provider-aware for proportional budgets)
44
+ const bracket = calculateBracket(input.remainingPercent, input.provider || 'claude');
42
45
 
43
46
  // 2. Build context for layer processors
44
47
  const ctx = {
@@ -52,6 +55,7 @@ export function runPrism(input) {
52
55
  handoff: input.handoff || {},
53
56
  artifacts: input.artifacts || [],
54
57
  taskCriteria: input.taskCriteria || [],
58
+ userPrompt: input.userPrompt || null,
55
59
  };
56
60
 
57
61
  // 3. Process each active layer with timeout protection
@@ -85,6 +89,12 @@ export function runPrism(input) {
85
89
  if (l4) { layers.push(l4); layerResults.l4 = l4; }
86
90
  }
87
91
 
92
+ // L5 — Keyword recall layer (dynamic rules based on user prompt)
93
+ if (isLayerActive(bracket.bracket, 'L5') && ctx.userPrompt) {
94
+ const l5 = safeProcess('L5', () => processL5(ctx), errors);
95
+ if (l5 && l5.matchCount > 0) { layers.push(l5); layerResults.l5 = l5; }
96
+ }
97
+
88
98
  // 4. Format output
89
99
  const xml = formatContext({
90
100
  bracket: bracket.bracket,
@@ -129,9 +139,9 @@ function safeProcess(layerName, processFn, errors) {
129
139
  export function getPrismInfo() {
130
140
  return {
131
141
  name: 'PRISM',
132
- version: '1.0.0',
133
- layers: 5,
134
- layerNames: ['L0 Constitution', 'L1 Global', 'L2 Agent', 'L3 Workflow', 'L4 Task'],
142
+ version: '1.1.0',
143
+ layers: 6,
144
+ layerNames: ['L0 Constitution', 'L1 Global', 'L2 Agent', 'L3 Workflow', 'L4 Task', 'L5 Keywords'],
135
145
  brackets: ['FRESH', 'MODERATE', 'DEPLETED', 'CRITICAL'],
136
146
  features: [
137
147
  'Priority-based context truncation',
@@ -139,6 +149,7 @@ export function getPrismInfo() {
139
149
  'Graceful degradation on layer failure',
140
150
  'XML structured output',
141
151
  'Bracket-aware layer activation',
152
+ 'Provider-aware proportional budgets',
142
153
  ],
143
154
  };
144
155
  }
@@ -15,10 +15,11 @@
15
15
  * @param {object} [options.l2] - L2 Agent result
16
16
  * @param {object} [options.l3] - L3 Workflow result
17
17
  * @param {object} [options.l4] - L4 Task result
18
+ * @param {object} [options.l5] - L5 Keywords result
18
19
  * @returns {string} XML context block
19
20
  */
20
21
  export function formatContext(options) {
21
- const { bracket, tokenBudget, l0, l1, l2, l3, l4 } = options;
22
+ const { bracket, tokenBudget, l0, l1, l2, l3, l4, l5 } = options;
22
23
  const sections = [];
23
24
 
24
25
  // L0 — Constitution (always present)
@@ -46,6 +47,11 @@ export function formatContext(options) {
46
47
  sections.push(formatTask(l4));
47
48
  }
48
49
 
50
+ // L5 — Keywords
51
+ if (l5 && l5.matchCount > 0) {
52
+ sections.push(formatKeywords(l5));
53
+ }
54
+
49
55
  let body = sections.join('\n\n');
50
56
 
51
57
  // Truncate if over budget (rough estimate: 1 token ≈ 4 chars)
@@ -154,6 +160,20 @@ function formatTask(l4) {
154
160
  return lines.join('\n');
155
161
  }
156
162
 
163
+ function formatKeywords(l5) {
164
+ const lines = [];
165
+ lines.push(` <keyword-rules matches="${l5.matchCount}">`);
166
+ for (const match of l5.matches) {
167
+ lines.push(` <topic name="${esc(match.topic)}">`);
168
+ for (const r of match.rules) {
169
+ lines.push(` <rule id="${esc(r.id)}" priority="${esc(r.priority)}">${esc(r.text)}</rule>`);
170
+ }
171
+ lines.push(` </topic>`);
172
+ }
173
+ lines.push(` </keyword-rules>`);
174
+ return lines.join('\n');
175
+ }
176
+
157
177
  /**
158
178
  * Truncate sections by removing lower-priority layers first (L4 → L3 → L2).
159
179
  * L0 and L1 are never truncated.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * L5 Keyword Recall Layer — Dynamic rule injection based on user prompt content.
3
+ *
4
+ * Scans the user prompt for topic keywords and loads matching rules from
5
+ * domains/keywords/*.yaml. This adds the WHAT dimension to PRISM context:
6
+ * L0-L4 handle WHO (agent) and WHERE (workflow/task)
7
+ * L5 handles WHAT (topic detection from user prompt)
8
+ *
9
+ * Only active in FRESH and MODERATE brackets (expensive layer).
10
+ */
11
+
12
+ import { loadKeywordDomains } from '../domain-loader.js';
13
+
14
+ /**
15
+ * Process L5: scan user prompt for keywords, return matching rules.
16
+ *
17
+ * @param {object} ctx - Pipeline context
18
+ * @param {string} ctx.domainsDir - Path to chati.dev/domains/
19
+ * @param {string} [ctx.userPrompt] - The user's prompt text to scan for keywords
20
+ * @returns {{ layer: string, matches: Array<{ topic: string, rules: object[] }>, matchCount: number }}
21
+ */
22
+ export function processL5(ctx) {
23
+ const result = {
24
+ layer: 'L5',
25
+ matches: [],
26
+ matchCount: 0,
27
+ };
28
+
29
+ if (!ctx.userPrompt || !ctx.domainsDir) {
30
+ return result;
31
+ }
32
+
33
+ const promptLower = ctx.userPrompt.toLowerCase();
34
+ const keywordDomains = loadKeywordDomains(ctx.domainsDir);
35
+
36
+ for (const [topic, domain] of keywordDomains) {
37
+ const keywords = domain.keywords || [];
38
+ const matched = keywords.some(kw => promptLower.includes(kw.toLowerCase()));
39
+
40
+ if (matched) {
41
+ const rules = (domain.rules || []).map(r => ({
42
+ id: r.id || `${topic}-unknown`,
43
+ text: r.text || '',
44
+ priority: r.priority || 'normal',
45
+ }));
46
+
47
+ result.matches.push({ topic, rules });
48
+ result.matchCount += rules.length;
49
+ }
50
+ }
51
+
52
+ return result;
53
+ }
@@ -1,16 +1,23 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
 
4
+ /** Provider context window limits (tokens). */
5
+ const PROVIDER_LIMITS = {
6
+ claude: 200_000,
7
+ gemini: 1_000_000,
8
+ codex: 128_000,
9
+ };
10
+
4
11
  const BRACKETS = [
5
- { name: 'FRESH', min: 60, max: 100, layers: ['L0', 'L1', 'L2', 'L3', 'L4'], budget: 8000 },
6
- { name: 'MODERATE', min: 40, max: 60, layers: ['L0', 'L1', 'L2', 'L3'], budget: 5000 },
7
- { name: 'DEPLETED', min: 25, max: 40, layers: ['L0', 'L1', 'L2'], budget: 3000 },
8
- { name: 'CRITICAL', min: 0, max: 25, layers: ['L0', 'L1'], budget: 1500 },
12
+ { name: 'FRESH', min: 60, max: 100, layers: ['L0', 'L1', 'L2', 'L3', 'L4', 'L5'], budgetRatio: 0.015 },
13
+ { name: 'MODERATE', min: 40, max: 60, layers: ['L0', 'L1', 'L2', 'L3', 'L5'], budgetRatio: 0.025 },
14
+ { name: 'DEPLETED', min: 25, max: 40, layers: ['L0', 'L1', 'L2'], budgetRatio: 0.040 },
15
+ { name: 'CRITICAL', min: 0, max: 25, layers: ['L0', 'L1'], budgetRatio: 0.050 },
9
16
  ];
10
17
  // NOTE: This BRACKETS array is intentionally duplicated from src/context/bracket-tracker.js
11
18
  // for module isolation. bracket-tracker.js is the canonical source of truth.
12
- // If bracket ranges or budgets change, update BOTH files.
13
- // Format differs: here = array with `budget`, there = object with `tokenBudget`.
19
+ // Progressive Reinforcement Model (v4.0): budgets as ratio of provider context window.
20
+ // If bracket ranges or ratios change, update BOTH files.
14
21
 
15
22
  /**
16
23
  * Get context status based on session state
@@ -19,10 +26,12 @@ const BRACKETS = [
19
26
  export function getContextStatus(targetDir) {
20
27
  const sessionPath = join(targetDir, '.chati', 'session.yaml');
21
28
  if (!existsSync(sessionPath)) {
29
+ const providerLimit = PROVIDER_LIMITS.claude;
22
30
  return {
23
31
  bracket: 'FRESH',
24
32
  activeLayers: BRACKETS[0].layers,
25
- tokenBudget: BRACKETS[0].budget,
33
+ tokenBudget: Math.round(BRACKETS[0].budgetRatio * providerLimit),
34
+ budgetRatio: BRACKETS[0].budgetRatio,
26
35
  memoryLevel: 'none',
27
36
  advisory: 'No active session — default FRESH bracket',
28
37
  };
@@ -44,12 +53,14 @@ export function getContextStatus(targetDir) {
44
53
 
45
54
  const bracket = BRACKETS.find(b => remainingPercent >= b.min && remainingPercent <= b.max) || BRACKETS[0];
46
55
 
47
- const memoryLevels = { FRESH: 'full', MODERATE: 'chunks', DEPLETED: 'metadata', CRITICAL: 'none' };
56
+ const memoryLevels = { FRESH: 'none', MODERATE: 'metadata', DEPLETED: 'chunks', CRITICAL: 'full' };
57
+ const providerLimit = PROVIDER_LIMITS.claude;
48
58
 
49
59
  return {
50
60
  bracket: bracket.name,
51
61
  activeLayers: bracket.layers,
52
- tokenBudget: bracket.budget,
62
+ tokenBudget: Math.round(bracket.budgetRatio * providerLimit),
63
+ budgetRatio: bracket.budgetRatio,
53
64
  memoryLevel: memoryLevels[bracket.name],
54
65
  remainingPercent,
55
66
  estimatedUsage,