jules-orchestrator-kit 0.66.0 → 0.67.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 +1 -1
- package/bin/agentctl.mjs +11 -1
- package/package.json +1 -1
- package/scripts/guard-reach-check.mjs +24 -0
- package/scripts/run-tests.mjs +18 -1
- package/src/guard-policy.mjs +106 -0
- package/src/security.mjs +40 -0
package/README.md
CHANGED
|
@@ -207,7 +207,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
207
207
|
* **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).
|
|
208
208
|
* **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.
|
|
209
209
|
* **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
|
|
210
|
-
* **Verified Test Suite:** Tested with **
|
|
210
|
+
* **Verified Test Suite:** Tested with **1154 unit tests across 166 suites passing in < 15.0s**.
|
|
211
211
|
|
|
212
212
|
<br/>
|
|
213
213
|
|
package/bin/agentctl.mjs
CHANGED
|
@@ -425,7 +425,17 @@ async function main() {
|
|
|
425
425
|
console.log(`-----------------------------------------------------`);
|
|
426
426
|
for (const p of res.phases) {
|
|
427
427
|
const status = p.ok ? "✅ PASS" : "❌ FAIL";
|
|
428
|
-
|
|
428
|
+
// Test tampering shares this phase, and its exit code, with the
|
|
429
|
+
// secret scanner. The codes are frozen, but reporting an assertion
|
|
430
|
+
// deletion under a bare "SECRETS" heading sent operators looking for
|
|
431
|
+
// a credential to rotate. Name what actually failed.
|
|
432
|
+
const tamperOnly =
|
|
433
|
+
p.phase === "secrets" &&
|
|
434
|
+
!p.ok &&
|
|
435
|
+
(p.findings || []).length > 0 &&
|
|
436
|
+
(p.findings || []).every((f) => f.type !== "SECRET_DETECTED" && !/SECRET/i.test(f.type || ""));
|
|
437
|
+
const label = tamperOnly ? "SECRETS (test integrity — no secret found)" : p.phase.toUpperCase();
|
|
438
|
+
console.log(` Phase [${label}] : ${status}`);
|
|
429
439
|
if (p.violations) {
|
|
430
440
|
p.violations.forEach((v) => console.log(` - Violation: ${v.file} (Rule: ${v.rule})`));
|
|
431
441
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.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",
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
SCOPE_CANARIES,
|
|
57
57
|
INNOCENT_EDITS,
|
|
58
58
|
UNREADABLE_DIALECTS,
|
|
59
|
+
CROSS_FILE_CASES,
|
|
59
60
|
} from "../src/guard-policy.mjs";
|
|
60
61
|
|
|
61
62
|
/**
|
|
@@ -185,6 +186,29 @@ const canaryResults = new Map();
|
|
|
185
186
|
);
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
{
|
|
190
|
+
const wrong = [];
|
|
191
|
+
for (const c of CROSS_FILE_CASES) {
|
|
192
|
+
const lines = [];
|
|
193
|
+
for (const f of c.files) {
|
|
194
|
+
lines.push(`--- a/${f.file}`, `+++ b/${f.file}`, "@@ -1,20 +1,20 @@", ` ${c.context}`);
|
|
195
|
+
for (const l of f.removed) lines.push(`-${l}`);
|
|
196
|
+
for (const l of f.added) lines.push(`+${l}`);
|
|
197
|
+
lines.push(` ${c.context}`);
|
|
198
|
+
}
|
|
199
|
+
const got = (checkTestTampering(lines.join("\n")).violations || []).map((v) => v.type).sort();
|
|
200
|
+
const want = [...c.expect].sort();
|
|
201
|
+
if (JSON.stringify(got) !== JSON.stringify(want)) {
|
|
202
|
+
wrong.push(`${c.id}: got ${JSON.stringify(got)}, policy says ${JSON.stringify(want)} (${c.why})`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
add(
|
|
206
|
+
"a move across files is judged as a move",
|
|
207
|
+
wrong.length === 0,
|
|
208
|
+
wrong.length ? wrong.join("; ") : `${CROSS_FILE_CASES.length} two-file diffs judged as declared`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
188
212
|
// --- 4. Predicate mutants ---------------------------------------------------
|
|
189
213
|
{
|
|
190
214
|
const survivors = [];
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readdirSync } from "node:fs";
|
|
2
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import { cpus } from "node:os";
|
|
6
6
|
|
|
7
7
|
const root = process.cwd();
|
|
8
8
|
const testDir = join(root, "test");
|
|
9
|
+
|
|
10
|
+
// The published package ships `scripts/` and not `test/`, so running
|
|
11
|
+
// `npm test` inside an installed copy crashed with a raw
|
|
12
|
+
// `ENOENT: no such file or directory, scandir '.../test'`. The suite is not
|
|
13
|
+
// missing; it was never part of the tarball. Say that, rather than leaving
|
|
14
|
+
// someone to work it out from a stack trace.
|
|
15
|
+
if (!existsSync(testDir)) {
|
|
16
|
+
console.error("No test/ directory here.");
|
|
17
|
+
console.error("");
|
|
18
|
+
console.error("If this is an installed copy of jules-orchestrator-kit, that is expected:");
|
|
19
|
+
console.error("the test suite is not part of the published package. Clone the repository");
|
|
20
|
+
console.error("to run it: git clone https://github.com/FullThrottle83/jules-orchestrator-kit");
|
|
21
|
+
console.error("");
|
|
22
|
+
console.error("To check an installed copy instead, run: npm run guard-reach");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
const testFiles = readdirSync(testDir)
|
|
10
27
|
.filter((f) => f.endsWith(".test.mjs"))
|
|
11
28
|
.sort()
|
package/src/guard-policy.mjs
CHANGED
|
@@ -235,6 +235,58 @@ export const TAMPER_CANARIES = [
|
|
|
235
235
|
added: [" $this->markTestSkipped(\"later\");"],
|
|
236
236
|
expect: "TEST_SKIP_INJECTION",
|
|
237
237
|
},
|
|
238
|
+
// Skipping from inside the test body, which is how every one of these
|
|
239
|
+
// ecosystems actually does it. The decorator and annotation forms were
|
|
240
|
+
// covered; the standard library's own method was not, so a test could be
|
|
241
|
+
// silenced with the most ordinary call in the language.
|
|
242
|
+
{
|
|
243
|
+
id: "skip-injection/unittest-body",
|
|
244
|
+
file: "tests/test_app.py",
|
|
245
|
+
context: "# app",
|
|
246
|
+
removed: [],
|
|
247
|
+
added: [' self.skipTest("disabled")'],
|
|
248
|
+
expect: "TEST_SKIP_INJECTION",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "skip-injection/pytest-body",
|
|
252
|
+
file: "tests/test_app.py",
|
|
253
|
+
context: "# app",
|
|
254
|
+
removed: [],
|
|
255
|
+
added: [' pytest.skip("not now")'],
|
|
256
|
+
expect: "TEST_SKIP_INJECTION",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "skip-injection/raise-skiptest",
|
|
260
|
+
file: "tests/test_app.py",
|
|
261
|
+
context: "# app",
|
|
262
|
+
removed: [],
|
|
263
|
+
added: [' raise unittest.SkipTest("nope")'],
|
|
264
|
+
expect: "TEST_SKIP_INJECTION",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: "skip-injection/mocha-body",
|
|
268
|
+
file: "test/app.test.js",
|
|
269
|
+
context: "// app",
|
|
270
|
+
removed: [],
|
|
271
|
+
added: [" this.skip();"],
|
|
272
|
+
expect: "TEST_SKIP_INJECTION",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "skip-injection/jest-todo",
|
|
276
|
+
file: "test/app.test.js",
|
|
277
|
+
context: "// app",
|
|
278
|
+
removed: [],
|
|
279
|
+
added: [' test.todo("adds two numbers");'],
|
|
280
|
+
expect: "TEST_SKIP_INJECTION",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: "skip-injection/go-skipnow",
|
|
284
|
+
file: "internal/calc/calc_test.go",
|
|
285
|
+
context: "// calc",
|
|
286
|
+
removed: [],
|
|
287
|
+
added: ["\tt.SkipNow()"],
|
|
288
|
+
expect: "TEST_SKIP_INJECTION",
|
|
289
|
+
},
|
|
238
290
|
{
|
|
239
291
|
id: "skip-injection/xctest",
|
|
240
292
|
file: "Tests/CalcTests/CalcTests.swift",
|
|
@@ -551,3 +603,57 @@ export const MULTILINE_CANARIES = [
|
|
|
551
603
|
expect: "ASSERTION_EXPECTATION_CHANGED",
|
|
552
604
|
},
|
|
553
605
|
];
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Diffs that touch two files at once, where the verdict depends on both.
|
|
609
|
+
*
|
|
610
|
+
* Assertion tracking is per file, so moving a test between files — ordinary
|
|
611
|
+
* refactoring, done constantly — read as deleting every assertion in it and
|
|
612
|
+
* was reported as CRITICAL tampering. The assertion still exists and still
|
|
613
|
+
* runs. What the removal check is for is verification that *disappeared*.
|
|
614
|
+
*
|
|
615
|
+
* `expect: []` means the whole diff must produce no finding.
|
|
616
|
+
*/
|
|
617
|
+
export const CROSS_FILE_CASES = [
|
|
618
|
+
{
|
|
619
|
+
id: "move-a-test-between-files",
|
|
620
|
+
context: "# ctx",
|
|
621
|
+
files: [
|
|
622
|
+
{
|
|
623
|
+
file: "test/test_app.py",
|
|
624
|
+
removed: [" def test_setattr(self):", " self.assertEqual(5, app.test)", " self.assertRaises(AttributeError, setattr, app, 't', 6)"],
|
|
625
|
+
added: [],
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
file: "test/test_html.py",
|
|
629
|
+
removed: [],
|
|
630
|
+
added: [" def test_setattr_moved(self):", " self.assertEqual(5, app.test)", " self.assertRaises(AttributeError, setattr, app, 't', 6)"],
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
expect: [],
|
|
634
|
+
why: "a move is not a removal",
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
id: "delete-a-test-outright",
|
|
638
|
+
context: "# ctx",
|
|
639
|
+
files: [
|
|
640
|
+
{
|
|
641
|
+
file: "test/test_app.py",
|
|
642
|
+
removed: [" def test_setattr(self):", " self.assertEqual(5, app.test)", " self.assertRaises(AttributeError, setattr, app, 't', 6)"],
|
|
643
|
+
added: [],
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
expect: ["ASSERTION_REMOVAL", "ASSERTION_REMOVAL"],
|
|
647
|
+
why: "nothing caught it on the way, so verification really did disappear",
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
id: "change-it-on-the-way-across",
|
|
651
|
+
context: "# ctx",
|
|
652
|
+
files: [
|
|
653
|
+
{ file: "test/test_app.py", removed: [" self.assertEqual(5, app.test)"], added: [] },
|
|
654
|
+
{ file: "test/test_html.py", removed: [], added: [" self.assertEqual(99, app.test)"] },
|
|
655
|
+
],
|
|
656
|
+
expect: ["ASSERTION_REMOVAL"],
|
|
657
|
+
why: "what arrived is a different claim; only an exact arrival is a move",
|
|
658
|
+
},
|
|
659
|
+
];
|
package/src/security.mjs
CHANGED
|
@@ -2195,6 +2195,17 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
2195
2195
|
{ pattern: /\b(?:xit|xdescribe|xcontext|xspecify)\b\s*["\x27]/i, desc: "Injected RSpec disabled example (xit)" },
|
|
2196
2196
|
{ pattern: /,\s*skip:\s*(?:true|["\x27])/i, desc: "Injected RSpec skip metadata (skip:)" },
|
|
2197
2197
|
{ pattern: /^\s*(?:skip|pending)\s*(?:["\x27(]|$)/i, desc: "Injected Minitest/RSpec skip statement" },
|
|
2198
|
+
// Skipping from inside the body, which is how these ecosystems actually
|
|
2199
|
+
// do it. Only the decorator and annotation forms were covered, so a test
|
|
2200
|
+
// could be silenced with the standard library's own method and the guard
|
|
2201
|
+
// said nothing: measured silent on six of seven in-body forms.
|
|
2202
|
+
{ pattern: /\bself\.skipTest\s*\(/i, desc: "Injected unittest skip (self.skipTest())" },
|
|
2203
|
+
{ pattern: /\braise\s+(?:unittest\.)?SkipTest\b/i, desc: "Injected unittest skip (raise SkipTest)" },
|
|
2204
|
+
{ pattern: /\bpytest\.skip\s*\(/i, desc: "Injected Pytest skip call (pytest.skip())" },
|
|
2205
|
+
{ pattern: /\bpytest\.xfail\s*\(/i, desc: "Injected Pytest expected-failure (pytest.xfail())" },
|
|
2206
|
+
{ pattern: /\bthis\.skip\s*\(/i, desc: "Injected Mocha skip (this.skip())" },
|
|
2207
|
+
{ pattern: /\b(?:it|test|describe|context)\.todo\s*\(/i, desc: "Injected todo placeholder (test.todo())" },
|
|
2208
|
+
{ pattern: /\bt\.Skip(?:Now|f)?\s*\(/, desc: "Injected Go test skip (t.Skip/t.Skipf/t.SkipNow)" },
|
|
2198
2209
|
];
|
|
2199
2210
|
|
|
2200
2211
|
// `#` and `--` belong here for the same reason the dialects belong in
|
|
@@ -2369,6 +2380,26 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
2369
2380
|
}
|
|
2370
2381
|
}
|
|
2371
2382
|
|
|
2383
|
+
// Assertions added in some *other* file of this same diff.
|
|
2384
|
+
//
|
|
2385
|
+
// Tracking is strictly per file, so moving a test from one file to another
|
|
2386
|
+
// — ordinary refactoring — read as deleting three assertions and was
|
|
2387
|
+
// reported as CRITICAL tampering. The assertion still exists and still
|
|
2388
|
+
// runs; it is in a different file. What the removal check is for is
|
|
2389
|
+
// verification that *disappeared*, and this has not.
|
|
2390
|
+
//
|
|
2391
|
+
// Deliberately exact on the assertion text: something that changed on the
|
|
2392
|
+
// way across is not a move, and is judged normally.
|
|
2393
|
+
const movedIn = new Map();
|
|
2394
|
+
for (const [file, stats] of fileAssertions.entries()) {
|
|
2395
|
+
for (const t of stats.addedTexts || []) {
|
|
2396
|
+
const key = collapseWhitespace(String(t.text ?? t)).trim();
|
|
2397
|
+
if (!key) continue;
|
|
2398
|
+
if (!movedIn.has(key)) movedIn.set(key, []);
|
|
2399
|
+
movedIn.get(key).push(file);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2372
2403
|
for (const [file, stats] of fileAssertions.entries()) {
|
|
2373
2404
|
// An expectation that was rewritten rather than removed.
|
|
2374
2405
|
//
|
|
@@ -2386,6 +2417,15 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
2386
2417
|
if (stats.removed.length > stats.added) {
|
|
2387
2418
|
const unreplaced = stats.removed.slice(stats.added);
|
|
2388
2419
|
for (const item of unreplaced) {
|
|
2420
|
+
// Did this exact assertion arrive somewhere else in the same diff?
|
|
2421
|
+
const key = collapseWhitespace(item.text).trim();
|
|
2422
|
+
const landing = movedIn.get(key);
|
|
2423
|
+
const elsewhere = landing ? landing.findIndex((f) => f !== file) : -1;
|
|
2424
|
+
if (elsewhere !== -1) {
|
|
2425
|
+
// Consume the landing so two removals cannot both claim one arrival.
|
|
2426
|
+
landing.splice(elsewhere, 1);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2389
2429
|
violations.push({
|
|
2390
2430
|
file,
|
|
2391
2431
|
line: item.line,
|