jules-orchestrator-kit 0.3.0 → 0.5.1
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/.github/workflows/agent-scope-guard.yml +56 -0
- 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 +11 -4
- package/scripts/jules-cleanup.mjs +200 -0
- package/scripts/jules-create.mjs +47 -0
- package/scripts/jules-dispatch.mjs +134 -104
- 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 +186 -37
- 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/bin/init.js
CHANGED
|
@@ -5,6 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
import readline from "node:readline/promises";
|
|
8
|
+
import zlib from "node:zlib";
|
|
9
|
+
import crypto from "node:crypto";
|
|
8
10
|
import { resolveProjectCommands } from "../scripts/command-resolver.mjs";
|
|
9
11
|
|
|
10
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -57,10 +59,13 @@ if (isForce) console.log("⚠️ Force mode enabled (existing files will be over
|
|
|
57
59
|
let answerRepoVal = "";
|
|
58
60
|
let answerBranchVal = "";
|
|
59
61
|
|
|
60
|
-
// Dual TTY Interactive Wizard
|
|
61
62
|
if (process.stdin.isTTY && (isInteractive || (!fs.existsSync(path.join(targetDir, ".agent/jules.yml")) && !process.env.CI))) {
|
|
62
63
|
try {
|
|
63
64
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
65
|
+
rl.on('SIGINT', () => {
|
|
66
|
+
console.log("\n🛑 Initialization aborted by user.");
|
|
67
|
+
process.exit(130);
|
|
68
|
+
});
|
|
64
69
|
const answerRepo = await rl.question("📦 Enter target GitHub repository (e.g. owner/repo) [optional]: ");
|
|
65
70
|
if (answerRepo.trim()) {
|
|
66
71
|
answerRepoVal = answerRepo.trim();
|
|
@@ -73,6 +78,10 @@ if (process.stdin.isTTY && (isInteractive || (!fs.existsSync(path.join(targetDir
|
|
|
73
78
|
}
|
|
74
79
|
rl.close();
|
|
75
80
|
} catch (err) {
|
|
81
|
+
if (err.name === 'AbortError') {
|
|
82
|
+
console.log("\n🛑 Initialization aborted by user.");
|
|
83
|
+
process.exit(130);
|
|
84
|
+
}
|
|
76
85
|
console.warn("⚠️ Interactive setup prompt failed:", err.message);
|
|
77
86
|
}
|
|
78
87
|
}
|
|
@@ -181,7 +190,7 @@ if (fs.existsSync(auditWfSource)) {
|
|
|
181
190
|
}
|
|
182
191
|
if (!fs.existsSync(auditWfTarget) || isForce) {
|
|
183
192
|
fs.copyFileSync(auditWfSource, auditWfTarget);
|
|
184
|
-
console.log("✅
|
|
193
|
+
console.log("✅ Added GitHub Actions workflow: .github/workflows/jules-audit.yml");
|
|
185
194
|
}
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -225,6 +234,8 @@ if (fs.existsSync(targetPkgPath) && targetDir !== kitRoot) {
|
|
|
225
234
|
const julesScripts = {
|
|
226
235
|
"jules:dispatch": "node scripts/jules-dispatch.mjs",
|
|
227
236
|
"jules:queue": "node scripts/jules-queue-runner.mjs",
|
|
237
|
+
"jules:create": "node scripts/jules-create.mjs",
|
|
238
|
+
"jules:status": "node scripts/jules-status.mjs",
|
|
228
239
|
"jules:audit": "node scripts/jules-self-audit.mjs",
|
|
229
240
|
"jules:swarm": "node scripts/jules-swarm.mjs",
|
|
230
241
|
"jules:nightly": "node scripts/jules-nightly.mjs"
|
|
@@ -239,7 +250,7 @@ if (fs.existsSync(targetPkgPath) && targetDir !== kitRoot) {
|
|
|
239
250
|
|
|
240
251
|
if (updated) {
|
|
241
252
|
fs.writeFileSync(targetPkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
242
|
-
console.log("✅
|
|
253
|
+
console.log("✅ Added Jules commands to package.json");
|
|
243
254
|
}
|
|
244
255
|
} catch (err) {
|
|
245
256
|
console.warn("⚠️ Failed to inject helper scripts into target package.json:", err.message);
|
|
@@ -247,7 +258,55 @@ if (fs.existsSync(targetPkgPath) && targetDir !== kitRoot) {
|
|
|
247
258
|
}
|
|
248
259
|
|
|
249
260
|
console.log("\n🎉 Google Jules Orchestration Kit successfully initialized!");
|
|
261
|
+
|
|
262
|
+
// 6. Generate Cryptographic Handshake (JULES_WEB_SETUP.md)
|
|
263
|
+
const agentState = {
|
|
264
|
+
v: 1,
|
|
265
|
+
schema: "jules.init/v1",
|
|
266
|
+
generatedAt: new Date().toISOString(),
|
|
267
|
+
repo: {
|
|
268
|
+
name: answerRepoVal || process.env.JULES_REPO || "unknown/repo",
|
|
269
|
+
branch: answerBranchVal || process.env.BASE_BRANCH || "main"
|
|
270
|
+
},
|
|
271
|
+
workspace: {
|
|
272
|
+
testCmd: detected.testCmd || "",
|
|
273
|
+
buildCmd: detected.buildCmd || "",
|
|
274
|
+
source: detected.source || "unknown"
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const canonicalJson = JSON.stringify(agentState);
|
|
279
|
+
const hash = crypto.createHash("sha256").update(canonicalJson, "utf8").digest("hex");
|
|
280
|
+
const compressed = zlib.brotliCompressSync(Buffer.from(canonicalJson, "utf8"));
|
|
281
|
+
const payloadToken = `JULES1.${hash}.${compressed.toString("base64url")}`;
|
|
282
|
+
|
|
283
|
+
const setupMdContent = `# JULES Web Setup
|
|
284
|
+
|
|
285
|
+
> **Generated**: ${agentState.generatedAt}
|
|
286
|
+
> **Setup Code**: \`${payloadToken}\`
|
|
287
|
+
|
|
288
|
+
## 🔗 Web Dashboard Setup
|
|
289
|
+
1. Open Jules Web UI (https://app.jules.ai/setup)
|
|
290
|
+
2. Paste your Setup Code:
|
|
291
|
+
\`\`\`
|
|
292
|
+
${payloadToken}
|
|
293
|
+
\`\`\`
|
|
294
|
+
|
|
295
|
+
## 🔧 Detected Configuration
|
|
296
|
+
* Test Command: \`${agentState.workspace.testCmd}\`
|
|
297
|
+
* Build Command: \`${agentState.workspace.buildCmd}\`
|
|
298
|
+
* Source: \`${agentState.workspace.source}\`
|
|
299
|
+
`;
|
|
300
|
+
|
|
301
|
+
fs.writeFileSync(path.join(targetDir, ".agent", "JULES_WEB_SETUP.md"), setupMdContent, "utf-8");
|
|
302
|
+
|
|
303
|
+
console.log("\n🔗 Web Dashboard Setup");
|
|
304
|
+
console.log(" Your local agent configurations are ready.");
|
|
305
|
+
console.log(` 👉 Setup Code: \x1b[36m${payloadToken}\x1b[0m`);
|
|
306
|
+
console.log("\n (A backup of this setup code was written to .agent/JULES_WEB_SETUP.md)");
|
|
250
307
|
console.log("\nNext Steps:");
|
|
251
|
-
console.log(" 1.
|
|
252
|
-
console.log(" 2.
|
|
253
|
-
console.log(" 3.
|
|
308
|
+
console.log(" 1. Paste the Setup Code into Jules Web UI.");
|
|
309
|
+
console.log(" 2. Set environment variables: JULES_REPO=\"owner/repo\"");
|
|
310
|
+
console.log(" 3. Scaffold a new task: npm run jules:create \"My Feature\"");
|
|
311
|
+
console.log(" 4. Dispatch the queue: npm run jules:queue");
|
|
312
|
+
console.log(" 5. Run pre-merge audit: npm run jules:audit\n");
|
package/index.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jules-orchestrator-kit Node.js SDK
|
|
3
|
+
* Primary entrypoint for programmatically orchestrating Google Jules workflows.
|
|
4
|
+
* Zero external dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { resolveProjectCommands, resolveWorkspaceExecutionBoundary } from "./scripts/command-resolver.mjs";
|
|
8
|
+
export { runSelfAudit, runPreflightSandbox, loadForbiddenPatterns, loadAllowedPatterns, matchGlob } from "./scripts/jules-self-audit.mjs";
|
|
9
|
+
export { scanCodebaseForTodos, runScanner } from "./scripts/jules-scan-todos.mjs";
|
|
10
|
+
export { log, logToHistory, ensureDir, resolveMarkdownConflict } from "./scripts/utils.mjs";
|
|
11
|
+
export { redactSecrets, getDynamicGuardrails } from "./scripts/jules-dispatch.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/FullThrottle83/jules-orchestrator-kit.git"
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
"jules-init": "bin/init.js",
|
|
15
15
|
"jules-orchestrator-kit": "bin/init.js"
|
|
16
16
|
},
|
|
17
|
+
"main": "index.mjs",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./index.mjs"
|
|
20
|
+
},
|
|
17
21
|
"files": [
|
|
22
|
+
"index.mjs",
|
|
18
23
|
"bin/",
|
|
19
24
|
"scripts/",
|
|
20
25
|
".agent/jules.yml",
|
|
@@ -27,13 +32,16 @@
|
|
|
27
32
|
"README.md",
|
|
28
33
|
"LICENSE"
|
|
29
34
|
],
|
|
30
|
-
|
|
31
35
|
"scripts": {
|
|
32
36
|
"init": "node bin/init.js",
|
|
33
37
|
"test": "node --test test/kit.test.mjs",
|
|
34
38
|
"jules:dispatch": "node scripts/jules-dispatch.mjs",
|
|
35
39
|
"jules:queue": "node scripts/jules-queue-runner.mjs",
|
|
40
|
+
"jules:create": "node scripts/jules-create.mjs",
|
|
41
|
+
"jules:status": "node scripts/jules-status.mjs",
|
|
36
42
|
"jules:audit": "node scripts/jules-self-audit.mjs",
|
|
43
|
+
"jules:cleanup": "node scripts/jules-cleanup.mjs",
|
|
44
|
+
"jules:scan": "node scripts/jules-scan-todos.mjs",
|
|
37
45
|
"jules:swarm": "node scripts/jules-swarm.mjs",
|
|
38
46
|
"jules:nightly": "node scripts/jules-nightly.mjs"
|
|
39
47
|
},
|
|
@@ -48,7 +56,6 @@
|
|
|
48
56
|
"mcp",
|
|
49
57
|
"automation"
|
|
50
58
|
],
|
|
51
|
-
"author": "
|
|
59
|
+
"author": "Google Open Source",
|
|
52
60
|
"license": "MIT"
|
|
53
61
|
}
|
|
54
|
-
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Jules Session Cleanup & PR Audit Utility
|
|
4
|
+
* Cross-references active/completed Jules REST API sessions with GitHub PR merge status
|
|
5
|
+
* to report and safely close merged or stale sessions.
|
|
6
|
+
* Zero external dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { log } from "./utils.mjs";
|
|
13
|
+
|
|
14
|
+
function loadEnv() {
|
|
15
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
16
|
+
if (fs.existsSync(envPath)) {
|
|
17
|
+
const lines = fs.readFileSync(envPath, "utf-8").split("\n");
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
21
|
+
const eqIdx = trimmed.indexOf("=");
|
|
22
|
+
if (eqIdx > 0) {
|
|
23
|
+
const key = trimmed.slice(0, eqIdx).trim().replace(/^export\s+/, "");
|
|
24
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
25
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
26
|
+
val = val.slice(1, -1);
|
|
27
|
+
}
|
|
28
|
+
if (!process.env[key]) {
|
|
29
|
+
process.env[key] = val;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
loadEnv();
|
|
36
|
+
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
const isDryRun = args.includes("--dry-run") || process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
|
|
39
|
+
const isCompact = args.includes("--compact");
|
|
40
|
+
const isMarkdown = args.includes("--markdown");
|
|
41
|
+
const shouldCloseMerged = args.includes("--close-merged");
|
|
42
|
+
|
|
43
|
+
let staleHours = 24;
|
|
44
|
+
const staleIdx = args.indexOf("--stale-hours");
|
|
45
|
+
if (staleIdx !== -1 && args[staleIdx + 1]) {
|
|
46
|
+
const parsed = parseFloat(args[staleIdx + 1]);
|
|
47
|
+
if (!isNaN(parsed) && parsed > 0) staleHours = parsed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const apiKey = process.env.JULES_API_KEY || process.env.GEMINI_API_KEY || "";
|
|
51
|
+
const apiUrl = process.env.JULES_API_URL || "https://jules.googleapis.com/v1alpha/sessions";
|
|
52
|
+
|
|
53
|
+
export function checkGitHubPrMerged(prUrl) {
|
|
54
|
+
if (!prUrl || typeof prUrl !== "string") return false;
|
|
55
|
+
try {
|
|
56
|
+
const output = execFileSync("gh", ["pr", "view", prUrl, "--json", "state,mergedAt"], {
|
|
57
|
+
encoding: "utf-8",
|
|
58
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
59
|
+
});
|
|
60
|
+
const parsed = JSON.parse(output);
|
|
61
|
+
return parsed.state === "MERGED" || Boolean(parsed.mergedAt);
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function extractPrUrls(outputs = []) {
|
|
68
|
+
const urls = [];
|
|
69
|
+
if (!Array.isArray(outputs)) return urls;
|
|
70
|
+
for (const item of outputs) {
|
|
71
|
+
const text = typeof item === "string" ? item : JSON.stringify(item);
|
|
72
|
+
const matches = text.match(/https:\/\/github\.com\/[^\/]+\/[^\/]+\/pull\/\d+/g);
|
|
73
|
+
if (matches) {
|
|
74
|
+
for (const m of matches) {
|
|
75
|
+
if (!urls.includes(m)) urls.push(m);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return urls;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function fetchJulesSessions(apiKeyOverride = apiKey) {
|
|
83
|
+
if (!apiKeyOverride) return [];
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(apiUrl, {
|
|
86
|
+
method: "GET",
|
|
87
|
+
headers: {
|
|
88
|
+
"X-Goog-Api-Key": apiKeyOverride,
|
|
89
|
+
"Content-Type": "application/json"
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) return [];
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
return data.sessions || data.items || [];
|
|
95
|
+
} catch (_) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function auditSessions(sessions, options = {}) {
|
|
101
|
+
const { staleHoursThreshold = 24 } = options;
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
|
|
104
|
+
const merged = [];
|
|
105
|
+
const active = [];
|
|
106
|
+
const stale = [];
|
|
107
|
+
|
|
108
|
+
for (const session of sessions) {
|
|
109
|
+
const sId = session.name || session.id || "unknown";
|
|
110
|
+
const title = session.title || "Untitled Session";
|
|
111
|
+
const state = session.state || "UNKNOWN";
|
|
112
|
+
const updatedAt = session.updateTime ? new Date(session.updateTime).getTime() : now;
|
|
113
|
+
const hoursInactive = (now - updatedAt) / (1000 * 60 * 60);
|
|
114
|
+
|
|
115
|
+
const prUrls = extractPrUrls(session.outputs || session.outputUrls || []);
|
|
116
|
+
let isPrMerged = false;
|
|
117
|
+
if (prUrls.length > 0) {
|
|
118
|
+
isPrMerged = prUrls.every((url) => checkGitHubPrMerged(url));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const item = { id: sId, title, state, hoursInactive: Math.round(hoursInactive * 10) / 10, prUrls };
|
|
122
|
+
|
|
123
|
+
if (isPrMerged) {
|
|
124
|
+
merged.push(item);
|
|
125
|
+
} else if (hoursInactive >= staleHoursThreshold && state !== "COMPLETED") {
|
|
126
|
+
stale.push(item);
|
|
127
|
+
} else {
|
|
128
|
+
active.push(item);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { merged, active, stale };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function runCleanup() {
|
|
136
|
+
if (!apiKey) {
|
|
137
|
+
log.info("JULES_API_KEY is not set. Skipping Jules REST API cleanup audit.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
log.header("Jules Session Audit & Cleanup");
|
|
142
|
+
log.info(`Fetching active sessions from Jules REST API...`);
|
|
143
|
+
|
|
144
|
+
const sessions = await fetchJulesSessions(apiKey);
|
|
145
|
+
if (sessions.length === 0) {
|
|
146
|
+
log.info("No active or past sessions found on Jules REST API.");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { merged, active, stale } = auditSessions(sessions, { staleHoursThreshold: staleHours });
|
|
151
|
+
|
|
152
|
+
if (isCompact) {
|
|
153
|
+
console.log(`Total: ${sessions.length} | Merged Candidates: ${merged.length} | Active: ${active.length} | Stale (> ${staleHours}h): ${stale.length}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isMarkdown) {
|
|
158
|
+
console.log(`\n### Jules Session Audit Summary\n`);
|
|
159
|
+
console.log(`| Category | Count |`);
|
|
160
|
+
console.log(`|---|---|`);
|
|
161
|
+
console.log(`| **Merged Candidates** | ${merged.length} |`);
|
|
162
|
+
console.log(`| **Active / In-Progress** | ${active.length} |`);
|
|
163
|
+
console.log(`| **Stale (> ${staleHours}h)** | ${stale.length} |`);
|
|
164
|
+
if (merged.length > 0) {
|
|
165
|
+
console.log(`\n#### Merged Sessions Ready for Cleanup\n`);
|
|
166
|
+
for (const m of merged) {
|
|
167
|
+
console.log(`- **${m.id}** (${m.title}) — PR: ${m.prUrls.join(", ") || "N/A"}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
log.info(`Found ${sessions.length} total session(s).`);
|
|
174
|
+
log.success(`Merged Cleanup Candidates: ${merged.length}`);
|
|
175
|
+
log.step("ℹ️", `Active Sessions: ${active.length}`);
|
|
176
|
+
if (stale.length > 0) {
|
|
177
|
+
log.warn(`Stale Sessions (> ${staleHours}h inactive): ${stale.length}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (merged.length > 0) {
|
|
181
|
+
console.table(merged.map((m) => ({ ID: m.id, Title: m.title, State: m.state, InactiveHours: m.hoursInactive })));
|
|
182
|
+
|
|
183
|
+
if (shouldCloseMerged && !isDryRun) {
|
|
184
|
+
for (const m of merged) {
|
|
185
|
+
log.step("🧹", `Deleting merged session: ${m.id}...`);
|
|
186
|
+
try {
|
|
187
|
+
const deleteUrl = `${apiUrl}/${m.id}`;
|
|
188
|
+
await fetch(deleteUrl, { method: "DELETE", headers: { "X-Goog-Api-Key": apiKey } });
|
|
189
|
+
log.success(`Deleted session ${m.id}`);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
log.warn(`Failed to delete session ${m.id}: ${err.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else if (isDryRun) {
|
|
195
|
+
log.dim(`[DRY RUN] Would delete ${merged.length} merged session(s). Use --close-merged to execute deletion.`);
|
|
196
|
+
} else {
|
|
197
|
+
log.info(`Run with \`--close-merged\` to delete the ${merged.length} merged session(s).`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Jules Task Scaffolding Utility
|
|
4
|
+
* Quickly creates a boilerplate markdown task in .agent/jules-queue/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { log, ensureDir } from "./utils.mjs";
|
|
10
|
+
|
|
11
|
+
const taskTitle = process.argv[2];
|
|
12
|
+
|
|
13
|
+
if (!taskTitle) {
|
|
14
|
+
log.error("Usage: npm run jules:create \"Task Title\"");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
|
|
19
|
+
ensureDir(queueDir);
|
|
20
|
+
|
|
21
|
+
const safeTitle = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
22
|
+
const filesInQueue = fs.readdirSync(queueDir).filter(f => f.endsWith(".md") && f !== "README.md");
|
|
23
|
+
const nextId = (filesInQueue.length + 1).toString().padStart(3, "0");
|
|
24
|
+
const filename = `TASK-${nextId}-${safeTitle}.md`;
|
|
25
|
+
const filepath = path.join(queueDir, filename);
|
|
26
|
+
|
|
27
|
+
const boilerplate = `# ${taskTitle}
|
|
28
|
+
|
|
29
|
+
## Objective
|
|
30
|
+
[Describe the primary goal of this task in a few sentences]
|
|
31
|
+
|
|
32
|
+
## Context & Files
|
|
33
|
+
[List the specific files Jules should focus on or any context required]
|
|
34
|
+
-
|
|
35
|
+
|
|
36
|
+
## Verification
|
|
37
|
+
[How should Jules verify that this task is completed? e.g., run tests, check UI]
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(filepath, boilerplate, "utf-8");
|
|
42
|
+
log.success(`Scaffolded new task: ${path.relative(process.cwd(), filepath)}`);
|
|
43
|
+
log.dim("Open this file, fill in the details, and run `npm run jules:queue` when ready!");
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log.error(`Failed to create task file: ${err.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|