chati-dev 2.1.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/framework/agents/build/dev.md +1 -0
- package/framework/agents/deploy/devops.md +1 -0
- package/framework/agents/discover/brief.md +1 -0
- package/framework/agents/discover/brownfield-wu.md +1 -0
- package/framework/agents/discover/greenfield-wu.md +1 -0
- package/framework/agents/plan/architect.md +1 -0
- package/framework/agents/plan/detail.md +1 -0
- package/framework/agents/plan/phases.md +1 -0
- package/framework/agents/plan/tasks.md +1 -0
- package/framework/agents/plan/ux.md +1 -0
- package/framework/agents/quality/qa-implementation.md +1 -0
- package/framework/agents/quality/qa-planning.md +1 -0
- package/framework/config.yaml +21 -2
- package/framework/constitution.md +66 -1
- package/framework/domains/agents/brownfield-wu.yaml +4 -0
- package/framework/domains/agents/dev.yaml +4 -0
- package/framework/domains/agents/orchestrator.yaml +8 -0
- package/framework/domains/constitution.yaml +28 -0
- package/framework/domains/global.yaml +20 -0
- package/framework/hooks/model-governance.js +17 -15
- package/framework/intelligence/context-engine.md +29 -0
- package/framework/intelligence/memory-layer.md +17 -0
- package/framework/orchestrator/chati.md +94 -14
- package/framework/schemas/config.schema.json +44 -0
- package/framework/schemas/session.schema.json +27 -0
- package/package.json +5 -1
- package/src/autonomy/build-loop.js +194 -0
- package/src/autonomy/build-state.js +269 -0
- package/src/autonomy/execution-profile.js +151 -0
- package/src/config/context-file-generator.js +209 -0
- package/src/gates/g2-qa-planning.js +4 -2
- package/src/gates/g4-qa-implementation.js +5 -2
- package/src/gates/gate-base.js +33 -1
- package/src/health/engine.js +250 -0
- package/src/intelligence/file-tracker.js +117 -0
- package/src/intelligence/timeline.js +144 -0
- package/src/memory/gotchas-auto-capture.js +253 -0
- package/src/terminal/adapters/claude-adapter.js +43 -0
- package/src/terminal/adapters/codex-adapter.js +41 -0
- package/src/terminal/adapters/copilot-adapter.js +38 -0
- package/src/terminal/adapters/gemini-adapter.js +42 -0
- package/src/terminal/adapters/index.js +8 -0
- package/src/terminal/cli-registry.js +218 -0
- package/src/terminal/prompt-builder.js +18 -15
- package/src/terminal/spawner.js +19 -9
- package/src/terminal/wave-analyzer.js +143 -0
- package/framework/manifest.json +0 -5
- package/framework/manifest.sig +0 -1
- /package/assets/{logo - co/314/201pia.png" → logo - c/303/263pia.png"} +0 -0
- /package/assets/{logo - co/314/201pia.svg" → logo - c/303/263pia.svg"} +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CLI Provider Registry for multi-CLI agent execution.
|
|
3
|
+
*
|
|
4
|
+
* Central registry of all supported CLI providers with their capabilities,
|
|
5
|
+
* command syntax, model maps, and feature support. This is the source of
|
|
6
|
+
* truth for multi-CLI governance (Constitution Article XIX).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import * as adapters from './adapters/index.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Provider Definitions
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} ProviderConfig
|
|
19
|
+
* @property {string} name - Provider identifier
|
|
20
|
+
* @property {string} command - CLI command name
|
|
21
|
+
* @property {string[]} baseArgs - Default CLI arguments for non-interactive mode
|
|
22
|
+
* @property {string} modelFlag - CLI flag for model selection
|
|
23
|
+
* @property {boolean} stdinSupport - Whether prompts can be piped via stdin
|
|
24
|
+
* @property {boolean} hooksSupport - Whether the CLI supports hooks (event middleware)
|
|
25
|
+
* @property {boolean} mcpSupport - Whether the CLI supports MCP servers
|
|
26
|
+
* @property {string|null} contextFile - Project context file name (CLAUDE.md, GEMINI.md, etc.)
|
|
27
|
+
* @property {Record<string, string>} modelMap - Tier-to-model-id mapping
|
|
28
|
+
* @property {object} adapter - CLI-specific adapter module
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** @type {Record<string, ProviderConfig>} */
|
|
32
|
+
const PROVIDERS = {
|
|
33
|
+
claude: {
|
|
34
|
+
name: 'claude',
|
|
35
|
+
command: 'claude',
|
|
36
|
+
baseArgs: ['--print', '--dangerously-skip-permissions'],
|
|
37
|
+
modelFlag: '--model',
|
|
38
|
+
stdinSupport: true,
|
|
39
|
+
hooksSupport: true,
|
|
40
|
+
mcpSupport: true,
|
|
41
|
+
contextFile: 'CLAUDE.md',
|
|
42
|
+
modelMap: {
|
|
43
|
+
opus: 'claude-opus-4-6',
|
|
44
|
+
sonnet: 'claude-sonnet-4-5-20250929',
|
|
45
|
+
haiku: 'claude-haiku-4-5-20251001',
|
|
46
|
+
},
|
|
47
|
+
adapter: adapters.claude,
|
|
48
|
+
},
|
|
49
|
+
gemini: {
|
|
50
|
+
name: 'gemini',
|
|
51
|
+
command: 'gemini',
|
|
52
|
+
baseArgs: ['--prompt'],
|
|
53
|
+
modelFlag: '--model',
|
|
54
|
+
stdinSupport: true,
|
|
55
|
+
hooksSupport: true,
|
|
56
|
+
mcpSupport: true,
|
|
57
|
+
contextFile: 'GEMINI.md',
|
|
58
|
+
modelMap: {
|
|
59
|
+
pro: 'gemini-2.5-pro',
|
|
60
|
+
flash: 'gemini-2.5-flash',
|
|
61
|
+
},
|
|
62
|
+
adapter: adapters.gemini,
|
|
63
|
+
},
|
|
64
|
+
codex: {
|
|
65
|
+
name: 'codex',
|
|
66
|
+
command: 'codex',
|
|
67
|
+
baseArgs: ['exec'],
|
|
68
|
+
modelFlag: '-m',
|
|
69
|
+
stdinSupport: true,
|
|
70
|
+
hooksSupport: false,
|
|
71
|
+
mcpSupport: true,
|
|
72
|
+
contextFile: 'AGENTS.md',
|
|
73
|
+
modelMap: {
|
|
74
|
+
codex: 'gpt-5.3-codex',
|
|
75
|
+
mini: 'gpt-5.1-codex-mini',
|
|
76
|
+
},
|
|
77
|
+
adapter: adapters.codex,
|
|
78
|
+
},
|
|
79
|
+
copilot: {
|
|
80
|
+
name: 'copilot',
|
|
81
|
+
command: 'copilot',
|
|
82
|
+
baseArgs: ['-p'],
|
|
83
|
+
modelFlag: '--model',
|
|
84
|
+
stdinSupport: true,
|
|
85
|
+
hooksSupport: true,
|
|
86
|
+
mcpSupport: true,
|
|
87
|
+
contextFile: null,
|
|
88
|
+
modelMap: {
|
|
89
|
+
'claude-sonnet': 'claude-sonnet-4.5',
|
|
90
|
+
'gpt-5': 'gpt-5.1',
|
|
91
|
+
},
|
|
92
|
+
adapter: adapters.copilot,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Provider Resolution
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get a provider configuration by name.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} name - Provider name (claude, gemini, codex, copilot)
|
|
104
|
+
* @returns {ProviderConfig}
|
|
105
|
+
* @throws {Error} When provider is not found
|
|
106
|
+
*/
|
|
107
|
+
export function getProvider(name) {
|
|
108
|
+
const provider = PROVIDERS[name];
|
|
109
|
+
if (!provider) {
|
|
110
|
+
throw new Error(`Unknown CLI provider: "${name}". Available: ${Object.keys(PROVIDERS).join(', ')}`);
|
|
111
|
+
}
|
|
112
|
+
return provider;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get all registered providers.
|
|
117
|
+
*
|
|
118
|
+
* @returns {Record<string, ProviderConfig>}
|
|
119
|
+
*/
|
|
120
|
+
export function getAllProviders() {
|
|
121
|
+
return { ...PROVIDERS };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load enabled providers from project config.yaml.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} projectDir - Project root directory
|
|
128
|
+
* @returns {{ primary: string, enabled: string[] }}
|
|
129
|
+
*/
|
|
130
|
+
export function loadEnabledProviders(projectDir) {
|
|
131
|
+
const configPath = join(projectDir, 'chati.dev', 'config.yaml');
|
|
132
|
+
if (!existsSync(configPath)) {
|
|
133
|
+
return { primary: 'claude', enabled: ['claude'] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
137
|
+
|
|
138
|
+
// Lightweight YAML extraction (avoid dependency in spawning path)
|
|
139
|
+
const enabled = [];
|
|
140
|
+
let primary = 'claude';
|
|
141
|
+
|
|
142
|
+
for (const name of Object.keys(PROVIDERS)) {
|
|
143
|
+
const enabledMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?enabled:\\s*(true|false)`, 'm'));
|
|
144
|
+
if (enabledMatch && enabledMatch[1] === 'true') {
|
|
145
|
+
enabled.push(name);
|
|
146
|
+
}
|
|
147
|
+
const primaryMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?primary:\\s*(true|false)`, 'm'));
|
|
148
|
+
if (primaryMatch && primaryMatch[1] === 'true') {
|
|
149
|
+
primary = name;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Claude is always enabled as fallback
|
|
154
|
+
if (!enabled.includes('claude')) {
|
|
155
|
+
enabled.unshift('claude');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { primary, enabled };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolve which provider should be used for a given agent.
|
|
163
|
+
* Priority: agent_overrides > agent default > primary provider.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} agent - Agent name
|
|
166
|
+
* @param {string} projectDir - Project root directory
|
|
167
|
+
* @param {Record<string, {provider: string, model: string, tier: string}>} agentModels - Agent model assignments
|
|
168
|
+
* @returns {{ provider: string, model: string }}
|
|
169
|
+
*/
|
|
170
|
+
export function resolveProviderForAgent(agent, projectDir, agentModels) {
|
|
171
|
+
const { primary, enabled } = loadEnabledProviders(projectDir);
|
|
172
|
+
|
|
173
|
+
// Check agent_overrides in config.yaml
|
|
174
|
+
const configPath = join(projectDir, 'chati.dev', 'config.yaml');
|
|
175
|
+
if (existsSync(configPath)) {
|
|
176
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
177
|
+
const overrideMatch = raw.match(new RegExp(`${agent}:\\s*\\{[^}]*provider:\\s*(\\w+)[^}]*model:\\s*(\\w+)`, 'm'));
|
|
178
|
+
if (overrideMatch) {
|
|
179
|
+
const overrideProvider = overrideMatch[1];
|
|
180
|
+
if (enabled.includes(overrideProvider)) {
|
|
181
|
+
return { provider: overrideProvider, model: overrideMatch[2] };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Use agent's default assignment
|
|
187
|
+
const agentConfig = agentModels[agent];
|
|
188
|
+
if (agentConfig) {
|
|
189
|
+
const agentProvider = agentConfig.provider || primary;
|
|
190
|
+
if (enabled.includes(agentProvider)) {
|
|
191
|
+
return { provider: agentProvider, model: agentConfig.model };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Fallback to primary
|
|
196
|
+
return { provider: primary, model: 'sonnet' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if a provider CLI is available on the system.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} name - Provider name
|
|
203
|
+
* @returns {Promise<boolean>}
|
|
204
|
+
*/
|
|
205
|
+
export async function isProviderAvailable(name) {
|
|
206
|
+
const provider = PROVIDERS[name];
|
|
207
|
+
if (!provider) return false;
|
|
208
|
+
|
|
209
|
+
const { execSync } = await import('child_process');
|
|
210
|
+
try {
|
|
211
|
+
execSync(`which ${provider.command}`, { stdio: 'ignore' });
|
|
212
|
+
return true;
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export { PROVIDERS };
|
|
@@ -18,19 +18,19 @@ import { getWriteScope } from './isolation.js';
|
|
|
18
18
|
// without circular dependency issues (the hook file runs main() on import
|
|
19
19
|
// which writes to stdout — we only need the data, so we inline the map).
|
|
20
20
|
const AGENT_MODELS = {
|
|
21
|
-
orchestrator: 'sonnet',
|
|
22
|
-
'greenfield-wu': 'haiku',
|
|
23
|
-
'brownfield-wu': 'opus',
|
|
24
|
-
brief: 'sonnet',
|
|
25
|
-
detail: 'opus',
|
|
26
|
-
architect: 'opus',
|
|
27
|
-
ux: 'sonnet',
|
|
28
|
-
phases: 'sonnet',
|
|
29
|
-
tasks: 'sonnet',
|
|
30
|
-
'qa-planning': 'opus',
|
|
31
|
-
'qa-implementation': 'opus',
|
|
32
|
-
dev: 'opus',
|
|
33
|
-
devops: 'sonnet',
|
|
21
|
+
orchestrator: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
|
|
22
|
+
'greenfield-wu': { provider: 'claude', model: 'haiku', tier: 'haiku' },
|
|
23
|
+
'brownfield-wu': { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
24
|
+
brief: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
|
|
25
|
+
detail: { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
26
|
+
architect: { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
27
|
+
ux: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
|
|
28
|
+
phases: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
|
|
29
|
+
tasks: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
|
|
30
|
+
'qa-planning': { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
31
|
+
'qa-implementation': { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
32
|
+
dev: { provider: 'claude', model: 'opus', tier: 'opus' },
|
|
33
|
+
devops: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -116,11 +116,13 @@ export function buildAgentPrompt(config) {
|
|
|
116
116
|
sections.push(buildOutputInstructions());
|
|
117
117
|
|
|
118
118
|
const prompt = sections.join('\n\n---\n\n');
|
|
119
|
-
const
|
|
119
|
+
const assignment = AGENT_MODELS[config.agent] || { provider: 'claude', model: 'sonnet', tier: 'sonnet' };
|
|
120
|
+
const model = assignment.model || assignment;
|
|
120
121
|
|
|
121
122
|
return {
|
|
122
123
|
prompt,
|
|
123
124
|
model,
|
|
125
|
+
provider: assignment.provider || 'claude',
|
|
124
126
|
metadata: {
|
|
125
127
|
agent: config.agent,
|
|
126
128
|
layers: prismResult.layerCount || 0,
|
|
@@ -266,7 +268,8 @@ function buildSessionSection(config) {
|
|
|
266
268
|
`- **User Level**: ${state.user_level || 'auto'}`,
|
|
267
269
|
`- **Execution Mode**: ${state.execution_mode || 'autonomous'}`,
|
|
268
270
|
`- **Your Agent**: ${config.agent}`,
|
|
269
|
-
`- **Your Model**: ${AGENT_MODELS[config.agent] || 'sonnet'}`,
|
|
271
|
+
`- **Your Model**: ${(AGENT_MODELS[config.agent]?.model) || 'sonnet'}`,
|
|
272
|
+
`- **Your Provider**: ${(AGENT_MODELS[config.agent]?.provider) || 'claude'}`,
|
|
270
273
|
];
|
|
271
274
|
|
|
272
275
|
return lines.join('\n');
|
package/src/terminal/spawner.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { spawn } from 'child_process';
|
|
11
11
|
import { validateWriteScopes, buildIsolationEnv } from './isolation.js';
|
|
12
|
+
import { getProvider } from './cli-registry.js';
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Helpers
|
|
@@ -103,17 +104,26 @@ export function buildSpawnCommand(config) {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
//
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
// Resolve CLI provider — defaults to claude for backwards compatibility
|
|
108
|
+
const providerName = config.provider || 'claude';
|
|
109
|
+
let command, args, prompt;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const provider = getProvider(providerName);
|
|
113
|
+
const adapterResult = provider.adapter.buildCommand(config, provider);
|
|
114
|
+
command = adapterResult.command;
|
|
115
|
+
args = adapterResult.args;
|
|
116
|
+
prompt = adapterResult.stdinPrompt;
|
|
117
|
+
} catch {
|
|
118
|
+
// Fallback to claude if provider resolution fails (backwards compatibility)
|
|
119
|
+
command = 'claude';
|
|
120
|
+
args = ['--print', '--dangerously-skip-permissions'];
|
|
121
|
+
if (config.model) {
|
|
122
|
+
args.push('--model', config.model);
|
|
123
|
+
}
|
|
124
|
+
prompt = config.prompt || null;
|
|
112
125
|
}
|
|
113
126
|
|
|
114
|
-
// Prompt is returned separately for stdin piping (avoids ARG_MAX limits)
|
|
115
|
-
const prompt = config.prompt || null;
|
|
116
|
-
|
|
117
127
|
return { command, args, env, terminalId, prompt };
|
|
118
128
|
}
|
|
119
129
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Wave analyzer for task parallelization.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes task dependencies to group them into waves —
|
|
5
|
+
* sets of tasks that can execute in parallel because they
|
|
6
|
+
* have no inter-dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} TaskNode
|
|
15
|
+
* @property {string} id - Task identifier
|
|
16
|
+
* @property {string[]} dependencies - IDs of tasks this depends on
|
|
17
|
+
* @property {string[]} [writeScope] - Files/dirs this task writes to
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} Wave
|
|
22
|
+
* @property {number} index - Wave number (0-based)
|
|
23
|
+
* @property {string[]} taskIds - Task IDs in this wave
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Wave Analysis
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a dependency graph and compute waves using topological sorting.
|
|
32
|
+
*
|
|
33
|
+
* @param {TaskNode[]} tasks - Array of tasks with dependencies
|
|
34
|
+
* @returns {Wave[]}
|
|
35
|
+
* @throws {Error} When circular dependencies are detected
|
|
36
|
+
*/
|
|
37
|
+
export function analyzeWaves(tasks) {
|
|
38
|
+
if (!tasks || tasks.length === 0) return [];
|
|
39
|
+
|
|
40
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
41
|
+
const inDegree = new Map();
|
|
42
|
+
const adjList = new Map();
|
|
43
|
+
|
|
44
|
+
// Initialize
|
|
45
|
+
for (const task of tasks) {
|
|
46
|
+
inDegree.set(task.id, 0);
|
|
47
|
+
adjList.set(task.id, []);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build adjacency and in-degree
|
|
51
|
+
for (const task of tasks) {
|
|
52
|
+
for (const dep of task.dependencies || []) {
|
|
53
|
+
if (taskMap.has(dep)) {
|
|
54
|
+
adjList.get(dep).push(task.id);
|
|
55
|
+
inDegree.set(task.id, (inDegree.get(task.id) || 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Kahn's algorithm with wave grouping
|
|
61
|
+
const waves = [];
|
|
62
|
+
let remaining = new Set(tasks.map((t) => t.id));
|
|
63
|
+
|
|
64
|
+
while (remaining.size > 0) {
|
|
65
|
+
// Find all tasks with no pending dependencies (in-degree 0)
|
|
66
|
+
const wave = [];
|
|
67
|
+
for (const id of remaining) {
|
|
68
|
+
if ((inDegree.get(id) || 0) === 0) {
|
|
69
|
+
wave.push(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (wave.length === 0) {
|
|
74
|
+
const circular = [...remaining].join(', ');
|
|
75
|
+
throw new Error(`Circular dependency detected among tasks: ${circular}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
waves.push({ index: waves.length, taskIds: wave });
|
|
79
|
+
|
|
80
|
+
// Remove wave tasks and update in-degrees
|
|
81
|
+
for (const id of wave) {
|
|
82
|
+
remaining.delete(id);
|
|
83
|
+
for (const dependent of adjList.get(id) || []) {
|
|
84
|
+
inDegree.set(dependent, (inDegree.get(dependent) || 0) - 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return waves;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate that tasks within a wave don't have write scope conflicts.
|
|
94
|
+
*
|
|
95
|
+
* @param {TaskNode[]} tasks - All tasks
|
|
96
|
+
* @param {Wave} wave - Wave to validate
|
|
97
|
+
* @returns {{ valid: boolean, conflicts: { taskA: string, taskB: string, path: string }[] }}
|
|
98
|
+
*/
|
|
99
|
+
export function validateWaveScopes(tasks, wave) {
|
|
100
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
101
|
+
const conflicts = [];
|
|
102
|
+
|
|
103
|
+
const waveTasks = wave.taskIds.map((id) => taskMap.get(id)).filter(Boolean);
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < waveTasks.length; i++) {
|
|
106
|
+
for (let j = i + 1; j < waveTasks.length; j++) {
|
|
107
|
+
const scopeA = waveTasks[i].writeScope || [];
|
|
108
|
+
const scopeB = waveTasks[j].writeScope || [];
|
|
109
|
+
|
|
110
|
+
for (const pathA of scopeA) {
|
|
111
|
+
for (const pathB of scopeB) {
|
|
112
|
+
if (pathA === pathB || pathA.startsWith(pathB + '/') || pathB.startsWith(pathA + '/')) {
|
|
113
|
+
conflicts.push({
|
|
114
|
+
taskA: waveTasks[i].id,
|
|
115
|
+
taskB: waveTasks[j].id,
|
|
116
|
+
path: pathA,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { valid: conflicts.length === 0, conflicts };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get a summary of the wave analysis.
|
|
129
|
+
*
|
|
130
|
+
* @param {Wave[]} waves
|
|
131
|
+
* @returns {{ totalWaves: number, totalTasks: number, maxParallel: number, sequential: boolean }}
|
|
132
|
+
*/
|
|
133
|
+
export function getWaveSummary(waves) {
|
|
134
|
+
const totalTasks = waves.reduce((sum, w) => sum + w.taskIds.length, 0);
|
|
135
|
+
const maxParallel = Math.max(...waves.map((w) => w.taskIds.length), 0);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
totalWaves: waves.length,
|
|
139
|
+
totalTasks,
|
|
140
|
+
maxParallel,
|
|
141
|
+
sequential: waves.every((w) => w.taskIds.length === 1),
|
|
142
|
+
};
|
|
143
|
+
}
|
package/framework/manifest.json
DELETED
package/framework/manifest.sig
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Wjy+M0Wcyt+KTzRu0E4fc8sB97cTe7ye+qEtNGq/av7AzcB2GsSNlwrOhfMV64ukMBHSrBMQ995sDbTtzNKADA==
|
|
File without changes
|
|
File without changes
|