jules-orchestrator-kit 0.72.2 → 0.73.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.
Files changed (80) hide show
  1. package/.agent/prompts/{Overseer.md → Auditor.md} +3 -3
  2. package/.agent/prompts/{Alchemist.md → Database.md} +1 -1
  3. package/.agent/prompts/Debugger.md +25 -0
  4. package/.agent/prompts/{Scribe.md → Docs.md} +8 -5
  5. package/.agent/prompts/{Spectator.md → E2E.md} +9 -6
  6. package/.agent/prompts/{Janitor.md → Hygiene.md} +2 -2
  7. package/.agent/prompts/{Bolt.md → Performance.md} +1 -1
  8. package/.agent/prompts/Resilience.md +20 -0
  9. package/.agent/prompts/Security.md +21 -0
  10. package/.agent/prompts/Testing.md +30 -0
  11. package/.agent/prompts/Types.md +19 -0
  12. package/.agent/rules/jules-protocol.md +4 -3
  13. package/AGENTS.md +78 -96
  14. package/CHANGELOG.md +193 -0
  15. package/JULES_RULES_TEMPLATE.md +83 -96
  16. package/LICENSE +1 -1
  17. package/README.md +77 -439
  18. package/ROADMAP_V1.md +22 -132
  19. package/bin/agentctl.mjs +443 -144
  20. package/bin/init.js +6 -3
  21. package/index.mjs +9 -6
  22. package/package.json +1 -1
  23. package/scripts/asset-integrity-check.mjs +1 -1
  24. package/scripts/doc-sync-check.mjs +47 -0
  25. package/scripts/generate-command-reference.mjs +39 -0
  26. package/scripts/jules-dispatch.mjs +12 -113
  27. package/scripts/jules-merge-swarm.mjs +8 -196
  28. package/scripts/jules-patch.mjs +7 -8
  29. package/scripts/jules-queue-runner.mjs +6 -8
  30. package/scripts/jules-scan-todos.mjs +10 -38
  31. package/scripts/jules-self-audit.mjs +8 -139
  32. package/scripts/jules-status.mjs +32 -38
  33. package/scripts/jules-webhook-receiver.mjs +1 -1
  34. package/src/assertions.mjs +5 -50
  35. package/src/bidi-guard.mjs +36 -0
  36. package/src/budget.mjs +3 -14
  37. package/src/config.mjs +2 -6
  38. package/src/dashboard.mjs +7 -9
  39. package/src/dispatch.mjs +212 -0
  40. package/src/engine.mjs +40 -16
  41. package/src/evidence.mjs +10 -41
  42. package/src/execution-envelope.mjs +13 -1
  43. package/src/flaky-ledger.mjs +1 -1
  44. package/src/fs-atomic.mjs +72 -0
  45. package/src/git.mjs +298 -27
  46. package/src/mcp.mjs +296 -7
  47. package/src/memory.mjs +0 -0
  48. package/src/merge-swarm.mjs +202 -0
  49. package/src/ops/cli-intent.mjs +1 -0
  50. package/src/ops/command-registry.mjs +796 -70
  51. package/src/ops/doctor-registry.mjs +134 -47
  52. package/src/ops/handover.mjs +3 -27
  53. package/src/ops/pr-harvest.mjs +1 -1
  54. package/src/prompt-guard.mjs +33 -3
  55. package/src/provider.mjs +51 -7
  56. package/src/remediation.mjs +2 -2
  57. package/src/role-resolver.mjs +113 -3
  58. package/src/router.mjs +19 -11
  59. package/src/runtime-env.mjs +67 -0
  60. package/src/scaffold.mjs +3 -1
  61. package/src/scope-guard.mjs +249 -0
  62. package/src/secret-scanner.mjs +530 -0
  63. package/src/security.mjs +81 -2972
  64. package/src/self-audit.mjs +140 -0
  65. package/src/session-ops.mjs +29 -0
  66. package/src/stability.mjs +8 -1
  67. package/src/stack-detector.mjs +5 -2
  68. package/src/state.mjs +45 -0
  69. package/src/swarm.mjs +76 -0
  70. package/src/task-optimizer.mjs +34 -7
  71. package/src/telemetry.mjs +23 -0
  72. package/src/test-tamper-guard.mjs +2173 -0
  73. package/src/todo-scanner.mjs +129 -0
  74. package/src/web-templates.mjs +3 -3
  75. package/src/webhook.mjs +10 -3
  76. package/src/wizard-init.mjs +12 -20
  77. package/src/wizard-oracle.mjs +4 -3
  78. package/src/wizard-task.mjs +37 -9
  79. package/.agent/prompts/Sentinel.md +0 -18
  80. package/scripts/utils.mjs +0 -241
