jules-orchestrator-kit 0.28.2 β†’ 0.29.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/README.md CHANGED
@@ -332,19 +332,21 @@ Native stdio server exposing task dispatch, gate verification, and risk auditing
332
332
 
333
333
  | Command | Usage | Description | Exit Codes |
334
334
  | :--- | :--- | :--- | :--- |
335
+ | `init` | `agentctl init [--interactive] [--tier pro]` | Interactive onboarding wizard & stack oracle inspector generating `.agent/config.yml`. | `0` (Created) |
336
+ | `task create` | `agentctl task create [--title <t>] [--prompt <p>]` | Interactively authors & scopes falsifiable task envelopes with secret scrubbing & preflight gate checks. | `0` (Queued), `1` (Unfalsifiable / Secret leak) |
335
337
  | `dispatch` | `agentctl dispatch --title <t> --prompt <p>` | Dispatches a single task to an AI agent in an isolated worktree. | `0` (Success), `1` (Arg error), `2` (429 Rate limit), `3` (Scope deny), `4` (OODA exhausted), `5` (Diff > 75KB), `6` (Secret leak) |
338
+ | `scan` | `agentctl scan` | Scans codebase for TODO/FIXME annotations to seed task authoring. | `0` (Scanned) |
336
339
  | `review-repair`| `agentctl review-repair <pr-comments.json>`| Parses GitHub PR review comments and synthesizes actionable OODA repair tasks. | `0` (Parsed), `1` (Missing file) |
337
340
  | `dashboard` | `agentctl dashboard [port]` | Starts zero-dependency local HTTP telemetry and audit visualizer dashboard. | `0` (Running) |
338
- | `gate` / `audit`| `agentctl gate --base main --json` | Runs security, secret scanning, and verification gate against current branch. | `0` (Approved), `3` (Scope violation), `5` (Diff limit), `6` (Secret leak) |
341
+ | `gate` / `audit`| `agentctl gate --mode working-tree --json` | Runs security, secret scanning, and verification gate against working tree or branch. | `0` (Approved), `3` (Scope violation), `5` (Diff limit), `6` (Secret leak) |
339
342
  | `bootstrap` | `agentctl bootstrap [--force] [--json]` | Inspects an untested repository and synthesizes `.agent/config.yml` with a zero-test verification oracle (`php -l`, `compileall`, `dotnet build`, `tsc`, `smoke`). | `0` (Bootstrapped / Existing) |
340
343
  | `queue` | `agentctl queue` | Consumes and executes pending markdown task envelopes in `.agent/queue/`. | `0` (Complete) |
341
344
  | `swarm` | `agentctl swarm` | Runs parallel multi-agent swarm across queued tasks with token-bucket concurrency. | `0` (Complete) |
342
345
  | `doctor` | `agentctl doctor` | Diagnostic inspect: displays detected stack, container wrapper, test command, and daily session budget. | `0` (Healthy) |
343
346
  | `lock` | `agentctl lock <acquire\|release\|status>`| Manages VFS mutex locks for multi-agent non-overlapping file ownership. | `0` (Locked/Released), `1` (Conflict) |
344
347
  | `clean` | `agentctl clean` | Prunes stale git worktrees, lockfiles, and temporary ledgers. | `0` (Clean) |
345
- | `init` | `agentctl init` | Scaffolds `.agent/` directory structure and default `.agent/config.yml`. | `0` (Created) |
346
348
  | `mcp` | `agentctl mcp` | Starts stdio Model Context Protocol (MCP) server for tool integration. | `0` / Stdio stream |
347
- | `version` | `agentctl version` | Outputs orchestrator kit semantic version (`v0.27.0`). | `0` |
349
+ | `version` | `agentctl version` | Outputs orchestrator kit semantic version (`v0.29.0`). | `0` |
348
350
 
349
351
  <br/>
350
352
 
@@ -353,14 +355,15 @@ Native stdio server exposing task dispatch, gate verification, and risk auditing
353
355
  <br/>
354
356
 
355
357
  <a id="roadmap"></a>
356
- ## πŸ—ΊοΈ v0.27+ Next-Gen Feature Roadmap
358
+ ## πŸ—ΊοΈ v0.29+ Feature Roadmap
357
359
 
358
360
  | Feature | Module / Command | Architectural Blueprint | Target Release |
359
361
  | :--- | :--- | :--- | :---: |
362
+ | **Onboarding & Stack Oracle Wizard** | `agentctl init --interactive` (`src/wizard-init.mjs`) | Zero-dependency interactive CLI wizard auto-detecting verification oracles, quota tiers, and preset workflows. | **v0.29.0** *(Shipped)* |
363
+ | **Guided Task Authoring Subsystem** | `agentctl task create` (`src/wizard-task.mjs`) | Guided task authoring with TODO candidate harvesting, Shannon entropy secret scrubbing, and guardrail footer synthesis. | **v0.29.0** *(Shipped)* |
360
364
  | **PR Review Auto-Remediation Loop** | `agentctl review-repair` (`src/review-repair.mjs`) | Ingests GitHub PR review comments (`CHANGES_REQUESTED`), extracts line/file context, and dispatches automated OODA repair turns until reviewer comments are resolved. | **v0.27.0** *(Shipped)* |
361
365
  | **Multi-Provider Failover Router** | `createFailoverProvider` (`src/provider.mjs`) | Ordered router (`["jules", "claude-code", "local-mcp"]`) that seamlessly falls back to secondary LLMs on HTTP 429 rate limits or 5xx service unavailability. | **v0.27.0** *(Shipped)* |
362
366
  | **Telemetry & Audit Web Dashboard**| `agentctl dashboard` (`src/dashboard.mjs`) | Zero-dependency local HTTP server displaying real-time DAG execution graphs, Wilson-Score flaky test ledgers, and SHA-256 telemetry chains. | **v0.27.0** *(Shipped)* |
363
- | **Cross-Language Contract Guard** | `hashCrossLanguageInterface` (`src/merge-blocks.mjs`)| Canonical SHA-256 schema hashing for OpenAPI/Protobuf specs across polyglot task dependencies in `DagExecutor`. | **v0.26.0** *(Shipped)* |
364
367
 
