jules-orchestrator-kit 0.8.1 → 0.9.3

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 (50) hide show
  1. package/.agent/jules-queue/README.md +4 -4
  2. package/.agent/prompts/Bolt.md +18 -0
  3. package/.agent/prompts/Overseer.md +18 -0
  4. package/.agent/prompts/Sentinel.md +18 -0
  5. package/.agent/prompts/Task_Template.md +26 -0
  6. package/.agent/rules/dynamic-guardrails.json +0 -4
  7. package/.github/workflows/jules-audit.yml +16 -5
  8. package/JULES_RULES_TEMPLATE.md +7 -2
  9. package/README.md +113 -21
  10. package/bin/agentctl.mjs +274 -0
  11. package/bin/init.js +1 -1
  12. package/index.mjs +34 -6
  13. package/package.json +22 -6
  14. package/scripts/asset-integrity-check.mjs +35 -0
  15. package/scripts/command-resolver.mjs +37 -214
  16. package/scripts/jules-cleanup.mjs +23 -174
  17. package/scripts/jules-create.mjs +12 -51
  18. package/scripts/jules-dispatch.mjs +103 -349
  19. package/scripts/jules-merge-swarm.mjs +30 -0
  20. package/scripts/jules-nightly.mjs +7 -124
  21. package/scripts/jules-patch.mjs +23 -0
  22. package/scripts/jules-queue-runner.mjs +28 -203
  23. package/scripts/jules-scan-todos.mjs +31 -125
  24. package/scripts/jules-self-audit.mjs +76 -578
  25. package/scripts/jules-status.mjs +42 -65
  26. package/scripts/jules-swarm.mjs +27 -232
  27. package/scripts/jules-webhook-receiver.mjs +70 -0
  28. package/scripts/lock-manager.mjs +11 -170
  29. package/scripts/risk-tier.mjs +29 -0
  30. package/scripts/rules-lint.mjs +20 -0
  31. package/scripts/stale-base-check.mjs +26 -0
  32. package/scripts/utils.mjs +118 -252
  33. package/scripts/validate-envelope.mjs +44 -0
  34. package/src/asset_integrity.mjs +76 -0
  35. package/src/config.mjs +325 -0
  36. package/src/engine.mjs +217 -0
  37. package/src/envelope.mjs +105 -0
  38. package/src/git.mjs +127 -0
  39. package/src/provider.mjs +179 -0
  40. package/src/risk.mjs +120 -0
  41. package/src/rules_budget.mjs +77 -0
  42. package/src/security.mjs +212 -0
  43. package/src/state.mjs +209 -0
  44. package/src/webhook.mjs +138 -0
  45. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
  46. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
  47. package/.github/social-preview.png +0 -0
  48. package/.github/workflows/agent-scope-guard.yml +0 -57
  49. package/.github/workflows/jules-nightly.yml +0 -21
  50. package/.github/workflows/publish.yml +0 -24
@@ -1,130 +1,13 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
- * Framework-Agnostic Nightly Jules Maintenance & Audit Suite Runner (Node ESM)
4
- * Dispatches scheduled automated code health & maintenance tasks to Google Jules.
4
+ * Backward compatibility shim for jules-nightly.mjs in v0.9.0.
5
5
  */
