chati-dev 4.5.13 → 4.5.14
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/framework/agents/build/dev.md +4 -5
- package/framework/agents/deploy/devops.md +9 -10
- package/framework/agents/discover/brief.md +7 -13
- package/framework/agents/discover/brownfield-wu.md +5 -11
- package/framework/agents/discover/greenfield-wu.md +7 -12
- package/framework/agents/plan/architect.md +1 -1
- package/framework/agents/plan/detail.md +5 -5
- package/framework/agents/plan/phases.md +4 -5
- package/framework/agents/plan/tasks.md +4 -5
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux.md +1 -1
- package/framework/agents/quality/qa-implementation.md +4 -5
- package/framework/agents/quality/qa-planning.md +4 -5
- package/framework/agents/quality/qa-visual.md +4 -4
- package/framework/agents/shared/visualizer.md +1 -1
- package/framework/config.yaml +5 -5
- package/framework/constitution.md +24 -26
- package/framework/context/governance.md +6 -6
- package/framework/context/root.md +4 -3
- package/framework/data/entity-registry.yaml +3 -3
- package/framework/data/model-limits.json +5 -2
- package/framework/hooks/model-governance.js +3 -2
- package/framework/hooks/prism-engine.js +1 -1
- 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 +12 -16
- package/framework/manifest.json +63 -63
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +40 -6
- package/framework/orchestrator/chati.md +12 -13
- package/framework/schemas/session.schema.json +1 -1
- package/package.json +1 -1
- package/src/config/context-file-generator.js +54 -246
- package/src/config/framework-adapter.js +45 -208
- package/src/context/bracket-tracker.js +3 -3
- package/src/context/engine.js +1 -1
- package/src/installer/core.js +190 -66
- package/src/installer/provider-overlay.js +1 -1
- package/src/installer/templates.js +55 -65
- package/src/installer-v2/index.js +2 -0
- package/src/memory/magic-docs.js +45 -26
- package/src/orchestrator/cli.js +50 -45
- package/src/orchestrator/handoff-engine.js +7 -5
- package/src/orchestrator/session-manager.js +24 -3
- package/src/terminal/cli-registry.js +5 -5
- package/src/terminal/prompt-builder.js +14 -10
- package/src/terminal/run-agent.js +10 -6
- package/src/terminal/run-parallel.js +6 -2
- package/src/terminal/run-team.js +7 -2
- package/src/terminal/spawner.js +34 -42
- package/src/utils/config-parser.js +2 -2
- package/src/utils/provider-limits.js +3 -1
- package/src/wizard/i18n.js +2 -2
package/src/memory/magic-docs.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Magic Docs
|
|
2
|
+
* Magic Docs: update Chati's provider-neutral project context after handoffs.
|
|
3
3
|
*
|
|
4
4
|
* Inspired by Claude Code's Magic Docs system that uses `# MAGIC DOC:` headers
|
|
5
5
|
* to trigger background self-updating documentation.
|
|
6
6
|
*
|
|
7
|
-
* This module updates only
|
|
8
|
-
*
|
|
7
|
+
* This module updates only `.chati/project-context.md`. Provider-native root
|
|
8
|
+
* files consume this canonical artifact and are never used as its source.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
|
|
14
14
|
const SECTION_HEADER = '## Current State';
|
|
@@ -20,7 +20,7 @@ const SECTION_HEADER = '## Current State';
|
|
|
20
20
|
const SECTION_REGEX = /^## Current State\n([\s\S]*?)(?=\n## |\n---|$(?![\s\S]))/m;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Update
|
|
23
|
+
* Update the neutral project context with current state after a handoff.
|
|
24
24
|
*
|
|
25
25
|
* @param {string} projectDir - Project root directory
|
|
26
26
|
* @param {object} context - Current state to write
|
|
@@ -28,12 +28,13 @@ const SECTION_REGEX = /^## Current State\n([\s\S]*?)(?=\n## |\n---|$(?![\s\S]))/
|
|
|
28
28
|
* @param {string} [context.pipelinePosition] - Pipeline phase/step
|
|
29
29
|
* @param {string} [context.activeTask] - Current task ID + title
|
|
30
30
|
* @param {number} [context.progress] - Pipeline completion percentage
|
|
31
|
+
* @param {number} [context.validationScore] - Latest independent validation score
|
|
31
32
|
* @param {Array<{what: string, why: string}>} [context.decisions] - Recent decisions
|
|
32
33
|
* @param {string[]} [context.nextSteps] - Planned next steps
|
|
33
34
|
* @returns {{ updated: boolean, path: string }}
|
|
34
35
|
*/
|
|
35
|
-
export function
|
|
36
|
-
const
|
|
36
|
+
export function updateProjectContext(projectDir, context = {}) {
|
|
37
|
+
const contextPath = join(projectDir, '.chati', 'project-context.md');
|
|
37
38
|
const timestamp = new Date().toISOString().split('T')[0];
|
|
38
39
|
|
|
39
40
|
// Build the new section content
|
|
@@ -42,7 +43,13 @@ export function updateClaudeMd(projectDir, context = {}) {
|
|
|
42
43
|
if (context.currentAgent) lines.push(`- **Agent**: ${context.currentAgent}`);
|
|
43
44
|
if (context.pipelinePosition) lines.push(`- **Pipeline**: ${context.pipelinePosition}`);
|
|
44
45
|
if (context.activeTask) lines.push(`- **Task**: ${context.activeTask}`);
|
|
45
|
-
|
|
46
|
+
const progress = typeof context.progress === 'number'
|
|
47
|
+
? context.progress
|
|
48
|
+
: (context.progress?.percent ?? context.progress?.progress);
|
|
49
|
+
if (progress !== undefined) lines.push(`- **Progress**: ${progress}%`);
|
|
50
|
+
if (typeof context.validationScore === 'number') {
|
|
51
|
+
lines.push(`- **Validation Score**: ${context.validationScore}`);
|
|
52
|
+
}
|
|
46
53
|
lines.push(`- **Last Updated**: ${timestamp}`);
|
|
47
54
|
|
|
48
55
|
if (context.decisions && context.decisions.length > 0) {
|
|
@@ -65,39 +72,51 @@ export function updateClaudeMd(projectDir, context = {}) {
|
|
|
65
72
|
const newSection = lines.join('\n');
|
|
66
73
|
|
|
67
74
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
writeFileSync(
|
|
71
|
-
return { updated: true, path:
|
|
75
|
+
mkdirSync(join(projectDir, '.chati'), { recursive: true });
|
|
76
|
+
if (!existsSync(contextPath)) {
|
|
77
|
+
writeFileSync(contextPath, newSection + '\n', 'utf-8');
|
|
78
|
+
return { updated: true, path: contextPath };
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
const content = readFileSync(
|
|
81
|
+
const content = readFileSync(contextPath, 'utf-8');
|
|
75
82
|
|
|
76
83
|
// Handle empty file as creation
|
|
77
84
|
if (!content || content.trim().length === 0) {
|
|
78
|
-
writeFileSync(
|
|
79
|
-
return { updated: true, path:
|
|
85
|
+
writeFileSync(contextPath, newSection + '\n', 'utf-8');
|
|
86
|
+
return { updated: true, path: contextPath };
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
let updatedContent;
|
|
83
90
|
if (SECTION_REGEX.test(content)) {
|
|
84
91
|
// Replace existing section
|
|
85
|
-
updatedContent = content.replace(SECTION_REGEX, newSection);
|
|
92
|
+
updatedContent = content.replace(SECTION_REGEX, `${newSection}\n`);
|
|
86
93
|
} else {
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
// Managed installer metadata must remain replaceable without swallowing
|
|
95
|
+
// runtime state. Insert state immediately after that managed block.
|
|
96
|
+
const managedEnd = '<!-- chati-context:end -->';
|
|
97
|
+
const managedEndIndex = content.indexOf(managedEnd);
|
|
98
|
+
if (managedEndIndex !== -1) {
|
|
99
|
+
const insertionPoint = managedEndIndex + managedEnd.length;
|
|
100
|
+
updatedContent = content.slice(0, insertionPoint) + '\n\n' + newSection + content.slice(insertionPoint);
|
|
92
101
|
} else {
|
|
93
|
-
//
|
|
94
|
-
|
|
102
|
+
// Legacy context: insert before the first secondary section.
|
|
103
|
+
const firstHeadingEnd = content.indexOf('\n## ');
|
|
104
|
+
if (firstHeadingEnd !== -1) {
|
|
105
|
+
// Insert before the first ## section
|
|
106
|
+
updatedContent = content.slice(0, firstHeadingEnd) + '\n\n' + newSection + '\n' + content.slice(firstHeadingEnd);
|
|
107
|
+
} else {
|
|
108
|
+
// Append at end
|
|
109
|
+
updatedContent = content.trimEnd() + '\n\n' + newSection + '\n';
|
|
110
|
+
}
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
writeFileSync(
|
|
99
|
-
return { updated: true, path:
|
|
114
|
+
writeFileSync(contextPath, updatedContent, 'utf-8');
|
|
115
|
+
return { updated: true, path: contextPath };
|
|
100
116
|
} catch { /* expected: operation may fail gracefully */
|
|
101
|
-
return { updated: false, path:
|
|
117
|
+
return { updated: false, path: contextPath };
|
|
102
118
|
}
|
|
103
119
|
}
|
|
120
|
+
|
|
121
|
+
// Backward-compatible export for integrations compiled against the old name.
|
|
122
|
+
export const updateClaudeMd = updateProjectContext;
|
package/src/orchestrator/cli.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
15
|
+
import { parseProviderConfig } from '../utils/config-parser.js';
|
|
15
16
|
import { isFeatureEnabled } from '../utils/feature-flags.js';
|
|
16
17
|
import { runScriptTask, formatScriptHandoff } from '../executors/runner.js';
|
|
17
18
|
|
|
@@ -52,7 +53,7 @@ import {
|
|
|
52
53
|
import {
|
|
53
54
|
recordEvent, EventType, clearTimeline,
|
|
54
55
|
} from '../intelligence/timeline.js';
|
|
55
|
-
import {
|
|
56
|
+
import { updateProjectContext } from '../memory/magic-docs.js';
|
|
56
57
|
import {
|
|
57
58
|
loadRuntimeInstallationV2, resolveRuntimeInvocationV2,
|
|
58
59
|
describeRuntimeInstallationV2, V2_PLANNING_AGENTS,
|
|
@@ -571,13 +572,14 @@ function sessionToPipelineState(session, projectDir) {
|
|
|
571
572
|
* Exported for testing (path resolution must hold in every install layout).
|
|
572
573
|
*/
|
|
573
574
|
export function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout, model = null, strictProvider = false, routing = {}) {
|
|
575
|
+
if (!provider) throw new Error('A routed provider is required to build an agent spawn command');
|
|
574
576
|
const parts = [
|
|
575
577
|
'node', RUNNER('run-agent.js'),
|
|
576
578
|
'--agent', agent,
|
|
577
579
|
'--task-id', `${agent}-task`,
|
|
578
580
|
'--project-dir', JSON.stringify(resolve(projectDir)),
|
|
579
581
|
'--previous-agent', previousAgent || 'none',
|
|
580
|
-
'--provider', provider
|
|
582
|
+
'--provider', provider,
|
|
581
583
|
...(model ? ['--model', model] : []),
|
|
582
584
|
...(routing.provider_id ? ['--provider-id', routing.provider_id] : []),
|
|
583
585
|
...(routing.reasoning_configuration ? ['--reasoning-configuration', routing.reasoning_configuration] : []),
|
|
@@ -593,6 +595,7 @@ export function buildSpawnCommand(agent, projectDir, previousAgent, provider, ti
|
|
|
593
595
|
* Exported for testing.
|
|
594
596
|
*/
|
|
595
597
|
export function buildParallelSpawnCommand(agents, projectDir, previousAgent, provider, timeout, model = null, strictProvider = false) {
|
|
598
|
+
if (!provider) throw new Error('A routed provider is required to build a parallel spawn command');
|
|
596
599
|
const taskIds = agents.map(a => `${a}-task`).join(',');
|
|
597
600
|
const parts = [
|
|
598
601
|
'node', RUNNER('run-parallel.js'),
|
|
@@ -600,7 +603,7 @@ export function buildParallelSpawnCommand(agents, projectDir, previousAgent, pro
|
|
|
600
603
|
'--task-ids', taskIds,
|
|
601
604
|
'--project-dir', JSON.stringify(resolve(projectDir)),
|
|
602
605
|
'--previous-agent', previousAgent || 'none',
|
|
603
|
-
'--provider', provider
|
|
606
|
+
'--provider', provider,
|
|
604
607
|
...(model ? ['--model', model] : []),
|
|
605
608
|
...(strictProvider ? ['--strict-provider'] : []),
|
|
606
609
|
'--timeout', String(timeout || 900000),
|
|
@@ -622,10 +625,10 @@ export function buildRoutedParallelSpawnCommands(agents, projectDir, previousAge
|
|
|
622
625
|
}
|
|
623
626
|
|
|
624
627
|
/**
|
|
625
|
-
*
|
|
626
|
-
*
|
|
628
|
+
* Legacy v1 model map retained only for pre-v2 installations. New installs
|
|
629
|
+
* resolve exact task bindings from the signed capability snapshot.
|
|
627
630
|
*/
|
|
628
|
-
const
|
|
631
|
+
const LEGACY_V1_AGENT_MODEL_MAP = {
|
|
629
632
|
'greenfield-wu': { provider: 'claude', model: 'haiku', upgrade: 'sonnet if multi-stack or enterprise' },
|
|
630
633
|
'brownfield-wu': { provider: 'claude', model: 'opus', upgrade: 'no downgrade' },
|
|
631
634
|
brief: { provider: 'claude', model: 'sonnet', upgrade: 'opus if enterprise or 10+ integrations' },
|
|
@@ -643,7 +646,8 @@ const AGENT_MODEL_MAP = {
|
|
|
643
646
|
|
|
644
647
|
/**
|
|
645
648
|
* Resolve provider and model for an agent.
|
|
646
|
-
*
|
|
649
|
+
* V2 uses the signed task catalog. Pre-v2 installations retain their explicit
|
|
650
|
+
* compatibility map and optional config overrides.
|
|
647
651
|
*/
|
|
648
652
|
export function resolveAgentModel(agent, projectDir) {
|
|
649
653
|
const v2Artifact = loadRuntimeInstallationV2(projectDir);
|
|
@@ -658,7 +662,7 @@ export function resolveAgentModel(agent, projectDir) {
|
|
|
658
662
|
source: 'installation-v2',
|
|
659
663
|
};
|
|
660
664
|
}
|
|
661
|
-
const defaults =
|
|
665
|
+
const defaults = LEGACY_V1_AGENT_MODEL_MAP[agent] || { provider: 'claude', model: 'sonnet', upgrade: '' };
|
|
662
666
|
|
|
663
667
|
// Check config.yaml for agent_overrides
|
|
664
668
|
try {
|
|
@@ -690,7 +694,7 @@ function estimateContextBracket(completedCount, totalCount) {
|
|
|
690
694
|
}
|
|
691
695
|
|
|
692
696
|
/**
|
|
693
|
-
* Write session lock
|
|
697
|
+
* Write session lock blocks to every enabled harness's native local file.
|
|
694
698
|
*/
|
|
695
699
|
function replaceBlock(content, startMarker, endMarker, newInner) {
|
|
696
700
|
const block = `${startMarker}\n${newInner}\n${endMarker}`;
|
|
@@ -704,6 +708,7 @@ function replaceBlock(content, startMarker, endMarker, newInner) {
|
|
|
704
708
|
|
|
705
709
|
function sessionLockTargets(projectDir) {
|
|
706
710
|
const frameworkDir = resolveFrameworkDir(projectDir);
|
|
711
|
+
const enabled = new Set(parseProviderConfig(projectDir).enabled);
|
|
707
712
|
return [
|
|
708
713
|
{ path: join(projectDir, 'CLAUDE.local.md'), invocation: '/chati', provider: 'claude' },
|
|
709
714
|
{ path: join(projectDir, 'AGENTS.override.md'), invocation: '$chati', provider: 'codex' },
|
|
@@ -714,7 +719,7 @@ function sessionLockTargets(projectDir) {
|
|
|
714
719
|
orchestratorPath: existsSync(join(projectDir, frameworkDir, '.adapted', target.provider, 'orchestrator', 'chati.md'))
|
|
715
720
|
? `${frameworkDir}/.adapted/${target.provider}/orchestrator/chati.md`
|
|
716
721
|
: `${frameworkDir}/orchestrator/chati.md`,
|
|
717
|
-
})).filter(({ path }) =>
|
|
722
|
+
})).filter(({ provider, path }) => enabled.has(provider) && existsSync(path));
|
|
718
723
|
}
|
|
719
724
|
|
|
720
725
|
function removeLegacyPauseSection(content) {
|
|
@@ -763,7 +768,7 @@ function writeSessionLock(projectDir, currentAgent, stateInfo = {}) {
|
|
|
763
768
|
}
|
|
764
769
|
|
|
765
770
|
/**
|
|
766
|
-
* Remove session lock
|
|
771
|
+
* Remove session lock blocks from every enabled harness's native local file.
|
|
767
772
|
*/
|
|
768
773
|
function removeSessionLock(projectDir, resumeMsg) {
|
|
769
774
|
const stateInner = `## Current State
|
|
@@ -1352,15 +1357,15 @@ async function handleScaffoldDecision(projectDir, session, decision, agent, scor
|
|
|
1352
1357
|
|
|
1353
1358
|
const nextAgent = advanceResult.nextAgent || '';
|
|
1354
1359
|
const progress = getPipelineProgress(advanceResult.state || pipelineState);
|
|
1355
|
-
try {
|
|
1360
|
+
try { updateProjectContext(projectDir, { currentAgent: nextAgent, progress: progress.progress }); } catch { /* non-fatal */ }
|
|
1356
1361
|
try {
|
|
1357
1362
|
if (nextAgent) {
|
|
1358
1363
|
writeSessionLock(projectDir, nextAgent, {
|
|
1359
1364
|
phase: updates.mode || session.mode,
|
|
1360
1365
|
mode: session.execution_mode || 'interactive',
|
|
1361
1366
|
position: updates.pipeline_position,
|
|
1362
|
-
total: progress.
|
|
1363
|
-
progress: progress.
|
|
1367
|
+
total: progress.totalAgents,
|
|
1368
|
+
progress: progress.progress,
|
|
1364
1369
|
});
|
|
1365
1370
|
}
|
|
1366
1371
|
} catch { /* non-fatal */ }
|
|
@@ -1954,9 +1959,9 @@ async function handleAdvance(projectDir, args) {
|
|
|
1954
1959
|
phase: 'rail', mode: session.execution_mode || 'interactive',
|
|
1955
1960
|
position: runtimePipeline.length, total: runtimePipeline.length, progress: 100,
|
|
1956
1961
|
});
|
|
1957
|
-
|
|
1962
|
+
updateProjectContext(projectDir, {
|
|
1958
1963
|
currentAgent: 'rail',
|
|
1959
|
-
progress:
|
|
1964
|
+
progress: 100,
|
|
1960
1965
|
});
|
|
1961
1966
|
} catch { /* non-fatal: the canonical session state has already been persisted */ }
|
|
1962
1967
|
return {
|
|
@@ -2069,12 +2074,12 @@ async function handleAdvance(projectDir, args) {
|
|
|
2069
2074
|
return errorResult(`Session update failed: ${saveResult.error}`, 'SESSION_WRITE_FAILED');
|
|
2070
2075
|
}
|
|
2071
2076
|
|
|
2072
|
-
// Refresh
|
|
2077
|
+
// Refresh the provider-neutral project context and native runtime locks.
|
|
2073
2078
|
// Non-fatal: if updates fail, pipeline state already advanced on disk.
|
|
2074
2079
|
const nextAgent = advanceResult.nextAgent || '';
|
|
2075
2080
|
const progress = getPipelineProgress(advanceResult.state || pipelineState);
|
|
2076
2081
|
try {
|
|
2077
|
-
|
|
2082
|
+
updateProjectContext(projectDir, { currentAgent: nextAgent, progress: progress.progress });
|
|
2078
2083
|
} catch { /* non-fatal */ }
|
|
2079
2084
|
try {
|
|
2080
2085
|
if (nextAgent) {
|
|
@@ -2082,8 +2087,8 @@ async function handleAdvance(projectDir, args) {
|
|
|
2082
2087
|
phase: updates.mode || session.mode,
|
|
2083
2088
|
mode: session.execution_mode || 'interactive',
|
|
2084
2089
|
position: updates.pipeline_position,
|
|
2085
|
-
total: progress.
|
|
2086
|
-
progress: progress.
|
|
2090
|
+
total: progress.totalAgents,
|
|
2091
|
+
progress: progress.progress,
|
|
2087
2092
|
});
|
|
2088
2093
|
} else {
|
|
2089
2094
|
// Pipeline complete: reset lock to INACTIVE
|
|
@@ -2237,6 +2242,7 @@ async function handleInit(projectDir, args) {
|
|
|
2237
2242
|
let preservedName = args.name || '';
|
|
2238
2243
|
let preservedIdes = [];
|
|
2239
2244
|
let preservedMcps = [];
|
|
2245
|
+
let preservedProviders = [];
|
|
2240
2246
|
try {
|
|
2241
2247
|
const existingPath = join(projectDir, '.chati', 'session.yaml');
|
|
2242
2248
|
if (existsSync(existingPath)) {
|
|
@@ -2244,8 +2250,12 @@ async function handleInit(projectDir, args) {
|
|
|
2244
2250
|
preservedName = preservedName || existing.project?.name || '';
|
|
2245
2251
|
preservedIdes = existing.ides || [];
|
|
2246
2252
|
preservedMcps = existing.mcps || [];
|
|
2253
|
+
preservedProviders = existing.providers_enabled || [];
|
|
2247
2254
|
}
|
|
2248
2255
|
} catch { /* expected: session may not exist */ }
|
|
2256
|
+
if (preservedProviders.length === 0) {
|
|
2257
|
+
preservedProviders = parseProviderConfig(projectDir).enabled;
|
|
2258
|
+
}
|
|
2249
2259
|
const name = preservedName;
|
|
2250
2260
|
|
|
2251
2261
|
const isGreenfield = type === 'greenfield';
|
|
@@ -2289,6 +2299,7 @@ async function handleInit(projectDir, args) {
|
|
|
2289
2299
|
language,
|
|
2290
2300
|
ides: preservedIdes,
|
|
2291
2301
|
mcps: preservedMcps,
|
|
2302
|
+
providersEnabled: preservedProviders,
|
|
2292
2303
|
});
|
|
2293
2304
|
|
|
2294
2305
|
if (!result.created) {
|
|
@@ -2395,7 +2406,7 @@ async function handleStatus(projectDir) {
|
|
|
2395
2406
|
// (no context_* fields yet), the block falls back to estimateContextBracket
|
|
2396
2407
|
// so the existing response shape stays stable for consumers.
|
|
2397
2408
|
const activeModel = session.active_model || null;
|
|
2398
|
-
const activeProvider = session.active_provider || (session.providers_enabled || [
|
|
2409
|
+
const activeProvider = session.active_provider || (session.providers_enabled || [])[0] || null;
|
|
2399
2410
|
const windowTokens = session.context_window_tokens != null
|
|
2400
2411
|
? session.context_window_tokens
|
|
2401
2412
|
: resolveContextLimit(activeModel, activeProvider);
|
|
@@ -2758,12 +2769,9 @@ function generateTeamId(slug) {
|
|
|
2758
2769
|
}
|
|
2759
2770
|
|
|
2760
2771
|
/**
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2763
|
-
*
|
|
2764
|
-
* spawn API. Gemini and Codex have no equivalent — calling spawn_team for those
|
|
2765
|
-
* providers would fail. We always fall back silently to spawn_parallel
|
|
2766
|
-
* (Gemini) or spawn_autonomous (Codex) when the active provider is not claude.
|
|
2772
|
+
* The legacy native team runner requires an explicitly active Claude harness.
|
|
2773
|
+
* Runtime v2 always uses the strict provider-neutral parallel runner, whose
|
|
2774
|
+
* members keep their own signed provider and model bindings.
|
|
2767
2775
|
*/
|
|
2768
2776
|
export function isAgentTeamsEnabled(projectDir) {
|
|
2769
2777
|
// The v1 Agent Teams runner is Claude-specific. Provider-neutral v2 routes
|
|
@@ -2777,26 +2785,15 @@ export function isAgentTeamsEnabled(projectDir) {
|
|
|
2777
2785
|
|
|
2778
2786
|
// Feature flag check
|
|
2779
2787
|
const flagMatch = raw.match(/agent_teams:\s*(true|false)/);
|
|
2780
|
-
|
|
2781
|
-
if (flagMatch) {
|
|
2782
|
-
if (flagMatch[1] !== 'true') return false;
|
|
2783
|
-
} else {
|
|
2784
|
-
// If field is absent (pre-v4.2.0 projects), default to true for Claude provider
|
|
2785
|
-
// Claude Code supports Agent tool natively; Gemini/Codex fall back to sequential
|
|
2786
|
-
const providerMatch = raw.match(/primary_provider:\s*["']?(\w+)/);
|
|
2787
|
-
const provider = providerMatch ? providerMatch[1].toLowerCase() : 'claude';
|
|
2788
|
-
if (provider !== 'claude') return false;
|
|
2789
|
-
}
|
|
2788
|
+
if (!flagMatch || flagMatch[1] !== 'true') return false;
|
|
2790
2789
|
|
|
2791
|
-
//
|
|
2792
|
-
//
|
|
2793
|
-
// field. Default to 'claude' if neither exists (early bootstrap).
|
|
2790
|
+
// Never infer a primary harness. The legacy native team path is available
|
|
2791
|
+
// only while the session explicitly records Claude as the active provider.
|
|
2794
2792
|
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
2795
|
-
if (!existsSync(sessionPath)) return
|
|
2793
|
+
if (!existsSync(sessionPath)) return false;
|
|
2796
2794
|
const sessionRaw = readFileSync(sessionPath, 'utf-8');
|
|
2797
|
-
const providerMatch = sessionRaw.match(/^\s*
|
|
2798
|
-
|
|
2799
|
-
return provider === 'claude';
|
|
2795
|
+
const providerMatch = sessionRaw.match(/^\s*active_provider:\s*["']?(\w+)/m);
|
|
2796
|
+
return providerMatch?.[1] === 'claude';
|
|
2800
2797
|
}
|
|
2801
2798
|
|
|
2802
2799
|
async function handleSpawnTeam(projectDir, args) {
|
|
@@ -2890,7 +2887,15 @@ async function handleSpawnTeam(projectDir, args) {
|
|
|
2890
2887
|
|
|
2891
2888
|
// Update session with team entry
|
|
2892
2889
|
const previousAgent = args['previous-agent'] || 'none';
|
|
2893
|
-
const
|
|
2890
|
+
const { session: activeSession } = loadSession(projectDir);
|
|
2891
|
+
const provider = activeSession?.active_provider || null;
|
|
2892
|
+
if (provider !== 'claude') {
|
|
2893
|
+
return {
|
|
2894
|
+
action: 'spawn_parallel',
|
|
2895
|
+
fallback_required: true,
|
|
2896
|
+
fallback_reason: 'native_team_requires_explicit_claude_session',
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2894
2899
|
|
|
2895
2900
|
try {
|
|
2896
2901
|
const { loaded, session } = loadSession(projectDir);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { buildHandoff, saveHandoff, loadHandoff } from '../tasks/handoff.js';
|
|
9
9
|
import { readAgentMemory } from '../memory/agent-memory.js';
|
|
10
10
|
import { getRelevantGotchas } from '../memory/gotchas.js';
|
|
11
|
-
import {
|
|
11
|
+
import { updateProjectContext } from '../memory/magic-docs.js';
|
|
12
12
|
import { generateContextFiles } from '../config/context-file-generator.js';
|
|
13
13
|
import { existsSync, readdirSync } from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
@@ -68,14 +68,16 @@ export function executeHandoff(projectDir, params) {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
71
|
+
// Update the provider-neutral Chati project context (Magic Docs).
|
|
72
72
|
try {
|
|
73
73
|
const agentName = params.fromTask?.agent || params.fromTask?.name || params.fromTask?.id?.split('-')[0] || 'agent';
|
|
74
|
-
|
|
74
|
+
updateProjectContext(projectDir, {
|
|
75
75
|
currentAgent: agentName,
|
|
76
76
|
pipelinePosition: handoff.status || 'in_progress',
|
|
77
77
|
activeTask: params.fromTask?.title || params.fromTask?.id || null,
|
|
78
|
-
|
|
78
|
+
validationScore: typeof params.validation?.score === 'number'
|
|
79
|
+
? params.validation.score
|
|
80
|
+
: null,
|
|
79
81
|
decisions: params.decisions
|
|
80
82
|
? Object.entries(params.decisions).map(([what, why]) => ({ what, why: String(why) }))
|
|
81
83
|
: [],
|
|
@@ -85,7 +87,7 @@ export function executeHandoff(projectDir, params) {
|
|
|
85
87
|
});
|
|
86
88
|
} catch { /* Magic Docs update is non-critical */ }
|
|
87
89
|
|
|
88
|
-
//
|
|
90
|
+
// Regenerate native harness contexts from the neutral canonical artifact.
|
|
89
91
|
try {
|
|
90
92
|
generateContextFiles(projectDir);
|
|
91
93
|
} catch { /* Context file sync is non-critical */ }
|
|
@@ -135,7 +135,7 @@ const DEFAULT_SESSION = {
|
|
|
135
135
|
// Execution Mode (Article XVII) — canonical. Article XVIII was deprecated
|
|
136
136
|
// 2026-04-18: execution_profile + profile_transitions were folded into
|
|
137
137
|
// execution_mode (set in DEFAULT_SESSION above as 'interactive'/'autonomous').
|
|
138
|
-
providers_enabled: [
|
|
138
|
+
providers_enabled: [],
|
|
139
139
|
// BUILD → DEPLOY preview state (user approves before devops ships).
|
|
140
140
|
preview: {
|
|
141
141
|
status: 'inactive',
|
|
@@ -176,6 +176,7 @@ export function initSession(projectDir, options = {}) {
|
|
|
176
176
|
started_at: new Date().toISOString(),
|
|
177
177
|
ides: options.ides || [],
|
|
178
178
|
mcps: options.mcps || [],
|
|
179
|
+
providers_enabled: options.providersEnabled || [],
|
|
179
180
|
};
|
|
180
181
|
|
|
181
182
|
try {
|
|
@@ -219,7 +220,8 @@ export function migrateSession(session) {
|
|
|
219
220
|
if (entry.task_id && entry.provider && entry.model) return entry;
|
|
220
221
|
const model = entry.actual || entry.recommended;
|
|
221
222
|
if (!entry.agent || !model || !entry.timestamp) return entry;
|
|
222
|
-
const provider = entry.provider ||
|
|
223
|
+
const provider = entry.provider || inferLegacyProvider(model);
|
|
224
|
+
if (!provider) return entry;
|
|
223
225
|
if (!['claude', 'codex', 'grok', 'gemini'].includes(provider)) return entry;
|
|
224
226
|
normalizedLegacySelections = true;
|
|
225
227
|
return {
|
|
@@ -279,7 +281,17 @@ export function migrateSession(session) {
|
|
|
279
281
|
if (!Array.isArray(session.decision_trail)) session.decision_trail = [];
|
|
280
282
|
if (!Array.isArray(session.teams)) session.teams = [];
|
|
281
283
|
if (!Array.isArray(session.team_events)) session.team_events = [];
|
|
282
|
-
if (!Array.isArray(session.providers_enabled))
|
|
284
|
+
if (!Array.isArray(session.providers_enabled)) {
|
|
285
|
+
const providersByIde = {
|
|
286
|
+
'claude-code': 'claude',
|
|
287
|
+
'gemini-cli': 'gemini',
|
|
288
|
+
'codex-cli': 'codex',
|
|
289
|
+
'grok-cli': 'grok',
|
|
290
|
+
};
|
|
291
|
+
const inferred = (session.ides || []).map(ide => providersByIde[ide]).filter(Boolean);
|
|
292
|
+
if (session.active_provider) inferred.unshift(session.active_provider);
|
|
293
|
+
session.providers_enabled = [...new Set(inferred)];
|
|
294
|
+
}
|
|
283
295
|
if (!Array.isArray(session.model_selections)) session.model_selections = [];
|
|
284
296
|
if (!session.preview || typeof session.preview !== 'object') {
|
|
285
297
|
session.preview = {
|
|
@@ -329,6 +341,15 @@ export function migrateSession(session) {
|
|
|
329
341
|
return { migrated: true, fromVersion, toVersion: CURRENT_SCHEMA_VERSION };
|
|
330
342
|
}
|
|
331
343
|
|
|
344
|
+
function inferLegacyProvider(model) {
|
|
345
|
+
const normalized = String(model || '').toLowerCase();
|
|
346
|
+
if (/claude|opus|sonnet|haiku|fable/.test(normalized)) return 'claude';
|
|
347
|
+
if (/grok/.test(normalized)) return 'grok';
|
|
348
|
+
if (/gemini/.test(normalized)) return 'gemini';
|
|
349
|
+
if (/gpt|codex|terra|luna/.test(normalized)) return 'codex';
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
332
353
|
/**
|
|
333
354
|
* Load current session state.
|
|
334
355
|
* @param {string} projectDir
|
|
@@ -128,7 +128,7 @@ export function getAllProviders() {
|
|
|
128
128
|
* Load enabled providers from project config.yaml.
|
|
129
129
|
*
|
|
130
130
|
* @param {string} projectDir - Project root directory
|
|
131
|
-
* @returns {{ primary: string, enabled: string[] }}
|
|
131
|
+
* @returns {{ primary: string|null, enabled: string[] }}
|
|
132
132
|
*/
|
|
133
133
|
export function loadEnabledProviders(projectDir) {
|
|
134
134
|
const { primary, enabled } = parseProviderConfig(projectDir);
|
|
@@ -136,7 +136,7 @@ export function loadEnabledProviders(projectDir) {
|
|
|
136
136
|
|
|
137
137
|
// Filter out invalid provider names (typos in config.yaml)
|
|
138
138
|
const validEnabled = enabled.filter(name => validNames.includes(name));
|
|
139
|
-
const validPrimary = validNames.includes(primary) ? primary :
|
|
139
|
+
const validPrimary = validNames.includes(primary) ? primary : null;
|
|
140
140
|
|
|
141
141
|
return { primary: validPrimary, enabled: validEnabled };
|
|
142
142
|
}
|
|
@@ -148,7 +148,7 @@ export function loadEnabledProviders(projectDir) {
|
|
|
148
148
|
* @param {string} agent - Agent name
|
|
149
149
|
* @param {string} projectDir - Project root directory
|
|
150
150
|
* @param {Record<string, {provider: string, model: string, tier: string}>} agentModels - Agent model assignments
|
|
151
|
-
* @returns {{ provider: string, model: string }}
|
|
151
|
+
* @returns {{ provider: string|null, model: string|null }}
|
|
152
152
|
*/
|
|
153
153
|
export function resolveProviderForAgent(agent, projectDir, agentModels) {
|
|
154
154
|
const { primary, enabled } = loadEnabledProviders(projectDir);
|
|
@@ -174,8 +174,8 @@ export function resolveProviderForAgent(agent, projectDir, agentModels) {
|
|
|
174
174
|
// Legacy fallback only. Installation v2 routes from the signed task catalog.
|
|
175
175
|
const fallbackProvider = primary || enabled[0];
|
|
176
176
|
const provider = PROVIDERS[fallbackProvider];
|
|
177
|
-
const defaultModel = provider ? Object.keys(provider.modelMap)[0] :
|
|
178
|
-
return { provider: fallbackProvider, model: defaultModel };
|
|
177
|
+
const defaultModel = provider ? Object.keys(provider.modelMap)[0] : null;
|
|
178
|
+
return { provider: fallbackProvider || null, model: defaultModel };
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @fileoverview Prompt builder for multi-terminal agent execution.
|
|
3
3
|
*
|
|
4
4
|
* Builds a complete, self-contained prompt for spawned agent terminals.
|
|
5
|
-
* When an agent runs in a separate
|
|
5
|
+
* When an agent runs in a separate provider process, it has zero
|
|
6
6
|
* conversation history — the prompt must contain everything: PRISM
|
|
7
7
|
* context, agent definition, previous handoff, write scope, and
|
|
8
8
|
* output format instructions.
|
|
@@ -18,7 +18,7 @@ import { resolveOverlayPath } from '../installer/provider-overlay.js';
|
|
|
18
18
|
import { resolveProviderForAgent } from './cli-registry.js';
|
|
19
19
|
import { buildCompactGotchasSummary } from '../memory/gotchas-injector.js';
|
|
20
20
|
import { estimateTokens } from './cost-tracker.js';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveContextLimit } from '../utils/provider-limits.js';
|
|
22
22
|
|
|
23
23
|
// Import AGENT_MODELS from model-governance (safe — named export,
|
|
24
24
|
// does not trigger main() which is guarded by fileURLToPath check).
|
|
@@ -152,6 +152,10 @@ export function buildAgentPrompt(config) {
|
|
|
152
152
|
if (!config.projectDir) {
|
|
153
153
|
throw new Error('buildAgentPrompt requires config.projectDir');
|
|
154
154
|
}
|
|
155
|
+
const runtimeV2 = existsSync(join(config.projectDir, '.chati', 'v2', 'installation.json'));
|
|
156
|
+
if (runtimeV2 && (!config.provider || !config.model)) {
|
|
157
|
+
throw new Error('Runtime v2 requires an exact routed provider and model binding');
|
|
158
|
+
}
|
|
155
159
|
|
|
156
160
|
const sections = [];
|
|
157
161
|
|
|
@@ -203,8 +207,8 @@ export function buildAgentPrompt(config) {
|
|
|
203
207
|
const resolved = (config.projectDir && AGENT_MODELS[config.agent])
|
|
204
208
|
? resolveProviderForAgent(config.agent, config.projectDir, AGENT_MODELS)
|
|
205
209
|
: { provider: staticAssignment.provider, model: staticAssignment.model };
|
|
206
|
-
const resolvedProvider = config.provider || resolved.provider ||
|
|
207
|
-
const model = config.model || resolved.model || staticAssignment.model
|
|
210
|
+
const resolvedProvider = config.provider || resolved.provider || staticAssignment.provider;
|
|
211
|
+
const model = config.model || resolved.model || staticAssignment.model;
|
|
208
212
|
|
|
209
213
|
// 8. Session context (uses resolved model/provider)
|
|
210
214
|
sections.push(buildSessionSection(config, { model, provider: resolvedProvider }));
|
|
@@ -391,8 +395,8 @@ function buildSessionSection(config, resolvedModelInfo = {}) {
|
|
|
391
395
|
`- **User Level**: ${state.user_level || 'auto'}`,
|
|
392
396
|
`- **Execution Mode**: ${state.execution_mode || 'autonomous'}`,
|
|
393
397
|
`- **Your Agent**: ${config.agent}`,
|
|
394
|
-
`- **Your Model**: ${resolvedModelInfo.model || '
|
|
395
|
-
`- **Your Provider**: ${resolvedModelInfo.provider || '
|
|
398
|
+
`- **Your Model**: ${resolvedModelInfo.model || 'unbound'}`,
|
|
399
|
+
`- **Your Provider**: ${resolvedModelInfo.provider || 'unbound'}`,
|
|
396
400
|
];
|
|
397
401
|
|
|
398
402
|
return lines.join('\n');
|
|
@@ -479,11 +483,11 @@ export function buildAntiLazinessSection(agent) {
|
|
|
479
483
|
* Validate prompt size against provider-specific limits.
|
|
480
484
|
*
|
|
481
485
|
* @param {string} prompt - The assembled prompt string
|
|
482
|
-
* @param {string} [provider=
|
|
486
|
+
* @param {string|null} [provider=null] - Provider name for limit lookup
|
|
483
487
|
* @returns {{ valid: boolean, level: string, ratio: number, estimatedTokens: number, limit: number, message: string|null }}
|
|
484
488
|
*/
|
|
485
|
-
export function validatePromptSize(prompt, provider =
|
|
486
|
-
const limit =
|
|
489
|
+
export function validatePromptSize(prompt, provider = null) {
|
|
490
|
+
const limit = resolveContextLimit(undefined, provider);
|
|
487
491
|
const tokens = estimateTokens(prompt);
|
|
488
492
|
const ratio = tokens / limit;
|
|
489
493
|
|
|
@@ -737,7 +741,7 @@ export function buildPresetSection(preset) {
|
|
|
737
741
|
* Build output format instructions so the agent produces a parseable handoff.
|
|
738
742
|
*/
|
|
739
743
|
function buildOutputInstructions(config) {
|
|
740
|
-
const providerValue = config?.provider || '
|
|
744
|
+
const providerValue = config?.provider || 'unbound';
|
|
741
745
|
const modelValue = config?.model || 'unknown';
|
|
742
746
|
|
|
743
747
|
return `<!-- OUTPUT INSTRUCTIONS -->
|