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.
- 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/.agent/rules/dynamic-guardrails.json +0 -4
- package/.github/workflows/jules-audit.yml +16 -5
- package/JULES_RULES_TEMPLATE.md +7 -2
- package/README.md +113 -21
- package/bin/agentctl.mjs +274 -0
- package/bin/init.js +1 -1
- package/index.mjs +34 -6
- package/package.json +22 -6
- package/scripts/asset-integrity-check.mjs +35 -0
- package/scripts/command-resolver.mjs +37 -214
- package/scripts/jules-cleanup.mjs +23 -174
- package/scripts/jules-create.mjs +12 -51
- package/scripts/jules-dispatch.mjs +103 -349
- package/scripts/jules-merge-swarm.mjs +30 -0
- package/scripts/jules-nightly.mjs +7 -124
- package/scripts/jules-patch.mjs +23 -0
- package/scripts/jules-queue-runner.mjs +28 -203
- package/scripts/jules-scan-todos.mjs +31 -125
- package/scripts/jules-self-audit.mjs +76 -578
- package/scripts/jules-status.mjs +42 -65
- package/scripts/jules-swarm.mjs +27 -232
- package/scripts/jules-webhook-receiver.mjs +70 -0
- package/scripts/lock-manager.mjs +11 -170
- 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 +118 -252
- 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/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 +209 -0
- package/src/webhook.mjs +138 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
- package/.github/social-preview.png +0 -0
- package/.github/workflows/agent-scope-guard.yml +0 -57
- package/.github/workflows/jules-nightly.yml +0 -21
- package/.github/workflows/publish.yml +0 -24
|
@@ -1,130 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
101
|
-
|
|
29
|
+
const completedDir = join(queueDir, "completed");
|
|
30
|
+
ensureDir(completedDir);
|
|
102
31
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
}
|