@litmers/cursorflow-orchestrator 0.1.30 β 0.1.34
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 +144 -52
- package/commands/cursorflow-add.md +159 -0
- package/commands/cursorflow-monitor.md +23 -2
- package/commands/cursorflow-new.md +87 -0
- package/dist/cli/add.d.ts +7 -0
- package/dist/cli/add.js +377 -0
- package/dist/cli/add.js.map +1 -0
- package/dist/cli/clean.js +1 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/config.d.ts +7 -0
- package/dist/cli/config.js +181 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.js +34 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +7 -33
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +51 -62
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/new.d.ts +7 -0
- package/dist/cli/new.js +232 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/prepare.js +95 -193
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +11 -47
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +27 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tasks.js +1 -2
- package/dist/cli/tasks.js.map +1 -1
- package/dist/core/failure-policy.d.ts +9 -0
- package/dist/core/failure-policy.js +9 -0
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/orchestrator.d.ts +20 -6
- package/dist/core/orchestrator.js +217 -331
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +27 -0
- package/dist/core/runner/agent.js +294 -0
- package/dist/core/runner/agent.js.map +1 -0
- package/dist/core/runner/index.d.ts +5 -0
- package/dist/core/runner/index.js +22 -0
- package/dist/core/runner/index.js.map +1 -0
- package/dist/core/runner/pipeline.d.ts +9 -0
- package/dist/core/runner/pipeline.js +539 -0
- package/dist/core/runner/pipeline.js.map +1 -0
- package/dist/core/runner/prompt.d.ts +25 -0
- package/dist/core/runner/prompt.js +175 -0
- package/dist/core/runner/prompt.js.map +1 -0
- package/dist/core/runner/task.d.ts +26 -0
- package/dist/core/runner/task.js +283 -0
- package/dist/core/runner/task.js.map +1 -0
- package/dist/core/runner/utils.d.ts +37 -0
- package/dist/core/runner/utils.js +161 -0
- package/dist/core/runner/utils.js.map +1 -0
- package/dist/core/runner.d.ts +2 -96
- package/dist/core/runner.js +11 -1136
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +326 -0
- package/dist/core/stall-detection.js +781 -0
- package/dist/core/stall-detection.js.map +1 -0
- package/dist/types/config.d.ts +6 -6
- package/dist/types/flow.d.ts +84 -0
- package/dist/types/flow.js +10 -0
- package/dist/types/flow.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/lane.d.ts +0 -2
- package/dist/types/logging.d.ts +5 -1
- package/dist/types/task.d.ts +7 -11
- package/dist/utils/config.js +7 -15
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/dependency.d.ts +36 -1
- package/dist/utils/dependency.js +256 -1
- package/dist/utils/dependency.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +45 -82
- package/dist/utils/enhanced-logger.js +238 -844
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +29 -0
- package/dist/utils/git.js +115 -5
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/state.js +0 -2
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +2 -2
- package/dist/utils/task-service.js +40 -31
- package/dist/utils/task-service.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/add.ts +397 -0
- package/src/cli/clean.ts +1 -0
- package/src/cli/config.ts +177 -0
- package/src/cli/index.ts +36 -32
- package/src/cli/logs.ts +7 -31
- package/src/cli/monitor.ts +55 -71
- package/src/cli/new.ts +235 -0
- package/src/cli/prepare.ts +98 -205
- package/src/cli/resume.ts +13 -56
- package/src/cli/run.ts +311 -306
- package/src/cli/tasks.ts +1 -2
- package/src/core/failure-policy.ts +9 -0
- package/src/core/orchestrator.ts +281 -375
- package/src/core/runner/agent.ts +314 -0
- package/src/core/runner/index.ts +6 -0
- package/src/core/runner/pipeline.ts +567 -0
- package/src/core/runner/prompt.ts +174 -0
- package/src/core/runner/task.ts +320 -0
- package/src/core/runner/utils.ts +142 -0
- package/src/core/runner.ts +8 -1347
- package/src/core/stall-detection.ts +936 -0
- package/src/types/config.ts +6 -6
- package/src/types/flow.ts +91 -0
- package/src/types/index.ts +15 -3
- package/src/types/lane.ts +0 -2
- package/src/types/logging.ts +5 -1
- package/src/types/task.ts +7 -11
- package/src/utils/config.ts +8 -16
- package/src/utils/dependency.ts +311 -2
- package/src/utils/enhanced-logger.ts +263 -927
- package/src/utils/git.ts +145 -5
- package/src/utils/state.ts +0 -2
- package/src/utils/task-service.ts +48 -40
- package/commands/cursorflow-review.md +0 -56
- package/commands/cursorflow-runs.md +0 -59
- package/dist/cli/runs.d.ts +0 -5
- package/dist/cli/runs.js +0 -214
- package/dist/cli/runs.js.map +0 -1
- package/dist/core/reviewer.d.ts +0 -66
- package/dist/core/reviewer.js +0 -265
- package/dist/core/reviewer.js.map +0 -1
- package/src/cli/runs.ts +0 -212
- package/src/core/reviewer.ts +0 -285
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as logger from '../../utils/logger';
|
|
3
|
+
import { safeJoin } from '../../utils/path';
|
|
4
|
+
import { DependencyPolicy, RunnerConfig } from '../../types';
|
|
5
|
+
import { DependencyResult } from './utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Dependency request file name - agent writes here when dependency changes are needed
|
|
9
|
+
*/
|
|
10
|
+
export const DEPENDENCY_REQUEST_FILE = '_cursorflow/dependency-request.json';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wrap prompt with dependency policy instructions (legacy, used by tests)
|
|
14
|
+
*/
|
|
15
|
+
export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
|
|
16
|
+
if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
|
|
17
|
+
return prompt;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let wrapped = `### π¦ Dependency Policy\n`;
|
|
21
|
+
wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
22
|
+
wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n\n`;
|
|
23
|
+
wrapped += prompt;
|
|
24
|
+
|
|
25
|
+
return wrapped;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap prompt with global context, dependency policy, and worktree instructions
|
|
30
|
+
*/
|
|
31
|
+
export function wrapPrompt(
|
|
32
|
+
prompt: string,
|
|
33
|
+
config: RunnerConfig,
|
|
34
|
+
options: {
|
|
35
|
+
noGit?: boolean;
|
|
36
|
+
isWorktree?: boolean;
|
|
37
|
+
dependencyResults?: DependencyResult[];
|
|
38
|
+
worktreePath?: string;
|
|
39
|
+
taskBranch?: string;
|
|
40
|
+
pipelineBranch?: string;
|
|
41
|
+
} = {}
|
|
42
|
+
): string {
|
|
43
|
+
const { noGit = false, isWorktree = true, dependencyResults = [], worktreePath, taskBranch, pipelineBranch } = options;
|
|
44
|
+
|
|
45
|
+
// 1. PREFIX: Environment & Worktree context
|
|
46
|
+
let wrapped = `### π Environment & Context\n`;
|
|
47
|
+
wrapped += `- **Workspace**: λΉμ μ λ
립λ **Git μν¬νΈλ¦¬** (νλ‘μ νΈ λ£¨νΈ)μμ μμ
μ€μ
λλ€.\n`;
|
|
48
|
+
wrapped += `- **CWD**: νμ¬ ν°λ―Έλκ³Ό μμ
κ²½λ‘λ μ΄λ―Έ μν¬νΈλ¦¬ 루νΈ(\`${worktreePath || 'current'}\`)λ‘ μ€μ λμ΄ μμ΅λλ€.\n`;
|
|
49
|
+
|
|
50
|
+
if (taskBranch && !noGit) {
|
|
51
|
+
wrapped += `- **Current Branch**: \`${taskBranch}\` (νμ¬ μμ
μ€μΈ λΈλμΉ)\n`;
|
|
52
|
+
wrapped += `- **Branch Check**: λ§μ½ λΈλμΉκ° λ€λ₯΄λ€λ©΄ \`git checkout ${taskBranch}\`λ₯Ό μ€ννμΈμ.\n`;
|
|
53
|
+
}
|
|
54
|
+
if (pipelineBranch && !noGit) {
|
|
55
|
+
wrapped += `- **Base Branch**: \`${pipelineBranch}\` (μ΄ μμ
μ κΈ°μ€μ΄ λλ μμ λΈλμΉ)\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (worktreePath) {
|
|
59
|
+
wrapped += `- **Worktree Path**: \`${worktreePath}\`\n`;
|
|
60
|
+
wrapped += `- **CRITICAL**: ν°λ―Έλ λͺ
λ Ήμ΄ μ€ν μ λ°λμ μν¬νΈλ¦¬ λ£¨νΈ λ΄μμ μ€νλκ³ μλμ§ νμΈνμΈμ.\n`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
wrapped += `- **Path Rule**: λͺ¨λ νμΌ μ°Έμ‘°λ μν¬νΈλ¦¬ λ£¨νΈ κΈ°μ€μ
λλ€.\n`;
|
|
64
|
+
|
|
65
|
+
if (isWorktree) {
|
|
66
|
+
wrapped += `- **File Availability**: Git μΆμ νμΌλ§ μ‘΄μ¬ν©λλ€. (node_modules, .env λ±μ κΈ°λ³Έμ μΌλ‘ μμ)\n`;
|
|
67
|
+
|
|
68
|
+
// Add environment file copy instructions
|
|
69
|
+
if (worktreePath) {
|
|
70
|
+
// Extract main repo path from worktree path (remove _cursorflow/worktrees/xxx part)
|
|
71
|
+
const mainRepoPath = worktreePath.replace(/\/_cursorflow\/worktrees\/[^/]+$/, '');
|
|
72
|
+
wrapped += `\n### π Environment Files Setup\n`;
|
|
73
|
+
wrapped += `μν¬νΈλ¦¬μ νκ²½λ³μ νμΌμ΄ μλ€λ©΄, λ©μΈ λ ν¬μμ 볡μ¬νμΈμ:\n`;
|
|
74
|
+
wrapped += `\`\`\`bash\n`;
|
|
75
|
+
wrapped += `# λ©μΈ λ ν¬ κ²½λ‘: ${mainRepoPath}\n`;
|
|
76
|
+
wrapped += `μμ 컀맨λ: [ ! -f .env ] && [ -f "${mainRepoPath}/.env" ] && cp "${mainRepoPath}/.env" .env\n`;
|
|
77
|
+
wrapped += `μμ 컀맨λ: [ ! -f .env.local ] && [ -f "${mainRepoPath}/.env.local" ] && cp "${mainRepoPath}/.env.local" .env.local\n`;
|
|
78
|
+
wrapped += `\`\`\`\n`;
|
|
79
|
+
wrapped += `β οΈ μ΄ μμ
μ **ν°λ―Έλ λͺ
λ Ήμ΄ μ€ν μ ** λ°λμ νμΈνμΈμ!\n`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Dependency Task Results (if available)
|
|
84
|
+
if (dependencyResults.length > 0) {
|
|
85
|
+
wrapped += `\n### π μμ‘΄ νμ€ν¬ κ²°κ³Ό\n`;
|
|
86
|
+
wrapped += `μ΄ νμ€ν¬κ° μμ‘΄νλ μ΄μ νμ€ν¬λ€μ μμ
κ²°κ³Όμ
λλ€.\n\n`;
|
|
87
|
+
|
|
88
|
+
for (const dep of dependencyResults) {
|
|
89
|
+
wrapped += `#### ${dep.taskId}\n`;
|
|
90
|
+
wrapped += `${dep.resultText}\n\n`;
|
|
91
|
+
}
|
|
92
|
+
wrapped += `---\n`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. Dependency Policy (Integrated)
|
|
96
|
+
const policy = config.dependencyPolicy;
|
|
97
|
+
wrapped += `\n### π¦ Dependency Policy\n`;
|
|
98
|
+
wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
99
|
+
wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
|
|
100
|
+
|
|
101
|
+
if (noGit) {
|
|
102
|
+
wrapped += `- NO_GIT_MODE: Git λͺ
λ Ήμ΄λ₯Ό μ¬μ©νμ§ λ§μΈμ. νμΌ μμ λ§ κ°λ₯ν©λλ€.\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
wrapped += `\n**π¦ Dependency Change Rules:**\n`;
|
|
106
|
+
wrapped += `1. μ½λλ₯Ό μμ νκΈ° μ , μμ‘΄μ± λ³κ²½μ΄ νμνμ§ **λ¨Όμ ** νλ¨νμΈμ.\n`;
|
|
107
|
+
wrapped += `2. μμ‘΄μ± λ³κ²½μ΄ νμνλ€λ©΄:\n`;
|
|
108
|
+
wrapped += ` - **λ€λ₯Έ νμΌμ μ λ μμ νμ§ λ§μΈμ.**\n`;
|
|
109
|
+
wrapped += ` - μλ JSONμ \`./${DEPENDENCY_REQUEST_FILE}\` νμΌμ μ μ₯νμΈμ:\n`;
|
|
110
|
+
wrapped += ` \`\`\`json\n`;
|
|
111
|
+
wrapped += ` {\n`;
|
|
112
|
+
wrapped += ` "reason": "μ μ΄ μμ‘΄μ±μ΄ νμνμ§ μ€λͺ
",\n`;
|
|
113
|
+
wrapped += ` "changes": ["add lodash@^4.17.21", "remove unused-pkg"],\n`;
|
|
114
|
+
wrapped += ` "commands": ["pnpm add lodash@^4.17.21", "pnpm remove unused-pkg"],\n`;
|
|
115
|
+
wrapped += ` "notes": "μΆκ° μ°Έκ³ μ¬ν (μ ν)" \n`;
|
|
116
|
+
wrapped += ` }\n`;
|
|
117
|
+
wrapped += ` \`\`\`\n`;
|
|
118
|
+
wrapped += ` - νμΌ μ μ₯ ν **μ¦μ μμ
μ μ’
λ£**νμΈμ. μ€μΌμ€νΈλ μ΄ν°κ° μ²λ¦¬ν©λλ€.\n`;
|
|
119
|
+
wrapped += `3. μμ‘΄μ± λ³κ²½μ΄ λΆνμνλ©΄ λ°λ‘ λ³Έ μμ
μ μ§ννμΈμ.\n`;
|
|
120
|
+
|
|
121
|
+
wrapped += `\n---\n\n${prompt}\n\n---\n`;
|
|
122
|
+
|
|
123
|
+
// 4. SUFFIX: Task Completion & Git Requirements
|
|
124
|
+
wrapped += `\n### π Task Completion Requirements\n`;
|
|
125
|
+
wrapped += `**λ°λμ λ€μ μμλ‘ μμ
μ λ§λ¬΄λ¦¬νμΈμ (λ§€μ° μ€μ):**\n\n`;
|
|
126
|
+
|
|
127
|
+
if (!noGit) {
|
|
128
|
+
wrapped += `1. **λ³κ²½ μ¬ν νμΈ**: \`git status\`μ \`git diff\`λ‘ μμ λ λ΄μ©μ μ΅μ’
νμΈνμΈμ.\n`;
|
|
129
|
+
wrapped += `2. **Git Commit & Push** (νμ!):\n`;
|
|
130
|
+
wrapped += ` \`\`\`bash\n`;
|
|
131
|
+
wrapped += ` git add -A\n`;
|
|
132
|
+
wrapped += ` git commit -m "feat: <μμ
λ΄μ© μμ½>"\n`;
|
|
133
|
+
wrapped += ` git push origin HEAD\n`;
|
|
134
|
+
wrapped += ` \`\`\`\n`;
|
|
135
|
+
wrapped += ` β οΈ **μ£Όμ**: 컀λ°κ³Ό νΈμλ₯Ό μλ΅νλ©΄ μ€μΌμ€νΈλ μ΄ν°κ° λ³κ²½ μ¬νμ μΈμνμ§ λͺ»νλ©° μμ
μ΄ μμ€λ©λλ€.\n\n`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
wrapped += `3. **μ΅μ’
μμ½**: μμ
μλ£ ν μλ νμμ ν¬ν¨νμ¬ μμ½ν΄ μ£ΌμΈμ:\n`;
|
|
139
|
+
wrapped += ` - **μμ λ νμΌ**: [νμΌλͺ
1, νμΌλͺ
2, ...]\n`;
|
|
140
|
+
wrapped += ` - **μμ
κ²°κ³Ό**: [ν΅μ¬ λ³κ²½ μ¬ν μμ½]\n`;
|
|
141
|
+
wrapped += ` - **μ»€λ° μ 보**: [git log --oneline -1 κ²°κ³Ό]\n\n`;
|
|
142
|
+
wrapped += `4. μ§μλ λ¬Έμ(docs/...)λ₯Ό μ°Ύμ μ μκ±°λ μκΈ°μΉ λͺ»ν μ€λ₯κ° λ°μνλ©΄ μ¦μ λ³΄κ³ νμΈμ.\n`;
|
|
143
|
+
|
|
144
|
+
return wrapped;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Apply file permissions based on dependency policy
|
|
149
|
+
*/
|
|
150
|
+
export function applyDependencyFilePermissions(worktreeDir: string, policy: DependencyPolicy): void {
|
|
151
|
+
const targets: string[] = [];
|
|
152
|
+
|
|
153
|
+
if (!policy.allowDependencyChange) {
|
|
154
|
+
targets.push('package.json');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (policy.lockfileReadOnly) {
|
|
158
|
+
targets.push('pnpm-lock.yaml', 'package-lock.json', 'yarn.lock');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const file of targets) {
|
|
162
|
+
const filePath = safeJoin(worktreeDir, file);
|
|
163
|
+
if (!fs.existsSync(filePath)) continue;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const stats = fs.statSync(filePath);
|
|
167
|
+
const mode = stats.mode & 0o777;
|
|
168
|
+
fs.chmodSync(filePath, mode & ~0o222); // Remove write bits
|
|
169
|
+
} catch {
|
|
170
|
+
// Best effort
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as git from '../../utils/git';
|
|
4
|
+
import * as logger from '../../utils/logger';
|
|
5
|
+
import { events } from '../../utils/events';
|
|
6
|
+
import { safeJoin } from '../../utils/path';
|
|
7
|
+
import { appendLog, createConversationEntry } from '../../utils/state';
|
|
8
|
+
import { Task, RunnerConfig, TaskExecutionResult, LaneState } from '../../types';
|
|
9
|
+
import { loadState } from '../../utils/state';
|
|
10
|
+
import { waitForTaskDependencies as waitForDeps, DependencyWaitOptions } from '../../utils/dependency';
|
|
11
|
+
import {
|
|
12
|
+
cursorAgentSend,
|
|
13
|
+
extractDependencyRequest
|
|
14
|
+
} from './agent';
|
|
15
|
+
import {
|
|
16
|
+
wrapPrompt,
|
|
17
|
+
applyDependencyFilePermissions
|
|
18
|
+
} from './prompt';
|
|
19
|
+
import {
|
|
20
|
+
loadDependencyResults,
|
|
21
|
+
saveTaskResult,
|
|
22
|
+
readDependencyRequestFile,
|
|
23
|
+
clearDependencyRequestFile,
|
|
24
|
+
DependencyResult
|
|
25
|
+
} from './utils';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wait for task-level dependencies to be completed by other lanes
|
|
29
|
+
* Now uses the enhanced dependency module with timeout support
|
|
30
|
+
*/
|
|
31
|
+
export async function waitForTaskDependencies(
|
|
32
|
+
deps: string[],
|
|
33
|
+
runDir: string,
|
|
34
|
+
options: DependencyWaitOptions = {}
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
if (!deps || deps.length === 0) return;
|
|
37
|
+
|
|
38
|
+
const lanesRoot = path.dirname(runDir);
|
|
39
|
+
|
|
40
|
+
const result = await waitForDeps(deps, lanesRoot, {
|
|
41
|
+
timeoutMs: options.timeoutMs || 30 * 60 * 1000, // 30 minutes default
|
|
42
|
+
pollIntervalMs: options.pollIntervalMs || 5000,
|
|
43
|
+
onTimeout: options.onTimeout || 'fail',
|
|
44
|
+
onProgress: (pending, completed) => {
|
|
45
|
+
if (completed.length > 0) {
|
|
46
|
+
logger.info(`Dependencies progress: ${completed.length}/${deps.length} completed`);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!result.success) {
|
|
52
|
+
if (result.timedOut) {
|
|
53
|
+
throw new Error(`Dependency wait timed out after ${Math.round(result.elapsedMs / 1000)}s. Pending: ${result.failedDependencies.join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Dependencies failed: ${result.failedDependencies.join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Merge branches from dependency lanes with safe merge and conflict pre-check
|
|
61
|
+
*/
|
|
62
|
+
export async function mergeDependencyBranches(deps: string[], runDir: string, worktreeDir: string, pipelineBranch: string): Promise<void> {
|
|
63
|
+
if (!deps || deps.length === 0) return;
|
|
64
|
+
|
|
65
|
+
const lanesRoot = path.dirname(runDir);
|
|
66
|
+
const lanesToMerge = new Set(deps.map(d => d.split(':')[0]!));
|
|
67
|
+
|
|
68
|
+
// Ensure we are on the pipeline branch before merging dependencies
|
|
69
|
+
logger.info(`π Syncing with ${pipelineBranch} before merging dependencies`);
|
|
70
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
71
|
+
|
|
72
|
+
for (const laneName of lanesToMerge) {
|
|
73
|
+
const depStatePath = safeJoin(lanesRoot, laneName, 'state.json');
|
|
74
|
+
if (!fs.existsSync(depStatePath)) continue;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const state = loadState<LaneState>(depStatePath);
|
|
78
|
+
if (!state?.pipelineBranch) continue;
|
|
79
|
+
|
|
80
|
+
logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
|
|
81
|
+
|
|
82
|
+
// Ensure we have the latest
|
|
83
|
+
git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
84
|
+
|
|
85
|
+
// Pre-check for conflicts before attempting merge
|
|
86
|
+
const conflictCheck = git.checkMergeConflict(state.pipelineBranch, { cwd: worktreeDir });
|
|
87
|
+
|
|
88
|
+
if (conflictCheck.willConflict) {
|
|
89
|
+
logger.warn(`β οΈ Pre-check: Merge conflict detected with ${laneName}`);
|
|
90
|
+
logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
|
|
91
|
+
|
|
92
|
+
// Emit event for potential auto-recovery or notification
|
|
93
|
+
events.emit('merge.conflict_detected', {
|
|
94
|
+
laneName,
|
|
95
|
+
targetBranch: state.pipelineBranch,
|
|
96
|
+
conflictingFiles: conflictCheck.conflictingFiles,
|
|
97
|
+
preCheck: true,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
throw new Error(`Pre-merge conflict check failed: ${conflictCheck.conflictingFiles.join(', ')}. Consider rebasing or resolving conflicts manually.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Use safe merge with conflict detection
|
|
104
|
+
const mergeResult = git.safeMerge(state.pipelineBranch, {
|
|
105
|
+
cwd: worktreeDir,
|
|
106
|
+
noFf: true,
|
|
107
|
+
message: `chore: merge task dependency from ${laneName}`,
|
|
108
|
+
abortOnConflict: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!mergeResult.success) {
|
|
112
|
+
if (mergeResult.conflict) {
|
|
113
|
+
logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
114
|
+
throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
throw new Error(mergeResult.error || 'Merge failed');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
logger.success(`β Merged ${laneName}`);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
logger.error(`Failed to merge branch from ${laneName}: ${e}`);
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Run a single task
|
|
129
|
+
*/
|
|
130
|
+
export async function runTask({
|
|
131
|
+
task,
|
|
132
|
+
config,
|
|
133
|
+
index,
|
|
134
|
+
worktreeDir,
|
|
135
|
+
pipelineBranch,
|
|
136
|
+
taskBranch,
|
|
137
|
+
chatId,
|
|
138
|
+
runDir,
|
|
139
|
+
runRoot,
|
|
140
|
+
noGit = false,
|
|
141
|
+
}: {
|
|
142
|
+
task: Task;
|
|
143
|
+
config: RunnerConfig;
|
|
144
|
+
index: number;
|
|
145
|
+
worktreeDir: string;
|
|
146
|
+
pipelineBranch: string;
|
|
147
|
+
taskBranch: string;
|
|
148
|
+
chatId: string;
|
|
149
|
+
runDir: string;
|
|
150
|
+
runRoot?: string;
|
|
151
|
+
noGit?: boolean;
|
|
152
|
+
}): Promise<TaskExecutionResult> {
|
|
153
|
+
// Calculate runRoot if not provided (runDir is lanes/{laneName}/, runRoot is parent of lanes/)
|
|
154
|
+
const calculatedRunRoot = runRoot || path.dirname(path.dirname(runDir));
|
|
155
|
+
const model = task.model || config.model || 'sonnet-4.5';
|
|
156
|
+
const timeout = task.timeout || config.timeout;
|
|
157
|
+
const convoPath = safeJoin(runDir, 'conversation.jsonl');
|
|
158
|
+
|
|
159
|
+
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
160
|
+
logger.info(`Model: ${model}`);
|
|
161
|
+
if (noGit) {
|
|
162
|
+
logger.info('π« noGit mode: skipping branch operations');
|
|
163
|
+
} else {
|
|
164
|
+
logger.info(`Branch: ${taskBranch}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
events.emit('task.started', {
|
|
168
|
+
taskName: task.name,
|
|
169
|
+
taskBranch,
|
|
170
|
+
index,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Sync pipelineBranch with remote before starting (skip in noGit mode)
|
|
174
|
+
if (!noGit) {
|
|
175
|
+
logger.info(`π Syncing ${pipelineBranch} with remote...`);
|
|
176
|
+
|
|
177
|
+
// Fetch latest from remote
|
|
178
|
+
try {
|
|
179
|
+
git.runGit(['fetch', 'origin', pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
180
|
+
} catch {
|
|
181
|
+
// Branch might not exist on remote yet - that's OK
|
|
182
|
+
logger.info(` Branch ${pipelineBranch} not yet on remote, skipping sync`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Try to fast-forward if behind
|
|
186
|
+
const syncResult = git.syncBranchWithRemote(pipelineBranch, { cwd: worktreeDir, createIfMissing: true });
|
|
187
|
+
if (syncResult.updated) {
|
|
188
|
+
logger.info(` β Updated ${pipelineBranch} with ${syncResult.behind || 0} new commits from remote`);
|
|
189
|
+
} else if (syncResult.error) {
|
|
190
|
+
logger.warn(` β οΈ Could not sync: ${syncResult.error}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Checkout task branch from pipeline branch (skip in noGit mode)
|
|
195
|
+
if (!noGit) {
|
|
196
|
+
logger.info(`πΏ Forking task branch: ${taskBranch} from ${pipelineBranch}`);
|
|
197
|
+
git.runGit(['checkout', '-B', taskBranch, pipelineBranch], { cwd: worktreeDir });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Apply dependency permissions
|
|
201
|
+
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
202
|
+
|
|
203
|
+
// Load dependency results if this task has dependsOn
|
|
204
|
+
let dependencyResults: DependencyResult[] = [];
|
|
205
|
+
if (task.dependsOn && task.dependsOn.length > 0) {
|
|
206
|
+
dependencyResults = loadDependencyResults(task.dependsOn, calculatedRunRoot);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Wrap prompt with context, dependency results, and completion instructions
|
|
210
|
+
const wrappedPrompt = wrapPrompt(task.prompt, config, {
|
|
211
|
+
noGit,
|
|
212
|
+
isWorktree: !noGit,
|
|
213
|
+
dependencyResults,
|
|
214
|
+
worktreePath: worktreeDir,
|
|
215
|
+
taskBranch,
|
|
216
|
+
pipelineBranch,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Log ONLY the original prompt to keep logs clean
|
|
220
|
+
appendLog(convoPath, createConversationEntry('user', task.prompt, {
|
|
221
|
+
task: task.name,
|
|
222
|
+
model,
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
logger.info('Sending prompt to agent...');
|
|
226
|
+
const startTime = Date.now();
|
|
227
|
+
events.emit('agent.prompt_sent', {
|
|
228
|
+
taskName: task.name,
|
|
229
|
+
model,
|
|
230
|
+
promptLength: wrappedPrompt.length,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const r1 = await cursorAgentSend({
|
|
234
|
+
workspaceDir: worktreeDir,
|
|
235
|
+
chatId,
|
|
236
|
+
prompt: wrappedPrompt,
|
|
237
|
+
model,
|
|
238
|
+
signalDir: runDir,
|
|
239
|
+
timeout,
|
|
240
|
+
enableIntervention: config.enableIntervention,
|
|
241
|
+
outputFormat: config.agentOutputFormat,
|
|
242
|
+
taskName: task.name,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const duration = Date.now() - startTime;
|
|
246
|
+
events.emit('agent.response_received', {
|
|
247
|
+
taskName: task.name,
|
|
248
|
+
ok: r1.ok,
|
|
249
|
+
duration,
|
|
250
|
+
responseLength: r1.resultText?.length || 0,
|
|
251
|
+
error: r1.error,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
|
|
255
|
+
task: task.name,
|
|
256
|
+
model,
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
if (!r1.ok) {
|
|
260
|
+
events.emit('task.failed', {
|
|
261
|
+
taskName: task.name,
|
|
262
|
+
taskBranch,
|
|
263
|
+
error: r1.error,
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
taskName: task.name,
|
|
267
|
+
taskBranch,
|
|
268
|
+
status: 'ERROR',
|
|
269
|
+
error: r1.error,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check for dependency request (file-based takes priority, then text-based)
|
|
274
|
+
const fileDepReq = readDependencyRequestFile(worktreeDir);
|
|
275
|
+
const textDepReq = extractDependencyRequest(r1.resultText || '');
|
|
276
|
+
|
|
277
|
+
// Determine which request to use (file-based is preferred as it's more structured)
|
|
278
|
+
const depReq = fileDepReq.required ? fileDepReq : textDepReq;
|
|
279
|
+
|
|
280
|
+
if (depReq.required) {
|
|
281
|
+
logger.info(`π¦ Dependency change requested: ${depReq.plan?.reason || 'No reason provided'}`);
|
|
282
|
+
|
|
283
|
+
if (depReq.plan) {
|
|
284
|
+
logger.info(` Commands: ${depReq.plan.commands.join(', ')}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!config.dependencyPolicy.allowDependencyChange) {
|
|
288
|
+
// Clear the file so it doesn't persist after resolution
|
|
289
|
+
clearDependencyRequestFile(worktreeDir);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
taskName: task.name,
|
|
293
|
+
taskBranch,
|
|
294
|
+
status: 'BLOCKED_DEPENDENCY',
|
|
295
|
+
dependencyRequest: depReq.plan || null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Push task branch (skip in noGit mode)
|
|
301
|
+
if (!noGit) {
|
|
302
|
+
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Save task result for dependency handoff
|
|
306
|
+
saveTaskResult(runDir, index, task.name, r1.resultText || '');
|
|
307
|
+
|
|
308
|
+
events.emit('task.completed', {
|
|
309
|
+
taskName: task.name,
|
|
310
|
+
taskBranch,
|
|
311
|
+
status: 'FINISHED',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
taskName: task.name,
|
|
316
|
+
taskBranch,
|
|
317
|
+
status: 'FINISHED',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as logger from '../../utils/logger';
|
|
4
|
+
import { safeJoin } from '../../utils/path';
|
|
5
|
+
import { DependencyRequestPlan } from '../../types';
|
|
6
|
+
import { DEPENDENCY_REQUEST_FILE } from './prompt';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Task result directory name - stores task completion results
|
|
10
|
+
*/
|
|
11
|
+
export const TASK_RESULTS_DIR = 'task-results';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dependency result interface
|
|
15
|
+
*/
|
|
16
|
+
export interface DependencyResult {
|
|
17
|
+
taskId: string
|
|
18
|
+
resultText: string; // Last response text
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Save task result (last response text) to file
|
|
23
|
+
* Stored in runDir/task-results/{NN}-{taskName}.txt
|
|
24
|
+
*/
|
|
25
|
+
export function saveTaskResult(runDir: string, taskIndex: number, taskName: string, resultText: string): void {
|
|
26
|
+
const resultsDir = safeJoin(runDir, TASK_RESULTS_DIR);
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
29
|
+
const paddedIndex = String(taskIndex + 1).padStart(2, '0');
|
|
30
|
+
const resultPath = safeJoin(resultsDir, `${paddedIndex}-${taskName}.txt`);
|
|
31
|
+
fs.writeFileSync(resultPath, resultText, 'utf8');
|
|
32
|
+
logger.info(`π Task result saved: ${resultPath}`);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
logger.warn(`Failed to save task result: ${e}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find task result file by task name (handles NN-taskName.txt format)
|
|
40
|
+
*/
|
|
41
|
+
export function findTaskResultFile(resultsDir: string, taskName: string): string | null {
|
|
42
|
+
if (!fs.existsSync(resultsDir)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const files = fs.readdirSync(resultsDir);
|
|
48
|
+
// Match pattern: NN-taskName.txt (e.g., 01-setup.txt, 02-implement.txt)
|
|
49
|
+
const pattern = new RegExp(`^\\d+-${taskName}\\.txt$`);
|
|
50
|
+
const matchedFile = files.find(f => pattern.test(f));
|
|
51
|
+
|
|
52
|
+
if (matchedFile) {
|
|
53
|
+
return safeJoin(resultsDir, matchedFile);
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
logger.warn(`Failed to scan task results directory: ${e}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load dependency results based on dependsOn list
|
|
64
|
+
* Reads from runRoot/lanes/{laneName}/task-results/{NN}-{taskName}.txt
|
|
65
|
+
*/
|
|
66
|
+
export function loadDependencyResults(dependsOn: string[], runRoot: string): DependencyResult[] {
|
|
67
|
+
const results: DependencyResult[] = [];
|
|
68
|
+
|
|
69
|
+
for (const dep of dependsOn) {
|
|
70
|
+
const parts = dep.split(':');
|
|
71
|
+
if (parts.length !== 2) {
|
|
72
|
+
logger.warn(`Invalid dependency format: ${dep} (expected "lane:task")`);
|
|
73
|
+
results.push({ taskId: dep, resultText: '(μλͺ»λ μμ‘΄μ± νμ)' });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const [laneName, taskName] = parts;
|
|
78
|
+
const resultsDir = safeJoin(runRoot, 'lanes', laneName!, TASK_RESULTS_DIR);
|
|
79
|
+
const resultPath = findTaskResultFile(resultsDir, taskName!);
|
|
80
|
+
|
|
81
|
+
if (resultPath && fs.existsSync(resultPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const text = fs.readFileSync(resultPath, 'utf8');
|
|
84
|
+
results.push({ taskId: dep, resultText: text });
|
|
85
|
+
logger.info(`π Loaded dependency result: ${dep}`);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
logger.warn(`Failed to read dependency result ${dep}: ${e}`);
|
|
88
|
+
results.push({ taskId: dep, resultText: '(μ½κΈ° μ€ν¨)' });
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
logger.warn(`Dependency result not found for: ${dep}`);
|
|
92
|
+
results.push({ taskId: dep, resultText: '(κ²°κ³Ό μμ)' });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read dependency request from file if it exists
|
|
101
|
+
*/
|
|
102
|
+
export function readDependencyRequestFile(worktreeDir: string): { required: boolean; plan?: DependencyRequestPlan } {
|
|
103
|
+
const filePath = safeJoin(worktreeDir, DEPENDENCY_REQUEST_FILE);
|
|
104
|
+
|
|
105
|
+
if (!fs.existsSync(filePath)) {
|
|
106
|
+
return { required: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
111
|
+
const plan = JSON.parse(content) as DependencyRequestPlan;
|
|
112
|
+
|
|
113
|
+
// Validate required fields
|
|
114
|
+
if (plan.reason && Array.isArray(plan.commands) && plan.commands.length > 0) {
|
|
115
|
+
logger.info(`π¦ Dependency request file detected: ${filePath}`);
|
|
116
|
+
return { required: true, plan };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
logger.warn(`Invalid dependency request file format: ${filePath}`);
|
|
120
|
+
return { required: false };
|
|
121
|
+
} catch (e) {
|
|
122
|
+
logger.warn(`Failed to parse dependency request file: ${e}`);
|
|
123
|
+
return { required: false };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Clear dependency request file after processing
|
|
129
|
+
*/
|
|
130
|
+
export function clearDependencyRequestFile(worktreeDir: string): void {
|
|
131
|
+
const filePath = safeJoin(worktreeDir, DEPENDENCY_REQUEST_FILE);
|
|
132
|
+
|
|
133
|
+
if (fs.existsSync(filePath)) {
|
|
134
|
+
try {
|
|
135
|
+
fs.unlinkSync(filePath);
|
|
136
|
+
logger.info(`ποΈ Cleared dependency request file: ${filePath}`);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
logger.warn(`Failed to clear dependency request file: ${e}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|