jules-orchestrator-kit 0.28.1 ā 0.29.0
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/bin/agentctl.mjs +73 -25
- package/index.mjs +14 -1
- package/package.json +1 -1
- package/src/dashboard.mjs +2 -2
- package/src/engine.mjs +5 -4
- package/src/mcp.mjs +1 -1
- package/src/provider.mjs +17 -4
- package/src/tui.mjs +407 -0
- package/src/wizard-init.mjs +243 -0
- package/src/wizard-oracle.mjs +155 -0
- package/src/wizard-task.mjs +193 -0
package/bin/agentctl.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { readFileSync,
|
|
4
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { loadConfig, resolveRoot, detectStack, bootstrapZeroTestRepo } from "../src/config.mjs";
|
|
7
7
|
import { gate, dispatch, run, isTaskFile } from "../src/engine.mjs";
|
|
8
|
-
import { acquireLock, releaseLock, lockStatus, checkDailyBudget, getQueueDir
|
|
8
|
+
import { acquireLock, releaseLock, lockStatus, checkDailyBudget, getQueueDir } from "../src/state.mjs";
|
|
9
9
|
import { worktreePrune } from "../src/git.mjs";
|
|
10
10
|
import { reapOrphanedIntents, reapStaleMutexDirs } from "../src/journal.mjs";
|
|
11
11
|
|
|
@@ -14,7 +14,7 @@ const command = args[0];
|
|
|
14
14
|
|
|
15
15
|
function printHelp() {
|
|
16
16
|
console.log(`
|
|
17
|
-
š agentctl v0.
|
|
17
|
+
š agentctl v0.29.0 ā Universal Agent Orchestrator & Safety Gatekeeper
|
|
18
18
|
|
|
19
19
|
Usage: agentctl <command> [options]
|
|
20
20
|
|
|
@@ -30,7 +30,8 @@ Commands:
|
|
|
30
30
|
bootstrap Bootstrap zero-test repository with verification oracle
|
|
31
31
|
review-repair Parse PR review comments and synthesize OODA repair tasks
|
|
32
32
|
dashboard Start local HTTP telemetry and audit dashboard
|
|
33
|
-
init Scaffold .agent/
|
|
33
|
+
init Scaffold .agent/ config and run onboarding wizard
|
|
34
|
+
task create Interactively author and scope a Jules task envelope
|
|
34
35
|
status Display queue and system status summary
|
|
35
36
|
scan Scan codebase for TODO/FIXME task candidates
|
|
36
37
|
version Output agentctl version
|
|
@@ -53,7 +54,7 @@ async function main() {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
56
|
-
console.log("agentctl v0.
|
|
57
|
+
console.log("agentctl v0.29.0");
|
|
57
58
|
process.exit(0);
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -264,7 +265,7 @@ async function main() {
|
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
case "doctor": {
|
|
267
|
-
console.log(`\nš agentctl System Diagnostics (v0.
|
|
268
|
+
console.log(`\nš agentctl System Diagnostics (v0.29.0)`);
|
|
268
269
|
console.log(`--------------------------------------------------`);
|
|
269
270
|
console.log(` Project Root : ${root}`);
|
|
270
271
|
console.log(` Config File : ${config._file || "None (Using defaults)"}`);
|
|
@@ -326,34 +327,81 @@ async function main() {
|
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
case "init": {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
330
|
+
const { values } = parseArgs({
|
|
331
|
+
args: args.slice(1),
|
|
332
|
+
options: {
|
|
333
|
+
interactive: { type: "boolean", short: "i" },
|
|
334
|
+
tier: { type: "string", short: "t" },
|
|
335
|
+
json: { type: "boolean", short: "j" },
|
|
336
|
+
},
|
|
337
|
+
allowPositionals: true,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const { runInitWizard } = await import("../src/wizard-init.mjs");
|
|
341
|
+
const res = await runInitWizard(root, {
|
|
342
|
+
interactive: values.interactive !== false,
|
|
343
|
+
tier: values.tier || "pro",
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (values.json) {
|
|
347
|
+
console.log(JSON.stringify(res, null, 2));
|
|
346
348
|
} else {
|
|
347
|
-
console.log(
|
|
349
|
+
console.log(`ā
Onboarding complete! Manifest generated at ${res.configPath}`);
|
|
350
|
+
console.log(` Tier: ${res.plan.tier.toUpperCase()} (${res.plan.limits.concurrency} worker(s), ${res.plan.limits.daily_tasks} daily tasks)`);
|
|
351
|
+
console.log(` Verification Test Command : "${res.plan.verify.test}"`);
|
|
352
|
+
console.log(` Active Presets : ${res.plan.presets.join(", ")}`);
|
|
348
353
|
}
|
|
349
354
|
process.exit(0);
|
|
350
355
|
break;
|
|
351
356
|
}
|
|
352
357
|
|
|
358
|
+
case "task": {
|
|
359
|
+
const subCommand = args[1] || "create";
|
|
360
|
+
if (subCommand === "create") {
|
|
361
|
+
const { values } = parseArgs({
|
|
362
|
+
args: args.slice(2),
|
|
363
|
+
options: {
|
|
364
|
+
title: { type: "string", short: "t" },
|
|
365
|
+
prompt: { type: "string", short: "p" },
|
|
366
|
+
"verify-cmd": { type: "string", short: "v" },
|
|
367
|
+
"auto-pr": { type: "boolean" },
|
|
368
|
+
"require-plan-approval": { type: "boolean" },
|
|
369
|
+
repoless: { type: "boolean" },
|
|
370
|
+
json: { type: "boolean", short: "j" },
|
|
371
|
+
},
|
|
372
|
+
allowPositionals: true,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const { runTaskCreateWizard } = await import("../src/wizard-task.mjs");
|
|
376
|
+
const res = await runTaskCreateWizard(root, {
|
|
377
|
+
title: values.title,
|
|
378
|
+
prompt: values.prompt,
|
|
379
|
+
verifyCmd: values["verify-cmd"],
|
|
380
|
+
autoPr: values["auto-pr"],
|
|
381
|
+
requirePlanApproval: values["require-plan-approval"],
|
|
382
|
+
repoless: values.repoless,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
if (values.json) {
|
|
386
|
+
console.log(JSON.stringify(res, null, 2));
|
|
387
|
+
} else {
|
|
388
|
+
console.log(`ā
Task synthesized & queued at ${res.taskFile}`);
|
|
389
|
+
console.log(` Task ID : ${res.plan.taskId}`);
|
|
390
|
+
console.log(` Title : ${res.plan.title}`);
|
|
391
|
+
console.log(` Auto-PR : ${res.plan.flags.autoPr}`);
|
|
392
|
+
}
|
|
393
|
+
process.exit(0);
|
|
394
|
+
} else {
|
|
395
|
+
console.error(`Unknown task subcommand '${subCommand}'. Supported: agentctl task create`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
|
|
353
401
|
case "status": {
|
|
354
402
|
const queueDir = getQueueDir(root);
|
|
355
403
|
const files = existsSync(queueDir) ? readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir)) : [];
|
|
356
|
-
console.log(`\nš agentctl Status Summary (v0.
|
|
404
|
+
console.log(`\nš agentctl Status Summary (v0.29.0)`);
|
|
357
405
|
console.log(`--------------------------------------------------`);
|
|
358
406
|
console.log(` Project Root : ${root}`);
|
|
359
407
|
console.log(` Pending Tasks : ${files.length}`);
|
package/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Google Jules Orchestrator Kit - Node.js SDK (v0.
|
|
2
|
+
* Google Jules Orchestrator Kit - Node.js SDK (v0.29.0)
|
|
3
3
|
*
|
|
4
4
|
* Exposes core orchestrator functions for programmatically driving agent tasks,
|
|
5
5
|
* security auditing, repo gating, and state operations.
|
|
@@ -73,3 +73,16 @@ export { getDynamicGuardrails, dispatchTask } from "./scripts/jules-dispatch.mjs
|
|
|
73
73
|
export { classifyQueueFailure } from "./scripts/jules-queue-runner.mjs";
|
|
74
74
|
export { extractPrUrls, auditSessions, buildSyncManifest, pushReservationManifest } from "./scripts/utils.mjs";
|
|
75
75
|
|
|
76
|
+
// Native TUI Primitives
|
|
77
|
+
export { isTTY, styleText, select, multiSelect, input, confirm, secretInput, spinner, ANSI } from "./src/tui.mjs";
|
|
78
|
+
|
|
79
|
+
// Stack Oracle & Verification Probes
|
|
80
|
+
export { detectStackOracles, runVerificationProbe } from "./src/wizard-oracle.mjs";
|
|
81
|
+
|
|
82
|
+
// Onboarding & Presets Engine
|
|
83
|
+
export { planInit, loadPresets, runInitWizard, TIER_PROFILES, BUILTIN_PRESETS } from "./src/wizard-init.mjs";
|
|
84
|
+
|
|
85
|
+
// Guided Task Authoring Subsystem
|
|
86
|
+
export { planTaskCreate, runTaskCreateWizard, GUARDRAIL_FOOTER } from "./src/wizard-task.mjs";
|
|
87
|
+
|
|
88
|
+
|
package/package.json
CHANGED
package/src/dashboard.mjs
CHANGED
|
@@ -32,7 +32,7 @@ export function getDashboardHtml(root = process.cwd()) {
|
|
|
32
32
|
</style>
|
|
33
33
|
</head>
|
|
34
34
|
<body>
|
|
35
|
-
<h1>š jules-orchestrator-kit Dashboard (v0.
|
|
35
|
+
<h1>š jules-orchestrator-kit Dashboard (v0.29.0)</h1>
|
|
36
36
|
<div class="subtitle">Repository: <code>${root}</code></div>
|
|
37
37
|
|
|
38
38
|
<div class="grid">
|
|
@@ -87,7 +87,7 @@ export function createDashboardServer({ root = process.cwd(), port: _port = 4100
|
|
|
87
87
|
|
|
88
88
|
if (url.pathname === "/api/status") {
|
|
89
89
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
90
|
-
return res.end(JSON.stringify({ ok: true, version: "0.
|
|
90
|
+
return res.end(JSON.stringify({ ok: true, version: "0.29.0", root, ts: new Date().toISOString() }));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
if (url.pathname === "/api/telemetry") {
|
package/src/engine.mjs
CHANGED
|
@@ -101,20 +101,21 @@ export async function gate(opts = {}) {
|
|
|
101
101
|
const root = opts.root || process.cwd();
|
|
102
102
|
const config = opts.config || loadConfig(root);
|
|
103
103
|
const base = opts.base || config.baseBranch || "main";
|
|
104
|
+
const mode = opts.mode || (opts.workingTree ? "working-tree" : (process.env.JULES_GATE_MODE || "working-tree"));
|
|
104
105
|
const progressBus = opts.progressBus;
|
|
105
106
|
const progressToken = opts.progressToken;
|
|
106
107
|
const phases = [];
|
|
107
108
|
|
|
108
|
-
appendTelemetry(root, "gate_started", { base });
|
|
109
|
+
appendTelemetry(root, "gate_started", { base, mode });
|
|
109
110
|
|
|
110
111
|
let files = [];
|
|
111
112
|
let bytes = 0;
|
|
112
113
|
let diffStr = "";
|
|
113
114
|
|
|
114
115
|
try {
|
|
115
|
-
files = changedFiles(root, base);
|
|
116
|
-
bytes = diffBytes(root, base);
|
|
117
|
-
diffStr = diffText(root, base);
|
|
116
|
+
files = changedFiles(root, base, mode);
|
|
117
|
+
bytes = diffBytes(root, base, mode);
|
|
118
|
+
diffStr = diffText(root, base, mode);
|
|
118
119
|
} catch (err) {
|
|
119
120
|
phases.push({ phase: "git_resolution", ok: false, error: err.message });
|
|
120
121
|
appendTelemetry(root, "gate_finished", { ok: false, code: 1, error: err.message });
|
package/src/mcp.mjs
CHANGED
package/src/provider.mjs
CHANGED
|
@@ -121,16 +121,23 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
121
121
|
throw new MissingApiKeyError();
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
const isRepoless = Boolean(task.repoless || ctx.repoless);
|
|
124
125
|
const rawRepo = task.source || ctx.source || process.env.JULES_REPO || "";
|
|
126
|
+
if (!rawRepo && !ctx.dryRun && providerSpec.name === "jules" && !isRepoless) {
|
|
127
|
+
throw new Error("Missing connected Jules repository source. Set JULES_REPO or pass source/repoless option.");
|
|
128
|
+
}
|
|
129
|
+
|
|
125
130
|
const sourceName = rawRepo
|
|
126
131
|
? (rawRepo.startsWith("sources/") ? rawRepo : `sources/github/${rawRepo}`)
|
|
127
|
-
:
|
|
132
|
+
: undefined;
|
|
133
|
+
|
|
134
|
+
const startingBranch = task.branch || ctx.branch || config.baseBranch || process.env.BASE_BRANCH || "main";
|
|
128
135
|
|
|
129
136
|
const data = {
|
|
130
137
|
prompt: task.prompt || "",
|
|
131
138
|
title: task.title || "Agent Task",
|
|
132
|
-
branch:
|
|
133
|
-
source: sourceName,
|
|
139
|
+
branch: startingBranch,
|
|
140
|
+
source: sourceName || "",
|
|
134
141
|
project: process.env.JULES_PROJECT_ID || "default",
|
|
135
142
|
...ctx,
|
|
136
143
|
};
|
|
@@ -176,9 +183,15 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
176
183
|
}
|
|
177
184
|
|
|
178
185
|
if (bodyObj && typeof bodyObj === "object") {
|
|
179
|
-
if (
|
|
186
|
+
if (isRepoless || !sourceName) {
|
|
180
187
|
delete bodyObj.sourceContext;
|
|
181
188
|
}
|
|
189
|
+
if (task.autoPr || ctx.autoPr) {
|
|
190
|
+
bodyObj.automationMode = "AUTO_CREATE_PR";
|
|
191
|
+
}
|
|
192
|
+
if (task.requirePlanApproval || ctx.requirePlanApproval) {
|
|
193
|
+
bodyObj.requirePlanApproval = true;
|
|
194
|
+
}
|
|
182
195
|
body = JSON.stringify(bodyObj);
|
|
183
196
|
}
|
|
184
197
|
|
package/src/tui.mjs
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { isatty } from "node:tty";
|
|
3
|
+
|
|
4
|
+
// ANSI Styling & Escapes
|
|
5
|
+
export const ANSI = {
|
|
6
|
+
reset: "\u001b[0m",
|
|
7
|
+
bold: "\u001b[1m",
|
|
8
|
+
dim: "\u001b[2m",
|
|
9
|
+
red: "\u001b[31m",
|
|
10
|
+
green: "\u001b[32m",
|
|
11
|
+
yellow: "\u001b[33m",
|
|
12
|
+
blue: "\u001b[34m",
|
|
13
|
+
cyan: "\u001b[36m",
|
|
14
|
+
white: "\u001b[37m",
|
|
15
|
+
bgCyan: "\u001b[46m",
|
|
16
|
+
bgBlue: "\u001b[44m",
|
|
17
|
+
hideCursor: "\u001b[?25l",
|
|
18
|
+
showCursor: "\u001b[?25h",
|
|
19
|
+
clearLine: "\u001b[2K\r",
|
|
20
|
+
cursorUp: (n = 1) => `\u001b[${n}A`,
|
|
21
|
+
cursorDown: (n = 1) => `\u001b[${n}B`,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if the given input stream is an interactive TTY terminal.
|
|
26
|
+
* @param {import("node:stream").Readable} [stream]
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
export function isTTY(stream = process.stdin) {
|
|
30
|
+
return Boolean(stream && (stream.isTTY || isatty(stream.fd)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format string with ANSI styling if stream supports TTY.
|
|
35
|
+
* @param {string} text
|
|
36
|
+
* @param {string} style
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function styleText(text, style) {
|
|
40
|
+
if (!style) return text;
|
|
41
|
+
return `${style}${text}${ANSI.reset}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read keypress in raw mode from TTY input stream.
|
|
46
|
+
* @param {import("node:stream").Readable} stream
|
|
47
|
+
* @returns {AsyncGenerator<string, void, unknown>}
|
|
48
|
+
*/
|
|
49
|
+
async function* readKeypresses(stream = process.stdin) {
|
|
50
|
+
const wasRaw = stream.isRaw;
|
|
51
|
+
if (stream.setRawMode) {
|
|
52
|
+
stream.setRawMode(true);
|
|
53
|
+
}
|
|
54
|
+
stream.resume();
|
|
55
|
+
stream.setEncoding("utf-8");
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
for await (const chunk of stream) {
|
|
59
|
+
const str = String(chunk);
|
|
60
|
+
if (str === "\u0003") {
|
|
61
|
+
// Ctrl+C SIGINT
|
|
62
|
+
process.stdout.write(ANSI.showCursor + "\n");
|
|
63
|
+
process.exit(130);
|
|
64
|
+
}
|
|
65
|
+
yield str;
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
if (stream.setRawMode) {
|
|
69
|
+
stream.setRawMode(Boolean(wasRaw));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Single-select interactive menu using TTY raw mode.
|
|
76
|
+
* @param {Array<{ label: string, value: any, description?: string }>} options
|
|
77
|
+
* @param {string} prompt
|
|
78
|
+
* @param {object} [opts]
|
|
79
|
+
* @returns {Promise<any>}
|
|
80
|
+
*/
|
|
81
|
+
export async function select(options, prompt, opts = {}) {
|
|
82
|
+
const stdin = opts.stdin || process.stdin;
|
|
83
|
+
const stdout = opts.stdout || process.stdout;
|
|
84
|
+
|
|
85
|
+
if (options.length === 0) {
|
|
86
|
+
throw new Error("select() requires at least one option.");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const defaultIdx = opts.defaultIdx ?? 0;
|
|
90
|
+
let currentIdx = Math.max(0, Math.min(options.length - 1, defaultIdx));
|
|
91
|
+
|
|
92
|
+
if (!isTTY(stdin)) {
|
|
93
|
+
// Non-TTY Headless fallback
|
|
94
|
+
if (opts.fallbackValue !== undefined) {
|
|
95
|
+
return opts.fallbackValue;
|
|
96
|
+
}
|
|
97
|
+
const val = options[currentIdx].value;
|
|
98
|
+
stdout.write(`[Headless Select] ${prompt}: Auto-selected default [${options[currentIdx].label}]\n`);
|
|
99
|
+
return val;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function render(isFinal = false) {
|
|
103
|
+
let out = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${prompt}${ANSI.reset}\n`;
|
|
104
|
+
options.forEach((opt, idx) => {
|
|
105
|
+
const isSelected = idx === currentIdx;
|
|
106
|
+
const cursor = isSelected ? `${ANSI.cyan}āÆ${ANSI.reset}` : " ";
|
|
107
|
+
const label = isSelected ? `${ANSI.bold}${ANSI.cyan}${opt.label}${ANSI.reset}` : opt.label;
|
|
108
|
+
const desc = opt.description ? ` ${ANSI.dim}(${opt.description})${ANSI.reset}` : "";
|
|
109
|
+
out += ` ${cursor} ${label}${desc}\n`;
|
|
110
|
+
});
|
|
111
|
+
if (!isFinal) {
|
|
112
|
+
out += `${ANSI.dim} (Use arrow keys or 1-${Math.min(9, options.length)} to select, Enter to confirm)${ANSI.reset}`;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
stdout.write(ANSI.hideCursor);
|
|
118
|
+
let renderedLines = options.length + 2;
|
|
119
|
+
stdout.write(render());
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
for await (const key of readKeypresses(stdin)) {
|
|
123
|
+
if (key === "\r" || key === "\n") {
|
|
124
|
+
// Confirm selection
|
|
125
|
+
stdout.write(ANSI.cursorUp(renderedLines) + ANSI.clearLine);
|
|
126
|
+
for (let i = 0; i < renderedLines; i++) {
|
|
127
|
+
stdout.write(ANSI.clearLine + ANSI.cursorDown(1));
|
|
128
|
+
}
|
|
129
|
+
stdout.write(ANSI.cursorUp(renderedLines));
|
|
130
|
+
stdout.write(`${ANSI.green}ā${ANSI.reset} ${ANSI.bold}${prompt}${ANSI.reset} ${ANSI.cyan}${options[currentIdx].label}${ANSI.reset}\n`);
|
|
131
|
+
return options[currentIdx].value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let moved = false;
|
|
135
|
+
if (key === "\u001b[A" || key === "k") {
|
|
136
|
+
// Up
|
|
137
|
+
currentIdx = (currentIdx - 1 + options.length) % options.length;
|
|
138
|
+
moved = true;
|
|
139
|
+
} else if (key === "\u001b[B" || key === "j") {
|
|
140
|
+
// Down
|
|
141
|
+
currentIdx = (currentIdx + 1) % options.length;
|
|
142
|
+
moved = true;
|
|
143
|
+
} else if (/^[1-9]$/.test(key)) {
|
|
144
|
+
const num = parseInt(key, 10) - 1;
|
|
145
|
+
if (num < options.length) {
|
|
146
|
+
currentIdx = num;
|
|
147
|
+
moved = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (moved) {
|
|
152
|
+
stdout.write(ANSI.cursorUp(renderedLines));
|
|
153
|
+
stdout.write(render());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
stdout.write(ANSI.showCursor);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Multi-select checkbox menu using TTY raw mode.
|
|
163
|
+
* @param {Array<{ label: string, value: any, checked?: boolean, description?: string }>} options
|
|
164
|
+
* @param {string} prompt
|
|
165
|
+
* @param {object} [opts]
|
|
166
|
+
* @returns {Promise<Array<any>>}
|
|
167
|
+
*/
|
|
168
|
+
export async function multiSelect(options, prompt, opts = {}) {
|
|
169
|
+
const stdin = opts.stdin || process.stdin;
|
|
170
|
+
const stdout = opts.stdout || process.stdout;
|
|
171
|
+
|
|
172
|
+
const selections = new Set();
|
|
173
|
+
options.forEach((opt, idx) => {
|
|
174
|
+
if (opt.checked) selections.add(idx);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!isTTY(stdin)) {
|
|
178
|
+
// Non-TTY Headless fallback
|
|
179
|
+
const selectedVals = options.filter((_, idx) => selections.has(idx)).map((o) => o.value);
|
|
180
|
+
stdout.write(`[Headless MultiSelect] ${prompt}: Auto-selected ${selectedVals.length} item(s)\n`);
|
|
181
|
+
return selectedVals;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let currentIdx = 0;
|
|
185
|
+
let renderedLines = options.length + 2;
|
|
186
|
+
|
|
187
|
+
function render() {
|
|
188
|
+
let out = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${prompt}${ANSI.reset}\n`;
|
|
189
|
+
options.forEach((opt, idx) => {
|
|
190
|
+
const isHover = idx === currentIdx;
|
|
191
|
+
const isChecked = selections.has(idx);
|
|
192
|
+
const cursor = isHover ? `${ANSI.cyan}āÆ${ANSI.reset}` : " ";
|
|
193
|
+
const box = isChecked ? `${ANSI.green}[x]${ANSI.reset}` : `${ANSI.dim}[ ]${ANSI.reset}`;
|
|
194
|
+
const label = isHover ? `${ANSI.bold}${opt.label}${ANSI.reset}` : opt.label;
|
|
195
|
+
const desc = opt.description ? ` ${ANSI.dim}(${opt.description})${ANSI.reset}` : "";
|
|
196
|
+
out += ` ${cursor} ${box} ${label}${desc}\n`;
|
|
197
|
+
});
|
|
198
|
+
out += `${ANSI.dim} (Space to toggle, 'a' select all, 'n' select none, Enter to confirm)${ANSI.reset}`;
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
stdout.write(ANSI.hideCursor);
|
|
203
|
+
stdout.write(render());
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
for await (const key of readKeypresses(stdin)) {
|
|
207
|
+
if (key === "\r" || key === "\n") {
|
|
208
|
+
const resultVals = options.filter((_, idx) => selections.has(idx)).map((o) => o.value);
|
|
209
|
+
stdout.write(ANSI.cursorUp(renderedLines));
|
|
210
|
+
for (let i = 0; i < renderedLines; i++) {
|
|
211
|
+
stdout.write(ANSI.clearLine + ANSI.cursorDown(1));
|
|
212
|
+
}
|
|
213
|
+
stdout.write(ANSI.cursorUp(renderedLines));
|
|
214
|
+
stdout.write(`${ANSI.green}ā${ANSI.reset} ${ANSI.bold}${prompt}${ANSI.reset} ${ANSI.cyan}${resultVals.length} selected${ANSI.reset}\n`);
|
|
215
|
+
return resultVals;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let redraw = false;
|
|
219
|
+
if (key === "\u001b[A" || key === "k") {
|
|
220
|
+
currentIdx = (currentIdx - 1 + options.length) % options.length;
|
|
221
|
+
redraw = true;
|
|
222
|
+
} else if (key === "\u001b[B" || key === "j") {
|
|
223
|
+
currentIdx = (currentIdx + 1) % options.length;
|
|
224
|
+
redraw = true;
|
|
225
|
+
} else if (key === " ") {
|
|
226
|
+
if (selections.has(currentIdx)) {
|
|
227
|
+
selections.delete(currentIdx);
|
|
228
|
+
} else {
|
|
229
|
+
selections.add(currentIdx);
|
|
230
|
+
}
|
|
231
|
+
redraw = true;
|
|
232
|
+
} else if (key === "a" || key === "A") {
|
|
233
|
+
options.forEach((_, idx) => selections.add(idx));
|
|
234
|
+
redraw = true;
|
|
235
|
+
} else if (key === "n" || key === "N") {
|
|
236
|
+
selections.clear();
|
|
237
|
+
redraw = true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (redraw) {
|
|
241
|
+
stdout.write(ANSI.cursorUp(renderedLines));
|
|
242
|
+
stdout.write(render());
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} finally {
|
|
246
|
+
stdout.write(ANSI.showCursor);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Text input prompt with validation.
|
|
252
|
+
* @param {string} prompt
|
|
253
|
+
* @param {object} [opts]
|
|
254
|
+
* @returns {Promise<string>}
|
|
255
|
+
*/
|
|
256
|
+
export async function input(prompt, opts = {}) {
|
|
257
|
+
const stdin = opts.stdin || process.stdin;
|
|
258
|
+
const stdout = opts.stdout || process.stdout;
|
|
259
|
+
const defaultValue = opts.defaultValue ?? "";
|
|
260
|
+
const validate = opts.validate || (() => true);
|
|
261
|
+
|
|
262
|
+
if (!isTTY(stdin)) {
|
|
263
|
+
if (opts.fallbackValue !== undefined) {
|
|
264
|
+
return opts.fallbackValue;
|
|
265
|
+
}
|
|
266
|
+
stdout.write(`[Headless Input] ${prompt}: Auto-selected default [${defaultValue}]\n`);
|
|
267
|
+
return defaultValue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
while (true) {
|
|
274
|
+
const defaultHint = defaultValue ? ` ${ANSI.dim}(${defaultValue})${ANSI.reset}` : "";
|
|
275
|
+
const query = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${prompt}${defaultHint}: `;
|
|
276
|
+
const rawAns = await rl.question(query);
|
|
277
|
+
const ans = rawAns.trim() || defaultValue;
|
|
278
|
+
|
|
279
|
+
const validRes = await validate(ans);
|
|
280
|
+
if (validRes === true) {
|
|
281
|
+
return ans;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const errMsg = typeof validRes === "string" ? validRes : "Invalid input";
|
|
285
|
+
stdout.write(`${ANSI.red}ā ${errMsg}${ANSI.reset}\n`);
|
|
286
|
+
}
|
|
287
|
+
} finally {
|
|
288
|
+
rl.close();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Confirmation prompt (Y/n).
|
|
294
|
+
* @param {string} prompt
|
|
295
|
+
* @param {boolean} [defaultVal=true]
|
|
296
|
+
* @param {object} [opts]
|
|
297
|
+
* @returns {Promise<boolean>}
|
|
298
|
+
*/
|
|
299
|
+
export async function confirm(prompt, defaultVal = true, opts = {}) {
|
|
300
|
+
const stdin = opts.stdin || process.stdin;
|
|
301
|
+
const stdout = opts.stdout || process.stdout;
|
|
302
|
+
|
|
303
|
+
if (!isTTY(stdin)) {
|
|
304
|
+
const val = opts.fallbackValue ?? defaultVal;
|
|
305
|
+
stdout.write(`[Headless Confirm] ${prompt}: Auto-selected [${val ? "Yes" : "No"}]\n`);
|
|
306
|
+
return val;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const hint = defaultVal ? "[Y/n]" : "[y/N]";
|
|
310
|
+
const ans = await input(`${prompt} ${ANSI.dim}${hint}${ANSI.reset}`, {
|
|
311
|
+
defaultValue: defaultVal ? "y" : "n",
|
|
312
|
+
stdin,
|
|
313
|
+
stdout,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return /^y(es)?$/i.test(ans.trim());
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Password / secret input prompt.
|
|
321
|
+
* @param {string} prompt
|
|
322
|
+
* @param {object} [opts]
|
|
323
|
+
* @returns {Promise<string>}
|
|
324
|
+
*/
|
|
325
|
+
export async function secretInput(prompt, opts = {}) {
|
|
326
|
+
const stdin = opts.stdin || process.stdin;
|
|
327
|
+
const stdout = opts.stdout || process.stdout;
|
|
328
|
+
|
|
329
|
+
if (!isTTY(stdin)) {
|
|
330
|
+
return opts.fallbackValue || "";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
stdout.write(`${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${prompt}:${ANSI.reset} `);
|
|
334
|
+
stdout.write(ANSI.hideCursor);
|
|
335
|
+
|
|
336
|
+
let inputStr = "";
|
|
337
|
+
try {
|
|
338
|
+
for await (const key of readKeypresses(stdin)) {
|
|
339
|
+
if (key === "\r" || key === "\n") {
|
|
340
|
+
stdout.write("\n");
|
|
341
|
+
return inputStr;
|
|
342
|
+
}
|
|
343
|
+
if (key === "\u007f" || key === "\b") {
|
|
344
|
+
// Backspace
|
|
345
|
+
if (inputStr.length > 0) {
|
|
346
|
+
inputStr = inputStr.slice(0, -1);
|
|
347
|
+
stdout.write("\b \b");
|
|
348
|
+
}
|
|
349
|
+
} else if (key.length === 1 && key >= " ") {
|
|
350
|
+
inputStr += key;
|
|
351
|
+
stdout.write("*");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} finally {
|
|
355
|
+
stdout.write(ANSI.showCursor);
|
|
356
|
+
}
|
|
357
|
+
return inputStr;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Terminal activity spinner indicator.
|
|
362
|
+
* @param {string} label
|
|
363
|
+
* @param {object} [opts]
|
|
364
|
+
* @returns {{ stop: (msg?: string) => void, fail: (msg?: string) => void }}
|
|
365
|
+
*/
|
|
366
|
+
export function spinner(label, opts = {}) {
|
|
367
|
+
const stdout = opts.stdout || process.stdout;
|
|
368
|
+
const frames = ["ā ", "ā ", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ", "ā "];
|
|
369
|
+
let frameIdx = 0;
|
|
370
|
+
let timer = null;
|
|
371
|
+
|
|
372
|
+
if (isTTY(stdout)) {
|
|
373
|
+
stdout.write(ANSI.hideCursor);
|
|
374
|
+
stdout.write(`${ANSI.cyan}${frames[0]}${ANSI.reset} ${label}`);
|
|
375
|
+
|
|
376
|
+
timer = setInterval(() => {
|
|
377
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
378
|
+
stdout.write(ANSI.clearLine);
|
|
379
|
+
stdout.write(`${ANSI.cyan}${frames[frameIdx]}${ANSI.reset} ${label}`);
|
|
380
|
+
}, 80);
|
|
381
|
+
} else {
|
|
382
|
+
stdout.write(`[Starting] ${label}...\n`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
stop(successMsg = `${label} complete`) {
|
|
387
|
+
if (timer) clearInterval(timer);
|
|
388
|
+
if (isTTY(stdout)) {
|
|
389
|
+
stdout.write(ANSI.clearLine);
|
|
390
|
+
stdout.write(`${ANSI.green}ā${ANSI.reset} ${successMsg}\n`);
|
|
391
|
+
stdout.write(ANSI.showCursor);
|
|
392
|
+
} else {
|
|
393
|
+
stdout.write(`[Completed] ${successMsg}\n`);
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
fail(errorMsg = `${label} failed`) {
|
|
397
|
+
if (timer) clearInterval(timer);
|
|
398
|
+
if (isTTY(stdout)) {
|
|
399
|
+
stdout.write(ANSI.clearLine);
|
|
400
|
+
stdout.write(`${ANSI.red}ā${ANSI.reset} ${errorMsg}\n`);
|
|
401
|
+
stdout.write(ANSI.showCursor);
|
|
402
|
+
} else {
|
|
403
|
+
stdout.write(`[Failed] ${errorMsg}\n`);
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseYaml } from "./config.mjs";
|
|
4
|
+
import { detectStackOracles, runVerificationProbe } from "./wizard-oracle.mjs";
|
|
5
|
+
import { select, multiSelect, input, confirm, spinner, isTTY } from "./tui.mjs";
|
|
6
|
+
|
|
7
|
+
export const TIER_PROFILES = {
|
|
8
|
+
free: {
|
|
9
|
+
concurrency: 1,
|
|
10
|
+
daily_tasks: 30,
|
|
11
|
+
stagger_ms: 3000,
|
|
12
|
+
diff_kb: 50,
|
|
13
|
+
},
|
|
14
|
+
pro: {
|
|
15
|
+
concurrency: 3,
|
|
16
|
+
daily_tasks: 300,
|
|
17
|
+
stagger_ms: 1500,
|
|
18
|
+
diff_kb: 75,
|
|
19
|
+
},
|
|
20
|
+
enterprise: {
|
|
21
|
+
concurrency: 10,
|
|
22
|
+
daily_tasks: 1000,
|
|
23
|
+
stagger_ms: 500,
|
|
24
|
+
diff_kb: 100,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const BUILTIN_PRESETS = [
|
|
29
|
+
{
|
|
30
|
+
id: "nightly-security-audit",
|
|
31
|
+
title: "Nightly Security & Vulnerability Audit",
|
|
32
|
+
description: "Scans codebase for hardcoded secrets, TODO annotations, and outdated dependencies.",
|
|
33
|
+
cron: "0 2 * * *",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "flaky-test-quarantine",
|
|
37
|
+
title: "Statistical Flaky Test Quarantine",
|
|
38
|
+
description: "Monitors test execution stability via Wilson score interval and isolates flaky tests.",
|
|
39
|
+
cron: "*/30 * * * *",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "multi-agent-refactor-swarm",
|
|
43
|
+
title: "Multi-Agent Parallel Refactor Swarm",
|
|
44
|
+
description: "Splits refactoring tasks across isolated workspace file boundaries.",
|
|
45
|
+
cron: "manual",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "doc-sync-sentinel",
|
|
49
|
+
title: "AGENTS.md & API Doc Sync Sentinel",
|
|
50
|
+
description: "Validates documentation integrity against actual exported symbol signatures.",
|
|
51
|
+
cron: "0 9 * * 1",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a deterministic onboarding plan for repository configuration.
|
|
57
|
+
* @param {string} [root=process.cwd()]
|
|
58
|
+
* @param {object} [options]
|
|
59
|
+
* @returns {{
|
|
60
|
+
* stack: string,
|
|
61
|
+
* tier: string,
|
|
62
|
+
* verify: { test: string, build: string, lint: string, typecheck: string },
|
|
63
|
+
* limits: { concurrency: number, daily_tasks: number, stagger_ms: number, diff_kb: number },
|
|
64
|
+
* presets: Array<string>,
|
|
65
|
+
* configYaml: string,
|
|
66
|
+
* julesYaml: string
|
|
67
|
+
* }}
|
|
68
|
+
*/
|
|
69
|
+
export function planInit(root = process.cwd(), options = {}) {
|
|
70
|
+
const oracle = detectStackOracles(root);
|
|
71
|
+
const tierName = options.tier || "pro";
|
|
72
|
+
const limits = TIER_PROFILES[tierName] || TIER_PROFILES.pro;
|
|
73
|
+
|
|
74
|
+
const verify = {
|
|
75
|
+
test: options.testCmd || oracle.candidates.testCmd || "",
|
|
76
|
+
build: options.buildCmd || oracle.candidates.buildCmd || "",
|
|
77
|
+
lint: options.lintCmd || oracle.candidates.lintCmd || "",
|
|
78
|
+
typecheck: options.typecheckCmd || oracle.candidates.typecheckCmd || "",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const selectedPresets = options.presets || ["nightly-security-audit", "flaky-test-quarantine"];
|
|
82
|
+
|
|
83
|
+
const configYaml = `# Google Jules Orchestrator Kit Config (v0.29.0)
|
|
84
|
+
version: 1
|
|
85
|
+
provider: jules
|
|
86
|
+
tier: ${tierName}
|
|
87
|
+
base_branch: ${options.baseBranch || "main"}
|
|
88
|
+
branch_prefix: agent/
|
|
89
|
+
|
|
90
|
+
limits:
|
|
91
|
+
concurrency: ${limits.concurrency}
|
|
92
|
+
daily_tasks: ${limits.daily_tasks}
|
|
93
|
+
stagger_ms: ${limits.stagger_ms}
|
|
94
|
+
diff_kb: ${limits.diff_kb}
|
|
95
|
+
|
|
96
|
+
verify:
|
|
97
|
+
test: "${verify.test}"
|
|
98
|
+
build: "${verify.build}"
|
|
99
|
+
lint: "${verify.lint}"
|
|
100
|
+
typecheck: "${verify.typecheck}"
|
|
101
|
+
|
|
102
|
+
presets:
|
|
103
|
+
${selectedPresets.map((p) => ` - ${p}`).join("\n")}
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
const julesYaml = `# Google Jules Agent Compatibility Manifest
|
|
107
|
+
version: 1
|
|
108
|
+
test_cmd: "${verify.test}"
|
|
109
|
+
build_cmd: "${verify.build}"
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
stack: oracle.stack,
|
|
114
|
+
tier: tierName,
|
|
115
|
+
verify,
|
|
116
|
+
limits,
|
|
117
|
+
presets: selectedPresets,
|
|
118
|
+
configYaml,
|
|
119
|
+
julesYaml,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load declarative presets from .agent/presets/*.yml or fallback to built-ins.
|
|
125
|
+
* @param {string} [root=process.cwd()]
|
|
126
|
+
* @returns {Array<{ id: string, title: string, description: string, cron: string }>}
|
|
127
|
+
*/
|
|
128
|
+
export function loadPresets(root = process.cwd()) {
|
|
129
|
+
const presetsDir = join(root, ".agent", "presets");
|
|
130
|
+
const presets = [...BUILTIN_PRESETS];
|
|
131
|
+
|
|
132
|
+
if (existsSync(presetsDir)) {
|
|
133
|
+
try {
|
|
134
|
+
const files = readdirSync(presetsDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
135
|
+
files.forEach((f) => {
|
|
136
|
+
try {
|
|
137
|
+
const content = readFileSync(join(presetsDir, f), "utf-8");
|
|
138
|
+
const parsed = parseYaml(content);
|
|
139
|
+
if (parsed && parsed.id && parsed.title) {
|
|
140
|
+
presets.push({
|
|
141
|
+
id: parsed.id,
|
|
142
|
+
title: parsed.title,
|
|
143
|
+
description: parsed.description || "",
|
|
144
|
+
cron: parsed.cron || "manual",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} catch (_) {}
|
|
148
|
+
});
|
|
149
|
+
} catch (_) {}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return presets;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Run interactive or headless onboarding wizard.
|
|
157
|
+
* @param {string} [root=process.cwd()]
|
|
158
|
+
* @param {object} [options]
|
|
159
|
+
* @returns {Promise<{ ok: boolean, configPath: string, plan: object }>}
|
|
160
|
+
*/
|
|
161
|
+
export async function runInitWizard(root = process.cwd(), options = {}) {
|
|
162
|
+
const interactive = options.interactive !== false && isTTY(options.stdin || process.stdin);
|
|
163
|
+
|
|
164
|
+
let selectedTier = options.tier || "pro";
|
|
165
|
+
let testCmd = options.testCmd;
|
|
166
|
+
let buildCmd = options.buildCmd;
|
|
167
|
+
let selectedPresets = options.presets;
|
|
168
|
+
|
|
169
|
+
if (interactive) {
|
|
170
|
+
const oracle = detectStackOracles(root);
|
|
171
|
+
const sp = spinner("Inspecting repository stack and verification oracles", options);
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
173
|
+
sp.stop(`Detected Stack: ${oracle.stack}`);
|
|
174
|
+
|
|
175
|
+
selectedTier = await select(
|
|
176
|
+
[
|
|
177
|
+
{ label: "Pro Tier (Recommended)", value: "pro", description: "3 parallel workers, 300 daily tasks, 75 KB diff limit" },
|
|
178
|
+
{ label: "Free / Individual Tier", value: "free", description: "1 worker, 30 daily tasks, 50 KB diff limit" },
|
|
179
|
+
{ label: "Custom Enterprise", value: "enterprise", description: "10 workers, 1000 daily tasks, 100 KB diff limit" },
|
|
180
|
+
],
|
|
181
|
+
"Select Jules Orchestrator Usage Tier",
|
|
182
|
+
options
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
testCmd = await input("Verification Test Command", {
|
|
186
|
+
defaultValue: oracle.candidates.testCmd || "npm test",
|
|
187
|
+
stdin: options.stdin,
|
|
188
|
+
stdout: options.stdout,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
buildCmd = await input("Verification Build Command", {
|
|
192
|
+
defaultValue: oracle.candidates.buildCmd || "",
|
|
193
|
+
stdin: options.stdin,
|
|
194
|
+
stdout: options.stdout,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const presetOptions = BUILTIN_PRESETS.map((p) => ({
|
|
198
|
+
label: p.title,
|
|
199
|
+
value: p.id,
|
|
200
|
+
checked: true,
|
|
201
|
+
description: p.description,
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
selectedPresets = await multiSelect(presetOptions, "Select Autonomous Workflows to Enable", options);
|
|
205
|
+
|
|
206
|
+
const shouldProbe = await confirm("Run verification probe on test command before saving?", true, options);
|
|
207
|
+
if (shouldProbe && testCmd) {
|
|
208
|
+
const probeSp = spinner(`Probing oracle: ${testCmd}`, options);
|
|
209
|
+
const probeRes = await runVerificationProbe(testCmd, root);
|
|
210
|
+
if (probeRes.ok) {
|
|
211
|
+
probeSp.stop(`Oracle verified successfully (${probeRes.durationMs}ms)`);
|
|
212
|
+
} else {
|
|
213
|
+
probeSp.fail(`Oracle verification probe failed (Exit ${probeRes.code})`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const plan = planInit(root, {
|
|
219
|
+
tier: selectedTier,
|
|
220
|
+
testCmd,
|
|
221
|
+
buildCmd,
|
|
222
|
+
presets: selectedPresets,
|
|
223
|
+
...options,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Atomic write plan
|
|
227
|
+
const agentDir = join(root, ".agent");
|
|
228
|
+
if (!existsSync(agentDir)) {
|
|
229
|
+
mkdirSync(agentDir, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const configPath = join(agentDir, "config.yml");
|
|
233
|
+
const julesPath = join(agentDir, "jules.yml");
|
|
234
|
+
|
|
235
|
+
writeFileSync(configPath, plan.configYaml, "utf-8");
|
|
236
|
+
writeFileSync(julesPath, plan.julesYaml, "utf-8");
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
configPath,
|
|
241
|
+
plan,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { detectStack, parseYaml } from "./config.mjs";
|
|
4
|
+
import { runCmd } from "./git.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inspect repository topology, workspace structure, and manifests to infer candidate verification oracles.
|
|
8
|
+
* @param {string} [root=process.cwd()]
|
|
9
|
+
* @returns {{
|
|
10
|
+
* stack: string,
|
|
11
|
+
* isMonorepo: boolean,
|
|
12
|
+
* packages: Array<{ path: string, name: string, stack: string }>,
|
|
13
|
+
* candidates: {
|
|
14
|
+
* testCmd: string,
|
|
15
|
+
* buildCmd: string,
|
|
16
|
+
* lintCmd: string,
|
|
17
|
+
* typecheckCmd: string
|
|
18
|
+
* }
|
|
19
|
+
* }}
|
|
20
|
+
*/
|
|
21
|
+
export function detectStackOracles(root = process.cwd()) {
|
|
22
|
+
const detected = detectStack(root);
|
|
23
|
+
const stack = detected.stack || "node";
|
|
24
|
+
|
|
25
|
+
const candidates = {
|
|
26
|
+
testCmd: detected.testCmd || "",
|
|
27
|
+
buildCmd: detected.buildCmd || "",
|
|
28
|
+
lintCmd: "",
|
|
29
|
+
typecheckCmd: "",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const isMonorepo =
|
|
33
|
+
existsSync(join(root, "turbo.json")) ||
|
|
34
|
+
existsSync(join(root, "pnpm-workspace.yaml")) ||
|
|
35
|
+
existsSync(join(root, "lerna.json")) ||
|
|
36
|
+
existsSync(join(root, "nx.json"));
|
|
37
|
+
|
|
38
|
+
const packages = [];
|
|
39
|
+
|
|
40
|
+
// Check Node.json package.json scripts
|
|
41
|
+
const pkgPath = join(root, "package.json");
|
|
42
|
+
if (existsSync(pkgPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
45
|
+
const scripts = pkg.scripts || {};
|
|
46
|
+
|
|
47
|
+
if (scripts.test) candidates.testCmd = candidates.testCmd || "npm test";
|
|
48
|
+
if (scripts.build) candidates.buildCmd = candidates.buildCmd || "npm run build";
|
|
49
|
+
if (scripts.lint) candidates.lintCmd = "npm run lint";
|
|
50
|
+
if (scripts.typecheck || scripts["type-check"]) {
|
|
51
|
+
candidates.typecheckCmd = scripts.typecheck ? "npm run typecheck" : "npm run type-check";
|
|
52
|
+
} else if (existsSync(join(root, "tsconfig.json"))) {
|
|
53
|
+
candidates.typecheckCmd = "npx tsc --noEmit";
|
|
54
|
+
}
|
|
55
|
+
} catch (_) {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check Cargo workspace
|
|
59
|
+
if (stack.includes("cargo") || existsSync(join(root, "Cargo.toml"))) {
|
|
60
|
+
const isCargoWs = existsSync(join(root, "Cargo.toml")) && readFileSync(join(root, "Cargo.toml"), "utf-8").includes("[workspace]");
|
|
61
|
+
const flag = isCargoWs ? " --workspace" : "";
|
|
62
|
+
candidates.testCmd = `cargo test${flag}`;
|
|
63
|
+
candidates.buildCmd = `cargo build${flag}`;
|
|
64
|
+
candidates.lintCmd = `cargo clippy${flag} -- -D warnings`;
|
|
65
|
+
candidates.typecheckCmd = `cargo check${flag}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check Go module
|
|
69
|
+
if (stack.includes("go") || existsSync(join(root, "go.mod"))) {
|
|
70
|
+
candidates.testCmd = candidates.testCmd || "go test ./...";
|
|
71
|
+
candidates.buildCmd = candidates.buildCmd || "go build ./...";
|
|
72
|
+
candidates.lintCmd = candidates.lintCmd || "golangci-lint run";
|
|
73
|
+
candidates.typecheckCmd = candidates.typecheckCmd || "go vet ./...";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check Python
|
|
77
|
+
if (stack.includes("pytest") || existsSync(join(root, "pyproject.toml")) || existsSync(join(root, "requirements.txt"))) {
|
|
78
|
+
candidates.testCmd = candidates.testCmd || "pytest";
|
|
79
|
+
candidates.buildCmd = candidates.buildCmd || "";
|
|
80
|
+
candidates.lintCmd = candidates.lintCmd || "flake8 .";
|
|
81
|
+
candidates.typecheckCmd = candidates.typecheckCmd || "mypy .";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Discover subpackages if monorepo
|
|
85
|
+
if (isMonorepo) {
|
|
86
|
+
if (existsSync(join(root, "pnpm-workspace.yaml"))) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = parseYaml(readFileSync(join(root, "pnpm-workspace.yaml"), "utf-8"));
|
|
89
|
+
const globs = parsed?.packages || ["packages/*"];
|
|
90
|
+
globs.forEach((pattern) => {
|
|
91
|
+
const dirBase = pattern.replace(/\/\*$/, "");
|
|
92
|
+
const fullDir = join(root, dirBase);
|
|
93
|
+
if (existsSync(fullDir)) {
|
|
94
|
+
const children = readdirSync(fullDir);
|
|
95
|
+
children.forEach((child) => {
|
|
96
|
+
const childPath = join(fullDir, child);
|
|
97
|
+
if (existsSync(join(childPath, "package.json"))) {
|
|
98
|
+
packages.push({
|
|
99
|
+
path: join(dirBase, child),
|
|
100
|
+
name: child,
|
|
101
|
+
stack: "node",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
} catch (_) {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
stack,
|
|
113
|
+
isMonorepo,
|
|
114
|
+
packages,
|
|
115
|
+
candidates,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Execute a candidate verification command to prove oracle validity.
|
|
121
|
+
* @param {string} cmd
|
|
122
|
+
* @param {string} [cwd=process.cwd()]
|
|
123
|
+
* @param {object} [options]
|
|
124
|
+
* @returns {Promise<{ ok: boolean, code: number, stdout: string, stderr: string, durationMs: number }>}
|
|
125
|
+
*/
|
|
126
|
+
export async function runVerificationProbe(cmd, cwd = process.cwd(), options = {}) {
|
|
127
|
+
const start = Date.now();
|
|
128
|
+
const timeoutMs = options.timeoutMs || 30_000;
|
|
129
|
+
|
|
130
|
+
if (!cmd || !cmd.trim()) {
|
|
131
|
+
return { ok: true, code: 0, stdout: "", stderr: "Empty oracle command (skipped)", durationMs: 0 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const res = runCmd(cmd, { cwd, timeout: timeoutMs });
|
|
136
|
+
const durationMs = Date.now() - start;
|
|
137
|
+
const ok = res.status === 0;
|
|
138
|
+
return {
|
|
139
|
+
ok,
|
|
140
|
+
code: res.status ?? (ok ? 0 : 1),
|
|
141
|
+
stdout: res.stdout || "",
|
|
142
|
+
stderr: res.stderr || "",
|
|
143
|
+
durationMs,
|
|
144
|
+
};
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const durationMs = Date.now() - start;
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
code: 1,
|
|
150
|
+
stdout: "",
|
|
151
|
+
stderr: err.message,
|
|
152
|
+
durationMs,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { loadConfig } from "./config.mjs";
|
|
4
|
+
import { gate } from "./engine.mjs";
|
|
5
|
+
import { scanDiff, shannonEntropy } from "./security.mjs";
|
|
6
|
+
import { scanCodebaseForTodos } from "../scripts/jules-scan-todos.mjs";
|
|
7
|
+
import { select, input, confirm, spinner, isTTY } from "./tui.mjs";
|
|
8
|
+
|
|
9
|
+
export const GUARDRAIL_FOOTER = `
|
|
10
|
+
---
|
|
11
|
+
HARD CONSTRAINTS:
|
|
12
|
+
- Do NOT modify package.json, pnpm-lock.yaml, tsconfig.json, or .github/ files.
|
|
13
|
+
- Diff Payload Governor: Keep total diff payload under 75 KB (\`git diff | wc -c\`).
|
|
14
|
+
- Falsifiable & Evidence-Based: Attach full terminal verification output to PR. Never weaken assertions or delete failing tests to force a pass.
|
|
15
|
+
- Read-Before-Write: Inspect existing symbol signatures, definitions, and call sites before making edits.
|
|
16
|
+
- BEFORE opening the PR: Run \`git fetch origin main && git rebase origin/main\`, then re-verify.
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pure planning core for task creation & envelope synthesis.
|
|
21
|
+
* @param {string} root
|
|
22
|
+
* @param {object} inputObj
|
|
23
|
+
* @returns {{
|
|
24
|
+
* ok: boolean,
|
|
25
|
+
* taskId: string,
|
|
26
|
+
* title: string,
|
|
27
|
+
* prompt: string,
|
|
28
|
+
* fullPrompt: string,
|
|
29
|
+
* verifyCmd: string,
|
|
30
|
+
* flags: { autoPr: boolean, requirePlanApproval: boolean, repoless: boolean, startingBranch: string },
|
|
31
|
+
* secretFindings: Array<any>,
|
|
32
|
+
* gateResult: object,
|
|
33
|
+
* taskFileContent: string
|
|
34
|
+
* }}
|
|
35
|
+
*/
|
|
36
|
+
export function planTaskCreate(root = process.cwd(), inputObj = {}) {
|
|
37
|
+
const config = loadConfig(root);
|
|
38
|
+
|
|
39
|
+
const title = inputObj.title || "Agent Task";
|
|
40
|
+
const rawPrompt = inputObj.prompt || "";
|
|
41
|
+
const verifyCmd = inputObj.verifyCmd || config.verify.test || config.verify.build || "";
|
|
42
|
+
|
|
43
|
+
// 1. Falsifiability check
|
|
44
|
+
if (!rawPrompt.trim()) {
|
|
45
|
+
throw new Error("Task prompt cannot be empty.");
|
|
46
|
+
}
|
|
47
|
+
if (!verifyCmd && !inputObj.allowUnverifiable) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"Unfalsifiable Task Rejected: Task must include a concrete verification test/build command. Configure verify.test in .agent/config.yml or pass --verify-cmd."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Secret Scrubbing Preflight
|
|
54
|
+
const secretFindings = [];
|
|
55
|
+
const secretScan = scanDiff(`+${rawPrompt}`);
|
|
56
|
+
if (!secretScan.ok) {
|
|
57
|
+
secretScan.findings.forEach((f) => secretFindings.push(f));
|
|
58
|
+
}
|
|
59
|
+
if (shannonEntropy(rawPrompt) > 4.5 && !inputObj.allowHighEntropy) {
|
|
60
|
+
secretFindings.push({ id: "HIGH_ENTROPY_PROMPT", line: 1 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (secretFindings.length > 0 && !inputObj.allowSecrets) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Pre-Dispatch Secret Leak Blocked: Prompt contains potential secrets or credentials (${secretFindings.length} finding(s)). Scrub keys before dispatching.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. Jules v1alpha Flags
|
|
70
|
+
const flags = {
|
|
71
|
+
autoPr: Boolean(inputObj.autoPr),
|
|
72
|
+
requirePlanApproval: Boolean(inputObj.requirePlanApproval),
|
|
73
|
+
repoless: Boolean(inputObj.repoless),
|
|
74
|
+
startingBranch: inputObj.startingBranch || config.baseBranch || "main",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// 4. Prompt Envelope & Guardrail Footer Synthesis
|
|
78
|
+
const fullPrompt = `[TASK INSTRUCTIONS]
|
|
79
|
+
${rawPrompt}
|
|
80
|
+
|
|
81
|
+
[VERIFICATION ORACLE]
|
|
82
|
+
Test/Verification Command: ${verifyCmd || "(None)"}
|
|
83
|
+
|
|
84
|
+
${GUARDRAIL_FOOTER}`;
|
|
85
|
+
|
|
86
|
+
const taskId = inputObj.id || `TASK-${Date.now().toString(36).toUpperCase()}`;
|
|
87
|
+
|
|
88
|
+
const taskFileContent = `# ${title}
|
|
89
|
+
# Task ID: ${taskId}
|
|
90
|
+
# Auto-PR: ${flags.autoPr} | Plan Approval: ${flags.requirePlanApproval} | Repoless: ${flags.repoless}
|
|
91
|
+
|
|
92
|
+
${fullPrompt}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
taskId,
|
|
98
|
+
title,
|
|
99
|
+
prompt: rawPrompt,
|
|
100
|
+
fullPrompt,
|
|
101
|
+
verifyCmd,
|
|
102
|
+
flags,
|
|
103
|
+
secretFindings,
|
|
104
|
+
taskFileContent,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Run interactive or headless task creation wizard.
|
|
110
|
+
* @param {string} [root=process.cwd()]
|
|
111
|
+
* @param {object} [options]
|
|
112
|
+
* @returns {Promise<{ ok: boolean, taskFile: string, plan: object }>}
|
|
113
|
+
*/
|
|
114
|
+
export async function runTaskCreateWizard(root = process.cwd(), options = {}) {
|
|
115
|
+
const interactive = options.interactive !== false && isTTY(options.stdin || process.stdin);
|
|
116
|
+
|
|
117
|
+
let title = options.title;
|
|
118
|
+
let promptText = options.prompt;
|
|
119
|
+
let verifyCmd = options.verifyCmd;
|
|
120
|
+
let autoPr = options.autoPr;
|
|
121
|
+
let requirePlanApproval = options.requirePlanApproval;
|
|
122
|
+
let repoless = options.repoless;
|
|
123
|
+
|
|
124
|
+
if (interactive) {
|
|
125
|
+
const sp = spinner("Analyzing codebase task candidates & gate policy", options);
|
|
126
|
+
const todos = scanCodebaseForTodos(root);
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
128
|
+
sp.stop(`Scanned ${todos.length} TODO/FIXME candidates`);
|
|
129
|
+
|
|
130
|
+
if (todos.length > 0 && !promptText) {
|
|
131
|
+
const useTodo = await confirm("Import prompt from scanned TODO candidate?", false, options);
|
|
132
|
+
if (useTodo) {
|
|
133
|
+
const selectedTodo = await select(
|
|
134
|
+
todos.slice(0, 10).map((t) => ({
|
|
135
|
+
label: `[${t.type}] ${t.text} (${t.file}:${t.line})`,
|
|
136
|
+
value: t,
|
|
137
|
+
})),
|
|
138
|
+
"Select TODO Candidate",
|
|
139
|
+
options
|
|
140
|
+
);
|
|
141
|
+
title = `Fix ${selectedTodo.type}: ${selectedTodo.text.slice(0, 40)}`;
|
|
142
|
+
promptText = `Resolve ${selectedTodo.type} in ${selectedTodo.file} at line ${selectedTodo.line}: ${selectedTodo.text}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
title = await input("Task Title", {
|
|
147
|
+
defaultValue: title || "Refactor Component Module",
|
|
148
|
+
stdin: options.stdin,
|
|
149
|
+
stdout: options.stdout,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
promptText = await input("Detailed Task Instructions", {
|
|
153
|
+
defaultValue: promptText || "Cleanly refactor module to improve readability and maintain full test coverage.",
|
|
154
|
+
stdin: options.stdin,
|
|
155
|
+
stdout: options.stdout,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
autoPr = await confirm("Enable Automatic PR Creation (automationMode: AUTO_CREATE_PR)?", true, options);
|
|
159
|
+
requirePlanApproval = await confirm("Require Plan Approval Gate before execution?", false, options);
|
|
160
|
+
repoless = await confirm("Dispatch in Repoless mode (no repo source context)?", false, options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const plan = planTaskCreate(root, {
|
|
164
|
+
title,
|
|
165
|
+
prompt: promptText,
|
|
166
|
+
verifyCmd,
|
|
167
|
+
autoPr,
|
|
168
|
+
requirePlanApproval,
|
|
169
|
+
repoless,
|
|
170
|
+
...options,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Perform Gate Preflight
|
|
174
|
+
const gateRes = await gate({ root, mode: "working-tree" });
|
|
175
|
+
if (!gateRes.ok && (gateRes.code === 3 || gateRes.code === 6) && !options.allowGateFailure) {
|
|
176
|
+
throw new Error(`Gate Preflight Rejected Task: Repository contains scope or secret violations (Exit ${gateRes.code}).`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Write Task File to .agent/queue/
|
|
180
|
+
const queueDir = join(root, ".agent", "queue");
|
|
181
|
+
if (!existsSync(queueDir)) {
|
|
182
|
+
mkdirSync(queueDir, { recursive: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const taskFile = join(queueDir, `${plan.taskId}.md`);
|
|
186
|
+
writeFileSync(taskFile, plan.taskFileContent, "utf-8");
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
taskFile,
|
|
191
|
+
plan,
|
|
192
|
+
};
|
|
193
|
+
}
|