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.
- package/README.md +4 -4
- package/framework/agents/plan/ux.md +41 -1
- package/framework/config.yaml +18 -3
- package/framework/constitution.md +18 -10
- package/framework/context/governance.md +2 -0
- package/framework/context/root.md +1 -1
- package/framework/data/entity-registry.yaml +43 -3
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +7 -3
- package/framework/domains/workflows/standard-flow.yaml +33 -0
- package/framework/i18n/en.yaml +11 -2
- package/framework/i18n/es.yaml +11 -2
- package/framework/i18n/fr.yaml +11 -2
- package/framework/i18n/pt.yaml +11 -2
- package/framework/intelligence/context-engine.md +22 -17
- package/framework/presets/nextjs.yaml +41 -0
- package/framework/presets/node-express.yaml +39 -0
- package/framework/presets/react-vite.yaml +37 -0
- package/framework/presets/supabase-fullstack.yaml +37 -0
- package/framework/templates/brandbook-tmpl.yaml +113 -0
- package/framework/templates/component-spec-tmpl.yaml +74 -0
- package/framework/templates/design-token-tmpl.yaml +55 -0
- package/framework/templates/icon-system-tmpl.yaml +95 -0
- package/package.json +1 -1
- package/scripts/bundle-framework.js +1 -1
- package/scripts/doctor/checks/agents.js +77 -0
- package/scripts/doctor/checks/constitution.js +41 -0
- package/scripts/doctor/checks/domain-alignment.js +58 -0
- package/scripts/doctor/checks/prism-layers.js +84 -0
- package/scripts/doctor/checks/registry.js +55 -0
- package/scripts/doctor/checks/schemas.js +61 -0
- package/scripts/doctor/fixes/reference-fix.js +100 -0
- package/scripts/doctor/fixes/registry-fix.js +56 -0
- package/scripts/doctor/index.js +212 -0
- package/scripts/health-check.js +8 -8
- package/src/autonomy/surface-criteria.js +226 -0
- package/src/context/bracket-tracker.js +44 -13
- package/src/context/domain-loader.js +22 -0
- package/src/context/engine.js +18 -7
- package/src/context/formatter.js +21 -1
- package/src/context/layers/l5-keywords.js +53 -0
- package/src/intelligence/context-status.js +20 -9
- package/src/intelligence/decision-engine.js +253 -0
- package/src/terminal/prompt-builder.js +341 -1
- package/src/terminal/run-agent.js +15 -0
- package/src/terminal/run-parallel.js +77 -5
- package/src/terminal/spawner.js +13 -0
- 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
|
|
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'],
|
|
13
|
-
MODERATE: { min: 40, max: 60, layers: ['L0', 'L1', 'L2', 'L3'],
|
|
14
|
-
DEPLETED: { min: 25, max: 40, layers: ['L0', 'L1', 'L2'],
|
|
15
|
-
CRITICAL: { min: 0, max: 25, layers: ['L0', 'L1'],
|
|
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: '
|
|
20
|
-
MODERATE: '
|
|
21
|
-
DEPLETED: '
|
|
22
|
-
CRITICAL: '
|
|
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
|
-
* @
|
|
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
|
|
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.
|
package/src/context/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PRISM Context Engine —
|
|
2
|
+
* PRISM Context Engine — 6-layer context injection pipeline.
|
|
3
3
|
*
|
|
4
|
-
* Orchestrates L0-
|
|
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.
|
|
133
|
-
layers:
|
|
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
|
}
|
package/src/context/formatter.js
CHANGED
|
@@ -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'],
|
|
6
|
-
{ name: 'MODERATE', min: 40, max: 60, layers: ['L0', 'L1', 'L2', 'L3'],
|
|
7
|
-
{ name: 'DEPLETED', min: 25, max: 40, layers: ['L0', 'L1', 'L2'],
|
|
8
|
-
{ name: 'CRITICAL', min: 0, max: 25, layers: ['L0', 'L1'],
|
|
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
|
-
//
|
|
13
|
-
//
|
|
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].
|
|
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: '
|
|
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.
|
|
62
|
+
tokenBudget: Math.round(bracket.budgetRatio * providerLimit),
|
|
63
|
+
budgetRatio: bracket.budgetRatio,
|
|
53
64
|
memoryLevel: memoryLevels[bracket.name],
|
|
54
65
|
remainingPercent,
|
|
55
66
|
estimatedUsage,
|