jules-orchestrator-kit 0.3.0 → 0.5.2
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/.agent/rules/jules-protocol.md +33 -0
- package/.env.example +49 -3
- package/.github/workflows/agent-scope-guard.yml +56 -0
- package/.github/workflows/jules-audit.yml +7 -1
- package/JULES_RULES_TEMPLATE.md +52 -0
- package/README.md +207 -152
- package/bin/init.js +65 -6
- package/index.mjs +11 -0
- package/package.json +12 -4
- package/scripts/jules-cleanup.mjs +200 -0
- package/scripts/jules-create.mjs +47 -0
- package/scripts/jules-dispatch.mjs +150 -107
- package/scripts/jules-nightly.mjs +37 -29
- package/scripts/jules-queue-runner.mjs +94 -40
- package/scripts/jules-scan-todos.mjs +142 -0
- package/scripts/jules-self-audit.mjs +172 -40
- package/scripts/jules-status.mjs +57 -0
- package/scripts/jules-swarm.mjs +68 -32
- package/scripts/lock-manager.mjs +147 -0
- package/scripts/utils.mjs +94 -0
package/scripts/jules-swarm.mjs
CHANGED
|
@@ -2,26 +2,27 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { log, sleep } from "./utils.mjs";
|
|
5
6
|
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
|
|
8
9
|
const tasksFile = process.argv[2];
|
|
9
10
|
|
|
10
11
|
if (!tasksFile) {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
log.error("Usage: node scripts/jules-swarm.mjs <path-to-tasks.json>");
|
|
13
|
+
log.error('Format of tasks.json: [ { "id": "t1", "title": "Task 1", "prompt": "Description 1", "scope": ["src/moduleA/**"] } ]');
|
|
13
14
|
process.exit(1);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const resolvedPath = path.resolve(process.cwd(), tasksFile);
|
|
17
18
|
if (!fs.existsSync(resolvedPath)) {
|
|
18
|
-
|
|
19
|
+
log.error(`Tasks file not found: ${resolvedPath}`);
|
|
19
20
|
process.exit(1);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const tasks = JSON.parse(fs.readFileSync(resolvedPath, "utf-8"));
|
|
23
24
|
if (!Array.isArray(tasks)) {
|
|
24
|
-
|
|
25
|
+
log.error("tasks.json must contain a JSON array of task objects.");
|
|
25
26
|
process.exit(1);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -36,7 +37,7 @@ const USE_WORKTREES = process.env.JULES_USE_WORKTREES === "true";
|
|
|
36
37
|
// Ensure root project path is preserved for history logs even when running in worktrees
|
|
37
38
|
process.env.JULES_PROJECT_ROOT = process.cwd();
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
log.info(`Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
|
|
40
41
|
|
|
41
42
|
const activeWorktrees = new Set();
|
|
42
43
|
|
|
@@ -59,14 +60,35 @@ function cleanupAllWorktreesSync() {
|
|
|
59
60
|
activeWorktrees.clear();
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function reapOrphanedWorktrees() {
|
|
64
|
+
const worktreesBase = path.resolve(process.cwd(), ".agent/worktrees");
|
|
65
|
+
if (!fs.existsSync(worktreesBase)) return;
|
|
66
|
+
try {
|
|
67
|
+
const entries = fs.readdirSync(worktreesBase);
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const wtPath = path.join(worktreesBase, entry);
|
|
70
|
+
try {
|
|
71
|
+
execFileSync("git", ["worktree", "remove", "--force", wtPath], { stdio: "ignore" });
|
|
72
|
+
} catch (_) {
|
|
73
|
+
try {
|
|
74
|
+
fs.rmSync(wtPath, { recursive: true, force: true });
|
|
75
|
+
} catch (_) {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (_) {}
|
|
79
|
+
try {
|
|
80
|
+
execFileSync("git", ["worktree", "prune"], { stdio: "ignore" });
|
|
81
|
+
} catch (_) {}
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
process.on("SIGINT", () => {
|
|
63
|
-
|
|
85
|
+
log.dim("SIGINT received. Cleaning up active Git worktrees...");
|
|
64
86
|
cleanupAllWorktreesSync();
|
|
65
87
|
process.exit(130);
|
|
66
88
|
});
|
|
67
89
|
|
|
68
90
|
process.on("SIGTERM", () => {
|
|
69
|
-
|
|
91
|
+
log.dim("SIGTERM received. Cleaning up active Git worktrees...");
|
|
70
92
|
cleanupAllWorktreesSync();
|
|
71
93
|
process.exit(143);
|
|
72
94
|
});
|
|
@@ -76,19 +98,27 @@ async function createWorktree(taskId) {
|
|
|
76
98
|
const wtDir = path.resolve(process.cwd(), `.agent/worktrees/${slug}`);
|
|
77
99
|
const branchName = `jules/${slug}`;
|
|
78
100
|
let item = null;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
|
|
84
|
-
item = { wtDir, branchName };
|
|
85
|
-
} catch (err) {
|
|
101
|
+
await fs.promises.mkdir(path.dirname(wtDir), { recursive: true });
|
|
102
|
+
|
|
103
|
+
const maxRetries = 3;
|
|
104
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
86
105
|
try {
|
|
87
|
-
await execFileAsync("git", ["worktree", "add", "
|
|
88
|
-
item = { wtDir, branchName
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
|
|
107
|
+
item = { wtDir, branchName };
|
|
108
|
+
break;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (attempt < maxRetries) {
|
|
111
|
+
const jitter = Math.floor(Math.random() * 200) + 100 * Math.pow(2, attempt);
|
|
112
|
+
await sleep(jitter);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await execFileAsync("git", ["worktree", "add", "--force", wtDir, "HEAD"]);
|
|
117
|
+
item = { wtDir, branchName: null };
|
|
118
|
+
} catch (fallbackErr) {
|
|
119
|
+
log.error(`Failed to create worktree for ${taskId}: ${fallbackErr.message}`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
92
122
|
}
|
|
93
123
|
}
|
|
94
124
|
if (item) activeWorktrees.add(item);
|
|
@@ -105,18 +135,21 @@ async function removeWorktree(wtDir, branchName) {
|
|
|
105
135
|
try {
|
|
106
136
|
await execFileAsync("git", ["worktree", "remove", "--force", wtDir]);
|
|
107
137
|
} catch (err) {
|
|
108
|
-
|
|
138
|
+
log.error(`Failed to remove worktree at ${wtDir}: ${err.message}`);
|
|
109
139
|
}
|
|
110
140
|
if (branchName) {
|
|
111
141
|
try {
|
|
112
142
|
await execFileAsync("git", ["branch", "-D", branchName]);
|
|
113
143
|
} catch (err) {
|
|
114
|
-
|
|
144
|
+
log.error(`Failed to delete swarm branch ${branchName}: ${err.message}`);
|
|
115
145
|
}
|
|
116
146
|
}
|
|
117
147
|
}
|
|
118
148
|
|
|
119
149
|
async function runSwarm() {
|
|
150
|
+
if (USE_WORKTREES) {
|
|
151
|
+
reapOrphanedWorktrees();
|
|
152
|
+
}
|
|
120
153
|
const results = [];
|
|
121
154
|
let hasFailure = false;
|
|
122
155
|
|
|
@@ -131,8 +164,7 @@ async function runSwarm() {
|
|
|
131
164
|
await sleep(STAGGER_MS * bIdx);
|
|
132
165
|
}
|
|
133
166
|
|
|
134
|
-
|
|
135
|
-
console.log(`[${taskNum}/${tasks.length}] Dispatching Swarm Task: ${task.title} (${taskId})`);
|
|
167
|
+
log.step("======", `[${taskNum}/${tasks.length}] Dispatching Swarm Task: ${task.title} (${taskId})`);
|
|
136
168
|
|
|
137
169
|
let wtInfo = null;
|
|
138
170
|
let execCwd = process.cwd();
|
|
@@ -141,7 +173,7 @@ async function runSwarm() {
|
|
|
141
173
|
wtInfo = await createWorktree(taskId);
|
|
142
174
|
if (wtInfo?.wtDir) {
|
|
143
175
|
execCwd = wtInfo.wtDir;
|
|
144
|
-
|
|
176
|
+
log.info(`Isolated Task ${taskId} in Git Worktree: ${wtInfo.wtDir}`);
|
|
145
177
|
}
|
|
146
178
|
}
|
|
147
179
|
|
|
@@ -156,12 +188,17 @@ async function runSwarm() {
|
|
|
156
188
|
const { stdout } = await execFileAsync("node", [dispatchScript, task.title, effectivePrompt], {
|
|
157
189
|
cwd: execCwd,
|
|
158
190
|
timeout: 15 * 60 * 1000,
|
|
159
|
-
env: {
|
|
191
|
+
env: {
|
|
192
|
+
...process.env,
|
|
193
|
+
JULES_PROJECT_ROOT: process.env.JULES_PROJECT_ROOT,
|
|
194
|
+
JULES_SLOT_INDEX: String(bIdx + 1),
|
|
195
|
+
JULES_SLOT_TOTAL: String(batch.length)
|
|
196
|
+
},
|
|
160
197
|
});
|
|
161
|
-
if (stdout)
|
|
198
|
+
if (stdout) log.dim(stdout.trim());
|
|
162
199
|
return { taskId, title: task.title, status: "SUCCESS" };
|
|
163
200
|
} catch (error) {
|
|
164
|
-
|
|
201
|
+
log.error(`Failed task [${task.title}]: ${error.message}`);
|
|
165
202
|
return { taskId, title: task.title, status: "FAILED", error: error.message };
|
|
166
203
|
} finally {
|
|
167
204
|
if (wtInfo?.wtDir) {
|
|
@@ -182,15 +219,14 @@ async function runSwarm() {
|
|
|
182
219
|
}
|
|
183
220
|
|
|
184
221
|
if (i + MAX_CONCURRENT < tasks.length) {
|
|
185
|
-
|
|
222
|
+
log.info(`Batch finished. Cooling down for 2s before next batch...`);
|
|
186
223
|
await sleep(2000);
|
|
187
224
|
}
|
|
188
225
|
}
|
|
189
226
|
|
|
190
|
-
|
|
191
|
-
console.log(`🎉 Swarm Dispatch Summary (${results.length} tasks processed):`);
|
|
227
|
+
log.header(`Swarm Dispatch Summary (${results.length} tasks processed):`);
|
|
192
228
|
results.forEach((res) => {
|
|
193
|
-
|
|
229
|
+
log.info(`[${res.status}] ${res.title}`);
|
|
194
230
|
});
|
|
195
231
|
|
|
196
232
|
if (hasFailure) {
|
|
@@ -199,6 +235,6 @@ async function runSwarm() {
|
|
|
199
235
|
}
|
|
200
236
|
|
|
201
237
|
runSwarm().catch((err) => {
|
|
202
|
-
|
|
238
|
+
log.error(`Unhandled swarm rejection: ${err.message}`);
|
|
203
239
|
process.exit(1);
|
|
204
240
|
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lock-manager.mjs
|
|
5
|
+
*
|
|
6
|
+
* Manages multi-agent file locks to prevent concurrent modification collisions.
|
|
7
|
+
* Uses .agent/sync-manifest.json as the lock registry.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/lock-manager.mjs acquire <agent_name> <task_id> <file_path1> <file_path2> [--unattended]
|
|
11
|
+
* node scripts/lock-manager.mjs release <task_id>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
const MANIFEST_PATH = path.join(process.cwd(), '.agent', 'sync-manifest.json');
|
|
18
|
+
const TTL_MS = 20 * 60 * 1000; // 20 minutes
|
|
19
|
+
|
|
20
|
+
async function readManifest() {
|
|
21
|
+
try {
|
|
22
|
+
const data = await fs.readFile(MANIFEST_PATH, 'utf-8');
|
|
23
|
+
return JSON.parse(data);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err.code === 'ENOENT') {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function writeManifest(manifest) {
|
|
33
|
+
await fs.mkdir(path.dirname(MANIFEST_PATH), { recursive: true });
|
|
34
|
+
await fs.writeFile(MANIFEST_PATH, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cleanExpiredLocks(manifest) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
let changed = false;
|
|
40
|
+
for (const [file, lock] of Object.entries(manifest)) {
|
|
41
|
+
if (lock.expiresAt < now) {
|
|
42
|
+
delete manifest[file];
|
|
43
|
+
changed = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return changed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function acquireLocks(agentName, taskId, files) {
|
|
50
|
+
const manifest = await readManifest();
|
|
51
|
+
cleanExpiredLocks(manifest);
|
|
52
|
+
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const expiresAt = now + TTL_MS;
|
|
55
|
+
|
|
56
|
+
const conflicts = [];
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const normalizedFile = path.normalize(file);
|
|
59
|
+
if (manifest[normalizedFile] && manifest[normalizedFile].taskId !== taskId) {
|
|
60
|
+
conflicts.push({
|
|
61
|
+
file: normalizedFile,
|
|
62
|
+
lockedBy: manifest[normalizedFile].agentName,
|
|
63
|
+
task: manifest[normalizedFile].taskId
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (conflicts.length > 0) {
|
|
69
|
+
console.error(`ERROR: Cannot acquire locks for task ${taskId}. Conflicts detected:`);
|
|
70
|
+
for (const conflict of conflicts) {
|
|
71
|
+
console.error(` - ${conflict.file} is locked by ${conflict.lockedBy} (Task: ${conflict.task})`);
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const normalizedFile = path.normalize(file);
|
|
78
|
+
manifest[normalizedFile] = {
|
|
79
|
+
agentName,
|
|
80
|
+
taskId,
|
|
81
|
+
acquiredAt: now,
|
|
82
|
+
expiresAt
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await writeManifest(manifest);
|
|
87
|
+
console.log(`SUCCESS: Acquired locks for task ${taskId} on ${files.length} files.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function releaseLocks(taskId) {
|
|
91
|
+
const manifest = await readManifest();
|
|
92
|
+
let releasedCount = 0;
|
|
93
|
+
|
|
94
|
+
for (const [file, lock] of Object.entries(manifest)) {
|
|
95
|
+
if (lock.taskId === taskId) {
|
|
96
|
+
delete manifest[file];
|
|
97
|
+
releasedCount++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (releasedCount > 0) {
|
|
102
|
+
await writeManifest(manifest);
|
|
103
|
+
console.log(`SUCCESS: Released ${releasedCount} locks for task ${taskId}.`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(`INFO: No locks found for task ${taskId}.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function main() {
|
|
110
|
+
const args = process.argv.slice(2);
|
|
111
|
+
const command = args[0];
|
|
112
|
+
|
|
113
|
+
if (command === 'acquire') {
|
|
114
|
+
const agentName = args[1];
|
|
115
|
+
const taskId = args[2];
|
|
116
|
+
const unattendedIndex = args.indexOf('--unattended');
|
|
117
|
+
|
|
118
|
+
let files = [];
|
|
119
|
+
if (unattendedIndex !== -1) {
|
|
120
|
+
files = args.slice(3, unattendedIndex).concat(args.slice(unattendedIndex + 1));
|
|
121
|
+
} else {
|
|
122
|
+
files = args.slice(3);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!agentName || !taskId || files.length === 0) {
|
|
126
|
+
console.error('Usage: acquire <agent_name> <task_id> <file_paths...>');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await acquireLocks(agentName, taskId, files);
|
|
131
|
+
} else if (command === 'release') {
|
|
132
|
+
const taskId = args[1];
|
|
133
|
+
if (!taskId) {
|
|
134
|
+
console.error('Usage: release <task_id>');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
await releaseLocks(taskId);
|
|
138
|
+
} else {
|
|
139
|
+
console.error('Unknown command. Use "acquire" or "release".');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main().catch(err => {
|
|
145
|
+
console.error('Unhandled error:', err);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
// ANSI colors for standardizing CLI DX
|
|
7
|
+
const isCI = !!process.env.CI;
|
|
8
|
+
const noColor = (!process.stdout.isTTY && !isCI) || process.env.NO_COLOR;
|
|
9
|
+
const c = (color, text) => noColor ? text : `\x1b[${color}m${text}\x1b[0m`;
|
|
10
|
+
|
|
11
|
+
export const log = {
|
|
12
|
+
info: (msg) => console.log(c(36, `ℹ️ ${msg}`)),
|
|
13
|
+
success: (msg) => console.log(c(32, `✅ ${msg}`)),
|
|
14
|
+
warn: (msg) => console.warn(c(33, `⚠️ ${msg}`)),
|
|
15
|
+
error: (msg) => {
|
|
16
|
+
if (isCI) console.log(`::error::${msg}`);
|
|
17
|
+
console.error(c(31, `❌ ${msg}`));
|
|
18
|
+
},
|
|
19
|
+
step: (stepStr, msg) => console.log(`${c(90, stepStr)} ${msg}`),
|
|
20
|
+
dim: (msg) => console.log(c(90, msg)),
|
|
21
|
+
header: (msg) => {
|
|
22
|
+
if (isCI) console.log(`::group::${msg}`);
|
|
23
|
+
console.log(`\n${c("1;35", `=== ${msg} ===`)}\n`);
|
|
24
|
+
},
|
|
25
|
+
groupEnd: () => {
|
|
26
|
+
if (isCI) console.log("::endgroup::");
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const sleep = promisify(setTimeout);
|
|
31
|
+
|
|
32
|
+
export function ensureDir(dirPath) {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(dirPath)) {
|
|
35
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
log.error(`EACCES: Insufficient permissions or failed to create directory: ${dirPath}`);
|
|
39
|
+
log.error(error.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function logToHistory(filename, content, type = "audit") {
|
|
45
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
46
|
+
const historyDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/history");
|
|
47
|
+
ensureDir(historyDir);
|
|
48
|
+
const filePath = path.join(historyDir, `${dateStr}-${filename}`);
|
|
49
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
50
|
+
return filePath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveMarkdownConflict(content) {
|
|
54
|
+
if (!content || typeof content !== "string") return "";
|
|
55
|
+
if (!content.includes("<<<<<<<")) return content;
|
|
56
|
+
|
|
57
|
+
const lines = content.split("\n");
|
|
58
|
+
const result = [];
|
|
59
|
+
let inConflict = false;
|
|
60
|
+
let headBuffer = [];
|
|
61
|
+
let devBuffer = [];
|
|
62
|
+
let section = null;
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
const line = lines[i];
|
|
66
|
+
if (line.startsWith("<<<<<<<")) {
|
|
67
|
+
inConflict = true;
|
|
68
|
+
section = "head";
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (line.startsWith("=======")) {
|
|
72
|
+
section = "dev";
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (line.startsWith(">>>>>>>")) {
|
|
76
|
+
result.push(...headBuffer);
|
|
77
|
+
result.push(...devBuffer);
|
|
78
|
+
headBuffer = [];
|
|
79
|
+
devBuffer = [];
|
|
80
|
+
inConflict = false;
|
|
81
|
+
section = null;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (inConflict) {
|
|
86
|
+
if (section === "head") headBuffer.push(line);
|
|
87
|
+
else if (section === "dev") devBuffer.push(line);
|
|
88
|
+
} else {
|
|
89
|
+
result.push(line);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result.join("\n");
|
|
94
|
+
}
|