@@ -1,148 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Self-audit gatekeeper: runs 4-phase safety gate against working tree or base branch.
4
+ * Self-audit CLI entrypoint.
5
+ *
6
+ * All audit logic lives in src/self-audit.mjs (P04 shim cleanup); this file
7
+ * is only the executable gate that CI (`jules-audit.yml`) invokes. The
8
+ * re-export below preserves the historical `scripts/jules-self-audit.mjs`
9
+ * import surface for external consumers.
5
10
  */
6
11
 
7
- import { existsSync } from "node:fs";
8
- import { join } from "node:path";
12
+ export * from "../src/self-audit.mjs";
9
13
 
10
- let gate, loadConfig, parseYaml, matchesGlob;
11
- try {
12
- ({ gate } = await import("../src/engine.mjs"));
13
- ({ loadConfig, parseYaml } = await import("../src/config.mjs"));
14
- ({ matchesGlob } = await import("../src/security.mjs"));
15
- } catch (_) {
16
- try {
17
- ({ gate, loadConfig, parseYaml, matchesGlob } = await import("jules-orchestrator-kit"));
18
- } catch (err) {
19
- console.error("Error: Could not resolve jules-orchestrator-kit modules.", err.message);
20
- process.exit(1);
21
- }
22
- }
23
-
24
- export function matchGlob(filepath, globPattern) {
25
- return matchesGlob(filepath, globPattern);
26
- }
27
-
28
- export const EXECUTION_CONFIG_FILES = ["package.json", "tsconfig.json", "Cargo.toml", "go.mod", "pyproject.toml", "vite.config.ts", "jest.config.js", ".npmrc"];
29
- export const RESTRICTED_AGENT_FILES = ["AGENTS.md", "JULES_RULES_TEMPLATE.md"];
30
- export const COMMAND_DEFINING_FILES = ["package.json", "Cargo.toml", "go.mod", "pyproject.toml", ".agent/jules.yml", ".agent/config.yml"];
31
-
32
- export function loadForbiddenPatterns(configContent = "") {
33
- const defaults = ["scripts/jules-*", ".agent/jules.yml", ".github/**", "**/*.pem"];
34
- if (!configContent) return defaults;
35
- const parsed = parseYaml(configContent);
36
- const userPaths = Array.isArray(parsed.forbidden_paths) ? parsed.forbidden_paths : [];
37
- return Array.from(new Set([...defaults, ...userPaths]));
38
- }
39
-
40
- export function loadAllowedPatterns(configContent = "") {
41
- if (!configContent) return [];
42
- const parsed = parseYaml(configContent);
43
- return Array.isArray(parsed.allow_paths) ? parsed.allow_paths : [];
44
- }
45
-
46
- export function validateJulesConfig(_configContent = "", jsonGuardrailsContent = "") {
47
- const errors = [];
48
- if (jsonGuardrailsContent) {
49
- try {
50
- const parsed = JSON.parse(jsonGuardrailsContent);
51
- if (Array.isArray(parsed.rules)) {
52
- for (const rule of parsed.rules) {
53
- try {
54
- new RegExp(rule.trigger, "i");
55
- } catch (err) {
56
- errors.push(`Invalid RegExp trigger: ${err.message}`);
57
- }
58
- }
59
- }
60
- } catch (err) {
61
- errors.push(`JSON Parse Error: ${err.message}`);
62
- }
63
- }
64
- return { ok: errors.length === 0, errors };
65
- }
66
-
67
- export function parseAndCleanStderr(str = "") {
68
- if (typeof str !== "string") return "";
69
- return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\u001b\[[0-9;]*m/g, "");
70
- }
71
-
72
- export function getOodaStateFile(mergeBase = "main") {
73
- return `.agent/state/ooda-${mergeBase}.json`;
74
- }
75
-
76
- export function logAuditMetrics() {}
77
-
78
- /**
79
- * Audit ledger states and verify integrity of ledger/state files.
80
- */
81
- export function auditLedgers(opts = {}) {
82
- const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
83
- const stateDir = join(root, ".agent", "state");
84
- const exists = existsSync(stateDir);
85
- return { ok: true, stateDir, exists };
86
- }
87
-
88
- /**
89
- * Audit git worktrees and active workspace boundaries.
90
- */
91
- export function auditWorktrees(opts = {}) {
92
- const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
93
- const worktreeDir = join(root, ".agent", "worktrees");
94
- const exists = existsSync(worktreeDir);
95
- return { ok: true, worktreeDir, exists };
96
- }
97
-
98
- /**
99
- * Audit gate rules (scope, payload governor, secret scanning, verification suite).
100
- */
101
- export async function auditGates(opts = {}) {
102
- const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
103
- const config = opts.config || loadConfig(root);
104
- const base = opts.base || process.env.BASE_BRANCH || config.baseBranch || "main";
105
-
106
- return await gate({
107
- root,
108
- config,
109
- base,
110
- fix: process.env.ALLOW_AUTO_REPAIR === "true",
111
- allowProtected: process.env.JULES_ALLOW_COMMAND_FILE_CHANGES === "true",
112
- });
113
- }
114
-
115
- /**
116
- * Main self-audit entrypoint executing validation passes.
117
- */
118
- export async function runSelfAudit(opts = {}) {
119
- const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
120
- const config = loadConfig(root);
121
- const base = opts.base || process.env.BASE_BRANCH || config.baseBranch || "main";
122
-
123
- const optsWithContext = { ...opts, root, config, base };
124
-
125
- const ledgersResult = auditLedgers(optsWithContext);
126
- const worktreesResult = auditWorktrees(optsWithContext);
127
- const gatesResult = await auditGates(optsWithContext);
128
-
129
- if (!gatesResult.ok) {
130
- return gatesResult;
131
- }
132
-
133
- return {
134
- ...gatesResult,
135
- audits: {
136
- ledgers: ledgersResult,
137
- worktrees: worktreesResult,
138
- gates: gatesResult,
139
- },
140
- };
141
- }
142
-
143
- export async function runPreflightSandbox() {
144
- return { ok: true };
145
- }
14
+ import { runSelfAudit } from "../src/self-audit.mjs";
146
15
 
