chati-dev 3.3.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/TERMS_OF_USE.md +56 -0
- package/framework/config.yaml +23 -3
- package/framework/constitution.md +12 -10
- package/framework/context/governance.md +2 -0
- package/framework/context/root.md +1 -1
- package/framework/intelligence/context-engine.md +22 -17
- package/package.json +3 -2
- 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/installer/templates.js +1 -1
- package/src/intelligence/context-status.js +20 -9
- package/src/intelligence/decision-engine.js +253 -0
- package/src/orchestrator/pipeline-manager.js +80 -9
- package/src/telemetry/config.js +1 -1
- package/src/telemetry/index.js +1 -1
- package/src/telemetry/schema.js +60 -11
- package/src/terminal/prompt-builder.js +341 -1
- package/src/terminal/run-agent.js +61 -1
- package/src/terminal/run-parallel.js +77 -5
- package/src/terminal/spawner.js +13 -0
- package/src/utils/feature-flags.js +106 -0
- package/src/wizard/i18n.js +8 -3
- package/src/wizard/index.js +35 -9
- package/src/wizard/questions.js +19 -10
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import { AGENT_PIPELINE, getNextAgent } from './agent-selector.js';
|
|
7
7
|
import { calculateBracket, estimateRemaining } from '../context/bracket-tracker.js';
|
|
8
|
-
import { track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
|
|
8
|
+
import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
|
|
9
9
|
import { sendEvents } from '../telemetry/sender.js';
|
|
10
|
-
import { getTelemetryConfig } from '../telemetry/config.js';
|
|
10
|
+
import { getTelemetryConfig, isEnabled as isTelemetryEnabled } from '../telemetry/config.js';
|
|
11
|
+
import { getCurrentVersion } from '../upgrade/checker.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Pipeline phases in order.
|
|
@@ -39,7 +40,7 @@ const QA_IMPLEMENTATION_THRESHOLD = 95;
|
|
|
39
40
|
* @returns {object} Pipeline state
|
|
40
41
|
*/
|
|
41
42
|
export function initPipeline(options = {}) {
|
|
42
|
-
const { isGreenfield = true, mode = 'discover' } = options;
|
|
43
|
+
const { isGreenfield = true, mode = 'discover', targetDir } = options;
|
|
43
44
|
|
|
44
45
|
const agents = {};
|
|
45
46
|
for (const agentDef of AGENT_PIPELINE) {
|
|
@@ -61,9 +62,25 @@ export function initPipeline(options = {}) {
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
const resolvedDir = targetDir || process.cwd();
|
|
66
|
+
|
|
67
|
+
// Initialize telemetry collector based on project config
|
|
68
|
+
initCollector(isTelemetryEnabled(resolvedDir));
|
|
69
|
+
|
|
70
|
+
const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
71
|
+
|
|
72
|
+
telemetryTrack('session_started', {
|
|
73
|
+
sessionId,
|
|
74
|
+
pipelineType: 'full',
|
|
75
|
+
mode,
|
|
76
|
+
});
|
|
77
|
+
|
|
64
78
|
return {
|
|
65
79
|
phase: mode,
|
|
66
80
|
isGreenfield,
|
|
81
|
+
sessionId,
|
|
82
|
+
targetDir: resolvedDir,
|
|
83
|
+
chatiVersion: getCurrentVersion(resolvedDir) || 'unknown',
|
|
67
84
|
startedAt: new Date().toISOString(),
|
|
68
85
|
completedAt: null,
|
|
69
86
|
agents,
|
|
@@ -96,7 +113,7 @@ const QUICK_FLOW_AGENTS = ['brief', 'dev', 'qa-implementation', 'devops'];
|
|
|
96
113
|
* @returns {object} Pipeline state
|
|
97
114
|
*/
|
|
98
115
|
export function initQuickFlowPipeline(options = {}) {
|
|
99
|
-
const { isGreenfield = false, mode = 'discover' } = options;
|
|
116
|
+
const { isGreenfield = false, mode = 'discover', targetDir } = options;
|
|
100
117
|
|
|
101
118
|
const agents = {};
|
|
102
119
|
for (const agentName of QUICK_FLOW_AGENTS) {
|
|
@@ -108,10 +125,26 @@ export function initQuickFlowPipeline(options = {}) {
|
|
|
108
125
|
};
|
|
109
126
|
}
|
|
110
127
|
|
|
128
|
+
const resolvedDir = targetDir || process.cwd();
|
|
129
|
+
|
|
130
|
+
// Initialize telemetry collector based on project config
|
|
131
|
+
initCollector(isTelemetryEnabled(resolvedDir));
|
|
132
|
+
|
|
133
|
+
const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
134
|
+
|
|
135
|
+
telemetryTrack('session_started', {
|
|
136
|
+
sessionId,
|
|
137
|
+
pipelineType: 'quick-flow',
|
|
138
|
+
mode,
|
|
139
|
+
});
|
|
140
|
+
|
|
111
141
|
return {
|
|
112
142
|
phase: mode,
|
|
113
143
|
isGreenfield,
|
|
114
144
|
isQuickFlow: true,
|
|
145
|
+
sessionId,
|
|
146
|
+
targetDir: resolvedDir,
|
|
147
|
+
chatiVersion: getCurrentVersion(resolvedDir) || 'unknown',
|
|
115
148
|
startedAt: new Date().toISOString(),
|
|
116
149
|
completedAt: null,
|
|
117
150
|
agents,
|
|
@@ -130,7 +163,7 @@ export function initQuickFlowPipeline(options = {}) {
|
|
|
130
163
|
* @returns {object} Pipeline state
|
|
131
164
|
*/
|
|
132
165
|
export function initStandardFlowPipeline(options = {}) {
|
|
133
|
-
const { isGreenfield = false, mode = 'discover' } = options;
|
|
166
|
+
const { isGreenfield = false, mode = 'discover', targetDir } = options;
|
|
134
167
|
|
|
135
168
|
const agents = {};
|
|
136
169
|
for (const agentName of STANDARD_FLOW_AGENTS) {
|
|
@@ -142,10 +175,26 @@ export function initStandardFlowPipeline(options = {}) {
|
|
|
142
175
|
};
|
|
143
176
|
}
|
|
144
177
|
|
|
178
|
+
const resolvedDir = targetDir || process.cwd();
|
|
179
|
+
|
|
180
|
+
// Initialize telemetry collector based on project config
|
|
181
|
+
initCollector(isTelemetryEnabled(resolvedDir));
|
|
182
|
+
|
|
183
|
+
const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
184
|
+
|
|
185
|
+
telemetryTrack('session_started', {
|
|
186
|
+
sessionId,
|
|
187
|
+
pipelineType: 'standard',
|
|
188
|
+
mode,
|
|
189
|
+
});
|
|
190
|
+
|
|
145
191
|
return {
|
|
146
192
|
phase: mode,
|
|
147
193
|
isGreenfield,
|
|
148
194
|
isStandardFlow: true,
|
|
195
|
+
sessionId,
|
|
196
|
+
targetDir: resolvedDir,
|
|
197
|
+
chatiVersion: getCurrentVersion(resolvedDir) || 'unknown',
|
|
149
198
|
startedAt: new Date().toISOString(),
|
|
150
199
|
completedAt: null,
|
|
151
200
|
agents,
|
|
@@ -245,13 +294,24 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
|
245
294
|
|
|
246
295
|
// Pipeline complete
|
|
247
296
|
newState.completedAt = new Date().toISOString();
|
|
297
|
+
const pipelineType1 = newState.isQuickFlow ? 'quick-flow' : newState.isStandardFlow ? 'standard' : 'full';
|
|
298
|
+
const totalDuration1 = Date.now() - new Date(newState.startedAt).getTime();
|
|
248
299
|
telemetryTrack('pipeline_completed', {
|
|
249
|
-
pipelineType:
|
|
250
|
-
totalDuration:
|
|
300
|
+
pipelineType: pipelineType1,
|
|
301
|
+
totalDuration: totalDuration1,
|
|
251
302
|
agentsRun: newState.completedAgents.length,
|
|
252
303
|
finalStatus: 'completed',
|
|
253
304
|
deviationCount: (newState.modeTransitions || []).length,
|
|
254
305
|
});
|
|
306
|
+
telemetryTrack('session_completed', {
|
|
307
|
+
sessionId: newState.sessionId || 'unknown',
|
|
308
|
+
pipelineType: pipelineType1,
|
|
309
|
+
mode: newState.phase === 'deploy' ? 'deploy' : 'build',
|
|
310
|
+
finalStage: newState.phase,
|
|
311
|
+
duration: totalDuration1,
|
|
312
|
+
agentCount: newState.completedAgents.length,
|
|
313
|
+
success: true,
|
|
314
|
+
});
|
|
255
315
|
const flushedEvents = telemetryFlush();
|
|
256
316
|
if (flushedEvents.length > 0) {
|
|
257
317
|
const tConfig = getTelemetryConfig(newState.targetDir || process.cwd());
|
|
@@ -306,13 +366,24 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
|
|
|
306
366
|
|
|
307
367
|
// Pipeline complete
|
|
308
368
|
newState.completedAt = new Date().toISOString();
|
|
369
|
+
const pipelineType2 = newState.isQuickFlow ? 'quick-flow' : newState.isStandardFlow ? 'standard' : 'full';
|
|
370
|
+
const totalDuration2 = Date.now() - new Date(newState.startedAt).getTime();
|
|
309
371
|
telemetryTrack('pipeline_completed', {
|
|
310
|
-
pipelineType:
|
|
311
|
-
totalDuration:
|
|
372
|
+
pipelineType: pipelineType2,
|
|
373
|
+
totalDuration: totalDuration2,
|
|
312
374
|
agentsRun: newState.completedAgents.length,
|
|
313
375
|
finalStatus: 'completed',
|
|
314
376
|
deviationCount: (newState.modeTransitions || []).length,
|
|
315
377
|
});
|
|
378
|
+
telemetryTrack('session_completed', {
|
|
379
|
+
sessionId: newState.sessionId || 'unknown',
|
|
380
|
+
pipelineType: pipelineType2,
|
|
381
|
+
mode: newState.phase === 'deploy' ? 'deploy' : 'build',
|
|
382
|
+
finalStage: newState.phase,
|
|
383
|
+
duration: totalDuration2,
|
|
384
|
+
agentCount: newState.completedAgents.length,
|
|
385
|
+
success: true,
|
|
386
|
+
});
|
|
316
387
|
const flushedEvents2 = telemetryFlush();
|
|
317
388
|
if (flushedEvents2.length > 0) {
|
|
318
389
|
const tConfig2 = getTelemetryConfig(newState.targetDir || process.cwd());
|
package/src/telemetry/config.js
CHANGED
|
@@ -24,7 +24,7 @@ export function getTelemetryConfig(targetDir) {
|
|
|
24
24
|
const configPath = join(targetDir, 'chati.dev', 'config.yaml');
|
|
25
25
|
|
|
26
26
|
const defaults = {
|
|
27
|
-
enabled:
|
|
27
|
+
enabled: true,
|
|
28
28
|
anonymousId: null,
|
|
29
29
|
endpoint: 'https://chati-telemetry.vercel.app/api/events',
|
|
30
30
|
apiKey: '10b0b54ba4f392fa46379ba778062ab0af5ca61e79609a7dce4aadd660104b56',
|
package/src/telemetry/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* All telemetry is opt-in and anonymous.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export { TELEMETRY_EVENTS, validateEvent } from './schema.js';
|
|
8
|
+
export { TELEMETRY_EVENTS, validateEvent, EVENT_PROPERTY_RULES } from './schema.js';
|
|
9
9
|
export { getTelemetryConfig, isEnabled, setEnabled, getAnonymousId } from './config.js';
|
|
10
10
|
export { initCollector, track, flush, getBufferSize, getStatus } from './collector.js';
|
|
11
11
|
export { sendEvents, DEFAULT_ENDPOINT } from './sender.js';
|
package/src/telemetry/schema.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Telemetry event schema and validation.
|
|
3
3
|
*
|
|
4
|
-
* Defines the
|
|
4
|
+
* Defines the 9 event types collected by telemetry.
|
|
5
5
|
* Zero PII — only anonymous usage metrics.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -16,6 +16,9 @@ export const TELEMETRY_EVENTS = [
|
|
|
16
16
|
'pipeline_completed',
|
|
17
17
|
'circuit_breaker_triggered',
|
|
18
18
|
'error_occurred',
|
|
19
|
+
'session_started',
|
|
20
|
+
'session_completed',
|
|
21
|
+
'token_usage',
|
|
19
22
|
];
|
|
20
23
|
|
|
21
24
|
// ---------------------------------------------------------------------------
|
|
@@ -44,18 +47,48 @@ const EVENT_PROPERTIES = {
|
|
|
44
47
|
error_occurred: [
|
|
45
48
|
'errorType', 'agent', 'provider', 'phase',
|
|
46
49
|
],
|
|
50
|
+
session_started: [
|
|
51
|
+
'sessionId', 'pipelineType', 'mode',
|
|
52
|
+
],
|
|
53
|
+
session_completed: [
|
|
54
|
+
'sessionId', 'pipelineType', 'mode', 'finalStage',
|
|
55
|
+
'duration', 'agentCount', 'success',
|
|
56
|
+
],
|
|
57
|
+
token_usage: [
|
|
58
|
+
'sessionId', 'agent', 'provider', 'model',
|
|
59
|
+
'inputTokens', 'outputTokens', 'totalTokens', 'estimatedCostUsd',
|
|
60
|
+
],
|
|
47
61
|
};
|
|
48
62
|
|
|
49
63
|
// ---------------------------------------------------------------------------
|
|
50
|
-
//
|
|
64
|
+
// Property Rules (required fields + numeric validation for structured types)
|
|
51
65
|
// ---------------------------------------------------------------------------
|
|
52
66
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
export const EVENT_PROPERTY_RULES = {
|
|
68
|
+
session_started: {
|
|
69
|
+
required: ['sessionId', 'pipelineType', 'mode'],
|
|
70
|
+
},
|
|
71
|
+
session_completed: {
|
|
72
|
+
required: ['sessionId', 'pipelineType', 'mode', 'finalStage', 'duration', 'agentCount', 'success'],
|
|
73
|
+
numeric: ['duration', 'agentCount'],
|
|
74
|
+
},
|
|
75
|
+
token_usage: {
|
|
76
|
+
required: ['sessionId', 'agent', 'provider', 'model', 'inputTokens', 'outputTokens', 'totalTokens', 'estimatedCostUsd'],
|
|
77
|
+
numeric: ['inputTokens', 'outputTokens', 'totalTokens', 'estimatedCostUsd'],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// PII patterns — fields that MUST NEVER appear in telemetry
|
|
83
|
+
// Regex patterns aligned with chati-telemetry backend (case-insensitive)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
const PII_PATTERNS = [
|
|
87
|
+
/^(file_?)?path$/i, /^file_?name$/i, /^dir(ectory)?$/i, /^cwd$/i,
|
|
88
|
+
/^api_?key$/i, /^(access_?)?token$/i, /^secret$/i, /^pass(word)?$/i,
|
|
89
|
+
/^cred(ential)?s?$/i, /^e?mail$/i, /^user_?name$/i, /^(full_?)?name$/i,
|
|
90
|
+
/^ip(_?addr(ess)?)?$/i, /^host_?name$/i, /^(source_?)?code$/i,
|
|
91
|
+
/^prompt$/i, /^message$/i, /^content$/i, /^stack_?trace$/i,
|
|
59
92
|
];
|
|
60
93
|
|
|
61
94
|
// ---------------------------------------------------------------------------
|
|
@@ -81,9 +114,9 @@ export function validateEvent(event) {
|
|
|
81
114
|
|
|
82
115
|
const props = event.properties || {};
|
|
83
116
|
|
|
84
|
-
// Check for PII
|
|
117
|
+
// Check for PII field names (regex-based, aligned with backend)
|
|
85
118
|
for (const key of Object.keys(props)) {
|
|
86
|
-
if (
|
|
119
|
+
if (PII_PATTERNS.some(pattern => pattern.test(key))) {
|
|
87
120
|
errors.push(`PII field detected: "${key}" — must not be included in telemetry`);
|
|
88
121
|
}
|
|
89
122
|
}
|
|
@@ -91,7 +124,7 @@ export function validateEvent(event) {
|
|
|
91
124
|
// Check for PII in values (paths, emails)
|
|
92
125
|
for (const [key, value] of Object.entries(props)) {
|
|
93
126
|
if (typeof value === 'string') {
|
|
94
|
-
if (
|
|
127
|
+
if (/\/users\//i.test(value) || /\\users\\/i.test(value) || /\/home\//i.test(value)) {
|
|
95
128
|
errors.push(`PII detected in "${key}": value contains filesystem path`);
|
|
96
129
|
}
|
|
97
130
|
if (value.includes('@') && value.includes('.')) {
|
|
@@ -100,5 +133,21 @@ export function validateEvent(event) {
|
|
|
100
133
|
}
|
|
101
134
|
}
|
|
102
135
|
|
|
136
|
+
// Property-level validation for event types with rules
|
|
137
|
+
const rules = EVENT_PROPERTY_RULES[event.type];
|
|
138
|
+
if (rules && props) {
|
|
139
|
+
const missing = rules.required.filter(key => !(key in props));
|
|
140
|
+
if (missing.length > 0) {
|
|
141
|
+
errors.push(`Missing required properties for "${event.type}": ${missing.join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
if (rules.numeric) {
|
|
144
|
+
for (const key of rules.numeric) {
|
|
145
|
+
if (key in props && (typeof props[key] !== 'number' || props[key] < 0)) {
|
|
146
|
+
errors.push(`Property "${key}" must be a non-negative number`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
103
152
|
return { valid: errors.length === 0, errors };
|
|
104
153
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* output format instructions.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync } from 'fs';
|
|
11
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { runPrism } from '../context/engine.js';
|
|
14
14
|
import { loadHandoff, formatHandoff } from '../tasks/handoff.js';
|
|
@@ -16,6 +16,7 @@ import { getWriteScope } from './isolation.js';
|
|
|
16
16
|
import { resolveOverlayPath } from '../installer/provider-overlay.js';
|
|
17
17
|
import { resolveProviderForAgent } from './cli-registry.js';
|
|
18
18
|
import { buildCompactGotchasSummary } from '../memory/gotchas-injector.js';
|
|
19
|
+
import { estimateTokens } from './cost-tracker.js';
|
|
19
20
|
|
|
20
21
|
// Import AGENT_MODELS from model-governance (safe — named export,
|
|
21
22
|
// does not trigger main() which is guarded by fileURLToPath check).
|
|
@@ -39,6 +40,69 @@ export const AGENT_FILE_MAP = {
|
|
|
39
40
|
devops: 'chati.dev/agents/deploy/devops.md',
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 3-Tier Tool Mesh — Token-aware tool loading profiles per agent.
|
|
45
|
+
*
|
|
46
|
+
* T1: Core tools (always loaded) — Read, Write, Edit, Bash, Grep, Glob
|
|
47
|
+
* T2: On-demand tools (loaded when relevant) — WebSearch, WebFetch, Task
|
|
48
|
+
* T3: External MCP tools (specialized) — playwright, docker, etc.
|
|
49
|
+
*
|
|
50
|
+
* Each agent gets a customized profile based on their role to save
|
|
51
|
+
* context window budget by not loading unnecessary tool instructions.
|
|
52
|
+
*/
|
|
53
|
+
export const TOOL_PROFILES = {
|
|
54
|
+
'greenfield-wu': { T1: ['Read', 'Glob', 'Grep', 'Bash'], T2: ['WebSearch'], T3: [] },
|
|
55
|
+
'brownfield-wu': { T1: ['Read', 'Glob', 'Grep', 'Bash'], T2: ['WebSearch'], T3: [] },
|
|
56
|
+
brief: { T1: ['Read', 'Glob', 'Grep'], T2: [], T3: [] },
|
|
57
|
+
detail: { T1: ['Read', 'Write', 'Edit', 'Glob', 'Grep'], T2: [], T3: [] },
|
|
58
|
+
architect: { T1: ['Read', 'Write', 'Edit', 'Glob', 'Grep'], T2: ['WebSearch', 'WebFetch'], T3: [] },
|
|
59
|
+
ux: { T1: ['Read', 'Write', 'Edit', 'Glob'], T2: ['WebSearch', 'WebFetch'], T3: [] },
|
|
60
|
+
phases: { T1: ['Read', 'Write', 'Edit', 'Glob', 'Grep'], T2: [], T3: [] },
|
|
61
|
+
tasks: { T1: ['Read', 'Write', 'Edit', 'Glob', 'Grep'], T2: [], T3: [] },
|
|
62
|
+
'qa-planning': { T1: ['Read', 'Glob', 'Grep'], T2: [], T3: [] },
|
|
63
|
+
dev: { T1: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'], T2: ['WebSearch', 'WebFetch', 'Task'], T3: ['playwright'] },
|
|
64
|
+
'qa-implementation': { T1: ['Read', 'Bash', 'Glob', 'Grep'], T2: ['Task'], T3: [] },
|
|
65
|
+
devops: { T1: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'], T2: ['WebSearch'], T3: ['docker'] },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the tool profile for an agent.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} agent - Agent name
|
|
72
|
+
* @returns {{ T1: string[], T2: string[], T3: string[] }}
|
|
73
|
+
*/
|
|
74
|
+
export function getToolProfile(agent) {
|
|
75
|
+
return TOOL_PROFILES[agent] || { T1: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'], T2: [], T3: [] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build tool mesh section for prompt injection.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} agent - Agent name
|
|
82
|
+
* @returns {string} Formatted tool mesh instructions
|
|
83
|
+
*/
|
|
84
|
+
export function buildToolMeshSection(agent) {
|
|
85
|
+
const profile = getToolProfile(agent);
|
|
86
|
+
const lines = [
|
|
87
|
+
'<!-- TOOL MESH -->',
|
|
88
|
+
'## Available Tools',
|
|
89
|
+
'',
|
|
90
|
+
`**Core (T1)**: ${profile.T1.join(', ')}`,
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
if (profile.T2.length > 0) {
|
|
94
|
+
lines.push(`**On-demand (T2)**: ${profile.T2.join(', ')} — use only when needed`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (profile.T3.length > 0) {
|
|
98
|
+
lines.push(`**External (T3)**: ${profile.T3.join(', ')} — available via MCP if configured`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push('', 'Prefer T1 tools for all standard operations. Use T2/T3 only when the task requires them.');
|
|
102
|
+
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
42
106
|
/**
|
|
43
107
|
* @typedef {object} PromptBuildConfig
|
|
44
108
|
* @property {string} agent - Agent name (e.g. 'detail')
|
|
@@ -127,6 +191,9 @@ export function buildAgentPrompt(config) {
|
|
|
127
191
|
|
|
128
192
|
const prompt = sections.join('\n\n---\n\n');
|
|
129
193
|
|
|
194
|
+
// 10. Prompt size guard — validate before returning
|
|
195
|
+
const sizeCheck = validatePromptSize(prompt, resolvedProvider);
|
|
196
|
+
|
|
130
197
|
return {
|
|
131
198
|
prompt,
|
|
132
199
|
model,
|
|
@@ -135,6 +202,7 @@ export function buildAgentPrompt(config) {
|
|
|
135
202
|
agent: config.agent,
|
|
136
203
|
layers: prismResult.layerCount || 0,
|
|
137
204
|
promptSize: prompt.length,
|
|
205
|
+
sizeCheck,
|
|
138
206
|
},
|
|
139
207
|
};
|
|
140
208
|
}
|
|
@@ -176,6 +244,7 @@ function buildPrismSection(config) {
|
|
|
176
244
|
handoff,
|
|
177
245
|
artifacts: state.artifacts || [],
|
|
178
246
|
taskCriteria: [],
|
|
247
|
+
userPrompt: config.additionalContext || null,
|
|
179
248
|
});
|
|
180
249
|
|
|
181
250
|
return {
|
|
@@ -292,6 +361,277 @@ function buildSessionSection(config, resolvedModelInfo = {}) {
|
|
|
292
361
|
return lines.join('\n');
|
|
293
362
|
}
|
|
294
363
|
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Prompt Size Guard
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Provider-specific token limits for prompt size validation.
|
|
370
|
+
*/
|
|
371
|
+
const PROVIDER_TOKEN_LIMITS = {
|
|
372
|
+
claude: 200_000,
|
|
373
|
+
gemini: 1_000_000,
|
|
374
|
+
codex: 128_000,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Validate prompt size against provider-specific limits.
|
|
379
|
+
*
|
|
380
|
+
* @param {string} prompt - The assembled prompt string
|
|
381
|
+
* @param {string} [provider='claude'] - Provider name for limit lookup
|
|
382
|
+
* @returns {{ valid: boolean, level: string, ratio: number, estimatedTokens: number, limit: number, message: string|null }}
|
|
383
|
+
*/
|
|
384
|
+
export function validatePromptSize(prompt, provider = 'claude') {
|
|
385
|
+
const limit = PROVIDER_TOKEN_LIMITS[provider] || PROVIDER_TOKEN_LIMITS.claude;
|
|
386
|
+
const tokens = estimateTokens(prompt);
|
|
387
|
+
const ratio = tokens / limit;
|
|
388
|
+
|
|
389
|
+
if (ratio >= 0.9) {
|
|
390
|
+
return {
|
|
391
|
+
valid: false,
|
|
392
|
+
level: 'error',
|
|
393
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
394
|
+
estimatedTokens: tokens,
|
|
395
|
+
limit,
|
|
396
|
+
message: `Prompt size (${tokens} tokens) exceeds 90% of ${provider} limit (${limit}). Prompt may be truncated or rejected.`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (ratio >= 0.7) {
|
|
401
|
+
return {
|
|
402
|
+
valid: true,
|
|
403
|
+
level: 'warning',
|
|
404
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
405
|
+
estimatedTokens: tokens,
|
|
406
|
+
limit,
|
|
407
|
+
message: `Prompt size (${tokens} tokens) exceeds 70% of ${provider} limit (${limit}). Consider reducing context.`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
valid: true,
|
|
413
|
+
level: 'ok',
|
|
414
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
415
|
+
estimatedTokens: tokens,
|
|
416
|
+
limit,
|
|
417
|
+
message: null,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// Tech Presets
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Load a tech preset YAML file by stack name.
|
|
427
|
+
*
|
|
428
|
+
* @param {string} stack - Stack identifier (e.g., 'nextjs', 'react-vite')
|
|
429
|
+
* @param {string} projectDir - Project root directory
|
|
430
|
+
* @returns {{ loaded: boolean, preset: object|null, stack: string }}
|
|
431
|
+
*/
|
|
432
|
+
export function loadPreset(stack, projectDir) {
|
|
433
|
+
if (!stack || !projectDir) {
|
|
434
|
+
return { loaded: false, preset: null, stack: stack || '' };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Check both deployed (chati.dev/presets/) and package (framework/presets/) locations
|
|
438
|
+
const locations = [
|
|
439
|
+
join(projectDir, 'chati.dev', 'presets'),
|
|
440
|
+
join(projectDir, 'framework', 'presets'),
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
for (const presetsDir of locations) {
|
|
444
|
+
const presetPath = join(presetsDir, `${stack}.yaml`);
|
|
445
|
+
if (!existsSync(presetPath)) continue;
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const raw = readFileSync(presetPath, 'utf-8');
|
|
449
|
+
const preset = parsePresetYaml(raw);
|
|
450
|
+
return { loaded: true, preset, stack };
|
|
451
|
+
} catch {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { loaded: false, preset: null, stack };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Detect which tech preset to load based on project files.
|
|
461
|
+
*
|
|
462
|
+
* @param {string} projectDir - Project root directory
|
|
463
|
+
* @returns {string|null} Detected stack name or null
|
|
464
|
+
*/
|
|
465
|
+
export function detectPreset(projectDir) {
|
|
466
|
+
if (!projectDir) return null;
|
|
467
|
+
|
|
468
|
+
// Check both deployed and package locations
|
|
469
|
+
const locations = [
|
|
470
|
+
join(projectDir, 'chati.dev', 'presets'),
|
|
471
|
+
join(projectDir, 'framework', 'presets'),
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
let presetsDir = null;
|
|
475
|
+
for (const loc of locations) {
|
|
476
|
+
if (existsSync(loc)) { presetsDir = loc; break; }
|
|
477
|
+
}
|
|
478
|
+
if (!presetsDir) return null;
|
|
479
|
+
|
|
480
|
+
// List all preset files
|
|
481
|
+
let presetFiles;
|
|
482
|
+
try {
|
|
483
|
+
presetFiles = readdirSync(presetsDir).filter(f => f.endsWith('.yaml'));
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Check package.json dependencies
|
|
489
|
+
const pkgPath = join(projectDir, 'package.json');
|
|
490
|
+
let pkgContent = '';
|
|
491
|
+
if (existsSync(pkgPath)) {
|
|
492
|
+
try { pkgContent = readFileSync(pkgPath, 'utf-8'); } catch { /* ignore */ }
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Check for detection markers in project root
|
|
496
|
+
for (const file of presetFiles) {
|
|
497
|
+
const raw = readFileSync(join(presetsDir, file), 'utf-8');
|
|
498
|
+
const preset = parsePresetYaml(raw);
|
|
499
|
+
if (!preset.detection || !Array.isArray(preset.detection)) continue;
|
|
500
|
+
|
|
501
|
+
for (const marker of preset.detection) {
|
|
502
|
+
// Check if marker is a file that exists in the project
|
|
503
|
+
if (existsSync(join(projectDir, marker))) {
|
|
504
|
+
return preset.stack || file.replace('.yaml', '');
|
|
505
|
+
}
|
|
506
|
+
// Check if marker is a dependency
|
|
507
|
+
if (pkgContent && pkgContent.includes(`"${marker}"`)) {
|
|
508
|
+
return preset.stack || file.replace('.yaml', '');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Parse a preset YAML file into a structured object.
|
|
518
|
+
* Uses lightweight regex parsing consistent with the framework.
|
|
519
|
+
*
|
|
520
|
+
* @param {string} raw - Raw YAML content
|
|
521
|
+
* @returns {object} Parsed preset
|
|
522
|
+
*/
|
|
523
|
+
export function parsePresetYaml(raw) {
|
|
524
|
+
const preset = {
|
|
525
|
+
stack: '',
|
|
526
|
+
displayName: '',
|
|
527
|
+
detection: [],
|
|
528
|
+
conventions: {},
|
|
529
|
+
structure: [],
|
|
530
|
+
rules: [],
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// stack:
|
|
534
|
+
const stackMatch = raw.match(/^stack:\s*(.+)$/m);
|
|
535
|
+
if (stackMatch) preset.stack = stackMatch[1].trim();
|
|
536
|
+
|
|
537
|
+
// displayName:
|
|
538
|
+
const nameMatch = raw.match(/^displayName:\s*"?([^"\n]+)"?$/m);
|
|
539
|
+
if (nameMatch) preset.displayName = nameMatch[1].trim();
|
|
540
|
+
|
|
541
|
+
// detection: (array)
|
|
542
|
+
const detectionMatch = raw.match(/^detection:\s*\n((?:\s+-\s*.+\n?)*)/m);
|
|
543
|
+
if (detectionMatch) {
|
|
544
|
+
const items = detectionMatch[1].matchAll(/^\s+-\s*"?([^"\n]+)"?\s*$/gm);
|
|
545
|
+
for (const item of items) {
|
|
546
|
+
preset.detection.push(item[1].trim());
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// conventions: (key-value map)
|
|
551
|
+
const convMatch = raw.match(/^conventions:\s*\n((?:\s+\w[\w]*:\s*.+\n?)*)/m);
|
|
552
|
+
if (convMatch) {
|
|
553
|
+
const pairs = convMatch[1].matchAll(/^\s+(\w[\w]*):\s*"?([^"\n]+)"?\s*$/gm);
|
|
554
|
+
for (const pair of pairs) {
|
|
555
|
+
preset.conventions[pair[1].trim()] = pair[2].trim();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// structure: (array of strings)
|
|
560
|
+
const structMatch = raw.match(/^structure:\s*\n((?:\s+-\s*.+\n?)*)/m);
|
|
561
|
+
if (structMatch) {
|
|
562
|
+
const items = structMatch[1].matchAll(/^\s+-\s*"?([^"\n]+)"?\s*$/gm);
|
|
563
|
+
for (const item of items) {
|
|
564
|
+
preset.structure.push(item[1].trim());
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// rules: (array of objects with id, text, priority)
|
|
569
|
+
const rulesMatch = raw.match(/^rules:\s*\n((?:\s+-.+\n(?:\s+\w.+\n?)*)*)/m);
|
|
570
|
+
if (rulesMatch) {
|
|
571
|
+
const ruleBlocks = rulesMatch[1].split(/(?=\s+-\s+id:)/);
|
|
572
|
+
for (const block of ruleBlocks) {
|
|
573
|
+
const idMatch = block.match(/id:\s*(\S+)/);
|
|
574
|
+
const textMatch = block.match(/text:\s*"?([^"\n]+)"?/);
|
|
575
|
+
const prioMatch = block.match(/priority:\s*(\S+)/);
|
|
576
|
+
if (idMatch && textMatch) {
|
|
577
|
+
preset.rules.push({
|
|
578
|
+
id: idMatch[1],
|
|
579
|
+
text: textMatch[1].trim(),
|
|
580
|
+
priority: prioMatch ? prioMatch[1] : 'normal',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return preset;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Build a formatted preset section for prompt injection.
|
|
591
|
+
*
|
|
592
|
+
* @param {object} preset - Parsed preset object
|
|
593
|
+
* @returns {string} Formatted preset section
|
|
594
|
+
*/
|
|
595
|
+
export function buildPresetSection(preset) {
|
|
596
|
+
if (!preset) return '';
|
|
597
|
+
|
|
598
|
+
const lines = [
|
|
599
|
+
'<!-- TECH PRESET -->',
|
|
600
|
+
`## Tech Preset: ${preset.displayName || preset.stack}`,
|
|
601
|
+
'',
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
// Conventions
|
|
605
|
+
if (preset.conventions && Object.keys(preset.conventions).length > 0) {
|
|
606
|
+
lines.push('### Conventions');
|
|
607
|
+
for (const [key, value] of Object.entries(preset.conventions)) {
|
|
608
|
+
lines.push(`- **${key}**: ${value}`);
|
|
609
|
+
}
|
|
610
|
+
lines.push('');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Structure
|
|
614
|
+
if (preset.structure && preset.structure.length > 0) {
|
|
615
|
+
lines.push('### Expected Structure');
|
|
616
|
+
for (const item of preset.structure) {
|
|
617
|
+
lines.push(`- ${item}`);
|
|
618
|
+
}
|
|
619
|
+
lines.push('');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Rules
|
|
623
|
+
if (preset.rules && preset.rules.length > 0) {
|
|
624
|
+
lines.push('### Rules');
|
|
625
|
+
for (const rule of preset.rules) {
|
|
626
|
+
const badge = rule.priority === 'critical' ? '[CRITICAL]' : rule.priority === 'high' ? '[HIGH]' : '';
|
|
627
|
+
lines.push(`- ${badge ? badge + ' ' : ''}${rule.text}`);
|
|
628
|
+
}
|
|
629
|
+
lines.push('');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return lines.join('\n');
|
|
633
|
+
}
|
|
634
|
+
|
|
295
635
|
/**
|
|
296
636
|
* Build output format instructions so the agent produces a parseable handoff.
|
|
297
637
|
*/
|