chati-dev 4.0.11 → 4.1.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 +14 -9
- package/bin/chati.js +17 -1
- package/framework/agents/build/dev.md +43 -10
- package/framework/agents/discover/brief.md +38 -8
- package/framework/agents/discover/brownfield-wu.md +44 -3
- package/framework/agents/discover/greenfield-wu.md +14 -0
- package/framework/agents/plan/architect-data-engineer.md +278 -0
- package/framework/agents/plan/architect-system.md +174 -0
- package/framework/agents/plan/architect.md +97 -301
- package/framework/agents/plan/detail.md +25 -0
- package/framework/agents/plan/tasks.md +29 -1
- package/framework/agents/plan/ux-brand-architect.md +215 -0
- package/framework/agents/plan/ux-component-engineer.md +289 -0
- package/framework/agents/plan/ux-researcher.md +166 -0
- package/framework/agents/plan/ux.md +126 -1008
- package/framework/agents/quality/qa-implementation.md +121 -22
- package/framework/agents/quality/qa-planning.md +18 -0
- package/framework/config.yaml +15 -4
- package/framework/constitution.md +9 -3
- package/framework/context/root.md +2 -2
- package/framework/data/entity-registry.yaml +59 -3
- package/framework/hooks/constitution-guard.js +67 -1
- package/framework/hooks/license-guard.js +4 -4
- package/framework/hooks/model-governance.js +2 -1
- package/framework/hooks/prism-engine.js +74 -6
- package/framework/hooks/read-protection.js +1 -1
- package/framework/hooks/session-digest.js +159 -7
- package/framework/hooks/settings.json +16 -4
- package/framework/hooks/style-guard.js +134 -0
- package/framework/hooks/undercover-guard.js +220 -0
- package/framework/i18n/en.yaml +2 -2
- package/framework/i18n/es.yaml +2 -2
- package/framework/i18n/fr.yaml +2 -2
- package/framework/i18n/pt.yaml +2 -2
- package/framework/intelligence/context-engine.md +21 -0
- package/framework/intelligence/hooks-performance.md +54 -0
- package/framework/intelligence/memory-layer.md +47 -0
- package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
- package/framework/orchestrator/chati.md +327 -1067
- package/framework/schemas/session.schema.json +1 -1
- package/framework/templates/brandbook-html-tmpl.md +107 -0
- package/framework/templates/session-memory-tmpl.yaml +68 -0
- package/package.json +1 -1
- package/scripts/doctor/checks/agents.js +1 -1
- package/scripts/health-check.js +1 -1
- package/src/autonomy/build-state.js +1 -1
- package/src/autonomy/worktree-manager.js +13 -13
- package/src/config/agent-customizer.js +1 -1
- package/src/config/gemini-hooks-generator.js +149 -6
- package/src/config/ide-configs.js +1 -1
- package/src/context/bracket-tracker.js +25 -4
- package/src/context/engine.js +37 -7
- package/src/context/formatter.js +45 -1
- package/src/context/index.js +3 -3
- package/src/dashboard/data-reader.js +7 -7
- package/src/decision/engine.js +2 -2
- package/src/decision/registry-healer.js +1 -1
- package/src/decision/registry-updater.js +2 -2
- package/src/extensions/loader.js +1 -1
- package/src/gates/g1-planning-complete.js +1 -1
- package/src/gates/g2-qa-planning.js +1 -1
- package/src/gates/g3-implementation.js +4 -4
- package/src/gates/g4-qa-implementation.js +2 -2
- package/src/health/auto-fix.js +3 -3
- package/src/health/engine.js +2 -2
- package/src/installer/brownfield-upgrader.js +3 -3
- package/src/installer/manifest.js +1 -1
- package/src/installer/transaction.js +1 -1
- package/src/installer/validator.js +4 -4
- package/src/intelligence/decision-engine.js +1 -1
- package/src/intelligence/memory-manager.js +1 -1
- package/src/intelligence/registry-manager.js +2 -2
- package/src/intelligence/timeline.js +1 -1
- package/src/license/client.js +1 -1
- package/src/license/commands.js +2 -2
- package/src/memory/agent-memory.js +3 -3
- package/src/memory/daily-digest.js +170 -0
- package/src/memory/dream.js +254 -0
- package/src/memory/gotchas.js +2 -2
- package/src/memory/index.js +18 -0
- package/src/memory/magic-docs.js +98 -0
- package/src/memory/memory-extractor.js +163 -0
- package/src/memory/session-digest.js +144 -6
- package/src/merger/yaml-merger.js +1 -1
- package/src/orchestrator/cli.js +980 -0
- package/src/orchestrator/handoff-engine.js +31 -4
- package/src/orchestrator/index.js +9 -0
- package/src/orchestrator/session-manager.js +1 -1
- package/src/preview/detector.js +3 -3
- package/src/preview/launcher.js +2 -2
- package/src/quality/metrics-collector.js +1 -1
- package/src/quality/test-runner.js +2 -4
- package/src/scanning/density-scanner.js +51 -0
- package/src/scanning/env-scanner.js +97 -0
- package/src/scanning/index.js +7 -0
- package/src/scanning/leakage-scanner.js +54 -0
- package/src/scanning/placeholder-scanner.js +40 -0
- package/src/scanning/security-scanner.js +94 -0
- package/src/security/bash-security.js +335 -0
- package/src/security/index.js +9 -0
- package/src/telemetry/config.js +3 -3
- package/src/telemetry/sender.js +1 -1
- package/src/terminal/cli-registry.js +1 -1
- package/src/terminal/cost-tracker.js +1 -2
- package/src/terminal/prompt-builder.js +11 -27
- package/src/terminal/rate-limiter.js +1 -2
- package/src/terminal/run-agent.js +3 -3
- package/src/terminal/spawner.js +41 -4
- package/src/upgrade/checker.js +2 -2
- package/src/upgrade/migrator.js +1 -1
- package/src/utils/config-parser.js +1 -1
- package/src/utils/file-lock.js +3 -3
- package/src/utils/provider-limits.js +35 -4
- package/src/wizard/i18n.js +4 -4
- package/src/wizard/index.js +2 -1
- package/src/wizard/questions.js +4 -2
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CLI handler for the `orchestrate` command.
|
|
3
|
+
*
|
|
4
|
+
* Exposes deterministic orchestration logic as JSON-outputting sub-commands.
|
|
5
|
+
* The thin orchestrator prompt calls these instead of reimplementing pipeline
|
|
6
|
+
* logic in natural language.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node bin/chati.js orchestrate <subcommand> [--flag value ...]
|
|
10
|
+
*
|
|
11
|
+
* Sub-commands: next, advance, init, validate-handoff, status, deviation, exit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
loadSession, initSession, updateSession, recordAgentCompletion,
|
|
16
|
+
recordModeTransition, getSessionSummary, validateSession,
|
|
17
|
+
releaseSession,
|
|
18
|
+
} from './index.js';
|
|
19
|
+
import {
|
|
20
|
+
getNextAgent, getAgentDefinition, AGENT_PIPELINE,
|
|
21
|
+
} from './index.js';
|
|
22
|
+
import {
|
|
23
|
+
initPipeline, initQuickFlowPipeline, initStandardFlowPipeline,
|
|
24
|
+
advancePipeline, checkPhaseTransition, getPipelineProgress,
|
|
25
|
+
isPipelineComplete, confirmPreview,
|
|
26
|
+
} from './index.js';
|
|
27
|
+
import {
|
|
28
|
+
validateHandoffIntegrity, loadHandoffContext, getHandoffHistory,
|
|
29
|
+
} from './index.js';
|
|
30
|
+
import { analyzeDeviationImpact, applyDeviation } from './index.js';
|
|
31
|
+
import { detectQuickFlow, detectStandardFlow } from './index.js';
|
|
32
|
+
import { AGENT_FILE_MAP } from '../terminal/prompt-builder.js';
|
|
33
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
34
|
+
import { join, resolve } from 'path';
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Constants
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
const INTERACTIVE_AGENTS = ['greenfield-wu', 'brownfield-wu', 'brief'];
|
|
41
|
+
|
|
42
|
+
const LOCK_START = '<!-- chati-lock:start -->';
|
|
43
|
+
const LOCK_END = '<!-- chati-lock:end -->';
|
|
44
|
+
|
|
45
|
+
const RESUME_MESSAGES = {
|
|
46
|
+
en: 'Session saved. Type /chati anytime to resume.',
|
|
47
|
+
pt: 'Sessao salva. Digite /chati para retomar.',
|
|
48
|
+
es: 'Sesion guardada. Escribe /chati para reanudar.',
|
|
49
|
+
fr: 'Session sauvee. Tapez /chati pour reprendre.',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Helpers
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function parseArgs(argv) {
|
|
57
|
+
const args = {};
|
|
58
|
+
for (let i = 0; i < argv.length; i++) {
|
|
59
|
+
const arg = argv[i];
|
|
60
|
+
if (arg.startsWith('--')) {
|
|
61
|
+
const key = arg.slice(2);
|
|
62
|
+
const next = argv[i + 1];
|
|
63
|
+
if (next && !next.startsWith('--')) {
|
|
64
|
+
args[key] = next;
|
|
65
|
+
i++;
|
|
66
|
+
} else {
|
|
67
|
+
args[key] = 'true';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function output(data) {
|
|
75
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function errorResult(message, code) {
|
|
79
|
+
return { action: 'error', error: message, code: code || 'UNKNOWN' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build a pipeline state object from session data.
|
|
84
|
+
* The session-manager and pipeline-manager use slightly different shapes;
|
|
85
|
+
* this bridges the two.
|
|
86
|
+
*/
|
|
87
|
+
function sessionToPipelineState(session, projectDir) {
|
|
88
|
+
const agents = {};
|
|
89
|
+
if (session.agents) {
|
|
90
|
+
for (const [name, data] of Object.entries(session.agents)) {
|
|
91
|
+
agents[name] = {
|
|
92
|
+
status: data.status || 'pending',
|
|
93
|
+
score: data.score ?? null,
|
|
94
|
+
startedAt: data.started_at || null,
|
|
95
|
+
completedAt: data.completed_at || null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
phase: session.mode || session.project?.state || 'discover',
|
|
102
|
+
isGreenfield: (session.project_type || session.project?.type) === 'greenfield',
|
|
103
|
+
sessionId: session.session_id || `${Date.now()}`,
|
|
104
|
+
targetDir: projectDir,
|
|
105
|
+
chatiVersion: session.version || 'unknown',
|
|
106
|
+
startedAt: session.started_at || new Date().toISOString(),
|
|
107
|
+
completedAt: null,
|
|
108
|
+
agents,
|
|
109
|
+
completedAgents: session.completed_agents || [],
|
|
110
|
+
currentAgent: session.current_agent || null,
|
|
111
|
+
modeTransitions: session.mode_transitions || [],
|
|
112
|
+
history: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the spawn command string for an autonomous agent.
|
|
118
|
+
*/
|
|
119
|
+
function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout) {
|
|
120
|
+
const parts = [
|
|
121
|
+
'node', 'packages/chati-dev/src/terminal/run-agent.js',
|
|
122
|
+
'--agent', agent,
|
|
123
|
+
'--task-id', `${agent}-task`,
|
|
124
|
+
'--project-dir', resolve(projectDir),
|
|
125
|
+
'--previous-agent', previousAgent || 'none',
|
|
126
|
+
'--provider', provider || 'claude',
|
|
127
|
+
'--timeout', String(timeout || 600000),
|
|
128
|
+
];
|
|
129
|
+
return parts.join(' ');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build the parallel spawn command string.
|
|
134
|
+
*/
|
|
135
|
+
function buildParallelSpawnCommand(agents, projectDir, previousAgent, provider, timeout) {
|
|
136
|
+
const taskIds = agents.map(a => `${a}-task`).join(',');
|
|
137
|
+
const parts = [
|
|
138
|
+
'node', 'packages/chati-dev/src/terminal/run-parallel.js',
|
|
139
|
+
'--agents', agents.join(','),
|
|
140
|
+
'--task-ids', taskIds,
|
|
141
|
+
'--project-dir', resolve(projectDir),
|
|
142
|
+
'--previous-agent', previousAgent || 'none',
|
|
143
|
+
'--provider', provider || 'claude',
|
|
144
|
+
'--timeout', String(timeout || 900000),
|
|
145
|
+
];
|
|
146
|
+
return parts.join(' ');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Model map — canonical model tier per agent.
|
|
151
|
+
* Matches framework/hooks/model-governance.js AGENT_MODELS.
|
|
152
|
+
*/
|
|
153
|
+
const AGENT_MODEL_MAP = {
|
|
154
|
+
'greenfield-wu': { provider: 'claude', model: 'haiku', upgrade: 'sonnet if multi-stack or enterprise' },
|
|
155
|
+
'brownfield-wu': { provider: 'claude', model: 'opus', upgrade: 'no downgrade' },
|
|
156
|
+
brief: { provider: 'claude', model: 'sonnet', upgrade: 'opus if enterprise or 10+ integrations' },
|
|
157
|
+
detail: { provider: 'claude', model: 'opus', upgrade: 'no downgrade' },
|
|
158
|
+
architect: { provider: 'claude', model: 'opus', upgrade: 'no downgrade' },
|
|
159
|
+
ux: { provider: 'claude', model: 'sonnet', upgrade: 'opus if design system from scratch' },
|
|
160
|
+
phases: { provider: 'claude', model: 'sonnet', upgrade: 'opus if 20+ requirements' },
|
|
161
|
+
tasks: { provider: 'claude', model: 'sonnet', upgrade: 'opus if 50+ tasks' },
|
|
162
|
+
'qa-planning': { provider: 'claude', model: 'opus', upgrade: 'no downgrade' },
|
|
163
|
+
dev: { provider: 'claude', model: 'opus', upgrade: 'no downgrade' },
|
|
164
|
+
'qa-implementation': { provider: 'claude', model: 'opus', upgrade: 'no downgrade' },
|
|
165
|
+
devops: { provider: 'claude', model: 'sonnet', upgrade: 'opus if multi-environment or IaC' },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve provider and model for an agent.
|
|
170
|
+
* Checks config.yaml for agent_overrides, falls back to AGENT_MODEL_MAP defaults.
|
|
171
|
+
*/
|
|
172
|
+
function resolveAgentModel(agent, projectDir) {
|
|
173
|
+
const defaults = AGENT_MODEL_MAP[agent] || { provider: 'claude', model: 'sonnet', upgrade: '' };
|
|
174
|
+
|
|
175
|
+
// Check config.yaml for agent_overrides
|
|
176
|
+
try {
|
|
177
|
+
const configPath = join(projectDir, 'chati.dev', 'config.yaml');
|
|
178
|
+
if (existsSync(configPath)) {
|
|
179
|
+
const configRaw = readFileSync(configPath, 'utf-8');
|
|
180
|
+
// Simple YAML parsing for agent_overrides section
|
|
181
|
+
const overrideMatch = configRaw.match(new RegExp(`${agent}:\\s*\\n\\s+provider:\\s*(\\w+)`, 'm'));
|
|
182
|
+
if (overrideMatch) {
|
|
183
|
+
return { ...defaults, provider: overrideMatch[1] };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch { /* expected: config may not exist or be malformed */ }
|
|
187
|
+
|
|
188
|
+
return defaults;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Estimate context bracket from pipeline progress.
|
|
193
|
+
*/
|
|
194
|
+
function estimateContextBracket(completedCount, totalCount) {
|
|
195
|
+
const progress = totalCount > 0 ? completedCount / totalCount : 0;
|
|
196
|
+
// Simple heuristic: context degrades with pipeline progress + conversation length
|
|
197
|
+
if (progress < 0.3) return { bracket: 'FRESH', remaining: 85 };
|
|
198
|
+
if (progress < 0.6) return { bracket: 'MODERATE', remaining: 60 };
|
|
199
|
+
if (progress < 0.85) return { bracket: 'DEPLETED', remaining: 30 };
|
|
200
|
+
return { bracket: 'CRITICAL', remaining: 10 };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Write session lock block to CLAUDE.local.md.
|
|
205
|
+
*/
|
|
206
|
+
function writeSessionLock(projectDir, currentAgent) {
|
|
207
|
+
const localMdPath = join(projectDir, 'CLAUDE.local.md');
|
|
208
|
+
let content = '';
|
|
209
|
+
if (existsSync(localMdPath)) {
|
|
210
|
+
content = readFileSync(localMdPath, 'utf-8');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Remove existing lock if present
|
|
214
|
+
const startIdx = content.indexOf(LOCK_START);
|
|
215
|
+
const endIdx = content.indexOf(LOCK_END);
|
|
216
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
217
|
+
content = content.slice(0, startIdx) + content.slice(endIdx + LOCK_END.length);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const lockBlock = `${LOCK_START}
|
|
221
|
+
## Session Lock -- ACTIVE
|
|
222
|
+
|
|
223
|
+
**Chati.dev session is ACTIVE.** Follow these rules for EVERY message:
|
|
224
|
+
|
|
225
|
+
1. Read \`chati.dev/orchestrator/chati.md\` and follow its routing logic
|
|
226
|
+
2. Route ALL user messages through the current agent: \`${currentAgent}\`
|
|
227
|
+
3. NEVER respond outside of the Chati.dev system
|
|
228
|
+
4. The ONLY way to exit is via \`/chati exit\`, \`/chati stop\`, or \`/chati quit\`
|
|
229
|
+
${LOCK_END}
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
content = content.trimEnd() + '\n\n' + lockBlock;
|
|
233
|
+
writeFileSync(localMdPath, content, 'utf-8');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Remove session lock block from CLAUDE.local.md.
|
|
238
|
+
*/
|
|
239
|
+
function removeSessionLock(projectDir, resumeMsg) {
|
|
240
|
+
const localMdPath = join(projectDir, 'CLAUDE.local.md');
|
|
241
|
+
if (!existsSync(localMdPath)) return;
|
|
242
|
+
|
|
243
|
+
let content = readFileSync(localMdPath, 'utf-8');
|
|
244
|
+
|
|
245
|
+
const startIdx = content.indexOf(LOCK_START);
|
|
246
|
+
const endIdx = content.indexOf(LOCK_END);
|
|
247
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
248
|
+
content = content.slice(0, startIdx) + content.slice(endIdx + LOCK_END.length);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (resumeMsg) {
|
|
252
|
+
content = content.trimEnd() + `\n\n## Session Paused\n\n${resumeMsg}\n`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
writeFileSync(localMdPath, content.trimEnd() + '\n', 'utf-8');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Sub-command handlers
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
async function handleNext(projectDir) {
|
|
263
|
+
const { loaded, session } = loadSession(projectDir);
|
|
264
|
+
if (!loaded || !session) {
|
|
265
|
+
return { action: 'setup', status_summary: 'No session found. Project needs initialization.' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const validation = validateSession(projectDir);
|
|
269
|
+
if (!validation.valid) {
|
|
270
|
+
return errorResult(`Session invalid: ${validation.reason}`, 'SESSION_INVALID');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const pipelineState = sessionToPipelineState(session, projectDir);
|
|
274
|
+
|
|
275
|
+
if (isPipelineComplete(pipelineState)) {
|
|
276
|
+
return {
|
|
277
|
+
action: 'complete',
|
|
278
|
+
pipeline_progress: getPipelineProgress(pipelineState),
|
|
279
|
+
session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode },
|
|
280
|
+
status_summary: 'Pipeline complete. All agents finished.',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check for in-progress agent (resume scenario)
|
|
285
|
+
const currentAgent = session.current_agent || null;
|
|
286
|
+
const completedAgents = session.completed_agents || [];
|
|
287
|
+
if (currentAgent && !completedAgents.includes(currentAgent)) {
|
|
288
|
+
const agent = currentAgent;
|
|
289
|
+
const agentStatus = session.agents?.[agent]?.status;
|
|
290
|
+
if (agentStatus === 'in_progress' || (agentStatus !== 'completed' && agentStatus !== 'skipped')) {
|
|
291
|
+
const agentFile = AGENT_FILE_MAP[agent] || null;
|
|
292
|
+
const agentDef = getAgentDefinition(agent);
|
|
293
|
+
const isInteractive = INTERACTIVE_AGENTS.includes(agent);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
action: isInteractive ? 'activate_interactive' : 'spawn_autonomous',
|
|
297
|
+
agent,
|
|
298
|
+
agent_file: agentFile,
|
|
299
|
+
phase: agentDef?.phase || session.mode,
|
|
300
|
+
spawn_command: isInteractive ? null : buildSpawnCommand(agent, projectDir, session.last_handoff || 'none', resolveAgentModel(agent, projectDir).provider, 600000),
|
|
301
|
+
parallel_spawn_command: null,
|
|
302
|
+
parallel_agents: [],
|
|
303
|
+
handoff_status: { valid: true, missing: [], warnings: ['Resuming in-progress agent'] },
|
|
304
|
+
gate_status: { canAdvance: true, reason: 'Agent already in progress' },
|
|
305
|
+
model_info: resolveAgentModel(agent, projectDir),
|
|
306
|
+
context_bracket: estimateContextBracket(completedAgents.length, AGENT_PIPELINE.length),
|
|
307
|
+
pipeline_progress: getPipelineProgress(pipelineState),
|
|
308
|
+
session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode, user_level: session.user_level || 'auto' },
|
|
309
|
+
status_summary: `Resuming ${agent} (in progress).`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Determine last completed agent
|
|
315
|
+
const lastAgent = session.last_handoff || session.current_agent || null;
|
|
316
|
+
|
|
317
|
+
// If no agent has started yet, route to first agent
|
|
318
|
+
if (!lastAgent || completedAgents.length === 0) {
|
|
319
|
+
const projectType = session.project_type || session.project?.type || 'greenfield';
|
|
320
|
+
const firstAgent = projectType === 'brownfield' ? 'brownfield-wu' : 'greenfield-wu';
|
|
321
|
+
const agentFile = AGENT_FILE_MAP[firstAgent] || null;
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
action: 'activate_interactive',
|
|
325
|
+
agent: firstAgent,
|
|
326
|
+
agent_file: agentFile,
|
|
327
|
+
phase: 'discover',
|
|
328
|
+
spawn_command: null,
|
|
329
|
+
parallel_spawn_command: null,
|
|
330
|
+
parallel_agents: [],
|
|
331
|
+
handoff_status: { valid: true, missing: [], warnings: [] },
|
|
332
|
+
gate_status: { canAdvance: true, reason: 'First agent in pipeline' },
|
|
333
|
+
model_info: resolveAgentModel(firstAgent, projectDir),
|
|
334
|
+
context_bracket: estimateContextBracket(0, AGENT_PIPELINE.length),
|
|
335
|
+
pipeline_progress: getPipelineProgress(pipelineState),
|
|
336
|
+
session: { language: session.language, project_type: projectType, execution_mode: session.execution_mode, user_level: session.user_level || 'auto' },
|
|
337
|
+
status_summary: `Starting pipeline with ${firstAgent}.`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Get next agent
|
|
342
|
+
const nextInfo = getNextAgent(lastAgent, completedAgents);
|
|
343
|
+
|
|
344
|
+
if (!nextInfo || !nextInfo.next) {
|
|
345
|
+
return {
|
|
346
|
+
action: 'complete',
|
|
347
|
+
pipeline_progress: getPipelineProgress(pipelineState),
|
|
348
|
+
session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode },
|
|
349
|
+
status_summary: 'Pipeline complete. All agents finished.',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const nextAgent = nextInfo.next;
|
|
354
|
+
const agentFile = AGENT_FILE_MAP[nextAgent] || null;
|
|
355
|
+
const agentDef = getAgentDefinition(nextAgent);
|
|
356
|
+
const isInteractive = INTERACTIVE_AGENTS.includes(nextAgent);
|
|
357
|
+
|
|
358
|
+
// Validate handoff from previous agent
|
|
359
|
+
let handoffStatus;
|
|
360
|
+
try {
|
|
361
|
+
const handoffCtx = await loadHandoffContext(projectDir, lastAgent);
|
|
362
|
+
if (handoffCtx.context.handoff) {
|
|
363
|
+
handoffStatus = validateHandoffIntegrity(handoffCtx.context.handoff, nextAgent);
|
|
364
|
+
} else {
|
|
365
|
+
handoffStatus = { valid: false, missing: ['handoff document'], warnings: [`No handoff found from ${lastAgent}`] };
|
|
366
|
+
}
|
|
367
|
+
} catch { /* expected: handoff may not exist for first agent */
|
|
368
|
+
handoffStatus = { valid: true, missing: [], warnings: ['Could not load handoff (may be first agent)'] };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check quality gate
|
|
372
|
+
const gateStatus = checkPhaseTransition(pipelineState);
|
|
373
|
+
|
|
374
|
+
// Resolve model for next agent
|
|
375
|
+
const modelInfo = resolveAgentModel(nextAgent, projectDir);
|
|
376
|
+
|
|
377
|
+
// Check for parallel group
|
|
378
|
+
let action, spawnCommand = null, parallelSpawnCommand = null, parallelAgents = [];
|
|
379
|
+
if (nextInfo.isParallel && nextInfo.group.length > 1) {
|
|
380
|
+
action = 'spawn_parallel';
|
|
381
|
+
parallelAgents = nextInfo.group;
|
|
382
|
+
parallelSpawnCommand = buildParallelSpawnCommand(nextInfo.group, projectDir, lastAgent, modelInfo.provider, 900000);
|
|
383
|
+
} else if (isInteractive) {
|
|
384
|
+
action = 'activate_interactive';
|
|
385
|
+
} else {
|
|
386
|
+
action = 'spawn_autonomous';
|
|
387
|
+
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const progress = getPipelineProgress(pipelineState);
|
|
391
|
+
const bracket = estimateContextBracket(completedAgents.length, AGENT_PIPELINE.length);
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
action,
|
|
395
|
+
agent: nextAgent,
|
|
396
|
+
agent_file: agentFile,
|
|
397
|
+
phase: agentDef?.phase || session.mode,
|
|
398
|
+
spawn_command: spawnCommand,
|
|
399
|
+
parallel_spawn_command: parallelSpawnCommand,
|
|
400
|
+
parallel_agents: parallelAgents,
|
|
401
|
+
handoff_status: handoffStatus,
|
|
402
|
+
gate_status: gateStatus,
|
|
403
|
+
model_info: modelInfo,
|
|
404
|
+
context_bracket: bracket,
|
|
405
|
+
pipeline_progress: progress,
|
|
406
|
+
session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode, user_level: session.user_level || 'auto' },
|
|
407
|
+
status_summary: `${lastAgent} completed. Next: ${nextAgent} (${action.replace('_', ' ')}, ${agentDef?.phase || 'unknown'} phase).`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function handleAdvance(projectDir, args) {
|
|
412
|
+
const agent = args.agent;
|
|
413
|
+
const score = parseInt(args.score, 10);
|
|
414
|
+
const status = args.status || 'completed';
|
|
415
|
+
const decision = args.decision || null;
|
|
416
|
+
|
|
417
|
+
if (!agent) return errorResult('Missing --agent flag', 'MISSING_AGENT');
|
|
418
|
+
if (isNaN(score)) return errorResult('Missing or invalid --score flag', 'MISSING_SCORE');
|
|
419
|
+
|
|
420
|
+
const { loaded, session } = loadSession(projectDir);
|
|
421
|
+
if (!loaded || !session) return errorResult('No session found', 'NO_SESSION');
|
|
422
|
+
|
|
423
|
+
// Handle user_preview decisions
|
|
424
|
+
if (decision) {
|
|
425
|
+
const pipelineState = sessionToPipelineState(session, projectDir);
|
|
426
|
+
const previewResult = confirmPreview(pipelineState, decision);
|
|
427
|
+
|
|
428
|
+
await updateSession(projectDir, {
|
|
429
|
+
current_agent: previewResult.nextAgent,
|
|
430
|
+
mode: previewResult.needsModeSwitch ? 'deploy' : session.mode,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (previewResult.needsModeSwitch) {
|
|
434
|
+
await recordModeTransition(projectDir, {
|
|
435
|
+
from: session.mode,
|
|
436
|
+
to: 'deploy',
|
|
437
|
+
trigger: `user_preview: ${decision}`,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const next = await handleNext(projectDir);
|
|
442
|
+
return { advanced: true, agent_completed: agent, score, decision, phase_transition: { triggered: previewResult.needsModeSwitch }, next };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Record completion
|
|
446
|
+
await recordAgentCompletion(projectDir, { agent, status, score });
|
|
447
|
+
|
|
448
|
+
// Advance pipeline
|
|
449
|
+
const pipelineState = sessionToPipelineState(session, projectDir);
|
|
450
|
+
pipelineState.completedAgents = [...(session.completed_agents || []), agent];
|
|
451
|
+
pipelineState.agents[agent] = { status: 'completed', score, startedAt: null, completedAt: new Date().toISOString() };
|
|
452
|
+
|
|
453
|
+
const advanceResult = advancePipeline(pipelineState, agent, { score });
|
|
454
|
+
|
|
455
|
+
// Update session with new state
|
|
456
|
+
const updates = {
|
|
457
|
+
current_agent: advanceResult.nextAgent || '',
|
|
458
|
+
last_handoff: agent,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Handle phase transition
|
|
462
|
+
let phaseTransition = { triggered: false };
|
|
463
|
+
if (advanceResult.needsModeSwitch) {
|
|
464
|
+
const newPhase = advanceResult.state?.phase || session.mode;
|
|
465
|
+
updates.mode = newPhase;
|
|
466
|
+
if (session.project) {
|
|
467
|
+
updates.project = { ...session.project, state: newPhase };
|
|
468
|
+
}
|
|
469
|
+
phaseTransition = { triggered: true, from: session.mode, to: newPhase };
|
|
470
|
+
|
|
471
|
+
await recordModeTransition(projectDir, {
|
|
472
|
+
from: session.mode,
|
|
473
|
+
to: newPhase,
|
|
474
|
+
trigger: `${agent} completed with score ${score}`,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
await updateSession(projectDir, updates);
|
|
479
|
+
|
|
480
|
+
// Check if user_preview is needed
|
|
481
|
+
if (advanceResult.nextAction === 'user_preview') {
|
|
482
|
+
return {
|
|
483
|
+
advanced: true,
|
|
484
|
+
agent_completed: agent,
|
|
485
|
+
score,
|
|
486
|
+
phase_transition: phaseTransition,
|
|
487
|
+
next: {
|
|
488
|
+
action: 'user_preview',
|
|
489
|
+
agent: agent,
|
|
490
|
+
pipeline_progress: getPipelineProgress(advanceResult.state || pipelineState),
|
|
491
|
+
session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode },
|
|
492
|
+
status_summary: `${agent} completed with score ${score}%. User preview required before deploy.`,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const next = await handleNext(projectDir);
|
|
498
|
+
return { advanced: true, agent_completed: agent, score, phase_transition: phaseTransition, next };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function handleInit(projectDir, args) {
|
|
502
|
+
// License validation at init time (moved from per-prompt UserPromptSubmit hook)
|
|
503
|
+
try {
|
|
504
|
+
const licensePath = join(process.env.HOME || '', '.chati-dev', 'license.yaml');
|
|
505
|
+
if (existsSync(licensePath)) {
|
|
506
|
+
const licenseRaw = readFileSync(licensePath, 'utf-8');
|
|
507
|
+
const statusMatch = licenseRaw.match(/status:\s*["']?(\w+)/);
|
|
508
|
+
const checkedMatch = licenseRaw.match(/checked_at:\s*["']?([^"'\n]+)/);
|
|
509
|
+
if (statusMatch && statusMatch[1] !== 'VALID') {
|
|
510
|
+
return errorResult(`License status: ${statusMatch[1]}. Run: npx chati-dev activate --key=YOUR-KEY`, 'LICENSE_INVALID');
|
|
511
|
+
}
|
|
512
|
+
// Check cache freshness (24h TTL)
|
|
513
|
+
if (checkedMatch) {
|
|
514
|
+
const age = Date.now() - new Date(checkedMatch[1]).getTime();
|
|
515
|
+
if (age > 24 * 60 * 60 * 1000) {
|
|
516
|
+
process.stderr.write('[chati] license cache expired (24h) — will refresh on next API call\n');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// No license file = allow (fail-open, same as license-guard behavior)
|
|
521
|
+
} catch { /* expected: license file may not exist */ }
|
|
522
|
+
|
|
523
|
+
const type = args.type || 'greenfield';
|
|
524
|
+
const language = args.language || 'en';
|
|
525
|
+
const name = args.name || '';
|
|
526
|
+
const workflow = args.workflow || 'full';
|
|
527
|
+
|
|
528
|
+
const isGreenfield = type === 'greenfield';
|
|
529
|
+
|
|
530
|
+
// Initialize pipeline state based on workflow
|
|
531
|
+
let pipelineAgents;
|
|
532
|
+
if (workflow === 'quick') {
|
|
533
|
+
const p = initQuickFlowPipeline({ isGreenfield, targetDir: projectDir });
|
|
534
|
+
pipelineAgents = Object.keys(p.agents);
|
|
535
|
+
} else if (workflow === 'standard') {
|
|
536
|
+
const p = initStandardFlowPipeline({ isGreenfield, targetDir: projectDir });
|
|
537
|
+
pipelineAgents = Object.keys(p.agents);
|
|
538
|
+
} else {
|
|
539
|
+
const p = initPipeline({ isGreenfield, targetDir: projectDir });
|
|
540
|
+
pipelineAgents = Object.keys(p.agents);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Initialize session
|
|
544
|
+
const result = initSession(projectDir, {
|
|
545
|
+
mode: 'discover',
|
|
546
|
+
projectName: name,
|
|
547
|
+
isGreenfield,
|
|
548
|
+
language,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
if (!result.created) {
|
|
552
|
+
return errorResult(`Failed to initialize session: ${result.error}`, 'INIT_FAILED');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Set workflow in session
|
|
556
|
+
await updateSession(projectDir, { workflow });
|
|
557
|
+
|
|
558
|
+
// Determine first agent
|
|
559
|
+
const firstAgent = isGreenfield ? 'greenfield-wu' : 'brownfield-wu';
|
|
560
|
+
const firstAgentFile = AGENT_FILE_MAP[firstAgent] || null;
|
|
561
|
+
|
|
562
|
+
// Update session with first agent
|
|
563
|
+
await updateSession(projectDir, { current_agent: firstAgent });
|
|
564
|
+
|
|
565
|
+
// Write session lock
|
|
566
|
+
try {
|
|
567
|
+
writeSessionLock(projectDir, firstAgent);
|
|
568
|
+
} catch (err) {
|
|
569
|
+
// Non-blocking — lock is nice-to-have, not critical
|
|
570
|
+
process.stderr.write(`[chati] cli lock-write: ${err.message}\n`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
initialized: true,
|
|
575
|
+
session_path: '.chati/session.yaml',
|
|
576
|
+
workflow,
|
|
577
|
+
pipeline_agents: pipelineAgents,
|
|
578
|
+
first_agent: firstAgent,
|
|
579
|
+
first_agent_file: firstAgentFile,
|
|
580
|
+
lock_written: true,
|
|
581
|
+
language,
|
|
582
|
+
project_type: type,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function handleValidateHandoff(projectDir, args) {
|
|
587
|
+
const from = args.from;
|
|
588
|
+
const to = args.to;
|
|
589
|
+
|
|
590
|
+
if (!from) return errorResult('Missing --from flag', 'MISSING_FROM');
|
|
591
|
+
|
|
592
|
+
let handoffCtx;
|
|
593
|
+
try {
|
|
594
|
+
handoffCtx = await loadHandoffContext(projectDir, from);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
return { valid: false, from, to: to || 'unknown', missing: ['handoff document'], warnings: [err.message], handoff_loaded: false, sources: [] };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const handoff = handoffCtx.context?.handoff || null;
|
|
600
|
+
if (!handoff) {
|
|
601
|
+
return { valid: false, from, to: to || 'unknown', missing: ['handoff document'], warnings: [`No handoff found from ${from}`], handoff_loaded: false, sources: handoffCtx.sources || [] };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const receivingAgent = to || getNextAgent(from, [from])?.next || 'unknown';
|
|
605
|
+
const integrity = validateHandoffIntegrity(handoff, receivingAgent);
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
valid: integrity.valid,
|
|
609
|
+
from,
|
|
610
|
+
to: receivingAgent,
|
|
611
|
+
missing: integrity.missing,
|
|
612
|
+
warnings: integrity.warnings,
|
|
613
|
+
handoff_loaded: true,
|
|
614
|
+
sources: handoffCtx.sources || [],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function handleStatus(projectDir) {
|
|
619
|
+
const { loaded, session } = loadSession(projectDir);
|
|
620
|
+
if (!loaded || !session) {
|
|
621
|
+
return errorResult('No session found. Run `orchestrate init` first.', 'NO_SESSION');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const summary = getSessionSummary(projectDir);
|
|
625
|
+
const pipelineState = sessionToPipelineState(session, projectDir);
|
|
626
|
+
const progress = getPipelineProgress(pipelineState);
|
|
627
|
+
|
|
628
|
+
let handoffHistory = [];
|
|
629
|
+
try {
|
|
630
|
+
handoffHistory = getHandoffHistory(projectDir);
|
|
631
|
+
} catch { /* expected: handoff files may not exist yet */
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Build model map for all agents
|
|
635
|
+
const agentModels = {};
|
|
636
|
+
for (const agentDef of AGENT_PIPELINE) {
|
|
637
|
+
agentModels[agentDef.name] = resolveAgentModel(agentDef.name, projectDir);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
session: summary.summary || summary,
|
|
642
|
+
pipeline: progress,
|
|
643
|
+
handoff_history: handoffHistory,
|
|
644
|
+
deviation_count: (session.deviations || []).length,
|
|
645
|
+
backlog: session.backlog || [],
|
|
646
|
+
agents: session.agents || {},
|
|
647
|
+
agent_models: agentModels,
|
|
648
|
+
context_bracket: estimateContextBracket((session.completed_agents || []).length, AGENT_PIPELINE.length),
|
|
649
|
+
user_level: session.user_level || 'auto',
|
|
650
|
+
execution_mode: session.execution_mode || 'interactive',
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function handleDeviation(projectDir, args) {
|
|
655
|
+
const type = args.type;
|
|
656
|
+
const target = args.target || null;
|
|
657
|
+
const confirm = args.confirm === 'true';
|
|
658
|
+
|
|
659
|
+
if (!type) return errorResult('Missing --type flag', 'MISSING_TYPE');
|
|
660
|
+
|
|
661
|
+
const { loaded, session } = loadSession(projectDir);
|
|
662
|
+
if (!loaded || !session) return errorResult('No session found', 'NO_SESSION');
|
|
663
|
+
|
|
664
|
+
const pipelineState = sessionToPipelineState(session, projectDir);
|
|
665
|
+
|
|
666
|
+
const details = { targetAgent: target };
|
|
667
|
+
if (args['features-add']) {
|
|
668
|
+
details.addedFeatures = args['features-add'].split(',').map(s => s.trim());
|
|
669
|
+
}
|
|
670
|
+
if (args['features-remove']) {
|
|
671
|
+
details.removedFeatures = args['features-remove'].split(',').map(s => s.trim());
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Phase 1: Impact analysis
|
|
675
|
+
const impact = analyzeDeviationImpact(type, pipelineState, details);
|
|
676
|
+
|
|
677
|
+
if (!confirm) {
|
|
678
|
+
return { impact, applied: false, changes: [], new_state: null };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Phase 2: Apply deviation
|
|
682
|
+
const result = applyDeviation(pipelineState, type, details);
|
|
683
|
+
|
|
684
|
+
if (result.applied) {
|
|
685
|
+
// Persist changes to session
|
|
686
|
+
const updates = {};
|
|
687
|
+
if (result.state.currentAgent !== session.current_agent) {
|
|
688
|
+
updates.current_agent = result.state.currentAgent || '';
|
|
689
|
+
}
|
|
690
|
+
// Update agent statuses
|
|
691
|
+
if (result.state.agents) {
|
|
692
|
+
const agentUpdates = {};
|
|
693
|
+
for (const [name, data] of Object.entries(result.state.agents)) {
|
|
694
|
+
agentUpdates[name] = {
|
|
695
|
+
status: data.status,
|
|
696
|
+
score: data.score ?? 0,
|
|
697
|
+
criteria_count: 0,
|
|
698
|
+
completed_at: data.completedAt || null,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
updates.agents = agentUpdates;
|
|
702
|
+
}
|
|
703
|
+
// Update completed_agents
|
|
704
|
+
if (result.state.completedAgents) {
|
|
705
|
+
updates.completed_agents = result.state.completedAgents;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
await updateSession(projectDir, updates);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
impact,
|
|
713
|
+
applied: result.applied,
|
|
714
|
+
changes: result.changes,
|
|
715
|
+
new_state: result.applied ? { current_agent: result.state.currentAgent } : null,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function handleExit(projectDir, args) {
|
|
720
|
+
const { loaded, session } = loadSession(projectDir);
|
|
721
|
+
if (!loaded || !session) {
|
|
722
|
+
return { exited: true, session_saved: false, lock_removed: false, resume_message: null };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Save current state
|
|
726
|
+
await updateSession(projectDir, {});
|
|
727
|
+
|
|
728
|
+
// Release session ownership
|
|
729
|
+
const userId = args['user-id'] || process.env.USER || 'default';
|
|
730
|
+
try {
|
|
731
|
+
releaseSession(projectDir, userId);
|
|
732
|
+
} catch { /* expected: session may already be released */ }
|
|
733
|
+
|
|
734
|
+
// Remove lock from CLAUDE.local.md
|
|
735
|
+
const lang = session.language || 'en';
|
|
736
|
+
const resumeMsg = RESUME_MESSAGES[lang] || RESUME_MESSAGES.en;
|
|
737
|
+
try {
|
|
738
|
+
removeSessionLock(projectDir, resumeMsg);
|
|
739
|
+
} catch { /* expected: lock file may not exist */ }
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
exited: true,
|
|
743
|
+
session_saved: true,
|
|
744
|
+
lock_removed: true,
|
|
745
|
+
resume_message: resumeMsg,
|
|
746
|
+
language: lang,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function handleProviders(projectDir) {
|
|
751
|
+
const agentModels = {};
|
|
752
|
+
for (const agentDef of AGENT_PIPELINE) {
|
|
753
|
+
agentModels[agentDef.name] = resolveAgentModel(agentDef.name, projectDir);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
primary_provider: 'claude',
|
|
758
|
+
agent_models: agentModels,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function handleDetectFlow(projectDir, args) {
|
|
763
|
+
const message = args.message || '';
|
|
764
|
+
if (!message) return errorResult('Missing --message flag', 'MISSING_MESSAGE');
|
|
765
|
+
|
|
766
|
+
const { loaded, session } = loadSession(projectDir);
|
|
767
|
+
const sessionContext = loaded && session ? { mode: session.mode, project_type: session.project_type } : {};
|
|
768
|
+
|
|
769
|
+
const quickResult = detectQuickFlow(message, sessionContext);
|
|
770
|
+
const standardResult = detectStandardFlow(message, sessionContext);
|
|
771
|
+
|
|
772
|
+
let recommended = 'full';
|
|
773
|
+
if (quickResult.isQuickFlow && quickResult.confidence >= 0.8) {
|
|
774
|
+
recommended = 'quick';
|
|
775
|
+
} else if (standardResult.isStandardFlow && standardResult.confidence >= 0.8) {
|
|
776
|
+
recommended = 'standard';
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
recommended,
|
|
781
|
+
quick_flow: quickResult,
|
|
782
|
+
standard_flow: standardResult,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function handleBacklog(projectDir, args) {
|
|
787
|
+
const action = args.action || 'list';
|
|
788
|
+
const { loaded, session } = loadSession(projectDir);
|
|
789
|
+
if (!loaded || !session) return errorResult('No session found', 'NO_SESSION');
|
|
790
|
+
|
|
791
|
+
const backlog = session.backlog || [];
|
|
792
|
+
|
|
793
|
+
if (action === 'list') {
|
|
794
|
+
return { backlog, count: backlog.length };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (action === 'add') {
|
|
798
|
+
const title = args.title;
|
|
799
|
+
const priority = args.priority || 'medium';
|
|
800
|
+
const source = args.source || 'user';
|
|
801
|
+
if (!title) return errorResult('Missing --title flag', 'MISSING_TITLE');
|
|
802
|
+
|
|
803
|
+
const item = {
|
|
804
|
+
id: `BL-${String(backlog.length + 1).padStart(3, '0')}`,
|
|
805
|
+
title,
|
|
806
|
+
priority,
|
|
807
|
+
status: 'pending',
|
|
808
|
+
source_agent: source,
|
|
809
|
+
created_at: new Date().toISOString(),
|
|
810
|
+
};
|
|
811
|
+
backlog.push(item);
|
|
812
|
+
await updateSession(projectDir, { backlog });
|
|
813
|
+
return { added: true, item, count: backlog.length };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return { backlog, count: backlog.length };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
// QA Scoring Handlers (deterministic — no LLM)
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
function handleQaPlanScore(args) {
|
|
824
|
+
let score = 100;
|
|
825
|
+
const penalties = {};
|
|
826
|
+
|
|
827
|
+
const penaltyMap = [
|
|
828
|
+
['orphans', 10, 'Orphaned requirement'],
|
|
829
|
+
['placeholders', 5, 'Placeholder in artifact'],
|
|
830
|
+
['weak-criteria', 10, 'Weak/vague criterion'],
|
|
831
|
+
['scope-creep', 15, 'Scope creep item'],
|
|
832
|
+
['gaps', 20, 'Critical gap'],
|
|
833
|
+
['fail-dimensions', 10, 'Failed validation dimension'],
|
|
834
|
+
];
|
|
835
|
+
|
|
836
|
+
for (const [key, weight, label] of penaltyMap) {
|
|
837
|
+
const count = parseInt(args[key]) || 0;
|
|
838
|
+
if (count > 0) {
|
|
839
|
+
const penalty = count * weight;
|
|
840
|
+
score -= penalty;
|
|
841
|
+
penalties[key] = { count, weight, penalty, label };
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (args['adversarial-incomplete'] === 'true') {
|
|
846
|
+
score -= 15;
|
|
847
|
+
penalties['adversarial-incomplete'] = { count: 1, weight: 15, penalty: 15, label: 'Adversarial review incomplete' };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
score = Math.max(0, score);
|
|
851
|
+
const threshold = 95;
|
|
852
|
+
const verdict = score >= threshold ? 'APPROVED' : score >= threshold - 5 ? 'NEEDS_REVISION' : 'BLOCKED';
|
|
853
|
+
|
|
854
|
+
return { score, threshold, verdict, penalties };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function handleQaImplScore(args) {
|
|
858
|
+
const weights = { tests: 0.25, coverage: 0.10, security: 0.25, quality: 0.15, acceptance: 0.10, adversarial: 0.15 };
|
|
859
|
+
|
|
860
|
+
const scores = {
|
|
861
|
+
tests: parseInt(args['tests-pass']) || 0,
|
|
862
|
+
coverage: parseInt(args.coverage) || 0,
|
|
863
|
+
security: (parseInt(args.critical) === 0 && parseInt(args.high) === 0) ? 100 : (parseInt(args.critical) > 0 ? 0 : 50),
|
|
864
|
+
quality: parseInt(args['code-quality']) || 0,
|
|
865
|
+
acceptance: parseInt(args.acceptance) || 0,
|
|
866
|
+
adversarial: parseInt(args.adversarial) || 0,
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const weighted = Object.entries(weights).reduce((sum, [k, w]) => sum + (scores[k] || 0) * w, 0);
|
|
870
|
+
const score = Math.round(weighted);
|
|
871
|
+
const threshold = 95;
|
|
872
|
+
const verdict = score >= threshold ? 'APPROVED' : score >= threshold - 5 ? 'NEEDS_REVISION' : 'BLOCKED';
|
|
873
|
+
|
|
874
|
+
return { score, threshold, verdict, breakdown: scores, weights };
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ---------------------------------------------------------------------------
|
|
878
|
+
// Scan Handler (deterministic — no LLM)
|
|
879
|
+
// ---------------------------------------------------------------------------
|
|
880
|
+
|
|
881
|
+
async function handleScan(projectDir, args) {
|
|
882
|
+
const type = args.type;
|
|
883
|
+
if (!type) return errorResult('Missing --type flag. Valid: placeholders, leakage, density, env, security', 'MISSING_TYPE');
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
switch (type) {
|
|
887
|
+
case 'placeholders': {
|
|
888
|
+
const file = args.file;
|
|
889
|
+
if (!file) return errorResult('Missing --file flag', 'MISSING_FILE');
|
|
890
|
+
const { scanPlaceholders } = await import('../scanning/placeholder-scanner.js');
|
|
891
|
+
const { join: joinPath } = await import('path');
|
|
892
|
+
return scanPlaceholders(joinPath(projectDir, file));
|
|
893
|
+
}
|
|
894
|
+
case 'leakage': {
|
|
895
|
+
const file = args.file;
|
|
896
|
+
if (!file) return errorResult('Missing --file flag', 'MISSING_FILE');
|
|
897
|
+
const { scanImplementationLeakage } = await import('../scanning/leakage-scanner.js');
|
|
898
|
+
const { join: joinPath } = await import('path');
|
|
899
|
+
return scanImplementationLeakage(joinPath(projectDir, file));
|
|
900
|
+
}
|
|
901
|
+
case 'density': {
|
|
902
|
+
const file = args.file;
|
|
903
|
+
if (!file) return errorResult('Missing --file flag', 'MISSING_FILE');
|
|
904
|
+
const { scanInformationDensity } = await import('../scanning/density-scanner.js');
|
|
905
|
+
const { join: joinPath } = await import('path');
|
|
906
|
+
return scanInformationDensity(joinPath(projectDir, file));
|
|
907
|
+
}
|
|
908
|
+
case 'env': {
|
|
909
|
+
const { scanEnvSync } = await import('../scanning/env-scanner.js');
|
|
910
|
+
return scanEnvSync(projectDir);
|
|
911
|
+
}
|
|
912
|
+
case 'security': {
|
|
913
|
+
const { scanProjectSecurity } = await import('../scanning/security-scanner.js');
|
|
914
|
+
return scanProjectSecurity(projectDir, { language: args.language, dir: args.dir });
|
|
915
|
+
}
|
|
916
|
+
default:
|
|
917
|
+
return errorResult(`Unknown scan type: ${type}. Valid: placeholders, leakage, density, env, security`, 'UNKNOWN_SCAN_TYPE');
|
|
918
|
+
}
|
|
919
|
+
} catch (err) {
|
|
920
|
+
return errorResult(`Scan failed: ${err.message}`, 'SCAN_ERROR');
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ---------------------------------------------------------------------------
|
|
925
|
+
// Main entry point
|
|
926
|
+
// ---------------------------------------------------------------------------
|
|
927
|
+
|
|
928
|
+
export async function runOrchestrate(subCommand, argv, projectDir) {
|
|
929
|
+
const args = parseArgs(argv);
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
switch (subCommand) {
|
|
933
|
+
case 'next':
|
|
934
|
+
output(await handleNext(projectDir));
|
|
935
|
+
break;
|
|
936
|
+
case 'advance':
|
|
937
|
+
output(await handleAdvance(projectDir, args));
|
|
938
|
+
break;
|
|
939
|
+
case 'init':
|
|
940
|
+
output(await handleInit(projectDir, args));
|
|
941
|
+
break;
|
|
942
|
+
case 'validate-handoff':
|
|
943
|
+
output(await handleValidateHandoff(projectDir, args));
|
|
944
|
+
break;
|
|
945
|
+
case 'status':
|
|
946
|
+
output(await handleStatus(projectDir));
|
|
947
|
+
break;
|
|
948
|
+
case 'deviation':
|
|
949
|
+
output(await handleDeviation(projectDir, args));
|
|
950
|
+
break;
|
|
951
|
+
case 'exit':
|
|
952
|
+
output(await handleExit(projectDir, args));
|
|
953
|
+
break;
|
|
954
|
+
case 'providers':
|
|
955
|
+
output(await handleProviders(projectDir));
|
|
956
|
+
break;
|
|
957
|
+
case 'detect-flow':
|
|
958
|
+
output(await handleDetectFlow(projectDir, args));
|
|
959
|
+
break;
|
|
960
|
+
case 'backlog':
|
|
961
|
+
output(await handleBacklog(projectDir, args));
|
|
962
|
+
break;
|
|
963
|
+
case 'qa-plan-score':
|
|
964
|
+
output(handleQaPlanScore(args));
|
|
965
|
+
break;
|
|
966
|
+
case 'qa-impl-score':
|
|
967
|
+
output(handleQaImplScore(args));
|
|
968
|
+
break;
|
|
969
|
+
case 'scan':
|
|
970
|
+
output(await handleScan(projectDir, args));
|
|
971
|
+
break;
|
|
972
|
+
default:
|
|
973
|
+
output(errorResult(`Unknown sub-command: ${subCommand}. Valid: next, advance, init, validate-handoff, status, deviation, exit, providers, detect-flow, backlog, qa-plan-score, qa-impl-score, scan`, 'UNKNOWN_COMMAND'));
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
} catch (err) {
|
|
977
|
+
output(errorResult(`Orchestrate error: ${err.message}`, 'INTERNAL_ERROR'));
|
|
978
|
+
process.exitCode = 1;
|
|
979
|
+
}
|
|
980
|
+
}
|