365
368
  <br/>
366
369
 
@@ -371,11 +374,10 @@ Native stdio server exposing task dispatch, gate verification, and risk auditing
371
374
  ## πŸ“– Recipes, Documentation & Prior Art
372
375
 
373
376
  - [**Google Jules Official Documentation**](https://jules.google) β€” Official platform overview and API specifications for Google Jules.
374
- - [**Google Labs Code Repositories**](https://github.com/google-labs-code) β€” Official Google Labs public GitHub organization (jules-action, jules-sdk).
377
+ - [**Onboarding & Task Wizard Specification**](./docs/ONBOARDING_AND_TASK_WIZARD_SPEC.md) β€” Normative architecture specification for zero-dependency TUI, stack oracle, and guided task authoring.
375
378
  - [**Universal Polyglot Architecture & Zero-Test Specification**](./docs/UNIVERSAL_POLYGLOT_ARCHITECTURE.md) β€” Comprehensive technical report on boundary resolution, OODA math, and B2B workflows.
376
379
  - [**v0.27.0 Architectural Audit & Platform Evolution**](./docs/V0.27_ARCHITECTURAL_AUDIT_AND_EVOLUTION.md) β€” PR review remediation, failover router, and local dashboard specs.
377
380
  - [**Examples & Task Envelope Recipes**](./EXAMPLES.md) β€” Production YAML and Markdown task envelopes.
378
- - [**Adversarial Security Audit Phase 4 Report**](./docs/AUDIT_REPORT.md) β€” CWE-77, CWE-1321, and CWE-183 security hardening analysis.
379
381
  - [**Changelog**](./CHANGELOG.md) β€” Full release history and migration guides.
380
382
 
381
383
  <br/>
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, writeFileSync, existsSync, readdirSync } from "node:fs";
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, ensureDir } from "../src/state.mjs";
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.28.2 β€” Universal Agent Orchestrator & Safety Gatekeeper
17
+ πŸš€ agentctl v0.29.1 β€” 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/ directory and config.yml
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.28.2");
57
+ console.log("agentctl v0.29.1");
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.28.2)`);
268
+ console.log(`\nπŸ” agentctl System Diagnostics (v0.29.1)`);
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 agentDir = join(root, ".agent");
330
- ensureDir(agentDir);
331
- const configPath = join(agentDir, "config.yml");
332
- if (!existsSync(configPath)) {
333
- writeFileSync(
334
- configPath,
335
- `version: 1
336
- provider: jules
337
- limits:
338
- diff_kb: 75
339
- daily_tasks: 300
340
- branch_prefix: agent/
341
- base_branch: main
342
- `,
343
- "utf-8"
344
- );
345
- console.log(`βœ… Created .agent/config.yml`);
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(`ℹ️ .agent/config.yml already exists.`);
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.28.2)`);
404
+ console.log(`\nπŸ“Š agentctl Status Summary (v0.29.1)`);
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.28.2)
2
+ * Google Jules Orchestrator Kit - Node.js SDK (v0.29.1)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.28.2",
3
+ "version": "0.29.1",
4
4
  "description": "Orchestration kit for running Google Jules autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
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.28.2)</h1>
35
+ <h1>πŸš€ jules-orchestrator-kit Dashboard (v0.29.1)</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.28.2", root, ts: new Date().toISOString() }));
90
+ return res.end(JSON.stringify({ ok: true, version: "0.29.1", root, ts: new Date().toISOString() }));
91
91
  }
92
92
 
