jules-orchestrator-kit 0.56.0 → 0.57.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/README.md CHANGED
@@ -204,7 +204,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
204
204
  * **Fail-Closed Security & Secret Redaction:** Evaluates explicit Deny rules before Allow rules against canonicalized, case-folded paths. Redacts high-entropy keys and base64-encoded credentials (such as Kubernetes `Secret` manifests).
205
205
  * **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors, with a `node --check` syntax-verification gate that transparently escalates a FAST-tier result to the primary provider if it left broken JS on disk.
206
206
  * **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
207
- * **Verified Test Suite:** Tested with **851 unit tests across 115 suites passing in < 14.0s**.
207
+ * **Verified Test Suite:** Tested with **864 unit tests across 121 suites passing in < 15.0s**.
208
208
 
209
209
  <br/>
210
210
 
package/bin/agentctl.mjs CHANGED
@@ -570,8 +570,16 @@ async function main() {
570
570
  if (values.committed) selectedMode = "committed";
571
571
  if (values["working-tree"]) selectedMode = "working-tree";
572
572
 
573
- const minScore = Number(values["min-score"]) || 80;
574
- const maxMutants = Number(values["max-mutants"]) || 20;
573
+ // `Number(x) || default` swallows a legitimate zero: `--min-score 0`,
574
+ // the way to run the harness for its report without a threshold, silently
575
+ // enforced 80. Both flags already carry a parseArgs default, so the only
576
+ // thing left to guard against is a value that is not a number at all.
577
+ const parseNumericFlag = (raw, fallback) => {
578
+ const n = Number(raw);
579
+ return Number.isFinite(n) ? n : fallback;
580
+ };
581
+ const minScore = parseNumericFlag(values["min-score"], 80);
582
+ const maxMutants = parseNumericFlag(values["max-mutants"], 20);
575
583
  const testCmd = values.cmd || config.verify?.test || "npm test";
576
584
 
577
585
  if (!values.json) {
@@ -597,7 +605,11 @@ async function main() {
597
605
  console.log(` • Mutants Killed (Fail) : ${report.killedMutants} ✅`);
598
606
  console.log(` • Mutants Survived (Pass) : ${report.survivedMutants} ${report.survivedMutants > 0 ? "⚠️" : ""}`);
599
607
  console.log(` • Errors / Timeouts : ${report.errorMutants}`);
600
- console.log(` • Mutation Score : ${report.mutationScore}% (Required: ${report.minScore}%)`);
608
+ console.log(
609
+ report.scored === false
610
+ ? ` • Mutation Score : n/a — ${report.reason}`
611
+ : ` • Mutation Score : ${report.mutationScore}% (Required: ${report.minScore}%)`
612
+ );
601
613
  console.log(` • Duration : ${report.durationMs}ms\n`);
602
614
 
603
615
  if (report.survivors.length > 0) {
@@ -1069,7 +1081,29 @@ async function main() {
1069
1081
  });
1070
1082
 
1071
1083
  const queueDir = getQueueDir(root);
1072
- const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
1084
+ // The DAG runner accepts `.json` and `.task` envelopes as well as
1085
+ // Markdown, and does its own discovery — but this count gated whether it
1086
+ // was ever called, using the Markdown-only filter. A queue holding only
1087
+ // JSON envelopes reported "0 queued task(s)" and did nothing, with no
1088
+ // indication that the files were there and understood.
1089
+ const queueEntries = readdirSync(queueDir);
1090
+ let files;
1091
+ if (values.dag) {
1092
+ const { isDagTaskFile } = await import("../src/dag-engine.mjs");
1093
+ files = queueEntries.filter((f) => {
1094
+ if (f === "completed" || f.startsWith(".")) return false;
1095
+ if (!/\.(md|json|task)$/.test(f)) return false;
1096
+ let content = "";
1097
+ try {
1098
+ content = readFileSync(join(queueDir, f), "utf-8");
1099
+ } catch (_) {
1100
+ return false;
1101
+ }
1102
+ return isDagTaskFile(f, content, isTaskFile);
1103
+ });
1104
+ } else {
1105
+ files = queueEntries.filter((f) => isTaskFile(f, queueDir));
1106
+ }
1073
1107
  console.log(`Found ${files.length} queued task(s) in .agent/jules-queue/`);
1074
1108
  if (files.length > 0) {
1075
1109
  const concurrency = values.concurrency ? Number(values.concurrency) : undefined;
@@ -1092,11 +1126,30 @@ async function main() {
1092
1126
  }
1093
1127
 
1094
1128
  case "swarm": {
1095
- console.log("🚀 Running Swarm Orchestrator...");
1129
+ // This case had no parseArgs at all: `--json` and `--dry-run` were
1130
+ // accepted by the shell, documented in the registry, and silently
1131
+ // discarded — so a rehearsal dispatched for real and a script asking for
1132
+ // JSON got decorated prose.
1133
+ const { values } = parseArgs({
1134
+ args: args.slice(1),
1135
+ options: {
1136
+ concurrency: { type: "string", short: "c" },
1137
+ "dry-run": { type: "boolean", short: "d" },
1138
+ json: { type: "boolean", short: "j" },
1139
+ },
1140
+ allowPositionals: true,
1141
+ strict: false,
1142
+ });
1143
+
1096
1144
  const queueDir = getQueueDir(root);
1097
1145
  const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
1146
+ if (!values.json) console.log("🚀 Running Swarm Orchestrator...");
1098
1147
  if (files.length === 0) {
1099
- console.log("No pending tasks found for swarm.");
1148
+ if (values.json) {
1149
+ console.log(JSON.stringify({ ok: true, processed: 0, results: [], dryRun: Boolean(values["dry-run"]) }, null, 2));
1150
+ } else {
1151
+ console.log("No pending tasks found for swarm.");
1152
+ }
1100
1153
  process.exit(0);
1101
1154
  }
1102
1155
  const tasks = files.map((f) => ({
@@ -1104,7 +1157,16 @@ async function main() {
1104
1157
  title: f.replace(/\.md$/, ""),
1105
1158
  prompt: readFileSync(join(queueDir, f), "utf-8"),
1106
1159
  }));
1107
- const results = await run(tasks, { root, config, concurrency: config.limits.concurrency || 3 });
1160
+ const results = await run(tasks, {
1161
+ root,
1162
+ config,
1163
+ concurrency: values.concurrency ? Number(values.concurrency) : config.limits.concurrency || 3,
1164
+ dryRun: values["dry-run"],
1165
+ });
1166
+ if (values.json) {
1167
+ console.log(JSON.stringify({ ok: true, dryRun: Boolean(values["dry-run"]), ...results }, null, 2));
1168
+ process.exit((results.results || []).some((r) => r && r.ok === false) ? 1 : 0);
1169
+ }
1108
1170
  process.exit(reportRunOutcome(results));
1109
1171
  break;
1110
1172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for autonomous coding agents — Google Jules, Claude Code, Codex and Gemini CLI.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -513,6 +513,11 @@ export function assertMutation(config = {}, root = process.cwd()) {
513
513
  });
514
514
 
515
515
  const diagnostics = [];
516
+ if (report.scored === false) {
517
+ // Not a failure, but not a pass worth trusting either: say so, so a green
518
+ // stage cannot be read as "the diff survived mutation testing".
519
+ diagnostics.push(report.reason || "No mutants could be generated from the added lines.");
520
+ }
516
521
  if (!report.ok) {
517
522
  diagnostics.push(
518
523
  `Mutation score ${report.mutationScore}% below required threshold of ${minScore}% (${report.killedMutants}/${report.totalMutants} killed, ${report.survivedMutants} survived).`
@@ -431,7 +431,7 @@ export function resolveAffectedTests(modifiedFiles = [], options = {}) {
431
431
  * @param {Function} isTaskFile - `isTaskFile` from engine.mjs, passed in to avoid a circular import.
432
432
  * @returns {boolean}
433
433
  */
434
- function isDagTaskFile(fileName, content, isTaskFile) {
434
+ export function isDagTaskFile(fileName, content, isTaskFile) {
435
435
  if (fileName.endsWith(".task")) return true;
436
436
  if (fileName.endsWith(".md")) return isTaskFile(fileName, content);
437
437
  if (fileName.endsWith(".json")) {
package/src/git.mjs CHANGED
@@ -384,8 +384,17 @@ export function diffText(root = process.cwd(), base = "main", mode = "committed"
384
384
  const fullPath = join(root, file);
385
385
  if (existsSync(fullPath)) {
386
386
  const content = readFileSync(fullPath, "utf-8");
387
+ const addedLines = content.split(/\r?\n/);
388
+ // The `@@` header is not decoration. Every consumer that needs to know
389
+ // *which* line an addition is on parses it — the mutation harness and
390
+ // the V8 diff-coverage mapper both walk hunks — so a synthetic diff
391
+ // without one made untracked files invisible to them. Both silently
392
+ // reported nothing to do for a brand-new file, which is precisely
393
+ // where untested code arrives. The secret scanner never noticed
394
+ // because it only reads `+` lines.
387
395
  untrackedDiff += `\ndiff --git a/${file} b/${file}\nnew file mode 100644\n--- /dev/null\n+++ b/${file}\n`;
388
- untrackedDiff += content.split(/\r?\n/).map((line) => `+${line}`).join("\n") + "\n";
396
+ untrackedDiff += `@@ -0,0 +1,${addedLines.length} @@\n`;
397
+ untrackedDiff += addedLines.map((line) => `+${line}`).join("\n") + "\n";
389
398
  }
390
399
  } catch (_) {}
391
400
  }
package/src/mutation.mjs CHANGED
@@ -676,13 +676,23 @@ export function runMutationTest(options = {}) {
676
676
  const selectedMutants = candidates.slice(0, maxMutants);
677
677
 
678
678
  if (selectedMutants.length === 0) {
679
+ // A diff with no mutable operators yields no mutants, and reporting that as
680
+ // "100%" told operators their untested code had a perfect score — the exact
681
+ // false confidence the harness exists to remove. There is no score to
682
+ // report, so there is none: `mutationScore` is null and `reason` says why.
683
+ //
684
+ // `ok` stays true. Nothing was falsifiable, so nothing failed to be
685
+ // falsified; failing here would block every diff that only adds imports,
686
+ // constants or markdown, and a gate that cries wolf gets switched off.
679
687
  return {
680
688
  ok: true,
681
689
  totalMutants: 0,
682
690
  killedMutants: 0,
683
691
  survivedMutants: 0,
684
692
  errorMutants: 0,
685
- mutationScore: 100,
693
+ mutationScore: null,
694
+ scored: false,
695
+ reason: "No mutable operators in the added lines — nothing to falsify, so no score was computed.",
686
696
  minScore,
687
697
  results: [],
688
698
  survivors: [],
@@ -115,23 +115,27 @@ export const COMMAND_REGISTRY = [
115
115
  },
116
116
  {
117
117
  id: "swarm",
118
+ // Described as an inspector — `mutates: false`, `risk: low` — while the
119
+ // handler dispatches every queued task in parallel and spends budget.
120
+ // `--interactive` was advertised and implemented nowhere.
118
121
  path: ["swarm"],
119
122
  title: "swarm",
120
- description: "Inspect active worker slots and concurrency scheduler",
123
+ description: "Dispatch every queued task in parallel across worker slots",
121
124
  category: "Operate",
122
- mutates: false,
123
- risk: "low",
124
- interactive: "optional",
125
+ mutates: true,
126
+ risk: "moderate",
127
+ interactive: "never",
125
128
  requiresRepository: true,
126
129
  shortcuts: ["s"],
127
130
  examples: [
131
+ "agentctl swarm --dry-run",
128
132
  "agentctl swarm",
129
- "agentctl swarm --interactive",
130
- "agentctl swarm --json",
133
+ "agentctl swarm --concurrency 3 --json",
131
134
  ],
132
135
  flags: [
133
- { name: "interactive", type: "boolean", description: "Open full-screen swarm dashboard" },
134
- { name: "json", type: "boolean", description: "Output structured JSON swarm snapshot" },
136
+ { name: "concurrency", type: "string", description: "Parallel worker slots (defaults to limits.concurrency)" },
137
+ { name: "dry-run", type: "boolean", description: "Report what would run without dispatching" },
138
+ { name: "json", type: "boolean", description: "Output structured JSON swarm result" },
135
139
  ],
136
140
  },
137
141
  {
@@ -1,5 +1,5 @@
1
1
  import { writeFileSync, mkdirSync } from "node:fs";
2
- import { join, basename } from "node:path";
2
+ import { join } from "node:path";
3
3
  import { detectPolyglotStack } from "../stack-detector.mjs";
4
4
  import { resolveRoot } from "../config.mjs";
5
5
  import { runCmd } from "../git.mjs";
@@ -26,45 +26,100 @@ export function scaffoldTddTest(spec = {}, options = {}) {
26
26
  const details = spec.spec || "Feature requirement specification assertion.";
27
27
 
28
28
  const stack = detectPolyglotStack(root);
29
- const testDir = join(root, "test");
30
- try {
31
- mkdirSync(testDir, { recursive: true });
32
- } catch (_) {}
33
29
 
34
- const fileName = `generated-${title}.test.mjs`;
35
- const filePath = join(testDir, fileName);
30
+ // The generated oracle has to be written in the language its runner speaks.
31
+ //
32
+ // This used to emit a Node test file for every stack and then, in a Python
33
+ // project, run `pytest generated-x.test.mjs`. pytest exits 4 on a file it
34
+ // cannot collect, and the cycle read any non-zero exit as RED — so it
35
+ // reported a verified failing test, and locked an uncollectable file into
36
+ // scope.deny, having proven nothing at all.
37
+ const identifier = title.replace(/-/g, "_");
38
+
39
+ // The one string that must appear in the runner's output for the failure to
40
+ // be the *assertion* failing rather than the file never being collected.
41
+ const redMarker = `TDD Assertion Failed: Requirement '${title}' is not yet implemented.`;
42
+ const comment = (prefix) => details.split("\n").map((line) => `${prefix} ${line}`).join("\n");
43
+
44
+ let relDir = "test";
45
+ let fileName = `generated-${title}.test.mjs`;
46
+ let codeContent;
47
+ let testCmdParts;
48
+ let testCmdStr;
49
+
50
+ if (stack.stack === "python" || stack.stack === "django") {
51
+ fileName = `test_generated_${identifier}.py`;
52
+ codeContent = `def test_tdd_oracle_${identifier}():
53
+ # REQUIREMENT SPECIFICATION:
54
+ ${comment(" #")}
55
+ is_implemented = False
56
+ assert is_implemented, "${redMarker}"
57
+ `;
58
+ testCmdParts = ["pytest", `${relDir}/${fileName}`];
59
+ testCmdStr = `pytest ${relDir}/${fileName}`;
60
+ } else if (stack.stack === "cargo") {
61
+ // Cargo discovers integration tests in tests/, not test/.
62
+ relDir = "tests";
63
+ fileName = `generated_${identifier}.rs`;
64
+ codeContent = `#[test]
65
+ fn tdd_oracle_${identifier}() {
66
+ // REQUIREMENT SPECIFICATION:
67
+ ${comment(" //")}
68
+ let is_implemented = false;
69
+ assert!(is_implemented, "${redMarker}");
70
+ }
71
+ `;
72
+ testCmdParts = ["cargo", "test", "--test", `generated_${identifier}`];
73
+ testCmdStr = `cargo test --test generated_${identifier}`;
74
+ } else if (stack.stack === "go") {
75
+ fileName = `generated_${identifier}_test.go`;
76
+ codeContent = `package ${relDir}
36
77
 
37
- const codeContent = `import test from "node:test";
78
+ import "testing"
79
+
80
+ func TestTddOracle${identifier.replace(/_/g, "")}(t *testing.T) {
81
+ // REQUIREMENT SPECIFICATION:
82
+ ${comment("\t//")}
83
+ isImplemented := false
84
+ if !isImplemented {
85
+ t.Fatalf("${redMarker}")
86
+ }
87
+ }
88
+ `;
89
+ testCmdParts = ["go", "test", `./${relDir}/`];
90
+ testCmdStr = `go test ./${relDir}/`;
91
+ } else {
92
+ codeContent = `import test from "node:test";
38
93
  import assert from "node:assert/strict";
39
94
 
40
95
  test("TDD Oracle: ${title}", () => {
41
96
  // REQUIREMENT SPECIFICATION:
42
- // ${details.replace(/\n/g, "\n // ")}
97
+ ${comment(" //")}
43
98
 
44
99
  const isImplemented = false;
45
- assert.equal(isImplemented, true, "TDD Assertion Failed: Requirement '${title}' is not yet implemented.");
100
+ assert.equal(isImplemented, true, "${redMarker}");
46
101
  });
47
102
  `;
103
+ testCmdParts = ["node", "--test", join(root, relDir, fileName)];
104
+ testCmdStr = `node --test ${relDir}/${fileName}`;
105
+ }
48
106
 
49
- writeFileSync(filePath, codeContent, "utf-8");
107
+ const testDir = join(root, relDir);
108
+ try {
109
+ mkdirSync(testDir, { recursive: true });
110
+ } catch (_) {}
50
111
 
51
- const relativePath = `test/${fileName}`;
52
- let testCmd = ["node", "--test", filePath];
53
- let testCmdStr = `node --test ${relativePath}`;
54
- if (stack.stack === "python") {
55
- testCmd = ["pytest", filePath];
56
- testCmdStr = `pytest ${relativePath}`;
57
- } else if (stack.stack === "cargo") {
58
- testCmd = ["cargo", "test", "--test", title];
59
- testCmdStr = `cargo test --test ${title}`;
60
- }
112
+ const filePath = join(testDir, fileName);
113
+ writeFileSync(filePath, codeContent, "utf-8");
61
114
 
62
115
  return {
63
116
  filePath,
64
- relativePath,
117
+ relativePath: `${relDir}/${fileName}`,
65
118
  codeContent,
66
- testCmd,
119
+ testCmd: testCmdParts,
67
120
  testCmdStr,
121
+ stack: stack.stack,
122
+ redMarker,
68
123
  };
69
124
  }
70
125
 
@@ -98,7 +153,21 @@ export async function runTddCycle(spec = {}, options = {}) {
98
153
 
99
154
  if (redOutput.status === 0) {
100
155
  throw new TddError(
101
- `TDD RED check failed: Test 'test/${basename(scaffolded.filePath)}' passed initially! TDD tests must be falsifiable and fail before implementation.`
156
+ `TDD RED check failed: Test '${scaffolded.relativePath}' passed initially! TDD tests must be falsifiable and fail before implementation.`
157
+ );
158
+ }
159
+
160
+ // A non-zero exit is not proof the assertion ran. `pytest` exits 4 on a file
161
+ // it cannot collect and 5 when it collects nothing; a runner pointed at a
162
+ // file it does not understand exits non-zero for that reason alone. Reading
163
+ // either as RED is how a Node test file in a Python project came to be
164
+ // reported as a verified failing oracle. The generated assertion carries a
165
+ // marker; if the runner never reached it, the marker is not in the output.
166
+ const redText = `${redOutput.stdout || ""}\n${redOutput.stderr || ""}`;
167
+ if (scaffolded.redMarker && !redText.includes(scaffolded.redMarker)) {
168
+ throw new TddError(
169
+ `TDD RED check inconclusive: '${scaffolded.testCmdStr}' exited ${redOutput.status} without reaching the generated assertion, so nothing was proven falsifiable. ` +
170
+ `The runner most likely could not collect '${scaffolded.relativePath}' (detected stack: ${scaffolded.stack}). Output:\n${redText.trim().slice(0, 500)}`
102
171
  );
103
172
  }
104
173