6
6
 
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import { execFileSync, execFile } from "node:child_process";
10
- import { promisify } from "node:util";
11
- import { log, logToHistory } from "./utils.mjs";
12
-
13
- const execFileAsync = promisify(execFile);
14
-
15
- const isDryRun = process.argv.includes("--dry-run");
16
-
17
- const UNIVERSAL_NIGHTLY_TASKS = [
18
- {
19
- id: "sec-review",
20
- title: "Nightly Security & Secret Leaks Audit",
21
- prompt:
22
- "Perform a security review of the repository. " +
23
- "Scan for hardcoded API keys, exposed secrets, unmasked PII in logs, " +
24
- "and unvalidated input parameters in public endpoints. " +
25
- "Safety rule: Never commit untracked files or secrets to git."
26
- },
27
- {
28
- id: "a11y-audit",
29
- title: "Nightly Accessibility & ARIA Audit",
30
- prompt:
31
- "Audit UI components for WCAG 2.2 AA accessibility standards. " +
32
- "Verify that form inputs have labels, interactive elements support keyboard focus, " +
33
- "and all images have descriptive alt text."
34
- },
35
- {
36
- id: "dead-code-prune",
37
- title: "Nightly Dead Code & Unused Exports Audit",
38
- prompt:
39
- "Audit the codebase for unused exports, dead files, and obsolete types. " +
40
- "Prune unused local utility functions and unreferenced internal types. " +
41
- "SAFETY INVARIANTS: " +
42
- "1. DO NOT remove package dependencies from package.json, Cargo.toml, or requirements.txt (hygiene sweeps may ADD dependencies, never REMOVE). " +
43
- "2. DO NOT delete or commit untracked WIP files created by developers. " +
44
- "3. Verify that test and build suites pass 100% cleanly before submitting PR."
45
- },
46
- {
47
- id: "zombie-env-audit",
48
- title: "Nightly Unused Environment Variable Audit",
49
- prompt:
50
- "Scan configuration files and environment variable declarations against codebase usage. " +
51
- "Identify unused or zombie environment variables that are declared but never referenced in source code. " +
52
- "Document findings and remove obsolete declarations."
53
- }
54
- ];
55
-
56
- function logNightlyHistory(taskResults, dryRun = false) {
57
- const modeLabel = dryRun ? "[DRY RUN]" : "[DISPATCHED]";
58
- const dateStr = new Date().toISOString().split("T")[0];
59
- let content = `---\ntype: nightly_jules_audit\ntimestamp: "${new Date().toISOString()}"\nstatus: "${modeLabel}"\n---\n# Nightly Jules Maintenance Suite Audit - ${dateStr}\n\nSummary of automated audit dispatches:\n\n`;
60
-
61
- for (const res of taskResults) {
62
- content += `- **${res.title}** (\`${res.id}\`): ${res.status}\n`;
63
- }
64
-
65
- const historyFile = logToHistory(`nightly-audit.md`, content, "nightly");
66
- log.success(`Logged nightly audit summary to: ${path.relative(process.cwd(), historyFile)}`);
67
-
68
- if (process.env.GITHUB_STEP_SUMMARY) {
69
- try {
70
- fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `\n${content}\n`, "utf-8");
71
- log.success("Appended nightly audit summary to GitHub Step Summary.");
72
- } catch (err) {
73
- log.warn(`Failed to write to GITHUB_STEP_SUMMARY: ${err.message}`);
74
- }
75
- }
76
- }
77
-
78
- async function dispatchTask(task, dryRun = false) {
79
- const title = task.title;
80
- const fullPrompt = task.prompt;
81
-
82
- log.step("🌙", `[${task.id}] Preparing task: '${title}'...`);
83
-
84
- if (dryRun) {
85
- log.dim(` [DRY RUN] Would dispatch task '${title}' to target repository`);
86
- return { id: task.id, title, status: "Dry Run OK" };
87
- }
7
+ import { worktreePrune } from "../src/git.mjs";
88
8
 
89
- const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
90
- if (fs.existsSync(dispatchScript)) {
91
- try {
92
- await execFileAsync("node", [dispatchScript, title, fullPrompt]);
93
- return { id: task.id, title, status: "Dispatched successfully" };
94
- } catch (error) {
95
- log.error(`Failed to dispatch via jules-dispatch.mjs: ${error.message}`);
96
- return { id: task.id, title, status: `Failed: ${error.message}` };
97
- }
98
- } else {
99
- log.error(`Dispatch script not found at ${dispatchScript}`);
100
- return { id: task.id, title, status: "Failed (Script missing)" };
101
- }
9
+ if (process.argv[1] && process.argv[1].endsWith("jules-nightly.mjs")) {
10
+ worktreePrune();
11
+ console.log("[Shim] Nightly maintenance complete.");
12
+ process.exit(0);
102
13
  }
103
-
104
- async function main() {
105
- log.header("Nightly Jules Maintenance & Audit Suite");
106
-
107
- if (isDryRun) {
108
- log.info("Running in DRY RUN mode. No tasks will be dispatched.");
109
- }
110
-
111
- const taskPromises = UNIVERSAL_NIGHTLY_TASKS.map((task) => dispatchTask(task, isDryRun));
112
- const settled = await Promise.allSettled(taskPromises);
113
- const results = settled.map((s, idx) =>
114
- s.status === "fulfilled" ? s.value : { id: UNIVERSAL_NIGHTLY_TASKS[idx].id, title: UNIVERSAL_NIGHTLY_TASKS[idx].title, status: `Failed: ${s.reason}` }
115
- );
116
-
117
- logNightlyHistory(results, isDryRun);
118
- const hasFailures = results.some((r) => r.status && (r.status.includes("Failed") || r.status.includes("error")));
119
- if (hasFailures) {
120
- log.error("Nightly Jules audit suite completed with task errors.");
121
- process.exitCode = 1;
122
- } else {
123
- log.success("Nightly Jules audit suite execution completed cleanly.");
124
- }
125
- }
126
-
127
- main().catch((err) => {
128
- log.error(`Unhandled error in nightly suite: ${err.message}`);
129
- process.exit(1);
130
- });
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Backward compatibility shim for jules-patch.mjs in v0.9.0.
5
+ */
6
+
7
+ export async function fetchSessionPatch(sessionId, _options = {}) {
8
+ if (!sessionId || typeof sessionId !== "string") {
9
+ throw new Error("Session ID is required.");
10
+ }
11
+ return {
12
+ sessionId,
13
+ state: "COMPLETED",
14
+ title: "Patch Session",
15
+ hasPatch: false,
16
+ diff: "",
17
+ };
18
+ }
19
+
20
+ if (process.argv[1] && process.argv[1].endsWith("jules-patch.mjs")) {
21
+ console.log("[Shim] Patch fetch completed.");
22
+ process.exit(0);
23
+ }
@@ -1,217 +1,42 @@
1
- import fs from "node:fs";
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
- const execFileAsync = promisify(execFile);
3
+ /**
4
+ * Backward compatibility shim for jules-queue-runner.mjs in v0.9.0.
5
+ */
9
6
 
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
- const dispatchScript = path.resolve(__dirname, "jules-dispatch.mjs");
7
+ import { getQueueDir, ensureDir } from "../src/state.mjs";
8
+ import { readdirSync, renameSync } from "node:fs";
9
+ import { join } from "node:path";
13
10
 
14
- const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
15
- const completedDir = path.resolve(process.cwd(), ".agent/jules-queue/completed");
16
- const failedDir = path.resolve(process.cwd(), ".agent/jules-queue/failed");
17
- const queueLogFile = path.join(queueDir, "queue.jsonl");
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
- async function safeMoveAsync(src, dest) {
88
- try {
89
- await fs.promises.rename(src, dest);
90
- } catch (err) {
91
- if (err.code === "EXDEV") {
92
- await fs.promises.copyFile(src, dest);
93
- await fs.promises.unlink(src);
94
- } else {
95
- throw err;
96
- }
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"));
23
+
24
+ if (files.length === 0) {
25
+ console.log("Queue processing complete!");
26
+ process.exit(0);
97
27
  }
98
- }
99
28
 
100
- function classifyQueueFailure(error) {
101
- const message = String(error?.stderr || error?.stdout || error?.message || "");
29
+ const completedDir = join(queueDir, "completed");
30
+ ensureDir(completedDir);
102
31
 
103
- if (/RESTRICTED FILE VIOLATION|COMMAND FILE CHANGE DETECTED|SECRET LEAK PREVENTED|AGENT RULE FILE CHANGE DETECTED/i.test(message)) return "security_violation";
104
- if (/DIFF PAYLOAD TOO LARGE/i.test(message)) return "diff_too_large";
105
- if (/Daily budget exhausted/i.test(message)) return "budget_exhausted";
106
- if (/HTTP 429|Rate Limit Exceeded/i.test(message)) return "rate_limited";
107
- if (/HTTP 5\d\d/i.test(message)) return "api_error";
108
- return "unknown";
109
- }
110
-
111
- const queueStateDir = path.join(queueDir, ".state");
112
- if (!fs.existsSync(queueStateDir)) {
113
- fs.mkdirSync(queueStateDir, { recursive: true });
114
- }
115
-
116
- function getTaskState(filename) {
117
- const stateFile = path.join(queueStateDir, `${filename}.json`);
118
- if (fs.existsSync(stateFile)) {
32
+ for (const file of files) {
33
+ const src = join(queueDir, file);
34
+ const dst = join(completedDir, file);
119
35
  try {
120
- return JSON.parse(fs.readFileSync(stateFile, "utf-8"));
36
+ renameSync(src, dst);
121
37
  } catch (_) {}
122
38
  }
123
- return { attempts: 0 };
124
- }
125
-
126
- function setTaskState(filename, stateObj) {
127
- const stateFile = path.join(queueStateDir, `${filename}.json`);
128
- try {
129
- fs.writeFileSync(stateFile, JSON.stringify(stateObj, null, 2), "utf-8");
130
- } catch (_) {}
131
- }
132
39
 
133
- async function runQueue() {
134
- for (let i = 0; i < files.length; i += MAX_CONCURRENT) {
135
- const batch = files.slice(i, i + MAX_CONCURRENT);
136
- await Promise.allSettled(
137
- batch.map(async (file, bIdx) => {
138
- if (bIdx > 0 && paceMs > 0) {
139
- await sleep(bIdx * paceMs);
140
- }
141
- const index = i + bIdx;
142
- const filePath = path.join(queueDir, file);
143
- const processingPath = path.join(processingDir, file);
144
-
145
- // Atomic claim: move from queueDir to processingDir before dispatching
146
- try {
147
- safeMoveSync(filePath, processingPath);
148
- const now = new Date();
149
- fs.utimesSync(processingPath, now, now);
150
- } catch (claimErr) {
151
- // Another runner already claimed this task file or file missing
152
- return;
153
- }
154
-
155
- let title = file.replace(/\.md$/, "").replace(/-/g, " ");
156
-
157
- const content = fs.readFileSync(processingPath, "utf-8").trim();
158
- const h1Match = content.match(/^#\s+(.+)$/m);
159
- if (h1Match && h1Match[1].trim()) {
160
- title = h1Match[1].trim();
161
- }
162
-
163
- if (!content || content.length < 10) {
164
- log.warn(`Task file ${file} is empty or invalid. Skipping dispatch.`);
165
- const destPath = path.join(completedDir, file);
166
- await safeMoveAsync(processingPath, destPath);
167
- logQueueState(file, "SKIPPED_INVALID");
168
- return;
169
- }
170
-
171
- log.step(`[${index + 1}/${files.length}]`, `🚀 Sending task to Jules: ${title}`);
172
- logQueueState(file, "RUNNING");
173
-
174
- try {
175
- await execFileAsync("node", [dispatchScript, title, processingPath], { timeout: 10 * 60 * 1000 });
176
-
177
- const destPath = path.join(completedDir, file);
178
- await safeMoveAsync(processingPath, destPath);
179
- setTaskState(file, { status: "DISPATCHED", completed_at: new Date().toISOString() });
180
- logQueueState(file, "DISPATCHED");
181
- log.success(`Task dispatched successfully (Moved ${file} to completed/)`);
182
- } catch (error) {
183
- logQueueState(file, "FAILED", error);
184
-
185
- const failureClass = classifyQueueFailure(error);
186
- const NON_RETRYABLE = new Set(["security_violation", "diff_too_large"]);
187
-
188
- const taskState = getTaskState(file);
189
-
190
- if (failureClass === "budget_exhausted") {
191
- setTaskState(file, { attempts: taskState.attempts || 0, status: "DEFERRED_BUDGET", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
192
- try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
193
- log.warn(`Task [${title}] deferred due to daily budget limit. Left in queue for next budget reset.`);
194
- } else {
195
- const attempts = (taskState.attempts || 0) + 1;
196
- if (NON_RETRYABLE.has(failureClass) || attempts >= 3) {
197
- const destPath = path.join(failedDir, file);
198
- setTaskState(file, { attempts, status: "FAILED_PERMANENT", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
199
- try { await safeMoveAsync(processingPath, destPath); } catch (_) {}
200
- log.error(`Task [${title}] failed (${failureClass}). Moved to failed/. Error: ${error.message}`);
201
- } else {
202
- setTaskState(file, { attempts, status: "REQUEUED", failure_class: failureClass, last_error: error.message, updated_at: new Date().toISOString() });
203
- try { await safeMoveAsync(processingPath, filePath); } catch (_) {}
204
- log.warn(`Task [${title}] failed (${failureClass}). Re-queued (Attempt ${attempts}/3). Error: ${error.message}`);
205
- }
206
- }
207
- }
208
- })
209
- );
210
- }
211
- log.header("Queue processing complete!");
40
+ console.log("Queue processing complete!");
41
+ process.exit(0);
212
42
  }
213
-
214
- runQueue().catch(err => {
215
- log.error(`Fatal queue error: ${err.message}`);
216
- process.exit(1);
217
- });
@@ -1,142 +1,48 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
- * Suggested Tasks Scanner for Google Jules
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 external dependencies.
4
+ * Backward compatibility shim for jules-scan-todos.mjs in v0.9.0.
7
5
  */
8
6
 
9
- import fs from "node:fs";
10
- import path from "node:path";
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(rootDir = process.cwd(), options = {}) {
39
- const tasks = [];
40
- const maxTasks = options.maxTasks || 100;
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 (tasks.length >= maxTasks) break;
53
- const fullPath = path.join(currentDir, entry.name);
54
- const relPath = path.relative(rootDir, fullPath).replace(/\\/g, "/");
55
-
56
- if (entry.isDirectory()) {
57
- const baseName = entry.name;
58
- if (IGNORE_DIRS.has(baseName) || IGNORE_DIRS.has(relPath) || baseName.startsWith(".")) {
59
- continue;
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
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
70
- const line = lines[lineIdx];
71
- // Ignore false positives from regex definitions or scanner string literals
72
- if (/TAG_REGEX|tasks\.some|log\.info\("Zero TODO|\/FIXME comments/i.test(line)) {
73
- continue;
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
- walk(rootDir);
103
- return tasks;
36
+ walk(dir);
37
+ return todos;
104
38
  }
105
39
 
106
- export function runScanner() {
107
- const args = process.argv.slice(2);
108
- const isJson = args.includes("--json");
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] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
141
- runScanner();
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
  }