chati-dev 3.2.5 → 3.3.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/LICENSE +96 -0
- package/bin/chati.js +46 -0
- package/framework/agents/build/dev.md +122 -1
- package/framework/agents/deploy/devops.md +128 -3
- package/framework/agents/discover/brief.md +77 -15
- package/framework/agents/discover/brownfield-wu.md +2 -2
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +3 -3
- package/framework/agents/plan/phases.md +127 -2
- package/framework/agents/plan/tasks.md +127 -2
- package/framework/agents/plan/ux.md +269 -22
- package/framework/agents/quality/qa-implementation.md +172 -8
- package/framework/agents/quality/qa-planning.md +147 -2
- package/framework/config.yaml +9 -5
- package/framework/constitution.md +7 -1
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +1 -1
- package/framework/hooks/constitution-guard.js +18 -2
- package/framework/hooks/mode-governance.js +3 -3
- package/framework/hooks/read-protection.js +10 -2
- package/framework/i18n/en.yaml +6 -0
- package/framework/i18n/es.yaml +6 -0
- package/framework/i18n/fr.yaml +6 -0
- package/framework/i18n/pt.yaml +6 -0
- package/framework/orchestrator/chati.md +102 -6
- package/framework/schemas/task.schema.json +1 -1
- package/framework/tasks/architect-dep-audit.md +128 -0
- package/framework/tasks/architect-stack-selection.md +28 -0
- package/framework/workflows/brownfield-fullstack.yaml +2 -2
- package/framework/workflows/brownfield-service.yaml +2 -2
- package/framework/workflows/brownfield-ui.yaml +2 -2
- package/framework/workflows/greenfield-fullstack.yaml +6 -2
- package/framework/workflows/quick-flow.yaml +7 -5
- package/framework/workflows/standard-flow.yaml +171 -0
- package/package.json +4 -2
- package/src/api/index.js +129 -0
- package/src/autonomy/build-loop.js +93 -6
- package/src/autonomy/build-state.js +20 -2
- package/src/autonomy/cause-analyzer.js +177 -0
- package/src/autonomy/escalation.js +214 -0
- package/src/autonomy/safety-net.js +23 -5
- package/src/autonomy/worktree-manager.js +245 -0
- package/src/config/agent-customizer.js +227 -0
- package/src/config/ide-configs.js +57 -27
- package/src/decision/analyzer.js +148 -0
- package/src/decision/registry-healer.js +38 -21
- package/src/extensions/loader.js +151 -0
- package/src/extensions/registry.js +134 -0
- package/src/gates/circuit-breaker.js +32 -0
- package/src/gates/g3-implementation.js +30 -4
- package/src/gates/g4-qa-implementation.js +34 -5
- package/src/gates/gate-base.js +9 -0
- package/src/health/auto-fix.js +216 -0
- package/src/installer/core.js +24 -11
- package/src/installer/provider-overlay.js +82 -0
- package/src/installer/templates.js +22 -10
- package/src/installer/transaction.js +3 -2
- package/src/installer/validator.js +74 -0
- package/src/intelligence/context-status.js +9 -5
- package/src/intelligence/document-sharder.js +221 -0
- package/src/intelligence/elicitation.js +265 -0
- package/src/intelligence/timeline.js +5 -0
- package/src/memory/gotchas.js +78 -2
- package/src/merger/semantic-merger.js +292 -0
- package/src/orchestrator/agent-selector.js +20 -0
- package/src/orchestrator/handoff-engine.js +77 -0
- package/src/orchestrator/index.js +0 -8
- package/src/orchestrator/intent-classifier.js +182 -0
- package/src/orchestrator/pipeline-manager.js +125 -1
- package/src/orchestrator/session-manager.js +164 -2
- package/src/quality/metrics-collector.js +283 -0
- package/src/quality/test-runner.js +368 -0
- package/src/telemetry/collector.js +83 -0
- package/src/telemetry/config.js +119 -0
- package/src/telemetry/index.js +11 -0
- package/src/telemetry/schema.js +104 -0
- package/src/telemetry/sender.js +60 -0
- package/src/terminal/cli-registry.js +7 -1
- package/src/terminal/cost-tracker.js +197 -0
- package/src/terminal/handoff-parser.js +61 -4
- package/src/terminal/prompt-builder.js +56 -18
- package/src/terminal/rate-limiter.js +172 -0
- package/src/terminal/run-agent.js +39 -0
- package/src/terminal/run-parallel.js +22 -1
- package/src/terminal/spawner.js +181 -3
- package/src/upgrade/migrator.js +2 -2
- package/src/utils/event-bus.js +126 -0
- package/src/utils/file-lock.js +291 -0
- package/src/utils/schema-validator.js +226 -0
- package/src/wizard/i18n.js +11 -0
- package/src/wizard/index.js +42 -20
- package/src/wizard/questions.js +200 -39
- package/src/autonomy/execution-profile.js +0 -151
- package/src/intelligence/file-tracker.js +0 -117
- package/src/memory/gotchas-auto-capture.js +0 -253
- package/src/orchestrator/pipeline-state.js +0 -223
- package/src/terminal/wave-analyzer.js +0 -143
|
@@ -13,6 +13,9 @@ import { join } from 'path';
|
|
|
13
13
|
import { runPrism } from '../context/engine.js';
|
|
14
14
|
import { loadHandoff, formatHandoff } from '../tasks/handoff.js';
|
|
15
15
|
import { getWriteScope } from './isolation.js';
|
|
16
|
+
import { resolveOverlayPath } from '../installer/provider-overlay.js';
|
|
17
|
+
import { resolveProviderForAgent } from './cli-registry.js';
|
|
18
|
+
import { buildCompactGotchasSummary } from '../memory/gotchas-injector.js';
|
|
16
19
|
|
|
17
20
|
// Import AGENT_MODELS from model-governance (safe — named export,
|
|
18
21
|
// does not trigger main() which is guarded by fileURLToPath check).
|
|
@@ -46,6 +49,7 @@ export const AGENT_FILE_MAP = {
|
|
|
46
49
|
* @property {object} [sessionState] - Parsed session.yaml fields
|
|
47
50
|
* @property {string[]} [writeScope] - Override write scope
|
|
48
51
|
* @property {string} [additionalContext] - Extra context (e.g. user answer to needs_input)
|
|
52
|
+
* @property {string} [provider] - Active CLI provider (for overlay resolution)
|
|
49
53
|
*/
|
|
50
54
|
|
|
51
55
|
/**
|
|
@@ -70,8 +74,8 @@ export function buildAgentPrompt(config) {
|
|
|
70
74
|
sections.push(prismResult.xml);
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
// 2. Agent definition (.md file)
|
|
74
|
-
const agentDef = loadAgentDefinition(config.agent, config.projectDir);
|
|
77
|
+
// 2. Agent definition (.md file — resolved via overlay for secondary providers)
|
|
78
|
+
const agentDef = loadAgentDefinition(config.agent, config.projectDir, config.provider);
|
|
75
79
|
if (agentDef) {
|
|
76
80
|
sections.push('<!-- AGENT DEFINITION -->\n' + agentDef);
|
|
77
81
|
}
|
|
@@ -82,7 +86,20 @@ export function buildAgentPrompt(config) {
|
|
|
82
86
|
sections.push(handoffSection);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
// 4.
|
|
89
|
+
// 4. Gotchas (relevant past errors)
|
|
90
|
+
try {
|
|
91
|
+
const gotchasSection = buildCompactGotchasSummary(config.projectDir, {
|
|
92
|
+
agent: config.agent,
|
|
93
|
+
task: config.taskId,
|
|
94
|
+
});
|
|
95
|
+
if (gotchasSection) {
|
|
96
|
+
sections.push('<!-- GOTCHAS -->\n' + gotchasSection);
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Gotchas are advisory — never block prompt building
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 5. Additional context (user input relay for needs_input)
|
|
86
103
|
if (config.additionalContext) {
|
|
87
104
|
sections.push(
|
|
88
105
|
'<!-- ADDITIONAL CONTEXT -->\n' +
|
|
@@ -91,23 +108,29 @@ export function buildAgentPrompt(config) {
|
|
|
91
108
|
);
|
|
92
109
|
}
|
|
93
110
|
|
|
94
|
-
//
|
|
111
|
+
// 6. Write scope instructions
|
|
95
112
|
sections.push(buildWriteScopeSection(config));
|
|
96
113
|
|
|
97
|
-
//
|
|
98
|
-
|
|
114
|
+
// 7. Resolve provider/model (needed by session context AND output instructions)
|
|
115
|
+
const staticAssignment = AGENT_MODELS[config.agent] || { provider: 'claude', model: 'sonnet', tier: 'sonnet' };
|
|
116
|
+
const resolved = (config.projectDir && AGENT_MODELS[config.agent])
|
|
117
|
+
? resolveProviderForAgent(config.agent, config.projectDir, AGENT_MODELS)
|
|
118
|
+
: { provider: staticAssignment.provider, model: staticAssignment.model };
|
|
119
|
+
const resolvedProvider = config.provider || resolved.provider || 'claude';
|
|
120
|
+
const model = config.model || resolved.model || staticAssignment.model || 'sonnet';
|
|
121
|
+
|
|
122
|
+
// 8. Session context (uses resolved model/provider)
|
|
123
|
+
sections.push(buildSessionSection(config, { model, provider: resolvedProvider }));
|
|
99
124
|
|
|
100
|
-
//
|
|
101
|
-
sections.push(buildOutputInstructions());
|
|
125
|
+
// 9. Output format instructions (handoff template — includes provider/model for audit)
|
|
126
|
+
sections.push(buildOutputInstructions({ provider: resolvedProvider, model }));
|
|
102
127
|
|
|
103
128
|
const prompt = sections.join('\n\n---\n\n');
|
|
104
|
-
const assignment = AGENT_MODELS[config.agent] || { provider: 'claude', model: 'sonnet', tier: 'sonnet' };
|
|
105
|
-
const model = assignment.model || assignment;
|
|
106
129
|
|
|
107
130
|
return {
|
|
108
131
|
prompt,
|
|
109
132
|
model,
|
|
110
|
-
provider:
|
|
133
|
+
provider: resolvedProvider,
|
|
111
134
|
metadata: {
|
|
112
135
|
agent: config.agent,
|
|
113
136
|
layers: prismResult.layerCount || 0,
|
|
@@ -163,14 +186,23 @@ function buildPrismSection(config) {
|
|
|
163
186
|
|
|
164
187
|
/**
|
|
165
188
|
* Load the agent's full .md definition file.
|
|
189
|
+
* For non-primary providers, resolves via overlay directory.
|
|
166
190
|
*/
|
|
167
|
-
function loadAgentDefinition(agent, projectDir) {
|
|
191
|
+
function loadAgentDefinition(agent, projectDir, provider = null) {
|
|
168
192
|
const relativePath = AGENT_FILE_MAP[agent];
|
|
169
193
|
if (!relativePath) return null;
|
|
170
194
|
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
// Strip 'chati.dev/' prefix for overlay resolution
|
|
196
|
+
const relToFramework = relativePath.replace(/^chati\.dev\//, '');
|
|
197
|
+
|
|
198
|
+
let fullPath;
|
|
199
|
+
if (provider && provider !== 'claude') {
|
|
200
|
+
fullPath = resolveOverlayPath(projectDir, relToFramework, provider);
|
|
201
|
+
} else {
|
|
202
|
+
fullPath = join(projectDir, relativePath);
|
|
203
|
+
}
|
|
173
204
|
|
|
205
|
+
if (!existsSync(fullPath)) return null;
|
|
174
206
|
return readFileSync(fullPath, 'utf-8');
|
|
175
207
|
}
|
|
176
208
|
|
|
@@ -238,7 +270,7 @@ function buildWriteScopeSection(config) {
|
|
|
238
270
|
/**
|
|
239
271
|
* Build session context section.
|
|
240
272
|
*/
|
|
241
|
-
function buildSessionSection(config) {
|
|
273
|
+
function buildSessionSection(config, resolvedModelInfo = {}) {
|
|
242
274
|
const state = config.sessionState || {};
|
|
243
275
|
const project = state.project || {};
|
|
244
276
|
|
|
@@ -253,8 +285,8 @@ function buildSessionSection(config) {
|
|
|
253
285
|
`- **User Level**: ${state.user_level || 'auto'}`,
|
|
254
286
|
`- **Execution Mode**: ${state.execution_mode || 'autonomous'}`,
|
|
255
287
|
`- **Your Agent**: ${config.agent}`,
|
|
256
|
-
`- **Your Model**: ${
|
|
257
|
-
`- **Your Provider**: ${
|
|
288
|
+
`- **Your Model**: ${resolvedModelInfo.model || 'sonnet'}`,
|
|
289
|
+
`- **Your Provider**: ${resolvedModelInfo.provider || 'claude'}`,
|
|
258
290
|
];
|
|
259
291
|
|
|
260
292
|
return lines.join('\n');
|
|
@@ -263,7 +295,10 @@ function buildSessionSection(config) {
|
|
|
263
295
|
/**
|
|
264
296
|
* Build output format instructions so the agent produces a parseable handoff.
|
|
265
297
|
*/
|
|
266
|
-
function buildOutputInstructions() {
|
|
298
|
+
function buildOutputInstructions(config) {
|
|
299
|
+
const providerValue = config?.provider || 'claude';
|
|
300
|
+
const modelValue = config?.model || 'unknown';
|
|
301
|
+
|
|
267
302
|
return `<!-- OUTPUT INSTRUCTIONS -->
|
|
268
303
|
## Output Instructions (MANDATORY)
|
|
269
304
|
|
|
@@ -274,6 +309,8 @@ This block is how the orchestrator reads your results. Without it, your work can
|
|
|
274
309
|
<chati-handoff>
|
|
275
310
|
status: complete
|
|
276
311
|
score: 95
|
|
312
|
+
provider: ${providerValue}
|
|
313
|
+
model: ${modelValue}
|
|
277
314
|
summary: One to three sentence summary of what was accomplished.
|
|
278
315
|
outputs:
|
|
279
316
|
- path/to/artifact1.md
|
|
@@ -297,5 +334,6 @@ needs_input_question: null
|
|
|
297
334
|
- The \`<chati-handoff>\` block MUST appear in your response
|
|
298
335
|
- Score must be 0-100 (95+ to pass quality gate)
|
|
299
336
|
- List ALL artifacts you created/modified in outputs
|
|
337
|
+
- The \`provider\` and \`model\` fields MUST match: provider: ${providerValue}, model: ${modelValue}
|
|
300
338
|
- If you need user input, set status to "needs_input" and write your question in needs_input_question`;
|
|
301
339
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rate limiting for terminal spawner.
|
|
3
|
+
*
|
|
4
|
+
* Prevents exceeding API rate limits when spawning multiple agent
|
|
5
|
+
* processes. Uses a sliding window counter per provider.
|
|
6
|
+
*
|
|
7
|
+
* Constitution Article XIX — Multi-CLI Governance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Default Limits
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default rate limits per provider (requests per minute).
|
|
16
|
+
* Conservative defaults — can be overridden at creation time.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_LIMITS = {
|
|
19
|
+
claude: 20,
|
|
20
|
+
gemini: 15,
|
|
21
|
+
codex: 10,
|
|
22
|
+
copilot: 10,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Sliding window size in milliseconds (1 minute). */
|
|
26
|
+
const WINDOW_MS = 60_000;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// RateLimiter Class
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} RateLimiterStats
|
|
34
|
+
* @property {number} used - Requests used in current window
|
|
35
|
+
* @property {number} limit - Maximum requests per window
|
|
36
|
+
* @property {number} windowStart - Window start timestamp (ms)
|
|
37
|
+
* @property {number|null} nextAvailable - Timestamp when next slot opens (null if available now)
|
|
38
|
+
* @property {string} provider - Provider name
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a rate limiter instance for a specific provider.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} provider - Provider name (claude, gemini, codex, copilot)
|
|
45
|
+
* @param {{ limit?: number }} [customLimits={}]
|
|
46
|
+
* @returns {{ canSpawn: () => boolean, recordSpawn: () => void, waitForSlot: () => Promise<void>, getStats: () => RateLimiterStats, reset: () => void }}
|
|
47
|
+
*/
|
|
48
|
+
export function createRateLimiter(provider, customLimits = {}) {
|
|
49
|
+
const limit = customLimits.limit || DEFAULT_LIMITS[provider] || 10;
|
|
50
|
+
|
|
51
|
+
/** @type {number[]} Timestamps of recorded spawns */
|
|
52
|
+
let timestamps = [];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Prune timestamps outside the sliding window.
|
|
56
|
+
*/
|
|
57
|
+
function prune() {
|
|
58
|
+
const cutoff = Date.now() - WINDOW_MS;
|
|
59
|
+
timestamps = timestamps.filter(t => t > cutoff);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a spawn is allowed under current rate limits.
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
function canSpawn() {
|
|
67
|
+
prune();
|
|
68
|
+
return timestamps.length < limit;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Record a spawn event.
|
|
73
|
+
*/
|
|
74
|
+
function recordSpawn() {
|
|
75
|
+
timestamps.push(Date.now());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wait until a rate limit slot becomes available.
|
|
80
|
+
* Resolves immediately if a slot is available.
|
|
81
|
+
*
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
function waitForSlot() {
|
|
85
|
+
if (canSpawn()) {
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Calculate when the oldest timestamp exits the window
|
|
90
|
+
prune();
|
|
91
|
+
if (timestamps.length === 0) {
|
|
92
|
+
return Promise.resolve();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const oldestInWindow = timestamps[0];
|
|
96
|
+
const waitTime = Math.max(0, (oldestInWindow + WINDOW_MS) - Date.now() + 10);
|
|
97
|
+
|
|
98
|
+
return new Promise(resolve => {
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
prune();
|
|
101
|
+
resolve();
|
|
102
|
+
}, waitTime);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get current rate limiter statistics.
|
|
108
|
+
* @returns {RateLimiterStats}
|
|
109
|
+
*/
|
|
110
|
+
function getStats() {
|
|
111
|
+
prune();
|
|
112
|
+
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
const windowStart = now - WINDOW_MS;
|
|
115
|
+
|
|
116
|
+
let nextAvailable = null;
|
|
117
|
+
if (timestamps.length >= limit && timestamps.length > 0) {
|
|
118
|
+
nextAvailable = timestamps[0] + WINDOW_MS;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
used: timestamps.length,
|
|
123
|
+
limit,
|
|
124
|
+
windowStart,
|
|
125
|
+
nextAvailable,
|
|
126
|
+
provider,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Reset the rate limiter (clear all recorded timestamps).
|
|
132
|
+
*/
|
|
133
|
+
function reset() {
|
|
134
|
+
timestamps = [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { canSpawn, recordSpawn, waitForSlot, getStats, reset };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Global Registry
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/** @type {Map<string, ReturnType<typeof createRateLimiter>>} */
|
|
145
|
+
const _registry = new Map();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get or create a rate limiter for a provider.
|
|
149
|
+
* Ensures one limiter per provider across the application.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} provider - Provider name
|
|
152
|
+
* @param {{ limit?: number }} [customLimits={}]
|
|
153
|
+
* @returns {ReturnType<typeof createRateLimiter>}
|
|
154
|
+
*/
|
|
155
|
+
export function getRateLimiter(provider, customLimits = {}) {
|
|
156
|
+
if (!_registry.has(provider)) {
|
|
157
|
+
_registry.set(provider, createRateLimiter(provider, customLimits));
|
|
158
|
+
}
|
|
159
|
+
return _registry.get(provider);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Clear all registered rate limiters (useful in tests).
|
|
164
|
+
*/
|
|
165
|
+
export function clearAllLimiters() {
|
|
166
|
+
_registry.clear();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Exported for testing.
|
|
171
|
+
*/
|
|
172
|
+
export { WINDOW_MS };
|
|
@@ -17,6 +17,8 @@ import { fileURLToPath } from 'url';
|
|
|
17
17
|
import { buildAgentPrompt } from './prompt-builder.js';
|
|
18
18
|
import { spawnTerminal } from './spawner.js';
|
|
19
19
|
import { parseAgentOutput } from './handoff-parser.js';
|
|
20
|
+
import { createCostTracker } from './cost-tracker.js';
|
|
21
|
+
import { track as telemetryTrack } from '../telemetry/collector.js';
|
|
20
22
|
|
|
21
23
|
// ---------------------------------------------------------------------------
|
|
22
24
|
// CLI argument parsing (no external deps)
|
|
@@ -85,6 +87,7 @@ async function main() {
|
|
|
85
87
|
workflow: args.workflow || null,
|
|
86
88
|
sessionState,
|
|
87
89
|
additionalContext: args['additional-context'] || null,
|
|
90
|
+
provider: args.provider || null,
|
|
88
91
|
});
|
|
89
92
|
} catch (err) {
|
|
90
93
|
outputError(`Failed to build prompt: ${err.message}`);
|
|
@@ -100,6 +103,7 @@ async function main() {
|
|
|
100
103
|
agent: args.agent,
|
|
101
104
|
taskId: args['task-id'],
|
|
102
105
|
model: promptResult.model,
|
|
106
|
+
provider: promptResult.provider,
|
|
103
107
|
prompt: promptResult.prompt,
|
|
104
108
|
workingDir: projectDir,
|
|
105
109
|
timeout,
|
|
@@ -121,6 +125,37 @@ async function main() {
|
|
|
121
125
|
const stdout = handle.stdout.join('');
|
|
122
126
|
const stderr = handle.stderr.join('');
|
|
123
127
|
|
|
128
|
+
// Track cost metrics
|
|
129
|
+
const tracker = createCostTracker();
|
|
130
|
+
const costRecord = tracker.recordExecution({
|
|
131
|
+
agent: args.agent,
|
|
132
|
+
model: promptResult.model,
|
|
133
|
+
provider: promptResult.provider || args.provider || 'claude',
|
|
134
|
+
taskId: args['task-id'],
|
|
135
|
+
inputText: promptResult.prompt || '',
|
|
136
|
+
outputText: stdout,
|
|
137
|
+
duration: elapsed,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const costEstimate = {
|
|
141
|
+
inputTokens: costRecord.inputTokens,
|
|
142
|
+
outputTokens: costRecord.outputTokens,
|
|
143
|
+
totalCost: costRecord.cost,
|
|
144
|
+
model: costRecord.model,
|
|
145
|
+
provider: costRecord.provider,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Track telemetry (fire-and-forget, non-blocking)
|
|
149
|
+
telemetryTrack('agent_completed', {
|
|
150
|
+
agent: args.agent,
|
|
151
|
+
provider: costRecord.provider,
|
|
152
|
+
model: costRecord.model,
|
|
153
|
+
duration: elapsed,
|
|
154
|
+
score: null, // Score is determined by the gate, not the agent
|
|
155
|
+
retryCount: 0,
|
|
156
|
+
pipelineType: sessionState?.isQuickFlow ? 'quick-flow' : 'standard',
|
|
157
|
+
});
|
|
158
|
+
|
|
124
159
|
// Parse the handoff from stdout
|
|
125
160
|
const parsed = parseAgentOutput(stdout);
|
|
126
161
|
|
|
@@ -129,9 +164,11 @@ async function main() {
|
|
|
129
164
|
status: parsed.handoff.status,
|
|
130
165
|
agent: args.agent,
|
|
131
166
|
model: promptResult.model,
|
|
167
|
+
provider: promptResult.provider || args.provider || 'claude',
|
|
132
168
|
exitCode: handle.exitCode,
|
|
133
169
|
handoff: parsed.handoff,
|
|
134
170
|
elapsed,
|
|
171
|
+
costEstimate,
|
|
135
172
|
});
|
|
136
173
|
} else {
|
|
137
174
|
// No handoff block found — return raw output
|
|
@@ -139,11 +176,13 @@ async function main() {
|
|
|
139
176
|
status: handle.exitCode === 0 ? 'partial' : 'error',
|
|
140
177
|
agent: args.agent,
|
|
141
178
|
model: promptResult.model,
|
|
179
|
+
provider: promptResult.provider || args.provider || 'claude',
|
|
142
180
|
exitCode: handle.exitCode,
|
|
143
181
|
handoff: null,
|
|
144
182
|
rawOutput: stdout.slice(0, 5000), // Truncate to avoid huge JSON
|
|
145
183
|
stderr: stderr.slice(0, 2000),
|
|
146
184
|
elapsed,
|
|
185
|
+
costEstimate,
|
|
147
186
|
});
|
|
148
187
|
}
|
|
149
188
|
|
|
@@ -20,6 +20,7 @@ import { spawnParallelGroup } from './spawner.js';
|
|
|
20
20
|
import { TerminalMonitor } from './monitor.js';
|
|
21
21
|
import { collectResults, mergeHandoffs, buildConsolidatedHandoff } from './collector.js';
|
|
22
22
|
import { parseAgentOutput } from './handoff-parser.js';
|
|
23
|
+
import { estimateTokens, COST_PER_1K } from './cost-tracker.js';
|
|
23
24
|
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// CLI argument parsing
|
|
@@ -100,12 +101,14 @@ async function main() {
|
|
|
100
101
|
projectDir,
|
|
101
102
|
previousAgent,
|
|
102
103
|
sessionState,
|
|
104
|
+
provider: args.provider || null,
|
|
103
105
|
});
|
|
104
106
|
|
|
105
107
|
configs.push({
|
|
106
108
|
agent: agents[i],
|
|
107
109
|
taskId: taskIds[i],
|
|
108
110
|
model: promptResult.model,
|
|
111
|
+
provider: promptResult.provider,
|
|
109
112
|
prompt: promptResult.prompt,
|
|
110
113
|
workingDir: projectDir,
|
|
111
114
|
timeout,
|
|
@@ -150,6 +153,23 @@ async function main() {
|
|
|
150
153
|
|
|
151
154
|
const elapsed = Date.now() - startTime;
|
|
152
155
|
|
|
156
|
+
// Cost estimation per agent
|
|
157
|
+
const costEstimates = configs.map((cfg, i) => {
|
|
158
|
+
const terminal = group.terminals[i];
|
|
159
|
+
const inputTokens = estimateTokens(cfg.prompt || '');
|
|
160
|
+
const outputTokens = estimateTokens((terminal?.stdout || []).join(''));
|
|
161
|
+
const modelKey = cfg.model || 'sonnet';
|
|
162
|
+
const rate = COST_PER_1K[modelKey] || COST_PER_1K.sonnet || 0.015;
|
|
163
|
+
return {
|
|
164
|
+
agent: cfg.agent,
|
|
165
|
+
model: modelKey,
|
|
166
|
+
provider: cfg.provider || 'claude',
|
|
167
|
+
inputTokens,
|
|
168
|
+
outputTokens,
|
|
169
|
+
estimatedCost: ((inputTokens + outputTokens) / 1000) * rate,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
153
173
|
// Collect and merge results
|
|
154
174
|
const rawResults = collectResults(group.groupId, group.terminals);
|
|
155
175
|
|
|
@@ -197,6 +217,7 @@ async function main() {
|
|
|
197
217
|
parallelActual: elapsed,
|
|
198
218
|
timeSaved: elapsed * (agents.length - 1),
|
|
199
219
|
},
|
|
220
|
+
costEstimate: costEstimates,
|
|
200
221
|
};
|
|
201
222
|
|
|
202
223
|
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
@@ -213,7 +234,7 @@ async function main() {
|
|
|
213
234
|
* GROUP 2 (dev tasks) → qa-implementation
|
|
214
235
|
*/
|
|
215
236
|
function determineNextAgent(agents) {
|
|
216
|
-
const groupKey = agents.sort().join(',');
|
|
237
|
+
const groupKey = [...agents].sort().join(',');
|
|
217
238
|
if (groupKey.includes('architect') && groupKey.includes('detail') && groupKey.includes('ux')) {
|
|
218
239
|
return 'phases';
|
|
219
240
|
}
|