147
16
  if (process.argv[1] && process.argv[1].endsWith("jules-self-audit.mjs")) {
148
17
  runSelfAudit()
@@ -2,50 +2,44 @@
2
2
 
3
3
  /**
4
4
  * Session status and budget status summarizer.
5
+ *
6
+ * categorizeTaskStatus moved to src/session-ops.mjs (P04 shim cleanup) and is
7
+ * re-exported here for historical importers; the CLI body now runs only under
8
+ * the entry guard, so importing this script no longer executes side effects.
5
9
  */
6
10
 
11
+ export { categorizeTaskStatus } from "../src/session-ops.mjs";
12
+
7
13
  import { checkDailyBudget } from "../src/state.mjs";
8
14
  import { readFileSync, existsSync } from "node:fs";
9
15
  import { join } from "node:path";
10
16
 
11
- export function categorizeTaskStatus(statusStr = "") {
12
- const s = String(statusStr).toUpperCase();
13
- if (s === "AWAITING_PLAN_APPROVAL" || s === "AWAITING_USER_FEEDBACK") {
14
- return "action_required";
15
- }
16
- if (s === "IN_PROGRESS" || s === "DISPATCHED") {
17
- return "in_progress";
18
- }
19
- if (s === "COMPLETED" || s === "FAILED") {
20
- return "completed";
21
- }
22
- return "unknown";
23
- }
17
+ if (process.argv[1] && process.argv[1].endsWith("jules-status.mjs")) {
18
+ const isJson = process.argv.includes("--json");
19
+ const root = process.env.JULES_PROJECT_ROOT || process.cwd();
20
+ const budget = checkDailyBudget(root);
24
21
 
25
- const isJson = process.argv.includes("--json");
26
- const root = process.env.JULES_PROJECT_ROOT || process.cwd();
27
- const budget = checkDailyBudget(root);
28
-
29
- let queueItems = [];
30
- const queueFile = join(root, ".agent/jules-queue/queue.jsonl");
31
- if (existsSync(queueFile)) {
32
- try {
33
- const raw = readFileSync(queueFile, "utf-8");
34
- queueItems = raw
35
- .split("\n")
36
- .filter(Boolean)
37
- .map((line) => {
38
- const parsed = JSON.parse(line);
39
- return {
40
- ...parsed,
41
- Status: parsed.Status || parsed.status || "PENDING",
42
- };
43
- });
44
- } catch (_) {}
45
- }
22
+ let queueItems = [];
23
+ const queueFile = join(root, ".agent/jules-queue/queue.jsonl");
24
+ if (existsSync(queueFile)) {
25
+ try {
26
+ const raw = readFileSync(queueFile, "utf-8");
27
+ queueItems = raw
28
+ .split("\n")
29
+ .filter(Boolean)
30
+ .map((line) => {
31
+ const parsed = JSON.parse(line);
32
+ return {
33
+ ...parsed,
34
+ Status: parsed.Status || parsed.status || "PENDING",
35
+ };
36
+ });
37
+ } catch (_) {}
38
+ }
46
39
 
47
- if (isJson) {
48
- console.log(JSON.stringify({ queue: queueItems, sessions: [], budget }));
49
- } else {
50
- console.log(`[Shim] Status: Daily Budget ${budget.used}/${budget.budget} used.`);
40
+ if (isJson) {
41
+ console.log(JSON.stringify({ queue: queueItems, sessions: [], budget }));
42
+ } else {
43
+ console.log(`[Shim] Status: Daily Budget ${budget.used}/${budget.budget} used.`);
44
+ }
51
45
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createWebhookServer } from "../src/webhook.mjs";
4
- import { loadEnv, log, timestamp } from "./utils.mjs";
4
+ import { loadEnv, log, timestamp } from "../src/runtime-env.mjs";
5
5
  import { spawn } from "node:child_process";
6
6
  import { resolveRoot } from "../src/config.mjs";
7
7
 
@@ -4,7 +4,7 @@ import { gzipSync } from "node:zlib";
4
4
  import { checkAssetIntegrity } from "./asset-integrity.mjs";
5
5
  import { checkRulesBudget } from "./rules-budget.mjs";
6
6
  import { runMutationTest } from "./mutation.mjs";
7
- import { checkTestTampering } from "./security.mjs";
7
+ import { checkTestTampering, matchesGlob } from "./security.mjs";
8
8
  import { runV8Coverage, calculateDiffCoverage } from "./coverage.mjs";
9
9
  import { runStabilityProbe } from "./stability.mjs";
10
10
  import { measureEventLoopDelay } from "./perf.mjs";
@@ -54,56 +54,11 @@ export function resolveBytesLimit(opts = {}) {
54
54
  }
55
55
 
56
56
  /**
57
- * Simple zero-dependency glob/pattern matcher supporting * and **.
58
- * @param {string} pathStr
59
- * @param {string} pattern
60
- * @returns {boolean}
57
+ * Glob matching is delegated to the single linear-time matcher in
58
+ * src/security.mjs (segment DP; no regex, so no catastrophic backtracking).
59
+ * Re-exported so `./assertions.mjs` callers keep the same entry point.
61
60
  */
62
- export function matchesGlob(pathStr, pattern) {
63
- if (!pattern || pattern === "*") return true;
64
- const normalizedPath = normalizePosix(pathStr).replace(/^\.\//, "");
65
- const normalizedPattern = normalizePosix(pattern).replace(/^\.\//, "");
66
-
67
- if (normalizedPath === normalizedPattern) return true;
68
-
69
- // Convert simple glob pattern to RegExp
70
- let regexStr = "^";
71
- let i = 0;
72
- while (i < normalizedPattern.length) {
73
- const c = normalizedPattern[i];
74
- if (c === "*" && normalizedPattern[i + 1] === "*") {
75
- if (normalizedPattern[i + 2] === "/") {
76
- regexStr += "(?:.*/)?";
77
- i += 3;
78
- continue;
79
- } else {
80
- regexStr += ".*";
81
- i += 2;
82
- continue;
83
- }
84
- } else if (c === "*") {
85
- regexStr += "[^/]*";
86
- i++;
87
- } else if (c === "?") {
88
- regexStr += "[^/]";
89
- i++;
90
- } else if (["\\", ".", "+", "^", "$", "{", "}", "(", ")", "|", "[", "]"].includes(c)) {
91
- regexStr += "\\" + c;
92
- i++;
93
- } else {
94
- regexStr += c;
95
- i++;
96
- }
97
- }
98
- regexStr += "$";
99
-
100
- try {
101
- const re = new RegExp(regexStr);
102
- return re.test(normalizedPath);
103
- } catch (_) {
104
- return normalizedPath.includes(normalizedPattern);
105
- }
106
- }
61
+ export { matchesGlob };
107
62
 
108
63
  /**
109
64
  * Asserts total byte size of a directory.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Trojan Source (CVE-2021-42574) BiDi override detection.
3
+ *
4
+ * Split out of src/security.mjs (P05). A Unicode directional override can make
5
+ * the line an agent reads differ from the line the parser executes, so the
6
+ * check runs on every added line of every non-markdown file before anything
7
+ * else looks at the diff.
8
+ */
9
+
10
+ export function checkTrojanSource(diffOrText = "", _options = {}) {
11
+ if (!diffOrText || typeof diffOrText !== "string") return { ok: true, violations: [] };
12
+
13
+ const violations = [];
14
+ const bidiRegex = /[\u202A\u202B\u202C\u202D\u202E\u2066\u2067\u2068\u2069\u061C\u200E\u200F]/;
15
+
16
+ const lines = diffOrText.split("\n");
17
+ // Only filter diff syntax if we actually start with typical diff headers
18
+ // This avoids treating text containing "+++ b/" as a diff mistakenly.
19
+ const isDiff = diffOrText.startsWith("--- a/") || diffOrText.startsWith("+++ b/") || diffOrText.includes("\n+++ b/");
20
+
21
+ const targetLines = lines.filter((line) => {
22
+ if (isDiff) {
23
+ return line.startsWith("+") && !line.startsWith("+++");
24
+ }
25
+ return true;
26
+ });
27
+
28
+ for (const line of targetLines) {
29
+ if (bidiRegex.test(line)) {
30
+ violations.push({ reason: "Trojan Source BiDi override detected: contains invisible directional control characters (CVE-2021-42574)." });
31
+ break; // One violation is enough for the file/diff block
32
+ }
33
+ }
34
+
35
+ return { ok: violations.length === 0, violations };
36
+ }
package/src/budget.mjs CHANGED
@@ -1,8 +1,9 @@
1
- import { existsSync, readFileSync, writeFileSync, openSync, fsyncSync, closeSync, renameSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { execSync } from "node:child_process";
4
4
  import { userInfo } from "node:os";
5
5
  import { resolveRoot } from "./config.mjs";
6
+ import { safeAtomicWrite } from "./security.mjs";
6
7
  import {
7
8
  getStateDir,
8
9
  ensureDir,
@@ -101,18 +102,6 @@ export function isDailyQuotaRejection(err) {
101
102
  return DAILY_QUOTA_HINT.test(text);
102
103
  }
103
104
 
104
- function writeAtomic(filePath, content) {
105
- const tmpPath = `${filePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
106
- const fd = openSync(tmpPath, "w");
107
- try {
108
- writeFileSync(fd, content, "utf-8");
109
- fsyncSync(fd);
110
- } finally {
111
- closeSync(fd);
112
- }
113
- renameSync(tmpPath, filePath);
114
- }
115
-
116
105
  /**
117
106
  * Read the stored ceiling, whenever it was observed.
118
107
  *
@@ -190,7 +179,7 @@ export function recordObservedCeiling(usedAtRejection, root = resolveRoot(), met
190
179
  observedAt: new Date().toISOString(),
191
180
  source: meta.source || "provider-rejection",
192
181
  };
193
- writeAtomic(join(stateDir, CEILING_FILE), JSON.stringify(record, null, 2) + "\n");
182
+ safeAtomicWrite(join(stateDir, CEILING_FILE), JSON.stringify(record, null, 2) + "\n");
194
183
 
195
184
  // Mirrored into the hash-chained ledger so the change is auditable; the JSON
196
185
  // file above is only a cheap index that survives ledger rotation.
package/src/config.mjs CHANGED
@@ -1001,12 +1001,8 @@ export function resolveTrustedPolicy(root = process.cwd(), baseRef = null, mode
1001
1001
  // 1. Resolve base branch
1002
1002
  let base = baseRef || opts.base;
1003
1003
  if (!base) {
1004
- if (mode === "committed") {
1005
- base = detectDefaultBranch(root) || "main";
1006
- } else {
1007
- const diskConfig = opts.config || (existsSync(join(root, ".agent/config.yml")) ? loadConfig(root) : null);
1008
- base = diskConfig?.baseBranch || detectDefaultBranch(root) || "main";
1009
- }
1004
+ const diskConfig = opts.config || (existsSync(join(root, ".agent/config.yml")) ? loadConfig(root) : null);
1005
+ base = diskConfig?.baseBranch || detectDefaultBranch(root) || "main";
1010
1006
  }
1011
1007
 
1012
1008
  // F08 Guard: In committed mode, base branch can never be HEAD
package/src/dashboard.mjs CHANGED
@@ -4,16 +4,11 @@
4
4
  */
5
5
 
6
6
  import http from "node:http";
7
- import { readFileSync } from "node:fs";
8
- import { fileURLToPath } from "node:url";
9
- import { join, dirname } from "node:path";
10
7
  import { readTelemetry, verifyTelemetryIntegrity } from "./telemetry.mjs";
11
8
  import { readVerifyRuns, flakyVerdict } from "./flaky-ledger.mjs";
12
9
  import { lockStatus } from "./state.mjs";
13
10
  import { KIT_VERSION } from "./version.mjs";
14
11
 
15
- const pkgVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")).version;
16
-
17
12
  export function getDashboardHtml(root = process.cwd()) {
18
13
  return `<!DOCTYPE html>
19
14
  <html lang="en">
@@ -38,7 +33,7 @@ export function getDashboardHtml(root = process.cwd()) {
38
33
  </style>
39
34
  </head>
40
35
  <body>
41
- <h1>🚀 jules-orchestrator-kit Dashboard (v${pkgVersion})</h1>
36
+ <h1>🚀 jules-orchestrator-kit Dashboard (v${KIT_VERSION})</h1>
42
37
  <div class="subtitle">Repository: <code>${root}</code></div>
43
38
 
44
39
  <div class="grid">
@@ -82,8 +77,9 @@ export function getDashboardHtml(root = process.cwd()) {
82
77
  </html>`;
83
78
  }
84
79
 
85
- export function createDashboardServer({ root = process.cwd(), port: _port = 4100 } = {}) {
86
- return http.createServer((req, res) => {
80
+ export function createDashboardServer({ root = process.cwd(), port = 4100 } = {}) {
81
+ const dashboardPort = Number(port);
82
+ const server = http.createServer((req, res) => {
87
83
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
88
84
 
89
85
  if (url.pathname === "/" || url.pathname === "/index.html") {
@@ -93,7 +89,7 @@ export function createDashboardServer({ root = process.cwd(), port: _port = 4100
93
89
 
94
90
  if (url.pathname === "/api/status") {
95
91
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
96
- return res.end(JSON.stringify({ ok: true, version: KIT_VERSION, root, ts: new Date().toISOString() }));
92
+ return res.end(JSON.stringify({ ok: true, version: KIT_VERSION, root, port: dashboardPort, ts: new Date().toISOString() }));
97
93
  }
98
94
 
99
95
  if (url.pathname === "/api/telemetry") {
@@ -124,6 +120,8 @@ export function createDashboardServer({ root = process.cwd(), port: _port = 4100
124
120
  res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
125
121
  res.end(JSON.stringify({ ok: false, error: "Not Found", path: url.pathname }));
126
122
  });
123
+ server.dashboardPort = dashboardPort;
124
+ return server;
127
125
  }
128
126
 
129
127
  export function startDashboardServer(port = 4100, root = process.cwd(), host = "127.0.0.1") {
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Task-dispatch helpers: dynamic guardrails, multimodal attachment
3
+ * collection, static preflight checks, swarm slot partitioning, and the
4
+ * programmatic `dispatchTask` wrapper around the engine.
5
+ *
6
+ * This is the home of the logic that used to live in
7
+ * `scripts/jules-dispatch.mjs`; that file is now a deprecated CLI entry
8
+ * point. Argv parsing stays a pure function so importing this module can
9
+ * never consume the importer's `process.argv` (D10).
10
+ */
11
+
12
+ import { dispatch } from "./engine.mjs";
13
+ import { loadConfig } from "./config.mjs";
14
+ import { existsSync, readFileSync, statSync } from "node:fs";
15
+ import { join } from "node:path";
16
+
17
+ /**
18
+ * Pure argv parser for the deprecated CLI shim. It used to run at module scope,
19
+ * so merely importing the script consumed the importer's process.argv and baked
20
+ * it into dispatchTask()'s defaults. It is now called only from the CLI entry
21
+ * guard at the bottom of scripts/jules-dispatch.mjs.
22
+ *
23
+ * @param {string[]} [argv]
24
+ * @returns {{ title: string, prompt: string, dryRun: boolean, repoless: boolean }}
25
+ */
26
+ export function parseDispatchArgs(argv = []) {
27
+ const args = Array.isArray(argv) ? argv : [];
28
+ let title = "Task Dispatch";
29
+ let prompt = "";
30
+ const dryRun =
31
+ process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1" || args.includes("--dry-run");
32
+ const repoless = args.includes("--repoless");
33
+
34
+ for (let i = 0; i < args.length; i++) {
35
+ if (args[i] === "--title" && args[i + 1]) title = args[i + 1];
36
+ else if (args[i] === "--prompt" && args[i + 1]) prompt = args[i + 1];
37
+ }
38
+
39
+ if (!prompt && args[0] && !args[0].startsWith("-")) {
40
+ title = args[0];
41
+ prompt = args[1] || args[0];
42
+ }
43
+
44
+ if (!prompt) {
45
+ prompt = "Execute task";
46
+ }
47
+
48
+ return { title, prompt, dryRun, repoless };
49
+ }
50
+
51
+ /**
52
+ * Built-in guardrail triggers, used when no project-level
53
+ * `.agent/rules/dynamic-guardrails.json` matches (or the file is absent).
54
+ *
55
+ * Matching is on word boundaries: the old substring checks fired on "sector"
56
+ * ("sec"), "breakfast" ("fast") and "keyboard" ("key"). Role labels are the
57
+ * canonical slugs (security, performance, hygiene, database), not the legacy
58
+ * RPG names.
59
+ */
60
+ const BUILTIN_GUARDRAIL_TRIGGERS = [
61
+ { pattern: /\b(auth|sec|key|token|secret)\b/i, guardrail: "security: SECRET REDACTION GUARDRAILS" },
62
+ { pattern: /\b(perf|optimiz\w*|fast|cache|memoiz\w*)\b/i, guardrail: "performance: Performance Guidance" },
63
+ { pattern: /\b(clean|refactor|lint|deadcode)\b/i, guardrail: "hygiene: Clean Code Guidance" },
64
+ {
65
+ pattern: /\b(db|database|sql\w*|schema|drizzle|postgres\w*|sqlite\w*|mysql\w*)\b/i,
66
+ guardrail: "database: DATABASE GUARDRAILS",
67
+ },
68
+ { pattern: /\b(css|theme|tailwind)\b/i, guardrail: "CSS & DESIGN GUARDRAILS" },
69
+ ];
70
+
71
+ /**
72
+ * Collects the guardrail blocks relevant to a prompt: built-in canonical-role
73
+ * triggers plus any project rules from
74
+ * `<projectRoot>/.agent/rules/dynamic-guardrails.json` (`{ rules:
75
+ * [{ trigger, guardrail }] }`, trigger compiled case-insensitively).
76
+ *
77
+ * @param {string} [promptText]
78
+ * @param {string} [projectRoot]
79
+ * @returns {string} Matching guardrails joined by newlines ("" when none match).
80
+ */
81
+ export function getDynamicGuardrails(promptText = "", projectRoot = process.cwd()) {
82
+ const text = promptText || "";
83
+ const rules = [];
84
+ for (const { pattern, guardrail } of BUILTIN_GUARDRAIL_TRIGGERS) {
85
+ if (pattern.test(text)) rules.push(guardrail);
86
+ }
87
+ try {
88
+ const rulesPath = join(projectRoot || process.cwd(), ".agent", "rules", "dynamic-guardrails.json");
89
+ if (existsSync(rulesPath)) {
90
+ const parsed = JSON.parse(readFileSync(rulesPath, "utf-8"));
91
+ const fileRules = Array.isArray(parsed) ? parsed : parsed?.rules;
92
+ if (Array.isArray(fileRules)) {
93
+ for (const rule of fileRules) {
94
+ if (!rule || typeof rule.trigger !== "string" || typeof rule.guardrail !== "string") continue;
95
+ let trigger;
96
+ try {
97
+ trigger = new RegExp(rule.trigger, "i");
98
+ } catch (_) {
99
+ continue;
100
+ }
101
+ if (trigger.test(text)) rules.push(rule.guardrail);
102
+ }
103
+ }
104
+ }
105
+ } catch (_) {
106
+ // A missing or corrupt rules file must not break dispatch; the built-ins above still apply.
107
+ }
108
+ return rules.join("\n");
109
+ }
110
+
111
+ export function extractImageAttachments(promptText = "", projectRoot = process.cwd()) {
112
+ if (!promptText || typeof promptText !== "string") return [];
113
+ const regex = /!\[([^\]]*)\]\(([^)]+)\)/g;
114
+ const matches = [];
115
+ let match;
116
+ while ((match = regex.exec(promptText)) !== null) {
117
+ const relPath = match[2].trim();
118
+ if (relPath.includes("..")) continue;
119
+ const absPath = join(projectRoot, relPath);
120
+ if (existsSync(absPath)) {
121
+ let mime = "image/png";
122
+ if (relPath.endsWith(".jpg") || relPath.endsWith(".jpeg")) mime = "image/jpeg";
123
+ if (relPath.endsWith(".gif")) mime = "image/gif";
124
+ if (relPath.endsWith(".webp")) mime = "image/webp";
125
+ const size = statSync(absPath).size;
126
+ matches.push({ relPath, absPath, mime, size });
127
+ }
128
+ }
129
+ return matches;
130
+ }
131
+
132
+ export function getMultimodalAttachmentDirective(attachments = []) {
133
+ if (!attachments || attachments.length === 0) return "";
134
+ return `### Multimodal Task Attachments (${attachments.length})\n` + attachments.map((a) => `- ${a.relPath}`).join("\n");
135
+ }
136
+
137
+ /**
138
+ * Real pre-flight manifest check: the project must carry a parseable
139
+ * package.json object. Previously this returned "PASSED" unconditionally, so a
140
+ * missing or corrupt manifest was reported as verified.
141
+ *
142
+ * @param {string} [projectRoot]
143
+ * @returns {string} "PASSED", or "FAILED: <reason>".
144
+ */
145
+ export function runPreflightStaticCheck(projectRoot = process.cwd()) {
146
+ const root = projectRoot || process.cwd();
147
+ const pkgPath = join(root, "package.json");
148
+ if (!existsSync(pkgPath)) {
149
+ return "FAILED: package.json not found";
150
+ }
151
+ let pkg;
152
+ try {
153
+ pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
154
+ } catch (err) {
155
+ return `FAILED: package.json is not valid JSON (${err.message})`;
156
+ }
157
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
158
+ return "FAILED: package.json must contain a JSON object";
159
+ }
160
+ return "PASSED";
161
+ }
162
+
163
+ /**
164
+ * Balanced letter partition for parallel swarm slots: the 26 letters are split
165
+ * into `total` contiguous ranges whose sizes differ by at most one
166
+ * (N=3 → A-I, J-R, S-Z). Out-of-range slots and slots past Z (total > 26) get
167
+ * an empty range. Totals <= 1 keep the historical "A-Z".
168
+ *
169
+ * @param {number|string} [slotIndex]
170
+ * @param {number|string} [totalSlots]
171
+ * @returns {string} e.g. "A-M", "J-R", "Z", or "".
172
+ */
173
+ export function getAlphaRange(slotIndex = 0, totalSlots = 1) {
174
+ const total = Number(totalSlots);
175
+ const idx = Number(slotIndex);
176
+ if (!Number.isFinite(total) || total <= 1) return "A-Z";
177
+ if (!Number.isInteger(idx) || idx < 0 || idx >= total) return "";
178
+ const base = Math.floor(26 / total);
179
+ const extra = 26 % total;
180
+ const count = base + (idx < extra ? 1 : 0);
181
+ if (count <= 0) return "";
182
+ const start = idx * base + Math.min(idx, extra);
183
+ const end = start + count - 1;
184
+ const first = String.fromCharCode(65 + start);
185
+ const last = String.fromCharCode(65 + end);
186
+ return first === last ? first : `${first}-${last}`;
187
+ }
188
+
189
+ export function getSlotPartitionDirective(slotIndex = 0, totalSlots = 1) {
190
+ const total = Number(totalSlots);
191
+ const idx = Number(slotIndex);
192
+ if (total <= 1) return "";
193
+ return `[PARALLEL SWARM SLOT ${idx} of ${total}] Range: ${getAlphaRange(idx, total)} (Partition Focus)`;
194
+ }
195
+
196
+ export async function dispatchTask(opts = {}) {
197
+ const root = process.env.JULES_PROJECT_ROOT || process.cwd();
198
+ const config = loadConfig(root);
199
+ const isDry =
200
+ opts.dryRun ?? (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1");
201
+ const taskTitle = opts.title || "Task Dispatch";
202
+ const taskPrompt = opts.prompt || "Execute task";
203
+
204
+ if (isDry) {
205
+ console.log(`[DRY RUN] Dispatching task: ${taskTitle}`);
206
+ console.log(`[DRY RUN] Dispatch payload prepared successfully`);
207
+ if (opts.repoless) {
208
+ console.log(`[DRY RUN] Target: (repoless / serverless)`);
209
+ }
210
+ }
211
+ return dispatch({ title: taskTitle, prompt: taskPrompt }, { config, dryRun: isDry });
212
+ }