orquesta-cli 0.1.26 → 0.2.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/dist/agents/planner/index.d.ts +2 -1
- package/dist/agents/planner/index.js +10 -2
- package/dist/core/config/config-manager.d.ts +11 -1
- package/dist/core/config/config-manager.js +60 -0
- package/dist/core/config/providers.js +16 -0
- package/dist/core/llm/llm-client.d.ts +1 -0
- package/dist/core/llm/llm-client.js +2 -0
- package/dist/core/project-context.d.ts +3 -0
- package/dist/core/project-context.js +54 -0
- package/dist/core/slash-command-handler.js +139 -0
- package/dist/orchestration/audit-log.d.ts +40 -0
- package/dist/orchestration/audit-log.js +156 -0
- package/dist/orchestration/index.d.ts +3 -0
- package/dist/orchestration/index.js +3 -0
- package/dist/orchestration/memory-store.d.ts +21 -0
- package/dist/orchestration/memory-store.js +102 -0
- package/dist/orchestration/parallel-orchestrator.d.ts +22 -0
- package/dist/orchestration/parallel-orchestrator.js +234 -0
- package/dist/orchestration/plan-executor.d.ts +1 -0
- package/dist/orchestration/plan-executor.js +160 -19
- package/dist/orchestration/worktree-manager.d.ts +25 -0
- package/dist/orchestration/worktree-manager.js +124 -0
- package/dist/tools/llm/simple/planning-tools.js +16 -2
- package/dist/types/index.d.ts +16 -0
- package/dist/ui/components/TodoListView.js +53 -15
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
async function isGitRepo(cwd) {
|
|
9
|
+
try {
|
|
10
|
+
await execAsync('git rev-parse --is-inside-work-tree', { cwd });
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function gitRepoRoot(cwd) {
|
|
18
|
+
const { stdout } = await execAsync('git rev-parse --show-toplevel', { cwd });
|
|
19
|
+
return stdout.trim();
|
|
20
|
+
}
|
|
21
|
+
export class WorktreeManager {
|
|
22
|
+
allocations = new Map();
|
|
23
|
+
tmpRoot;
|
|
24
|
+
constructor(tmpRoot) {
|
|
25
|
+
this.tmpRoot = tmpRoot ?? path.join(os.tmpdir(), 'orquesta-cli-worktrees');
|
|
26
|
+
}
|
|
27
|
+
async allocate(id, hostCwd = process.cwd()) {
|
|
28
|
+
if (this.allocations.has(id)) {
|
|
29
|
+
throw new Error(`Worktree already allocated for id=${id}`);
|
|
30
|
+
}
|
|
31
|
+
const gitMode = await isGitRepo(hostCwd);
|
|
32
|
+
let allocation;
|
|
33
|
+
if (gitMode) {
|
|
34
|
+
const repoRoot = await gitRepoRoot(hostCwd);
|
|
35
|
+
const branch = `orquesta/worker-${id}-${Date.now()}`;
|
|
36
|
+
const worktreeDir = path.join(this.tmpRoot, 'git', id);
|
|
37
|
+
await fs.mkdir(path.dirname(worktreeDir), { recursive: true });
|
|
38
|
+
await execAsync(`git worktree add -b ${branch} "${worktreeDir}" HEAD`, { cwd: repoRoot });
|
|
39
|
+
allocation = { id, path: worktreeDir, backend: 'git', branch };
|
|
40
|
+
logger.flow('Allocated git worktree', { id, branch, path: worktreeDir });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const copyDir = path.join(this.tmpRoot, 'copy', id);
|
|
44
|
+
await fs.mkdir(copyDir, { recursive: true });
|
|
45
|
+
try {
|
|
46
|
+
await execAsync(`rsync -a --exclude='.git' "${hostCwd}/" "${copyDir}/"`);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
await execAsync(`cp -a "${hostCwd}/." "${copyDir}/"`);
|
|
50
|
+
}
|
|
51
|
+
allocation = { id, path: copyDir, backend: 'copy' };
|
|
52
|
+
logger.flow('Allocated copy worktree', { id, path: copyDir });
|
|
53
|
+
}
|
|
54
|
+
this.allocations.set(id, allocation);
|
|
55
|
+
return allocation;
|
|
56
|
+
}
|
|
57
|
+
async mergeBack(id, hostCwd = process.cwd()) {
|
|
58
|
+
const alloc = this.allocations.get(id);
|
|
59
|
+
if (!alloc) {
|
|
60
|
+
return { success: false, filesChanged: [], conflicts: [], error: `No allocation for id=${id}` };
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
if (alloc.backend === 'git' && alloc.branch) {
|
|
64
|
+
const repoRoot = await gitRepoRoot(hostCwd);
|
|
65
|
+
const { stdout: diffStat } = await execAsync(`git diff --name-only HEAD ${alloc.branch}`, { cwd: repoRoot });
|
|
66
|
+
const filesChanged = diffStat.split('\n').map(s => s.trim()).filter(Boolean);
|
|
67
|
+
try {
|
|
68
|
+
await execAsync(`git merge --no-ff ${alloc.branch}`, { cwd: repoRoot });
|
|
69
|
+
return { success: true, filesChanged, conflicts: [] };
|
|
70
|
+
}
|
|
71
|
+
catch (mergeError) {
|
|
72
|
+
const { stdout: conflicts } = await execAsync('git diff --name-only --diff-filter=U', { cwd: repoRoot }).catch(() => ({ stdout: '' }));
|
|
73
|
+
await execAsync('git merge --abort', { cwd: repoRoot }).catch(() => { });
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
filesChanged,
|
|
77
|
+
conflicts: conflicts.split('\n').map(s => s.trim()).filter(Boolean),
|
|
78
|
+
error: mergeError.message,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const { stdout: rawDiff } = await execAsync(`rsync -avc --dry-run --exclude='.git' "${alloc.path}/" "${hostCwd}/"`).catch(() => ({ stdout: '' }));
|
|
83
|
+
const filesChanged = rawDiff
|
|
84
|
+
.split('\n')
|
|
85
|
+
.filter(line => line && !line.startsWith('sending') && !line.startsWith('total') && !line.startsWith('sent ') && !line.includes('xfer#'))
|
|
86
|
+
.map(s => s.trim())
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
await execAsync(`rsync -a --exclude='.git' "${alloc.path}/" "${hostCwd}/"`);
|
|
89
|
+
return { success: true, filesChanged, conflicts: [] };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return { success: false, filesChanged: [], conflicts: [], error: error.message };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async release(id, hostCwd = process.cwd()) {
|
|
96
|
+
const alloc = this.allocations.get(id);
|
|
97
|
+
if (!alloc)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
if (alloc.backend === 'git') {
|
|
101
|
+
const repoRoot = await gitRepoRoot(hostCwd);
|
|
102
|
+
await execAsync(`git worktree remove --force "${alloc.path}"`, { cwd: repoRoot }).catch(() => { });
|
|
103
|
+
if (alloc.branch) {
|
|
104
|
+
await execAsync(`git branch -D ${alloc.branch}`, { cwd: repoRoot }).catch(() => { });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
await fs.rm(alloc.path, { recursive: true, force: true }).catch(() => { });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
this.allocations.delete(id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async releaseAll(hostCwd = process.cwd()) {
|
|
116
|
+
const ids = Array.from(this.allocations.keys());
|
|
117
|
+
await Promise.all(ids.map(id => this.release(id, hostCwd)));
|
|
118
|
+
}
|
|
119
|
+
listActive() {
|
|
120
|
+
return Array.from(this.allocations.values());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export const worktreeManager = new WorktreeManager();
|
|
124
|
+
//# sourceMappingURL=worktree-manager.js.map
|
|
@@ -19,7 +19,8 @@ DO NOT use for:
|
|
|
19
19
|
Guidelines:
|
|
20
20
|
- 1-5 TODOs (even 1 is fine for simple actions!)
|
|
21
21
|
- Actionable titles that clearly describe what to do
|
|
22
|
-
-
|
|
22
|
+
- Use 'dependsOn' to mark dependencies — TODOs without dependsOn that share no prerequisites run in PARALLEL
|
|
23
|
+
- Mark 'requiresFilesystem: true' for TODOs that write to disk (so the orchestrator isolates them)
|
|
23
24
|
- Include details in title
|
|
24
25
|
|
|
25
26
|
IMPORTANT: Write TODO titles in the user's language.`,
|
|
@@ -28,7 +29,7 @@ IMPORTANT: Write TODO titles in the user's language.`,
|
|
|
28
29
|
properties: {
|
|
29
30
|
todos: {
|
|
30
31
|
type: 'array',
|
|
31
|
-
description: 'List of TODO items',
|
|
32
|
+
description: 'List of TODO items. Independent items (no dependsOn) are dispatched concurrently when parallel execution is enabled.',
|
|
32
33
|
items: {
|
|
33
34
|
type: 'object',
|
|
34
35
|
properties: {
|
|
@@ -40,6 +41,19 @@ IMPORTANT: Write TODO titles in the user's language.`,
|
|
|
40
41
|
type: 'string',
|
|
41
42
|
description: 'Clear, actionable task title (in user language)',
|
|
42
43
|
},
|
|
44
|
+
dependsOn: {
|
|
45
|
+
type: 'array',
|
|
46
|
+
items: { type: 'string' },
|
|
47
|
+
description: 'Optional. IDs of other TODOs that must complete before this one starts. Omit for tasks that can run independently.',
|
|
48
|
+
},
|
|
49
|
+
requiresFilesystem: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
description: 'Optional. True when this TODO writes to the filesystem (file edits, shell commands that modify state). Used to allocate per-worker worktree.',
|
|
52
|
+
},
|
|
53
|
+
parallelGroup: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Optional grouping label so the UI can visually group sibling parallel branches.',
|
|
56
|
+
},
|
|
43
57
|
},
|
|
44
58
|
required: ['id', 'title'],
|
|
45
59
|
},
|
package/dist/types/index.d.ts
CHANGED
|
@@ -103,6 +103,18 @@ export interface OrquestaConfig {
|
|
|
103
103
|
lastSyncAt?: string;
|
|
104
104
|
connectedAt?: string;
|
|
105
105
|
}
|
|
106
|
+
export type OrchestrationRole = 'planner' | 'executor' | 'refiner';
|
|
107
|
+
export interface RoleModels {
|
|
108
|
+
planner?: string;
|
|
109
|
+
executor?: string;
|
|
110
|
+
refiner?: string;
|
|
111
|
+
}
|
|
112
|
+
export interface OrchestrationConfig {
|
|
113
|
+
roleModels?: RoleModels;
|
|
114
|
+
maxParallelWorkers?: number;
|
|
115
|
+
refinerEnabled?: boolean;
|
|
116
|
+
worktreeIsolation?: boolean;
|
|
117
|
+
}
|
|
106
118
|
export interface OpenConfig {
|
|
107
119
|
version: string;
|
|
108
120
|
currentEndpoint?: string;
|
|
@@ -117,6 +129,7 @@ export interface OpenConfig {
|
|
|
117
129
|
enabledTools?: string[];
|
|
118
130
|
safeEnvVars?: string[];
|
|
119
131
|
orquesta?: OrquestaConfig;
|
|
132
|
+
orchestration?: OrchestrationConfig;
|
|
120
133
|
}
|
|
121
134
|
export interface TodoItem {
|
|
122
135
|
id: string;
|
|
@@ -124,6 +137,9 @@ export interface TodoItem {
|
|
|
124
137
|
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
|
125
138
|
result?: string;
|
|
126
139
|
error?: string;
|
|
140
|
+
dependsOn?: string[];
|
|
141
|
+
requiresFilesystem?: boolean;
|
|
142
|
+
parallelGroup?: string;
|
|
127
143
|
}
|
|
128
144
|
export interface PlanningResult {
|
|
129
145
|
todos: TodoItem[];
|
|
@@ -42,26 +42,64 @@ export const TodoListView = ({ todos, showProgressBar = true, }) => {
|
|
|
42
42
|
});
|
|
43
43
|
}, [todos]);
|
|
44
44
|
const completedCount = todos.filter(t => t.status === 'completed').length;
|
|
45
|
+
const inProgressCount = todos.filter(t => t.status === 'in_progress').length;
|
|
45
46
|
const totalCount = todos.length;
|
|
46
47
|
if (totalCount === 0) {
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
50
|
+
const hasParallelGroups = todos.some(t => t.parallelGroup);
|
|
51
|
+
const isParallel = inProgressCount >= 2 || hasParallelGroups;
|
|
52
|
+
const renderTodoRow = (todo, indent = 0) => {
|
|
53
|
+
const config = STATUS_CONFIG[todo.status] || STATUS_CONFIG.pending;
|
|
54
|
+
const isInProgress = todo.status === 'in_progress';
|
|
55
|
+
const isCompleted = todo.status === 'completed';
|
|
56
|
+
return (React.createElement(Box, { key: todo.id, flexDirection: "column", marginLeft: indent },
|
|
57
|
+
React.createElement(Box, null,
|
|
58
|
+
React.createElement(Box, { width: 2 }, isInProgress ? (React.createElement(Text, { color: "blueBright" },
|
|
59
|
+
React.createElement(Spinner, { type: "dots2" }))) : (React.createElement(Text, { color: config.color }, config.icon))),
|
|
60
|
+
React.createElement(Text, { color: isCompleted ? 'gray' : isInProgress ? 'white' : 'gray', dimColor: isCompleted, strikethrough: isCompleted, bold: isInProgress }, todo.title),
|
|
61
|
+
isInProgress && React.createElement(Text, { color: "blueBright" }, " \u2190"),
|
|
62
|
+
todo.dependsOn && todo.dependsOn.length > 0 && (React.createElement(Text, { color: "gray", dimColor: true },
|
|
63
|
+
" (after ",
|
|
64
|
+
todo.dependsOn.join(','),
|
|
65
|
+
")")),
|
|
66
|
+
todo.requiresFilesystem && isInProgress && (React.createElement(Text, { color: "yellow", dimColor: true }, " [worktree]"))),
|
|
67
|
+
todo.error && (React.createElement(Box, { marginLeft: 2 },
|
|
68
|
+
React.createElement(Text, { color: "red", dimColor: true },
|
|
69
|
+
"\u26A0 ",
|
|
70
|
+
todo.error)))));
|
|
71
|
+
};
|
|
72
|
+
const sections = [];
|
|
73
|
+
if (hasParallelGroups) {
|
|
74
|
+
const seen = new Map();
|
|
75
|
+
for (const t of todos) {
|
|
76
|
+
const key = t.parallelGroup ?? null;
|
|
77
|
+
let s = seen.get(key);
|
|
78
|
+
if (!s) {
|
|
79
|
+
s = { label: t.parallelGroup ?? null, items: [] };
|
|
80
|
+
seen.set(key, s);
|
|
81
|
+
sections.push(s);
|
|
82
|
+
}
|
|
83
|
+
s.items.push(t);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
sections.push({ label: null, items: todos });
|
|
88
|
+
}
|
|
49
89
|
return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
todo.error)))));
|
|
64
|
-
}),
|
|
90
|
+
isParallel && (React.createElement(Box, { marginBottom: 1 },
|
|
91
|
+
React.createElement(Text, { color: "magentaBright", bold: true },
|
|
92
|
+
"\u2AF6\u2AF6 ",
|
|
93
|
+
inProgressCount,
|
|
94
|
+
" worker",
|
|
95
|
+
inProgressCount === 1 ? '' : 's',
|
|
96
|
+
" running in parallel"))),
|
|
97
|
+
sections.map((section, sIdx) => (React.createElement(Box, { key: `section-${sIdx}-${section.label ?? 'flat'}`, flexDirection: "column" },
|
|
98
|
+
section.label && (React.createElement(Box, { marginTop: sIdx === 0 ? 0 : 1 },
|
|
99
|
+
React.createElement(Text, { color: "cyan", dimColor: true },
|
|
100
|
+
"\u250C\u2500 ",
|
|
101
|
+
section.label))),
|
|
102
|
+
section.items.map(t => renderTodoRow(t, section.label ? 2 : 0))))),
|
|
65
103
|
showProgressBar && (React.createElement(Box, { marginTop: 1 },
|
|
66
104
|
React.createElement(ProgressBar, { completed: completedCount, total: totalCount, width: 20 })))));
|
|
67
105
|
};
|