93
93
  if (url.pathname === "/api/telemetry") {
package/src/engine.mjs CHANGED
@@ -46,14 +46,8 @@ export function isTaskFile(fileName, queueDirOrContent = null) {
46
46
  }
47
47
  if (typeof content === "string") {
48
48
  const trimmed = content.trimStart();
49
- if (trimmed.startsWith("---")) {
50
- const endFm = trimmed.indexOf("\n---", 3);
51
- if (endFm !== -1) {
52
- const frontMatter = trimmed.slice(3, endFm);
53
- if (/(?:taskId|envelopeId|baseRef|baseSha|task|title|id|version|capabilities|scope|verify):/i.test(frontMatter)) {
54
- return true;
55
- }
56
- }
49
+ if (trimmed.includes("JULES_TASK_ENVELOPE") || trimmed.includes("# Task ID:") || trimmed.startsWith("---")) {
50
+ return true;
57
51
  }
58
52
  }
59
53
  } catch (_) {}
package/src/mcp.mjs CHANGED
@@ -9,7 +9,7 @@ import { ProgressBus } from "./mcp-progress.mjs";
9
9
 
10
10
  export const MCP_SERVER_INFO = {
11
11
  name: "jules-orchestrator-kit",
12
- version: "0.28.2",
12
+ version: "0.29.1",
13
13
  };
14
14
 
15
15
  export const MAX_MCP_FRAME_SIZE = 4 * 1024 * 1024; // 4 MB memory safety ceiling
package/src/security.mjs CHANGED
@@ -13,7 +13,7 @@ export const HIGH_CONFIDENCE_PATTERNS = [
13
13
 
14
14
  /\bAKIA[0-9A-Z]{16}\b/g,
15
15
  /\bASIA[0-9A-Z]{16}\b/g,
16
- /\baws_secret_access_key\s*=\s*['"]?[A-Za-z0-9\/+=]{40}['"]?/g,
16
+ /\baws_secret_access_key\s*=\s*['"]?[A-Za-z0-9\/+=]{40}['"]?/gi,
17
17
 
18
18
  /-----BEGIN (?:RSA|DSA|EC|OPENSSH|PRIVATE)(?:\s+PRIVATE)? KEY-----[\s\S]*?-----END (?:RSA|DSA|EC|OPENSSH|PRIVATE)(?:\s+PRIVATE)? KEY-----/g,
19
19
  /PuTTY-User-Key-File-[0-9]:[^\n]+/g,
package/src/tui.mjs ADDED
@@ -0,0 +1,414 @@
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
+ export class WizardCancelledError extends Error {
45
+ constructor(message = "Wizard cancelled by user") {
46
+ super(message);
47
+ this.name = "WizardCancelledError";
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Read keypress in raw mode from TTY input stream.
53
+ * @param {import("node:stream").Readable} stream
54
+ * @returns {AsyncGenerator<string, void, unknown>}
55
+ */
56
+ async function* readKeypresses(stream = process.stdin) {
57
+ const wasRaw = stream.isRaw;
58
+ if (stream.setRawMode) {
59
+ stream.setRawMode(true);
60
+ }
61
+ stream.resume();
62
+ stream.setEncoding("utf-8");
63
+
64
+ try {
65
+ for await (const chunk of stream) {
66
+ const str = String(chunk);
67
+ if (str === "\u0003") {
68
+ // Ctrl+C SIGINT
69
+ process.stdout.write(ANSI.showCursor + "\n");
70
+ throw new WizardCancelledError("Wizard operation cancelled by user (SIGINT).");
71
+ }
72
+ yield str;
73
+ }
74
+ } finally {
75
+ if (stream.setRawMode) {
76
+ stream.setRawMode(Boolean(wasRaw));
77
+ }
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Single-select interactive menu using TTY raw mode.
83
+ * @param {Array<{ label: string, value: any, description?: string }>} options
84
+ * @param {string} prompt
85
+ * @param {object} [opts]
86
+ * @returns {Promise<any>}
87
+ */
88
+ export async function select(options, prompt, opts = {}) {
89
+ const stdin = opts.stdin || process.stdin;
90
+ const stdout = opts.stdout || process.stdout;
91
+
92
+ if (options.length === 0) {
93
+ throw new Error("select() requires at least one option.");
94
+ }
95
+
96
+ const defaultIdx = opts.defaultIdx ?? 0;
97
+ let currentIdx = Math.max(0, Math.min(options.length - 1, defaultIdx));
98
+
99
+ if (!isTTY(stdin)) {
100
+ // Non-TTY Headless fallback
101
+ if (opts.fallbackValue !== undefined) {
102
+ return opts.fallbackValue;
103
+ }
104
+ const val = options[currentIdx].value;
105
+ stdout.write(`[Headless Select] ${prompt}: Auto-selected default [${options[currentIdx].label}]\n`);
106
+ return val;
107
+ }
108
+
109
+ function render(isFinal = false) {
110
+ let out = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${prompt}${ANSI.reset}\n`;
111
+ options.forEach((opt, idx) => {
112
+ const isSelected = idx === currentIdx;
113
+ const cursor = isSelected ? `${ANSI.cyan}❯${ANSI.reset}` : " ";
114
+ const label = isSelected ? `${ANSI.bold}${ANSI.cyan}${opt.label}${ANSI.reset}` : opt.label;
115
+ const desc = opt.description ? ` ${ANSI.dim}(${opt.description})${ANSI.reset}` : "";
116
+ out += ` ${cursor} ${label}${desc}\n`;
117
+ });
118
+ if (!isFinal) {
119
+ out += `${ANSI.dim} (Use arrow keys or 1-${Math.min(9, options.length)} to select, Enter to confirm)${ANSI.reset}`;
120
+ }
121
+ return out;
122
+ }
123
+
124
+ stdout.write(ANSI.hideCursor);
125
+ let renderedLines = options.length + 2;
126
+ stdout.write(render());
127
+
128
+ try {
129
+ for await (const key of readKeypresses(stdin)) {
130
+ if (key === "\r" || key === "\n") {
131
+ // Confirm selection
132
+ stdout.write(ANSI.cursorUp(renderedLines) + ANSI.clearLine);
133
+ for (let i = 0; i < renderedLines; i++) {
134
+ stdout.write(ANSI.clearLine + ANSI.cursorDown(1));
135
+ }
136
+ stdout.write(ANSI.cursorUp(renderedLines));
137
+ stdout.write(`${ANSI.green}βœ”${ANSI.reset} ${ANSI.bold}${prompt}${ANSI.reset} ${ANSI.cyan}${options[currentIdx].label}${ANSI.reset}\n`);
138
+ return options[currentIdx].value;
139
+ }
140
+
141
+ let moved = false;
142
+ if (key === "\u001b[A" || key === "k") {
143
+ // Up
144
+ currentIdx = (currentIdx - 1 + options.length) % options.length;
145
+ moved = true;
146
+ } else if (key === "\u001b[B" || key === "j") {
147
+ // Down
148
+ currentIdx = (currentIdx + 1) % options.length;
149
+ moved = true;
150
+ } else if (/^[1-9]$/.test(key)) {
151
+ const num = parseInt(key, 10) - 1;
152
+ if (num < options.length) {
153
+ currentIdx = num;
154
+ moved = true;
155
+ }
156
+ }
157
+
158
+ if (moved) {
159
+ stdout.write(ANSI.cursorUp(renderedLines));
160
+ stdout.write(render());
161
+ }
162
+ }
163
+ } finally {
164
+ stdout.write(ANSI.showCursor);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Multi-select checkbox menu using TTY raw mode.
170
+ * @param {Array<{ label: string, value: any, checked?: boolean, description?: string }>} options
171
+ * @param {string} prompt
172
+ * @param {object} [opts]
173
+ * @returns {Promise<Array<any>>}
174
+ */
175
+ export async function multiSelect(options, prompt, opts = {}) {
176
+ const stdin = opts.stdin || process.stdin;
177
+ const stdout = opts.stdout || process.stdout;
178
+
179
+ const selections = new Set();
180
+ options.forEach((opt, idx) => {
181
+ if (opt.checked) selections.add(idx);
182
+ });
183
+
184
+ if (!isTTY(stdin)) {
185
+ // Non-TTY Headless fallback
186
+ const selectedVals = options.filter((_, idx) => selections.has(idx)).map((o) => o.value);
187
+ stdout.write(`[Headless MultiSelect] ${prompt}: Auto-selected ${selectedVals.length} item(s)\n`);
188
+ return selectedVals;
189
+ }
190
+
191
+ let currentIdx = 0;
192
+ let renderedLines = options.length + 2;
193
+
194
+ function render() {
195
+ let out = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${prompt}${ANSI.reset}\n`;
196
+ options.forEach((opt, idx) => {
197
+ const isHover = idx === currentIdx;
198
+ const isChecked = selections.has(idx);
199
+ const cursor = isHover ? `${ANSI.cyan}❯${ANSI.reset}` : " ";
200
+ const box = isChecked ? `${ANSI.green}[x]${ANSI.reset}` : `${ANSI.dim}[ ]${ANSI.reset}`;
201
+ const label = isHover ? `${ANSI.bold}${opt.label}${ANSI.reset}` : opt.label;
202
+ const desc = opt.description ? ` ${ANSI.dim}(${opt.description})${ANSI.reset}` : "";
203
+ out += ` ${cursor} ${box} ${label}${desc}\n`;
204
+ });
205
+ out += `${ANSI.dim} (Space to toggle, 'a' select all, 'n' select none, Enter to confirm)${ANSI.reset}`;
206
+ return out;
207
+ }
208
+
209
+ stdout.write(ANSI.hideCursor);
210
+ stdout.write(render());
211
+
212
+ try {
213
+ for await (const key of readKeypresses(stdin)) {
214
+ if (key === "\r" || key === "\n") {
215
+ const resultVals = options.filter((_, idx) => selections.has(idx)).map((o) => o.value);
216
+ stdout.write(ANSI.cursorUp(renderedLines));
217
+ for (let i = 0; i < renderedLines; i++) {
218
+ stdout.write(ANSI.clearLine + ANSI.cursorDown(1));
219
+ }
220
+ stdout.write(ANSI.cursorUp(renderedLines));
221
+ stdout.write(`${ANSI.green}βœ”${ANSI.reset} ${ANSI.bold}${prompt}${ANSI.reset} ${ANSI.cyan}${resultVals.length} selected${ANSI.reset}\n`);
222
+ return resultVals;
223
+ }
224
+
225
+ let redraw = false;
226
+ if (key === "\u001b[A" || key === "k") {
227
+ currentIdx = (currentIdx - 1 + options.length) % options.length;
228
+ redraw = true;
229
+ } else if (key === "\u001b[B" || key === "j") {
230
+ currentIdx = (currentIdx + 1) % options.length;
231
+ redraw = true;
232
+ } else if (key === " ") {
233
+ if (selections.has(currentIdx)) {
234
+ selections.delete(currentIdx);
235
+ } else {
236
+ selections.add(currentIdx);
237
+ }
238
+ redraw = true;
239
+ } else if (key === "a" || key === "A") {
240
+ options.forEach((_, idx) => selections.add(idx));
241
+ redraw = true;
242
+ } else if (key === "n" || key === "N") {
243
+ selections.clear();
244
+ redraw = true;
245
+ }
246
+
247
+ if (redraw) {
248
+ stdout.write(ANSI.cursorUp(renderedLines));
249
+ stdout.write(render());
250
+ }
251
+ }
252
+ } finally {
253
+ stdout.write(ANSI.showCursor);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Text input prompt with validation.
259
+ * @param {string} prompt
260
+ * @param {object} [opts]
261
+ * @returns {Promise<string>}
262
+ */
263
+ export async function input(prompt, opts = {}) {
264
+ const stdin = opts.stdin || process.stdin;
265
+ const stdout = opts.stdout || process.stdout;
266
+ const defaultValue = opts.defaultValue ?? "";
267
+ const validate = opts.validate || (() => true);
268
+
269
+ if (!isTTY(stdin)) {
270
+ if (opts.fallbackValue !== undefined) {
271
+ return opts.fallbackValue;
272
+ }
273
+ stdout.write(`[Headless Input] ${prompt}: Auto-selected default [${defaultValue}]\n`);
274
+ return defaultValue;
275
+ }
276
+
277
+ const rl = createInterface({ input: stdin, output: stdout });
278
+
279
+ try {
280
+ while (true) {
281
+ const defaultHint = defaultValue ? ` ${ANSI.dim}(${defaultValue})${ANSI.reset}` : "";
282
+ const query = `${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${prompt}${defaultHint}: `;
283
+ const rawAns = await rl.question(query);
284
+ const ans = rawAns.trim() || defaultValue;
285
+
286
+ const validRes = await validate(ans);
287
+ if (validRes === true) {
288
+ return ans;
289
+ }
290
+
291
+ const errMsg = typeof validRes === "string" ? validRes : "Invalid input";
292
+ stdout.write(`${ANSI.red}βœ– ${errMsg}${ANSI.reset}\n`);
293
+ }
294
+ } finally {
295
+ rl.close();
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Confirmation prompt (Y/n).
301
+ * @param {string} prompt
302
+ * @param {boolean} [defaultVal=true]
303
+ * @param {object} [opts]
304
+ * @returns {Promise<boolean>}
305
+ */
306
+ export async function confirm(prompt, defaultVal = true, opts = {}) {
307
+ const stdin = opts.stdin || process.stdin;
308
+ const stdout = opts.stdout || process.stdout;
309
+
310
+ if (!isTTY(stdin)) {
311
+ const val = opts.fallbackValue ?? defaultVal;
312
+ stdout.write(`[Headless Confirm] ${prompt}: Auto-selected [${val ? "Yes" : "No"}]\n`);
313
+ return val;
314
+ }
315
+
316
+ const hint = defaultVal ? "[Y/n]" : "[y/N]";
317
+ const ans = await input(`${prompt} ${ANSI.dim}${hint}${ANSI.reset}`, {
318
+ defaultValue: defaultVal ? "y" : "n",
319
+ stdin,
320
+ stdout,
321
+ });
322
+
323
+ return /^y(es)?$/i.test(ans.trim());
324
+ }
325
+
326
+ /**
327
+ * Password / secret input prompt.
328
+ * @param {string} prompt
329
+ * @param {object} [opts]
330
+ * @returns {Promise<string>}
331
+ */
332
+ export async function secretInput(prompt, opts = {}) {
333
+ const stdin = opts.stdin || process.stdin;
334
+ const stdout = opts.stdout || process.stdout;
335
+
336
+ if (!isTTY(stdin)) {
337
+ return opts.fallbackValue || "";
338
+ }
339
+
340
+ stdout.write(`${ANSI.bold}${ANSI.cyan}?${ANSI.reset} ${ANSI.bold}${prompt}:${ANSI.reset} `);
341
+ stdout.write(ANSI.hideCursor);
342
+
343
+ let inputStr = "";
344
+ try {
345
+ for await (const key of readKeypresses(stdin)) {
346
+ if (key === "\r" || key === "\n") {
347
+ stdout.write("\n");
348
+ return inputStr;
349
+ }
350
+ if (key === "\u007f" || key === "\b") {
351
+ // Backspace
352
+ if (inputStr.length > 0) {
353
+ inputStr = inputStr.slice(0, -1);
354
+ stdout.write("\b \b");
355
+ }
356
+ } else if (key.length === 1 && key >= " ") {
357
+ inputStr += key;
358
+ stdout.write("*");
359
+ }
360
+ }
361
+ } finally {
362
+ stdout.write(ANSI.showCursor);
363
+ }
364
+ return inputStr;
365
+ }
366
+
367
+ /**
368
+ * Terminal activity spinner indicator.
369
+ * @param {string} label
370
+ * @param {object} [opts]
371
+ * @returns {{ stop: (msg?: string) => void, fail: (msg?: string) => void }}
372
+ */
373
+ export function spinner(label, opts = {}) {
374
+ const stdout = opts.stdout || process.stdout;
375
+ const frames = ["β ‹", "β ™", "β Ή", "β Έ", "β Ό", "β ΄", "β ¦", "β §", "β ‡", "⠏"];
376
+ let frameIdx = 0;
377
+ let timer = null;
378
+
379
+ if (isTTY(stdout)) {
380
+ stdout.write(ANSI.hideCursor);
381
+ stdout.write(`${ANSI.cyan}${frames[0]}${ANSI.reset} ${label}`);
382
+
383
+ timer = setInterval(() => {
384
+ frameIdx = (frameIdx + 1) % frames.length;
385
+ stdout.write(ANSI.clearLine);
386
+ stdout.write(`${ANSI.cyan}${frames[frameIdx]}${ANSI.reset} ${label}`);
387
+ }, 80);
388
+ } else {
389
+ stdout.write(`[Starting] ${label}...\n`);
390
+ }
391
+
392
+ return {
393
+ stop(successMsg = `${label} complete`) {
394
+ if (timer) clearInterval(timer);
395
+ if (isTTY(stdout)) {
396
+ stdout.write(ANSI.clearLine);
397
+ stdout.write(`${ANSI.green}βœ”${ANSI.reset} ${successMsg}\n`);
398
+ stdout.write(ANSI.showCursor);
399
+ } else {
400
+ stdout.write(`[Completed] ${successMsg}\n`);
401
+ }
402
+ },
403
+ fail(errorMsg = `${label} failed`) {
404
+ if (timer) clearInterval(timer);
405
+ if (isTTY(stdout)) {
406
+ stdout.write(ANSI.clearLine);
407
+ stdout.write(`${ANSI.red}βœ–${ANSI.reset} ${errorMsg}\n`);
408
+ stdout.write(ANSI.showCursor);
409
+ } else {
410
+ stdout.write(`[Failed] ${errorMsg}\n`);
411
+ }
412
+ },
413
+ };
414
+ }
@@ -0,0 +1,273 @@
1
+ import { existsSync, readFileSync, writeFileSync, openSync, fsyncSync, closeSync, renameSync, 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
+ /**
8
+ * Write a file atomically using a temporary file and atomic rename.
9
+ * @param {string} filePath
10
+ * @param {string} content
11
+ */
12
+ function writeAtomic(filePath, content) {
13
+ const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
14
+ const fd = openSync(tmpPath, "w");
15
+ try {
16
+ writeFileSync(fd, content, "utf-8");
17
+ fsyncSync(fd);
18
+ } finally {
19
+ closeSync(fd);
20
+ }
21
+ renameSync(tmpPath, filePath);
22
+ }
23
+
24
+ export const TIER_PROFILES = {
25
+ free: {
26
+ concurrency: 1,
27
+ daily_tasks: 30,
28
+ stagger_ms: 3000,
29
+ diff_kb: 50,
30
+ },
31
+ pro: {
32
+ concurrency: 3,
33
+ daily_tasks: 300,
34
+ stagger_ms: 1500,
35
+ diff_kb: 75,
36
+ },
37
+ enterprise: {
38
+ concurrency: 10,
39
+ daily_tasks: 1000,
40
+ stagger_ms: 500,
41
+ diff_kb: 100,
42
+ },
43
+ };
44
+
45
+ export const BUILTIN_PRESETS = [
46
+ {
47
+ id: "nightly-security-audit",
48
+ title: "Nightly Security & Vulnerability Audit",
49
+ description: "Scans codebase for hardcoded secrets, TODO annotations, and outdated dependencies.",
50
+ cron: "0 2 * * *",
51
+ },
52
+ {
53
+ id: "flaky-test-quarantine",
54
+ title: "Statistical Flaky Test Quarantine",
55
+ description: "Monitors test execution stability via Wilson score interval and isolates flaky tests.",
56
+ cron: "*/30 * * * *",
57
+ },
58
+ {
59
+ id: "multi-agent-refactor-swarm",
60
+ title: "Multi-Agent Parallel Refactor Swarm",
61
+ description: "Splits refactoring tasks across isolated workspace file boundaries.",
62
+ cron: "manual",
63
+ },
64
+ {
65
+ id: "doc-sync-sentinel",
66
+ title: "AGENTS.md & API Doc Sync Sentinel",
67
+ description: "Validates documentation integrity against actual exported symbol signatures.",
68
+ cron: "0 9 * * 1",
69
+ },
70
+ ];
71
+
72
+ /**
73
+ * Generate a deterministic onboarding plan for repository configuration.
74
+ * @param {string} [root=process.cwd()]
75
+ * @param {object} [options]
76
+ * @returns {{
77
+ * stack: string,
78
+ * tier: string,
79
+ * verify: { test: string, build: string, lint: string, typecheck: string },
80
+ * limits: { concurrency: number, daily_tasks: number, stagger_ms: number, diff_kb: number },
81
+ * presets: Array<string>,
82
+ * configYaml: string,
83
+ * julesYaml: string
84
+ * }}
85
+ */
86
+ export function planInit(root = process.cwd(), options = {}) {
87
+ const oracle = detectStackOracles(root);
88
+ const tierName = options.tier || "pro";
89
+ const limits = TIER_PROFILES[tierName] || TIER_PROFILES.pro;
90
+
91
+ // Preserve existing config if present
92
+ let existingConfig = {};
93
+ const existingConfigPath = join(root, ".agent", "config.yml");
94
+ if (existsSync(existingConfigPath)) {
95
+ try {
96
+ existingConfig = parseYaml(readFileSync(existingConfigPath, "utf-8")) || {};
97
+ } catch (_) {}
98
+ }
99
+
100
+ const verify = {
101
+ test: options.testCmd || existingConfig.verify?.test || oracle.candidates.testCmd || "",
102
+ build: options.buildCmd || existingConfig.verify?.build || oracle.candidates.buildCmd || "",
103
+ lint: options.lintCmd || existingConfig.verify?.lint || oracle.candidates.lintCmd || "",
104
+ typecheck: options.typecheckCmd || existingConfig.verify?.typecheck || oracle.candidates.typecheckCmd || "",
105
+ };
106
+
107
+ const selectedPresets = options.presets || existingConfig.presets || ["nightly-security-audit", "flaky-test-quarantine"];
108
+
109
+ const configYaml = `# Google Jules Orchestrator Kit Config (v0.29.0)
110
+ version: 1
111
+ provider: ${existingConfig.provider || "jules"}
112
+ tier: ${tierName}
113
+ base_branch: ${options.baseBranch || existingConfig.base_branch || "main"}
114
+ branch_prefix: ${existingConfig.branch_prefix || "agent/"}
115
+
116
+ limits:
117
+ concurrency: ${limits.concurrency}
118
+ daily_tasks: ${limits.daily_tasks}
119
+ stagger_ms: ${limits.stagger_ms}
120
+ diff_kb: ${limits.diff_kb}
121
+
122
+ verify:
123
+ test: "${verify.test}"
124
+ build: "${verify.build}"
125
+ lint: "${verify.lint}"
126
+ typecheck: "${verify.typecheck}"
127
+
128
+ presets:
129
+ ${selectedPresets.map((p) => ` - ${p}`).join("\n")}
130
+ `;
131
+
132
+ const julesYaml = `# Google Jules Agent Compatibility Manifest
133
+ version: 1
134
+ test_cmd: "${verify.test}"
135
+ build_cmd: "${verify.build}"
136
+ `;
137
+
138
+ return {
139
+ stack: oracle.stack,
140
+ tier: tierName,
141
+ verify,
142
+ limits,
143
+ presets: selectedPresets,
144
+ configYaml,
145
+ julesYaml,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Load declarative presets from .agent/presets/*.yml or fallback to built-ins.
151
+ * @param {string} [root=process.cwd()]
152
+ * @returns {Array<{ id: string, title: string, description: string, cron: string }>}
153
+ */
154
+ export function loadPresets(root = process.cwd()) {
155
+ const presetsDir = join(root, ".agent", "presets");
156
+ const presets = [...BUILTIN_PRESETS];
157
+
158
+ if (existsSync(presetsDir)) {
159
+ try {
160
+ const files = readdirSync(presetsDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
161
+ files.forEach((f) => {
162
+ try {
163
+ const content = readFileSync(join(presetsDir, f), "utf-8");
164
+ const parsed = parseYaml(content);
165
+ if (parsed && parsed.id && parsed.title) {
166
+ presets.push({
167
+ id: parsed.id,
168
+ title: parsed.title,
169
+ description: parsed.description || "",
170
+ cron: parsed.cron || "manual",
171
+ });
172
+ }
173
+ } catch (_) {}
174
+ });
175
+ } catch (_) {}
176
+ }
177
+
178
+ return presets;
179
+ }
180
+
181
+ /**
182
+ * Run interactive or headless onboarding wizard.
183
+ * @param {string} [root=process.cwd()]
184
+ * @param {object} [options]
185
+ * @returns {Promise<{ ok: boolean, configPath: string, plan: object }>}
186
+ */
187
+ export async function runInitWizard(root = process.cwd(), options = {}) {
188
+ const interactive = options.interactive !== false && isTTY(options.stdin || process.stdin);
189
+
190
+ if (!interactive && !options.allowDefaults && !options.tier) {
191
+ throw new Error("Non-interactive init requires explicit options or allowDefaults: true");
192
+ }
193
+
194
+ let selectedTier = options.tier || "pro";
195
+ let testCmd = options.testCmd;
196
+ let buildCmd = options.buildCmd;
197
+ let selectedPresets = options.presets;
198
+
199
+ if (interactive) {
200
+ const oracle = detectStackOracles(root);
201
+ const sp = spinner("Inspecting repository stack and verification oracles", options);
202
+ await new Promise((resolve) => setTimeout(resolve, 300));
203
+ sp.stop(`Detected Stack: ${oracle.stack}`);
204
+
205
+ selectedTier = await select(
206
+ [
207
+ { label: "Pro Tier (Recommended)", value: "pro", description: "3 parallel workers, 300 daily tasks, 75 KB diff limit" },
208
+ { label: "Free / Individual Tier", value: "free", description: "1 worker, 30 daily tasks, 50 KB diff limit" },
209
+ { label: "Custom Enterprise", value: "enterprise", description: "10 workers, 1000 daily tasks, 100 KB diff limit" },
210
+ ],
211
+ "Select Jules Orchestrator Usage Tier",
212
+ options
213
+ );
214
+
215
+ testCmd = await input("Verification Test Command", {
216
+ defaultValue: oracle.candidates.testCmd || "npm test",
217
+ stdin: options.stdin,
218
+ stdout: options.stdout,
219
+ });
220
+
221
+ buildCmd = await input("Verification Build Command", {
222
+ defaultValue: oracle.candidates.buildCmd || "",
223
+ stdin: options.stdin,
224
+ stdout: options.stdout,
225
+ });
226
+
227
+ const presetOptions = BUILTIN_PRESETS.map((p) => ({
228
+ label: p.title,
229
+ value: p.id,
230
+ checked: true,
231
+ description: p.description,
232
+ }));
233
+
234
+ selectedPresets = await multiSelect(presetOptions, "Select Autonomous Workflows to Enable", options);
235
+
236
+ const shouldProbe = await confirm("Run verification probe on test command before saving?", true, options);
237
+ if (shouldProbe && testCmd) {
238
+ const probeSp = spinner(`Probing oracle: ${testCmd}`, options);
239
+ const probeRes = await runVerificationProbe(testCmd, root);
240
+ if (probeRes.ok) {
241
+ probeSp.stop(`Oracle verified successfully (${probeRes.durationMs}ms)`);
242
+ } else {
243
+ probeSp.fail(`Oracle verification probe failed (Exit ${probeRes.code})`);
244
+ }
245
+ }
246
+ }
247
+
248
+ const plan = planInit(root, {
249
+ tier: selectedTier,
250
+ testCmd,
251
+ buildCmd,
252
+ presets: selectedPresets,
253
+ ...options,
254
+ });
255
+
256
+ // Atomic write plan
257
+ const agentDir = join(root, ".agent");
258
+ if (!existsSync(agentDir)) {
259
+ mkdirSync(agentDir, { recursive: true });
260
+ }
261
+
262
+ const configPath = join(agentDir, "config.yml");
263
+ const julesPath = join(agentDir, "jules.yml");
264
+
265
+ writeAtomic(configPath, plan.configYaml);
266
+ writeAtomic(julesPath, plan.julesYaml);
267
+
268
+ return {
269
+ ok: true,
270
+ configPath,
271
+ plan,
272
+ };
273
+ }
@@ -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,223 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { loadConfig } from "./config.mjs";
4
+ import { gate } from "./engine.mjs";
5
+ import { scanDiff, shannonEntropy } from "./security.mjs";
6
+ import { getQueueDir } from "./state.mjs";
7
+ import { scanCodebaseForTodos } from "../scripts/jules-scan-todos.mjs";
8
+ import { select, input, confirm, spinner, isTTY } from "./tui.mjs";
9
+
10
+ export const GUARDRAIL_FOOTER = `
11
+ ---
12
+ HARD CONSTRAINTS:
13
+ - Do NOT modify package.json, pnpm-lock.yaml, tsconfig.json, or .github/ files.
14
+ - Diff Payload Governor: Keep total diff payload under 75 KB (\`git diff | wc -c\`).
15
+ - Falsifiable & Evidence-Based: Attach full terminal verification output to PR. Never weaken assertions or delete failing tests to force a pass.
16
+ - Read-Before-Write: Inspect existing symbol signatures, definitions, and call sites before making edits.
17
+ - BEFORE opening the PR: Run \`git fetch origin main && git rebase origin/main\`, then re-verify.
18
+ `;
19
+
20
+ const TRIVIAL_ORACLES = new Set(["true", "echo", ":", "false", "exit 0", "exit 1", "echo ok"]);
21
+
22
+ /**
23
+ * Pure planning core for task creation & envelope synthesis.
24
+ * @param {string} root
25
+ * @param {object} inputObj
26
+ * @returns {{
27
+ * ok: boolean,
28
+ * taskId: string,
29
+ * title: string,
30
+ * prompt: string,
31
+ * fullPrompt: string,
32
+ * verifyCmd: string,
33
+ * flags: { autoPr: boolean, requirePlanApproval: boolean, repoless: boolean, startingBranch: string },
34
+ * secretFindings: Array<any>,
35
+ * taskFileContent: string
36
+ * }}
37
+ */
38
+ export function planTaskCreate(root = process.cwd(), inputObj = {}) {
39
+ const config = loadConfig(root);
40
+
41
+ const title = inputObj.title || "Agent Task";
42
+ const rawPrompt = inputObj.prompt || "";
43
+ const verifyCmd = (inputObj.verifyCmd || config.verify.test || config.verify.build || "").trim();
44
+
45
+ // 1. Falsifiability check
46
+ if (!rawPrompt.trim()) {
47
+ throw new Error("Task prompt cannot be empty.");
48
+ }
49
+ const cleanCmd = verifyCmd.toLowerCase().replace(/['"]/g, "").trim();
50
+ if ((!verifyCmd || TRIVIAL_ORACLES.has(cleanCmd)) && !inputObj.allowUnverifiable) {
51
+ throw new Error(
52
+ "Unfalsifiable Task Rejected: Task must include a non-trivial verification test/build command. Configure verify.test in .agent/config.yml or pass --verify-cmd."
53
+ );
54
+ }
55
+
56
+ // 2. Secret Scrubbing Preflight (multiline diff formatting)
57
+ const secretFindings = [];
58
+ const multilineDiff = rawPrompt.split("\n").map((line) => `+${line}`).join("\n");
59
+ const secretScan = scanDiff(multilineDiff);
60
+ if (!secretScan.ok) {
61
+ secretScan.findings.forEach((f) => secretFindings.push({ id: f.type || f.id || "SECRET_LEAK", ...f }));
62
+ }
63
+ if (shannonEntropy(rawPrompt) > 4.5 && !inputObj.allowHighEntropy) {
64
+ secretFindings.push({ id: "HIGH_ENTROPY_PROMPT", line: 1 });
65
+ }
66
+
67
+ // High-confidence secrets cannot be bypassed
68
+ const highConfidenceFindings = secretFindings.filter((f) => f.id !== "HIGH_ENTROPY_PROMPT");
69
+ if (highConfidenceFindings.length > 0) {
70
+ throw new Error(
71
+ `Pre-Dispatch Secret Leak Blocked: Prompt contains ${highConfidenceFindings.length} high-confidence secret finding(s). High-confidence credentials cannot be bypassed.`
72
+ );
73
+ }
74
+ if (secretFindings.length > 0 && !inputObj.allowSecrets) {
75
+ throw new Error(
76
+ `Pre-Dispatch Secret Leak Blocked: Prompt contains potential secrets or credentials (${secretFindings.length} finding(s)). Scrub keys before dispatching.`
77
+ );
78
+ }
79
+
80
+ // 3. Jules v1alpha Flags
81
+ const flags = {
82
+ autoPr: Boolean(inputObj.autoPr),
83
+ requirePlanApproval: Boolean(inputObj.requirePlanApproval),
84
+ repoless: Boolean(inputObj.repoless),
85
+ startingBranch: inputObj.startingBranch || config.baseBranch || "main",
86
+ };
87
+
88
+ // 4. Task ID Sanitization (Path Traversal Guard)
89
+ const rawId = inputObj.id || `TASK-${Date.now().toString(36).toUpperCase()}`;
90
+ const taskId = String(rawId).replace(/[^a-zA-Z0-9_-]/g, "_");
91
+
92
+ // 5. Prompt Envelope & Guardrail Footer Synthesis
93
+ const fullPrompt = `[TASK INSTRUCTIONS]
94
+ ${rawPrompt}
95
+
96
+ [VERIFICATION ORACLE]
97
+ Test/Verification Command: ${verifyCmd || "(None)"}
98
+
99
+ ${GUARDRAIL_FOOTER}`;
100
+
101
+ const envelopeMetadata = {
102
+ version: 1,
103
+ id: taskId,
104
+ title,
105
+ flags,
106
+ verifyCmd,
107
+ };
108
+
109
+ const taskFileContent = `<!-- JULES_TASK_ENVELOPE: ${JSON.stringify(envelopeMetadata)} -->
110
+ # ${title}
111
+ # Task ID: ${taskId}
112
+ # Auto-PR: ${flags.autoPr} | Plan Approval: ${flags.requirePlanApproval} | Repoless: ${flags.repoless}
113
+
114
+ ${fullPrompt}
115
+ `;
116
+
117
+ return {
118
+ ok: true,
119
+ taskId,
120
+ title,
121
+ prompt: rawPrompt,
122
+ fullPrompt,
123
+ verifyCmd,
124
+ flags,
125
+ secretFindings,
126
+ taskFileContent,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Run interactive or headless task creation wizard.
132
+ * @param {string} [root=process.cwd()]
133
+ * @param {object} [options]
134
+ * @returns {Promise<{ ok: boolean, taskFile: string, plan: object }>}
135
+ */
136
+ export async function runTaskCreateWizard(root = process.cwd(), options = {}) {
137
+ const interactive = options.interactive !== false && isTTY(options.stdin || process.stdin);
138
+
139
+ let title = options.title;
140
+ let promptText = options.prompt;
141
+ let verifyCmd = options.verifyCmd;
142
+ let autoPr = options.autoPr;
143
+ let requirePlanApproval = options.requirePlanApproval;
144
+ let repoless = options.repoless;
145
+
146
+ if (interactive) {
147
+ const sp = spinner("Analyzing codebase task candidates & gate policy", options);
148
+ const todos = scanCodebaseForTodos(root);
149
+ await new Promise((resolve) => setTimeout(resolve, 200));
150
+ sp.stop(`Scanned ${todos.length} TODO/FIXME candidates`);
151
+
152
+ if (todos.length > 0 && !promptText) {
153
+ const useTodo = await confirm("Import prompt from scanned TODO candidate?", false, options);
154
+ if (useTodo) {
155
+ const selectedTodo = await select(
156
+ todos.slice(0, 10).map((t) => ({
157
+ label: `[${t.type}] ${t.text} (${t.file}:${t.line})`,
158
+ value: t,
159
+ })),
160
+ "Select TODO Candidate",
161
+ options
162
+ );
163
+ title = `Fix ${selectedTodo.type}: ${selectedTodo.text.slice(0, 40)}`;
164
+ promptText = `Resolve ${selectedTodo.type} in ${selectedTodo.file} at line ${selectedTodo.line}: ${selectedTodo.text}`;
165
+ }
166
+ }
167
+
168
+ title = await input("Task Title", {
169
+ defaultValue: title || "Refactor Component Module",
170
+ stdin: options.stdin,
171
+ stdout: options.stdout,
172
+ });
173
+
174
+ promptText = await input("Detailed Task Instructions", {
175
+ defaultValue: promptText || "Cleanly refactor module to improve readability and maintain full test coverage.",
176
+ stdin: options.stdin,
177
+ stdout: options.stdout,
178
+ });
179
+
180
+ autoPr = await confirm("Enable Automatic PR Creation (automationMode: AUTO_CREATE_PR)?", true, options);
181
+ requirePlanApproval = await confirm("Require Plan Approval Gate before execution?", false, options);
182
+ repoless = await confirm("Dispatch in Repoless mode (no repo source context)?", false, options);
183
+ }
184
+
185
+ const plan = planTaskCreate(root, {
186
+ title,
187
+ prompt: promptText,
188
+ verifyCmd,
189
+ autoPr,
190
+ requirePlanApproval,
191
+ repoless,
192
+ ...options,
193
+ });
194
+
195
+ // Perform Gate Preflight
196
+ const gateRes = await gate({ root, mode: "working-tree" });
197
+ if (!gateRes.ok && (gateRes.code === 3 || gateRes.code === 6) && !options.allowGateFailure) {
198
+ // Filter out uncommitted .agent/ config scope warnings if user initialized locally
199
+ const realViolations = gateRes.phases[0]?.violations?.filter((v) => !v.file.startsWith(".agent/")) || [];
200
+ if (realViolations.length > 0 || gateRes.code === 6) {
201
+ throw new Error(`Gate Preflight Rejected Task: Repository contains scope or secret violations (Exit ${gateRes.code}).`);
202
+ }
203
+ }
204
+
205
+ // Write Task File to canonical queue directory (getQueueDir)
206
+ const queueDir = getQueueDir(root);
207
+ if (!existsSync(queueDir)) {
208
+ mkdirSync(queueDir, { recursive: true });
209
+ }
210
+
211
+ const taskFile = resolve(queueDir, `${plan.taskId}.md`);
212
+ if (!taskFile.startsWith(resolve(queueDir))) {
213
+ throw new Error("Task ID path traversal blocked.");
214
+ }
215
+
216
+ writeFileSync(taskFile, plan.taskFileContent, "utf-8");
217
+
218
+ return {
219
+ ok: true,
220
+ taskFile,
221
+ plan,
222
+ };
223
+ }