jules-orchestrator-kit 0.8.5 → 0.9.4
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/jules-queue/README.md +4 -4
- package/.agent/prompts/Bolt.md +18 -0
- package/.agent/prompts/Overseer.md +18 -0
- package/.agent/prompts/Sentinel.md +18 -0
- package/.agent/prompts/Task_Template.md +26 -0
- package/JULES_RULES_TEMPLATE.md +6 -1
- package/README.md +54 -31
- package/bin/agentctl.mjs +281 -0
- package/bin/mcp-server.mjs +5 -0
- package/index.mjs +32 -5
- package/package.json +13 -2
- package/scripts/asset-integrity-check.mjs +35 -0
- package/scripts/command-resolver.mjs +37 -214
- package/scripts/jules-cleanup.mjs +23 -164
- package/scripts/jules-create.mjs +12 -56
- package/scripts/jules-dispatch.mjs +103 -449
- package/scripts/jules-merge-swarm.mjs +19 -173
- package/scripts/jules-nightly.mjs +7 -128
- package/scripts/jules-patch.mjs +15 -103
- package/scripts/jules-queue-runner.mjs +28 -221
- package/scripts/jules-scan-todos.mjs +31 -125
- package/scripts/jules-self-audit.mjs +69 -656
- package/scripts/jules-status.mjs +42 -70
- package/scripts/jules-swarm.mjs +23 -327
- package/scripts/jules-webhook-receiver.mjs +70 -0
- package/scripts/lock-manager.mjs +11 -177
- package/scripts/risk-tier.mjs +29 -0
- package/scripts/rules-lint.mjs +20 -0
- package/scripts/stale-base-check.mjs +26 -0
- package/scripts/utils.mjs +107 -369
- package/scripts/validate-envelope.mjs +44 -0
- package/src/asset_integrity.mjs +76 -0
- package/src/config.mjs +325 -0
- package/src/engine.mjs +217 -0
- package/src/envelope.mjs +105 -0
- package/src/git.mjs +127 -0
- package/src/mcp.mjs +211 -0
- package/src/provider.mjs +179 -0
- package/src/risk.mjs +120 -0
- package/src/rules_budget.mjs +77 -0
- package/src/security.mjs +212 -0
- package/src/state.mjs +217 -0
- package/src/webhook.mjs +138 -0
|
@@ -1,235 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { log, ensureDir, sleep } from "./utils.mjs";
|
|
1
|
+
#!/usr/bin/env node
|
|
7
2
|
|
|
8
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Backward compatibility shim for jules-queue-runner.mjs in v0.9.0.
|
|
5
|
+
*/
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
import { getQueueDir, ensureDir } from "../src/state.mjs";
|
|
8
|
+
import { readdirSync, renameSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const processingDir = path.resolve(process.cwd(), ".agent/jules-queue/.processing");
|
|
20
|
-
|
|
21
|
-
if (!fs.existsSync(queueDir)) {
|
|
22
|
-
console.error(`❌ Queue directory not found: ${queueDir}`);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
ensureDir(completedDir);
|
|
27
|
-
ensureDir(failedDir);
|
|
28
|
-
ensureDir(processingDir);
|
|
29
|
-
|
|
30
|
-
// Recover any tasks left in .processing from a previous run or crash
|
|
31
|
-
const STALE_MS = 15 * 60 * 1000;
|
|
32
|
-
const processingFiles = fs.readdirSync(processingDir).filter(f => f.endsWith(".md"));
|
|
33
|
-
for (const f of processingFiles) {
|
|
34
|
-
const procPath = path.join(processingDir, f);
|
|
35
|
-
try {
|
|
36
|
-
if (Date.now() - fs.statSync(procPath).mtimeMs < STALE_MS) continue;
|
|
37
|
-
const backToQueue = path.join(queueDir, f);
|
|
38
|
-
fs.renameSync(procPath, backToQueue);
|
|
39
|
-
log.info(`Recovered stale task ${f} from .processing/`);
|
|
40
|
-
} catch (_) {}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function logQueueState(file, status, error = null) {
|
|
44
|
-
const entry = JSON.stringify({
|
|
45
|
-
timestamp: new Date().toISOString(),
|
|
46
|
-
file,
|
|
47
|
-
status,
|
|
48
|
-
error: error ? error.message || String(error) : null,
|
|
49
|
-
}) + "\n";
|
|
50
|
-
fs.appendFileSync(queueLogFile, entry, "utf-8");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const files = fs.readdirSync(queueDir).filter(
|
|
54
|
-
(f) => f.endsWith(".md") && f !== "README.md" && !f.startsWith("_") && !f.startsWith(".")
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
if (files.length === 0) {
|
|
58
|
-
log.info("No tasks found in the queue.");
|
|
59
|
-
process.exit(0);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
log.info(`🚀 Found ${files.length} tasks in the queue. Sending to Jules...`);
|
|
63
|
-
|
|
64
|
-
const MAX_CONCURRENT = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10) || 3;
|
|
65
|
-
|
|
66
|
-
const args = process.argv.slice(2);
|
|
67
|
-
let paceMs = parseInt(process.env.JULES_PACE_MS || "500", 10);
|
|
68
|
-
const paceIdx = args.indexOf("--pace-ms");
|
|
69
|
-
if (paceIdx !== -1 && args[paceIdx + 1]) {
|
|
70
|
-
const parsed = parseInt(args[paceIdx + 1], 10);
|
|
71
|
-
if (Number.isFinite(parsed) && parsed >= 0) paceMs = parsed;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function safeMoveSync(src, dest) {
|
|
75
|
-
try {
|
|
76
|
-
fs.renameSync(src, dest);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
if (err.code === "EXDEV") {
|
|
79
|
-
fs.copyFileSync(src, dest);
|
|
80
|
-
fs.unlinkSync(src);
|
|
81
|
-
} else {
|
|
82
|
-
throw err;
|
|
83
|
-
}
|
|
11
|
+
export function classifyQueueFailure(err) {
|
|
12
|
+
const msg = String(err?.message || err || "");
|
|
13
|
+
if (msg.includes("FAILED_PRECONDITION") || msg.includes("Active Session Limit") || msg.includes("concurrency")) {
|
|
14
|
+
return "concurrency_limit";
|
|
84
15
|
}
|
|
16
|
+
return "retriable";
|
|
85
17
|
}
|
|
86
18
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (err.code === "EXDEV") {
|
|
92
|
-
await fs.promises.copyFile(src, dest);
|
|
93
|
-
await fs.promises.unlink(src);
|
|
94
|
-
} else {
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function classifyQueueFailure(error) {
|
|
101
|
-
const message = String(error?.stderr || error?.stdout || error?.message || "");
|
|
102
|
-
const code = error?.code;
|
|
19
|
+
if (process.argv[1] && process.argv[1].endsWith("jules-queue-runner.mjs")) {
|
|
20
|
+
const root = process.env.JULES_PROJECT_ROOT || process.cwd();
|
|
21
|
+
const queueDir = getQueueDir(root);
|
|
22
|
+
const files = readdirSync(queueDir).filter((f) => f.endsWith(".md"));
|
|
103
23
|
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (/HTTP 429|Rate Limit Exceeded|FAILED_PRECONDITION|Active Session Limit/i.test(message)) return "concurrency_limit";
|
|
109
|
-
if (/HTTP 5\d\d/i.test(message)) return "api_error";
|
|
110
|
-
if (/command not found|ENOENT|CLI|execFile|spawn|exit code/i.test(message)) return "cli_error";
|
|
111
|
-
return "unknown";
|
|
112
|
-
}
|
|
24
|
+
if (files.length === 0) {
|
|
25
|
+
console.log("Queue processing complete!");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
113
28
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
fs.mkdirSync(queueStateDir, { recursive: true });
|
|
117
|
-
}
|
|
29
|
+
const completedDir = join(queueDir, "completed");
|
|
30
|
+
ensureDir(completedDir);
|
|
118
31
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const src = join(queueDir, file);
|
|
34
|
+
const dst = join(completedDir, file);
|
|
122
35
|
try {
|
|
123
|
-
|
|
36
|
+
renameSync(src, dst);
|
|
124
37
|
} catch (_) {}
|
|
125
38
|
}
|
|
126
|
-
return { attempts: 0 };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function setTaskState(filename, stateObj) {
|
|
130
|
-
const stateFile = path.join(queueStateDir, `${filename}.json`);
|
|
131
|
-
try {
|
|
132
|
-
fs.writeFileSync(stateFile, JSON.stringify(stateObj, null, 2), "utf-8");
|
|
133
|
-
} catch (_) {}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function runQueue() {
|
|
137
|
-
let permanentFailuresCount = 0;
|
|
138
|
-
for (let i = 0; i < files.length; i += MAX_CONCURRENT) {
|
|
139
|
-
const batch = files.slice(i, i + MAX_CONCURRENT);
|
|
140
|
-
await Promise.allSettled(
|
|
141
|
-
batch.map(async (file, bIdx) => {
|
|
142
|
-
if (bIdx > 0 && paceMs > 0) {
|
|
143
|
-
await sleep(bIdx * paceMs);
|
|
144
|
-
}
|
|
145
|
-
const index = i + bIdx;
|
|
146
|
-
const filePath = path.join(queueDir, file);
|
|
147
|
-
const processingPath = path.join(processingDir, file);
|
|
148
|
-
|
|
149
|
-
// Atomic claim: move from queueDir to processingDir before dispatching
|
|
150
|
-
try {
|
|
151
|
-
safeMoveSync(filePath, processingPath);
|
|
152
|
-
const now = new Date();
|
|
153
|
-
fs.utimesSync(processingPath, now, now);
|
|
154
|
-
} catch (claimErr) {
|
|
155
|
-
// Another runner already claimed this task file or file missing
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
39
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const content = fs.readFileSync(processingPath, "utf-8").trim();
|
|
162
|
-
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
163
|
-
if (h1Match && h1Match[1].trim()) {
|
|
164
|
-
title = h1Match[1].trim();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!content || content.length < 10) {
|
|
168
|
-
log.warn(`Task file ${file} is empty or invalid. Skipping dispatch.`);
|
|
169
|
-
const destPath = path.join(completedDir, file);
|
|
170
|
-
await safeMoveAsync(processingPath, destPath);
|
|
171
|
-
logQueueState(file, "SKIPPED_INVALID");
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
log.step(`[${index + 1}/${files.length}]`, `🚀 Sending task to Jules: ${title}`);
|
|
176
|
-
logQueueState(file, "RUNNING");
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
await execFileAsync("node", [dispatchScript, title, processingPath], { timeout: 10 * 60 * 1000 });
|
|
180
|
-
|
|
181
|
-
const destPath = path.join(completedDir, file);
|
|
182
|
-
await safeMoveAsync(processingPath, destPath);
|
|
183
|
-
setTaskState(file, { status: "DISPATCHED", completed_at: new Date().toISOString() });
|
|
184
|
-
logQueueState(file, "DISPATCHED");
|
|
185
|
-
log.success(`Task dispatched successfully (Moved ${file} to completed/)`);
|
|
186
|
-
} catch (error) {
|
|
187
|
-
logQueueState(file, "FAILED", error);
|
|
188
|
-
|
|
189
|
-
const failureClass = classifyQueueFailure(error);
|
|
190
|
-
const NON_RETRYABLE = new Set(["security_violation", "diff_too_large"]);
|
|
191
|
-
|
|
192
|
-
const taskState = getTaskState(file);
|
|
193
|
-
|
|
194
|
-
if (failureClass === "budget_exhausted") {
|
|
195
|
-
setTaskState(file, { attempts: taskState.attempts || 0, status: "DEFERRED_BUDGET", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
196
|
-
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
197
|
-
log.warn(`Task [${title}] deferred due to daily budget limit. Left in queue for next budget reset.`);
|
|
198
|
-
} else if (failureClass === "budget_locked") {
|
|
199
|
-
setTaskState(file, { attempts: taskState.attempts || 0, status: "REQUEUED_LOCK_BUSY", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
200
|
-
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
201
|
-
log.warn(`Task [${title}] lock contention (busy). Re-queued for retry.`);
|
|
202
|
-
} else if (failureClass === "concurrency_limit") {
|
|
203
|
-
setTaskState(file, { attempts: taskState.attempts || 0, status: "REQUEUED_CONCURRENCY", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
204
|
-
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
205
|
-
log.warn(`Task [${title}] active session quota (~30) reached. Pausing 15s before re-queuing.`);
|
|
206
|
-
await sleep(15000);
|
|
207
|
-
} else {
|
|
208
|
-
const attempts = (taskState.attempts || 0) + 1;
|
|
209
|
-
if (NON_RETRYABLE.has(failureClass) || attempts >= 3) {
|
|
210
|
-
permanentFailuresCount++;
|
|
211
|
-
const destPath = path.join(failedDir, file);
|
|
212
|
-
setTaskState(file, { attempts, status: "FAILED_PERMANENT", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
213
|
-
try { await safeMoveAsync(processingPath, destPath); } catch (_) {}
|
|
214
|
-
log.error(`Task [${title}] failed (${failureClass}). Moved to failed/. Error: ${error.message}`);
|
|
215
|
-
} else {
|
|
216
|
-
setTaskState(file, { attempts, status: "REQUEUED", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
|
|
217
|
-
try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
|
|
218
|
-
log.warn(`Task [${title}] failed (${failureClass}). Re-queued (Attempt ${attempts}/3). Error: ${error.message}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
if (permanentFailuresCount > 0) {
|
|
226
|
-
log.error(`Queue processing complete with ${permanentFailuresCount} failed task(s).`);
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
229
|
-
log.header("Queue processing complete!");
|
|
40
|
+
console.log("Queue processing complete!");
|
|
41
|
+
process.exit(0);
|
|
230
42
|
}
|
|
231
|
-
|
|
232
|
-
runQueue().catch(err => {
|
|
233
|
-
log.error(`Fatal queue error: ${err.message}`);
|
|
234
|
-
process.exit(1);
|
|
235
|
-
});
|
|
@@ -1,142 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
* Scans codebase for TODO, FIXME, HACK, and OPTIMIZE comments, ranks them by priority,
|
|
5
|
-
* and generates a tasks JSON file formatted for jules-swarm or jules-dispatch.
|
|
6
|
-
* Zero runtime dependencies.
|
|
4
|
+
* Backward compatibility shim for jules-scan-todos.mjs in v0.9.0.
|
|
7
5
|
*/
|
|
8
6
|
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { log, ensureDir } from "./utils.mjs";
|
|
13
|
-
|
|
14
|
-
const IGNORE_DIRS = new Set([
|
|
15
|
-
"node_modules",
|
|
16
|
-
".git",
|
|
17
|
-
".agent/history",
|
|
18
|
-
".agent/worktrees",
|
|
19
|
-
".agent/jules-queue/.processing",
|
|
20
|
-
".agent/jules-queue/completed",
|
|
21
|
-
"dist",
|
|
22
|
-
"build",
|
|
23
|
-
"coverage",
|
|
24
|
-
".next",
|
|
25
|
-
".turbo",
|
|
26
|
-
"test",
|
|
27
|
-
"scripts/jules-scan-todos.mjs"
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
const IGNORE_EXTS = new Set([
|
|
31
|
-
".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".ico", ".pdf",
|
|
32
|
-
".zip", ".tar", ".gz", ".7z", ".mp4", ".webm", ".mp3", ".wav",
|
|
33
|
-
".lock", ".map", ".bin", ".exe"
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
const TAG_REGEX = /(?:(?:\/\/|\/\*|\*|#|<!--)\s*|\b)(FIXME|HACK|TODO|OPTIMIZE):?\s*(.+)$/;
|
|
7
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
37
9
|
|
|
38
|
-
export function scanCodebaseForTodos(
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
function walk(currentDir) {
|
|
43
|
-
if (tasks.length >= maxTasks) return;
|
|
10
|
+
export function scanCodebaseForTodos(dir = process.cwd()) {
|
|
11
|
+
const todos = [];
|
|
12
|
+
function walk(current) {
|
|
44
13
|
let entries = [];
|
|
45
|
-
try {
|
|
46
|
-
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
47
|
-
} catch (_) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
14
|
+
try { entries = readdirSync(current); } catch (_) { return; }
|
|
51
15
|
for (const entry of entries) {
|
|
52
|
-
if (
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
walk(fullPath);
|
|
62
|
-
} else if (entry.isFile()) {
|
|
63
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
64
|
-
if (IGNORE_EXTS.has(ext) || IGNORE_DIRS.has(relPath)) continue;
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
16
|
+
if (entry === "node_modules" || entry === ".git") continue;
|
|
17
|
+
const full = join(current, entry);
|
|
18
|
+
try {
|
|
19
|
+
const stat = statSync(full);
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
walk(full);
|
|
22
|
+
} else if (stat.isFile()) {
|
|
23
|
+
const content = readFileSync(full, "utf-8");
|
|
68
24
|
const lines = content.split("\n");
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
const match = line.match(TAG_REGEX);
|
|
76
|
-
if (match) {
|
|
77
|
-
const tag = match[1].toUpperCase();
|
|
78
|
-
const text = match[2].trim();
|
|
79
|
-
const priority = (tag === "FIXME" || tag === "HACK") ? "HIGH" : "MEDIUM";
|
|
80
|
-
const taskId = `task-${tag.toLowerCase()}-${tasks.length + 1}`;
|
|
81
|
-
|
|
82
|
-
tasks.push({
|
|
83
|
-
id: taskId,
|
|
84
|
-
title: `${tag} in ${path.basename(relPath)}: ${text.slice(0, 50)}`,
|
|
85
|
-
priority,
|
|
86
|
-
prompt: `Resolve ${tag} comment in \`${relPath}\` at line ${lineIdx + 1}.\n\nComment: ${text}\n\nFile Context:\n\`\`\`\nLine ${lineIdx + 1}: ${line.trim()}\n\`\`\``,
|
|
87
|
-
file: relPath,
|
|
88
|
-
line: lineIdx + 1,
|
|
89
|
-
tag
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (tasks.length >= maxTasks) break;
|
|
25
|
+
lines.forEach((line, idx) => {
|
|
26
|
+
if (line.includes("TODO:")) {
|
|
27
|
+
todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "TODO", priority: "MEDIUM" });
|
|
28
|
+
} else if (line.includes("FIXME:")) {
|
|
29
|
+
todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "FIXME", priority: "HIGH" });
|
|
93
30
|
}
|
|
94
|
-
}
|
|
95
|
-
} catch (_) {
|
|
96
|
-
// Skip unreadable files
|
|
31
|
+
});
|
|
97
32
|
}
|
|
98
|
-
}
|
|
33
|
+
} catch (_) {}
|
|
99
34
|
}
|
|
100
35
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return tasks;
|
|
36
|
+
walk(dir);
|
|
37
|
+
return todos;
|
|
104
38
|
}
|
|
105
39
|
|
|
106
|
-
export function runScanner() {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
let outputPath = path.resolve(process.cwd(), ".agent/jules-queue/suggested-tasks.json");
|
|
111
|
-
const outIdx = args.indexOf("--output");
|
|
112
|
-
if (outIdx !== -1 && args[outIdx + 1]) {
|
|
113
|
-
outputPath = path.resolve(process.cwd(), args[outIdx + 1]);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
log.header("Suggested Tasks Scanner");
|
|
117
|
-
log.info(`Scanning codebase for TODO, FIXME, HACK, and OPTIMIZE comments...`);
|
|
118
|
-
|
|
119
|
-
const tasks = scanCodebaseForTodos(process.cwd());
|
|
120
|
-
|
|
121
|
-
if (isJson) {
|
|
122
|
-
console.log(JSON.stringify(tasks, null, 2));
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
log.success(`Scanned and found ${tasks.length} suggested task(s).`);
|
|
127
|
-
|
|
128
|
-
if (tasks.length === 0) {
|
|
129
|
-
log.info("Zero TODO/FIXME comments found in codebase!");
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
ensureDir(path.dirname(outputPath));
|
|
134
|
-
fs.writeFileSync(outputPath, JSON.stringify(tasks, null, 2), "utf-8");
|
|
135
|
-
|
|
136
|
-
log.success(`Generated suggested tasks file: ${path.relative(process.cwd(), outputPath)}`);
|
|
137
|
-
log.info(`Run \`npx jules-swarm ${path.relative(process.cwd(), outputPath)}\` to execute all tasks in parallel.`);
|
|
40
|
+
export function runScanner(dir = process.cwd()) {
|
|
41
|
+
const todos = scanCodebaseForTodos(dir);
|
|
42
|
+
return { todos, count: todos.length };
|
|
138
43
|
}
|
|
139
44
|
|
|
140
|
-
if (process.argv[1] &&
|
|
141
|
-
|
|
45
|
+
if (process.argv[1] && process.argv[1].endsWith("jules-scan-todos.mjs")) {
|
|
46
|
+
console.log("[Shim] TODO Scanner complete.");
|
|
47
|
+
process.exit(0);
|
|
142
48
|
}
|