@litmers/cursorflow-orchestrator 0.1.31 β†’ 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.
Files changed (129) hide show
  1. package/README.md +144 -52
  2. package/commands/cursorflow-add.md +159 -0
  3. package/commands/cursorflow-monitor.md +23 -2
  4. package/commands/cursorflow-new.md +87 -0
  5. package/dist/cli/add.d.ts +7 -0
  6. package/dist/cli/add.js +377 -0
  7. package/dist/cli/add.js.map +1 -0
  8. package/dist/cli/clean.js +1 -0
  9. package/dist/cli/clean.js.map +1 -1
  10. package/dist/cli/config.d.ts +7 -0
  11. package/dist/cli/config.js +181 -0
  12. package/dist/cli/config.js.map +1 -0
  13. package/dist/cli/index.js +34 -30
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/logs.js +7 -33
  16. package/dist/cli/logs.js.map +1 -1
  17. package/dist/cli/monitor.js +51 -62
  18. package/dist/cli/monitor.js.map +1 -1
  19. package/dist/cli/new.d.ts +7 -0
  20. package/dist/cli/new.js +232 -0
  21. package/dist/cli/new.js.map +1 -0
  22. package/dist/cli/prepare.js +95 -193
  23. package/dist/cli/prepare.js.map +1 -1
  24. package/dist/cli/resume.js +11 -47
  25. package/dist/cli/resume.js.map +1 -1
  26. package/dist/cli/run.js +27 -22
  27. package/dist/cli/run.js.map +1 -1
  28. package/dist/cli/tasks.js +1 -2
  29. package/dist/cli/tasks.js.map +1 -1
  30. package/dist/core/failure-policy.d.ts +9 -0
  31. package/dist/core/failure-policy.js +9 -0
  32. package/dist/core/failure-policy.js.map +1 -1
  33. package/dist/core/orchestrator.d.ts +20 -6
  34. package/dist/core/orchestrator.js +213 -333
  35. package/dist/core/orchestrator.js.map +1 -1
  36. package/dist/core/runner/agent.d.ts +27 -0
  37. package/dist/core/runner/agent.js +294 -0
  38. package/dist/core/runner/agent.js.map +1 -0
  39. package/dist/core/runner/index.d.ts +5 -0
  40. package/dist/core/runner/index.js +22 -0
  41. package/dist/core/runner/index.js.map +1 -0
  42. package/dist/core/runner/pipeline.d.ts +9 -0
  43. package/dist/core/runner/pipeline.js +539 -0
  44. package/dist/core/runner/pipeline.js.map +1 -0
  45. package/dist/core/runner/prompt.d.ts +25 -0
  46. package/dist/core/runner/prompt.js +175 -0
  47. package/dist/core/runner/prompt.js.map +1 -0
  48. package/dist/core/runner/task.d.ts +26 -0
  49. package/dist/core/runner/task.js +283 -0
  50. package/dist/core/runner/task.js.map +1 -0
  51. package/dist/core/runner/utils.d.ts +37 -0
  52. package/dist/core/runner/utils.js +161 -0
  53. package/dist/core/runner/utils.js.map +1 -0
  54. package/dist/core/runner.d.ts +2 -96
  55. package/dist/core/runner.js +11 -1136
  56. package/dist/core/runner.js.map +1 -1
  57. package/dist/core/stall-detection.d.ts +326 -0
  58. package/dist/core/stall-detection.js +781 -0
  59. package/dist/core/stall-detection.js.map +1 -0
  60. package/dist/types/config.d.ts +6 -6
  61. package/dist/types/flow.d.ts +84 -0
  62. package/dist/types/flow.js +10 -0
  63. package/dist/types/flow.js.map +1 -0
  64. package/dist/types/index.d.ts +1 -0
  65. package/dist/types/index.js +3 -3
  66. package/dist/types/index.js.map +1 -1
  67. package/dist/types/lane.d.ts +0 -2
  68. package/dist/types/logging.d.ts +5 -1
  69. package/dist/types/task.d.ts +7 -11
  70. package/dist/utils/config.js +7 -15
  71. package/dist/utils/config.js.map +1 -1
  72. package/dist/utils/dependency.d.ts +36 -1
  73. package/dist/utils/dependency.js +256 -1
  74. package/dist/utils/dependency.js.map +1 -1
  75. package/dist/utils/enhanced-logger.d.ts +45 -82
  76. package/dist/utils/enhanced-logger.js +238 -844
  77. package/dist/utils/enhanced-logger.js.map +1 -1
  78. package/dist/utils/git.d.ts +29 -0
  79. package/dist/utils/git.js +115 -5
  80. package/dist/utils/git.js.map +1 -1
  81. package/dist/utils/state.js +0 -2
  82. package/dist/utils/state.js.map +1 -1
  83. package/dist/utils/task-service.d.ts +2 -2
  84. package/dist/utils/task-service.js +40 -31
  85. package/dist/utils/task-service.js.map +1 -1
  86. package/package.json +4 -3
  87. package/src/cli/add.ts +397 -0
  88. package/src/cli/clean.ts +1 -0
  89. package/src/cli/config.ts +177 -0
  90. package/src/cli/index.ts +36 -32
  91. package/src/cli/logs.ts +7 -31
  92. package/src/cli/monitor.ts +55 -71
  93. package/src/cli/new.ts +235 -0
  94. package/src/cli/prepare.ts +98 -205
  95. package/src/cli/resume.ts +13 -56
  96. package/src/cli/run.ts +311 -306
  97. package/src/cli/tasks.ts +1 -2
  98. package/src/core/failure-policy.ts +9 -0
  99. package/src/core/orchestrator.ts +277 -378
  100. package/src/core/runner/agent.ts +314 -0
  101. package/src/core/runner/index.ts +6 -0
  102. package/src/core/runner/pipeline.ts +567 -0
  103. package/src/core/runner/prompt.ts +174 -0
  104. package/src/core/runner/task.ts +320 -0
  105. package/src/core/runner/utils.ts +142 -0
  106. package/src/core/runner.ts +8 -1347
  107. package/src/core/stall-detection.ts +936 -0
  108. package/src/types/config.ts +6 -6
  109. package/src/types/flow.ts +91 -0
  110. package/src/types/index.ts +15 -3
  111. package/src/types/lane.ts +0 -2
  112. package/src/types/logging.ts +5 -1
  113. package/src/types/task.ts +7 -11
  114. package/src/utils/config.ts +8 -16
  115. package/src/utils/dependency.ts +311 -2
  116. package/src/utils/enhanced-logger.ts +263 -927
  117. package/src/utils/git.ts +145 -5
  118. package/src/utils/state.ts +0 -2
  119. package/src/utils/task-service.ts +48 -40
  120. package/commands/cursorflow-review.md +0 -56
  121. package/commands/cursorflow-runs.md +0 -59
  122. package/dist/cli/runs.d.ts +0 -5
  123. package/dist/cli/runs.js +0 -214
  124. package/dist/cli/runs.js.map +0 -1
  125. package/dist/core/reviewer.d.ts +0 -66
  126. package/dist/core/reviewer.js +0 -265
  127. package/dist/core/reviewer.js.map +0 -1
  128. package/src/cli/runs.ts +0 -212
  129. 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
+