jules-orchestrator-kit 0.52.0 → 0.52.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
@@ -59,7 +59,7 @@ npx jules-orchestrator-kit init
59
59
  ```bash
60
60
  # 2. Commit what init wrote — .agent/config.yml is on the gate's deny list by
61
61
  # design, so leaving it uncommitted makes the first gate reject your tree
62
- git add .agent AGENTS.md .gitignore && git commit -m "chore: add agent config"
62
+ git add .agent AGENTS.md SPEC.md CONSTRAINTS.md .gitignore && git commit -m "chore: add agent config"
63
63
  ```
64
64
 
65
65
  ```bash
@@ -143,7 +143,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
143
143
  * **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).
144
144
  * **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.
145
145
  * **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
146
- * **Verified Test Suite:** Tested with **797 unit tests across 91 suites passing in < 10.0s**.
146
+ * **Verified Test Suite:** Tested with **802 unit tests across 91 suites passing in < 10.0s**.
147
147
 
148
148
  <br/>
149
149
 
package/bin/agentctl.mjs CHANGED
@@ -228,9 +228,16 @@ async function main() {
228
228
  process.exit(0);
229
229
  }
230
230
 
231
- // Intercept --help / -h on any subcommand (e.g. `agentctl init --help`)
231
+ // Resolve --help / -h on subcommands (e.g. `agentctl init --help`, `agentctl mutate --help`)
232
232
  const subArgs = args.slice(1);
233
233
  if (subArgs.includes("--help") || subArgs.includes("-h")) {
234
+ const { getCommandDescriptor, formatCommandHelp } = await import("../src/ops/command-registry.mjs");
235
+ const subSub = subArgs[0] && !subArgs[0].startsWith("-") ? `${command} ${subArgs[0]}` : command;
236
+ const desc = getCommandDescriptor(subSub) || getCommandDescriptor(command);
237
+ if (desc) {
238
+ console.log(formatCommandHelp(desc));
239
+ process.exit(0);
240
+ }
234
241
  printHelp();
235
242
  process.exit(0);
236
243
  }
@@ -1236,6 +1243,9 @@ async function main() {
1236
1243
  args: args.slice(1),
1237
1244
  options: {
1238
1245
  interactive: { type: "boolean", short: "i" },
1246
+ "non-interactive": { type: "boolean" },
1247
+ "no-interactive": { type: "boolean" },
1248
+ yes: { type: "boolean", short: "y" },
1239
1249
  tier: { type: "string", short: "t" },
1240
1250
  json: { type: "boolean", short: "j" },
1241
1251
  "dry-run": { type: "boolean", short: "d" },
@@ -1244,9 +1254,11 @@ async function main() {
1244
1254
  allowPositionals: true,
1245
1255
  });
1246
1256
 
1257
+ const isInteractive = values.interactive !== false && !values["non-interactive"] && !values["no-interactive"] && !values.yes;
1258
+
1247
1259
  const { runInitWizard } = await import("../src/wizard-init.mjs");
1248
1260
  const res = await runInitWizard(root, {
1249
- interactive: values.interactive !== false,
1261
+ interactive: isInteractive,
1250
1262
  // No `|| "pro"`: a hardcoded default here overrode both the tier picked
1251
1263
  // in the menu and the tier already recorded in .agent/config.yml on a
1252
1264
  // re-run. Undefined lets the wizard seed the menu from the existing
@@ -1267,7 +1279,11 @@ async function main() {
1267
1279
  } else {
1268
1280
  console.log(`✅ Onboarding complete! Manifest generated at ${res.configPath}`);
1269
1281
  console.log(` Tier: ${res.plan.tier.toUpperCase()} (${res.plan.limits.concurrency} worker(s), ${res.plan.limits.daily_tasks} daily tasks)`);
1270
- console.log(` Verification Test Command : "${res.plan.verify.test}"`);
1282
+ if (res.plan.verify.test) {
1283
+ console.log(` Verification Test Command : "${res.plan.verify.test}"`);
1284
+ } else {
1285
+ console.log(` Verification Test Command : None detected (run "agentctl bootstrap" to create a test oracle)`);
1286
+ }
1271
1287
  console.log(` Active Presets : ${res.plan.presets.join(", ")}`);
1272
1288
  for (const item of scaffold.created) {
1273
1289
  console.log(` Scaffolded : ${item}`);
@@ -1281,8 +1297,10 @@ async function main() {
1281
1297
  // uncommitted meant the very first `agentctl gate` rejected the working
1282
1298
  // tree for files init had just written, which reads as the tool
1283
1299
  // catching the user cheating on step three.
1284
- console.log(`\n Commit the manifest so the gate does not read it as an agent edit:`);
1285
- console.log(` git add .agent AGENTS.md .gitignore && git commit -m "chore: add agent config"`);
1300
+ const rootContracts = ["SPEC.md", "CONSTRAINTS.md", "DESIGN.md"].filter((f) => existsSync(join(root, f)));
1301
+ const filesToAdd = [".agent", "AGENTS.md", ...rootContracts, ".gitignore"].filter((f) => existsSync(join(root, f)));
1302
+ console.log(`\n Commit the manifest and contracts so the gate does not read them as agent edits:`);
1303
+ console.log(` git add ${filesToAdd.join(" ")} && git commit -m "chore: add agent config"`);
1286
1304
 
1287
1305
  const { resolveNextStep, renderNextStep } = await import("../src/ops/next-step.mjs");
1288
1306
  const next = resolveNextStep(root);
@@ -1310,12 +1328,19 @@ async function main() {
1310
1328
  "auto-pr": { type: "boolean" },
1311
1329
  "require-plan-approval": { type: "boolean" },
1312
1330
  repoless: { type: "boolean" },
1331
+ interactive: { type: "boolean", short: "i" },
1332
+ "non-interactive": { type: "boolean" },
1333
+ "no-interactive": { type: "boolean" },
1334
+ yes: { type: "boolean", short: "y" },
1313
1335
  json: { type: "boolean", short: "j" },
1314
1336
  "dry-run": { type: "boolean", short: "d" },
1315
1337
  },
1316
1338
  allowPositionals: true,
1317
1339
  });
1318
1340
 
1341
+ const isNonInteractive = Boolean(values["non-interactive"] || values["no-interactive"] || values.yes);
1342
+ const isInteractive = values.interactive === true ? true : (isNonInteractive ? false : undefined);
1343
+
1319
1344
  const { runTaskCreateWizard } = await import("../src/wizard-task.mjs");
1320
1345
  const res = await runTaskCreateWizard(root, {
1321
1346
  title: values.title,
@@ -1328,6 +1353,7 @@ async function main() {
1328
1353
  autoPr: values["auto-pr"],
1329
1354
  requirePlanApproval: values["require-plan-approval"],
1330
1355
  repoless: values.repoless,
1356
+ interactive: isInteractive,
1331
1357
  });
1332
1358
 
1333
1359
  if (values.json) {
@@ -1475,7 +1501,7 @@ async function main() {
1475
1501
  const todos = scanCodebaseForTodos(root);
1476
1502
  console.log(`\n🔍 Scanned ${todos.length} TODO/FIXME annotation(s).`);
1477
1503
  for (const t of todos.slice(0, 10)) {
1478
- console.log(` - ${t.file}:${t.line} [${t.type}] ${t.text}`);
1504
+ console.log(` - ${t.file}:${t.line} [${t.type || t.tag || "TODO"}] ${t.text}`);
1479
1505
  }
1480
1506
  if (todos.length > 10) {
1481
1507
  console.log(` ... and ${todos.length - 10} more.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
4
4
  "description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for Google Jules (jules) autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,9 +24,9 @@ export function scanCodebaseForTodos(dir = process.cwd()) {
24
24
  const lines = content.split("\n");
25
25
  lines.forEach((line, idx) => {
26
26
  if (line.includes("TODO:")) {
27
- todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "TODO", priority: "MEDIUM" });
27
+ todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "TODO", type: "TODO", priority: "MEDIUM" });
28
28
  } else if (line.includes("FIXME:")) {
29
- todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "FIXME", priority: "HIGH" });
29
+ todos.push({ file: full, line: idx + 1, text: line.trim(), tag: "FIXME", type: "FIXME", priority: "HIGH" });
30
30
  }
31
31
  });
32
32
  }
@@ -141,10 +141,23 @@ export const COMMAND_REGISTRY = [
141
141
  "agentctl task create --interactive",
142
142
  ],
143
143
  flags: [
144
- { name: "title", type: "string", description: "Short title for task" },
145
- { name: "prompt", type: "string", description: "Detailed task instructions" },
146
- { name: "verify-cmd", type: "string", description: "Verification command" },
147
- { name: "interactive", type: "boolean", description: "Launch interactive task wizard" },
144
+ { name: "title", type: "string", description: "Short title for task (-t)" },
145
+ { name: "prompt", type: "string", description: "Detailed task instructions (-p)" },
146
+ { name: "prompt-file", type: "string", description: "Read prompt from file (-f)" },
147
+ { name: "role", type: "string", description: "Specialist role (overseer, bolt, sentinel, janitor, a11y, scribe, spectator, alchemist) (-r)" },
148
+ { name: "tier", type: "string", description: "Execution tier override (fast | complex)" },
149
+ { name: "template", type: "string", description: "Task template preset ID" },
150
+ { name: "depends-on", type: "string", description: "Comma-separated task dependency IDs" },
151
+ { name: "verify-cmd", type: "string", description: "Verification command override (-v)" },
152
+ { name: "auto-pr", type: "boolean", description: "Automatically create GitHub PR upon completion" },
153
+ { name: "require-plan-approval", type: "boolean", description: "Require approval of agent plan before execution" },
154
+ { name: "repoless", type: "boolean", description: "Execute in repoless sandbox mode" },
155
+ { name: "interactive", type: "boolean", description: "Launch interactive task wizard (-i)" },
156
+ { name: "non-interactive", type: "boolean", description: "Bypass interactive prompts and use CLI flags" },
157
+ { name: "no-interactive", type: "boolean", description: "Alias for --non-interactive" },
158
+ { name: "yes", type: "boolean", description: "Accept default values non-interactively (-y)" },
159
+ { name: "dry-run", type: "boolean", description: "Simulate task envelope creation without queueing (-d)" },
160
+ { name: "json", type: "boolean", description: "Output structured JSON envelope (-j)" },
148
161
  ],
149
162
  },
150
163
  {
@@ -187,9 +200,14 @@ export const COMMAND_REGISTRY = [
187
200
  "agentctl init --tier pro --yes",
188
201
  ],
189
202
  flags: [
190
- { name: "tier", type: "string", description: "Target configuration tier (free, pro, ultra, enterprise)" },
191
- { name: "interactive", type: "boolean", description: "Launch interactive onboarding wizard" },
192
- { name: "yes", type: "boolean", description: "Accept auto-detected Stack Oracle defaults" },
203
+ { name: "tier", type: "string", description: "Target configuration tier (free, pro, ultra, enterprise) (-t)" },
204
+ { name: "interactive", type: "boolean", description: "Launch interactive onboarding wizard (-i)" },
205
+ { name: "non-interactive", type: "boolean", description: "Run non-interactively with defaults" },
206
+ { name: "no-interactive", type: "boolean", description: "Alias for --non-interactive" },
207
+ { name: "yes", type: "boolean", description: "Accept auto-detected Stack Oracle defaults (-y)" },
208
+ { name: "force", type: "boolean", description: "Force overwrite existing config and assets (-f)" },
209
+ { name: "dry-run", type: "boolean", description: "Preview plan without writing files (-d)" },
210
+ { name: "json", type: "boolean", description: "Output structured JSON manifest (-j)" },
193
211
  ],
194
212
  },
195
213
  {
@@ -333,6 +351,185 @@ export const COMMAND_REGISTRY = [
333
351
  { name: "limit", type: "string", description: "Maximum handovers to list" },
334
352
  ],
335
353
  },
354
+ {
355
+ id: "mutate",
356
+ path: ["mutate"],
357
+ title: "mutate",
358
+ description: "Run zero-dependency diff mutation testing harness",
359
+ category: "Inspect",
360
+ mutates: false,
361
+ risk: "low",
362
+ interactive: "never",
363
+ requiresRepository: true,
364
+ shortcuts: ["mutation"],
365
+ examples: [
366
+ "agentctl mutate",
367
+ "agentctl mutate --min-score 80",
368
+ 'agentctl mutate --max-mutants 20 --test-cmd "npm test"',
369
+ ],
370
+ flags: [
371
+ { name: "min-score", type: "string", description: "Target minimum mutation score threshold (0-100)" },
372
+ { name: "max-mutants", type: "string", description: "Maximum number of mutants to generate and test" },
373
+ { name: "test-cmd", type: "string", description: "Test command override" },
374
+ { name: "verify-cmd", type: "string", description: "Alias for test-cmd" },
375
+ { name: "tier", type: "string", description: "Execution tier (fast | complex)" },
376
+ { name: "dry-run", type: "boolean", description: "Simulate mutation analysis without executing tests (-d)" },
377
+ { name: "json", type: "boolean", description: "Output structured JSON mutation report (-j)" },
378
+ ],
379
+ },
380
+ {
381
+ id: "coverage",
382
+ path: ["coverage"],
383
+ title: "coverage",
384
+ description: "Run native zero-dependency V8 diff coverage check",
385
+ category: "Inspect",
386
+ mutates: false,
387
+ risk: "low",
388
+ interactive: "never",
389
+ requiresRepository: true,
390
+ shortcuts: ["cov"],
391
+ examples: [
392
+ "agentctl coverage",
393
+ "agentctl coverage --min 80",
394
+ 'agentctl coverage --cmd "node --test"',
395
+ ],
396
+ flags: [
397
+ { name: "min", type: "string", description: "Minimum diff line coverage percentage required" },
398
+ { name: "cmd", type: "string", description: "Test command override to run with V8 coverage" },
399
+ { name: "test-cmd", type: "string", description: "Alias for cmd" },
400
+ { name: "verify-cmd", type: "string", description: "Alias for cmd" },
401
+ { name: "dry-run", type: "boolean", description: "Simulate coverage collection without running (-d)" },
402
+ { name: "json", type: "boolean", description: "Output structured JSON coverage report (-j)" },
403
+ ],
404
+ },
405
+ {
406
+ id: "gate",
407
+ path: ["gate"],
408
+ title: "gate",
409
+ description: "Run CI security, rules, and stack verification gate",
410
+ category: "Inspect",
411
+ mutates: false,
412
+ risk: "low",
413
+ interactive: "never",
414
+ requiresRepository: true,
415
+ shortcuts: ["audit", "check"],
416
+ examples: [
417
+ "agentctl gate",
418
+ "agentctl gate --mode working-tree",
419
+ "agentctl gate --fix",
420
+ "agentctl gate --json",
421
+ ],
422
+ flags: [
423
+ { name: "mode", type: "string", description: "Evaluation mode (working-tree | committed | staged) (-m)" },
424
+ { name: "tier", type: "string", description: "Enforce specific tier policies (free | pro | ultra | enterprise)" },
425
+ { name: "allow-protected", type: "boolean", description: "Bypass protected path checks for authorized maintainers" },
426
+ { name: "fix", type: "boolean", description: "Trigger automated OODA self-repair loop on failure" },
427
+ { name: "strict-locks", type: "boolean", description: "Enforce strict anti-tampering verification on test files" },
428
+ { name: "dry-run", type: "boolean", description: "Simulate gate evaluation without persisting evidence (-d)" },
429
+ { name: "json", type: "boolean", description: "Output structured JSON gate report (-j)" },
430
+ ],
431
+ },
432
+ {
433
+ id: "probe",
434
+ path: ["probe"],
435
+ title: "probe",
436
+ description: "Run test flakiness stability probe across N repetitions",
437
+ category: "Inspect",
438
+ mutates: false,
439
+ risk: "low",
440
+ interactive: "never",
441
+ requiresRepository: true,
442
+ shortcuts: ["stability"],
443
+ examples: [
444
+ "agentctl probe",
445
+ "agentctl probe --repeat 10",
446
+ 'agentctl probe --repeat 5 --cmd "pytest"',
447
+ ],
448
+ flags: [
449
+ { name: "repeat", type: "string", description: "Number of probe executions (default: 5)" },
450
+ { name: "cmd", type: "string", description: "Test command to probe" },
451
+ { name: "test-cmd", type: "string", description: "Alias for cmd" },
452
+ { name: "verify-cmd", type: "string", description: "Alias for cmd" },
453
+ { name: "dry-run", type: "boolean", description: "Preview probe plan without execution (-d)" },
454
+ { name: "json", type: "boolean", description: "Output structured JSON probe telemetry (-j)" },
455
+ ],
456
+ },
457
+ {
458
+ id: "perf",
459
+ path: ["perf"],
460
+ title: "perf",
461
+ description: "Monitor Node.js event loop delay and Big-O performance lag",
462
+ category: "Inspect",
463
+ mutates: false,
464
+ risk: "low",
465
+ interactive: "never",
466
+ requiresRepository: true,
467
+ shortcuts: ["event-loop"],
468
+ examples: [
469
+ "agentctl perf",
470
+ 'agentctl perf --max-ms 50 --cmd "npm test"',
471
+ ],
472
+ flags: [
473
+ { name: "max-ms", type: "string", description: "Maximum allowable event loop delay threshold in ms" },
474
+ { name: "cmd", type: "string", description: "Command to execute during performance monitoring" },
475
+ { name: "test-cmd", type: "string", description: "Alias for cmd" },
476
+ { name: "verify-cmd", type: "string", description: "Alias for cmd" },
477
+ { name: "dry-run", type: "boolean", description: "Preview performance monitoring configuration (-d)" },
478
+ { name: "json", type: "boolean", description: "Output structured JSON performance report (-j)" },
479
+ ],
480
+ },
481
+ {
482
+ id: "dispatch",
483
+ path: ["dispatch"],
484
+ title: "dispatch",
485
+ description: "Dispatch a single task to an AI agent",
486
+ category: "Operate",
487
+ mutates: true,
488
+ risk: "moderate",
489
+ interactive: "never",
490
+ requiresRepository: true,
491
+ shortcuts: ["create"],
492
+ examples: [
493
+ 'agentctl dispatch --prompt "Add retry handling to src/webhook.js"',
494
+ 'agentctl dispatch -p "Fix type errors" --role bolt --tier fast',
495
+ 'agentctl dispatch --prompt-file task.md --dry-run',
496
+ ],
497
+ flags: [
498
+ { name: "prompt", type: "string", description: "Task prompt instructions (-p)" },
499
+ { name: "prompt-file", type: "string", description: "Read prompt from file (-f)" },
500
+ { name: "role", type: "string", description: "Specialist role (overseer, bolt, sentinel, janitor, a11y, scribe, spectator, alchemist) (-r)" },
501
+ { name: "tier", type: "string", description: "Execution tier override (fast | complex)" },
502
+ { name: "check-premise", type: "boolean", description: "Verify premise locally before dispatching" },
503
+ { name: "source", type: "string", description: "Jules repository source identifier (-s)" },
504
+ { name: "branch", type: "string", description: "Starting branch for task execution (-b)" },
505
+ { name: "repoless", type: "boolean", description: "Dispatch task in repoless execution mode" },
506
+ { name: "auto-pr", type: "boolean", description: "Automatically create PR upon completion" },
507
+ { name: "require-plan-approval", type: "boolean", description: "Require human approval for proposed plan" },
508
+ { name: "dry-run", type: "boolean", description: "Simulate dispatch without sending to provider (-d)" },
509
+ { name: "json", type: "boolean", description: "Output JSON structured dispatch result (-j)" },
510
+ ],
511
+ },
512
+ {
513
+ id: "bootstrap",
514
+ path: ["bootstrap"],
515
+ title: "bootstrap",
516
+ description: "Bootstrap zero-test repository with verification oracle",
517
+ category: "Configure",
518
+ mutates: true,
519
+ risk: "moderate",
520
+ interactive: "never",
521
+ requiresRepository: true,
522
+ shortcuts: ["bs"],
523
+ examples: [
524
+ "agentctl bootstrap",
525
+ "agentctl bootstrap --force",
526
+ ],
527
+ flags: [
528
+ { name: "force", type: "boolean", description: "Overwrite existing verification oracle or config (-f)" },
529
+ { name: "dry-run", type: "boolean", description: "Simulate bootstrap without writing files (-d)" },
530
+ { name: "json", type: "boolean", description: "Output JSON structured bootstrap result (-j)" },
531
+ ],
532
+ },
336
533
  ];
337
534
 
338
535
  /**
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
+ import { isTaskFile } from "../engine.mjs";
4
5
 
5
6
  /**
6
7
  * Work out the one thing the operator should do next.
@@ -66,7 +67,7 @@ export function resolveNextStep(root, env = process.env) {
66
67
  const fallbackQueueDir = join(root, ".agent", "queue");
67
68
  const queueDir = existsSync(primaryQueueDir) ? primaryQueueDir : fallbackQueueDir;
68
69
  const queued = existsSync(queueDir)
69
- ? readdirSync(queueDir).filter((f) => f.endsWith(".md") || f.endsWith(".json") || f.endsWith(".yml")).length
70
+ ? readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir)).length
70
71
  : 0;
71
72
  if (queued > 0) {
72
73
  return {
@@ -210,8 +210,12 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
210
210
  setupCmd = "npx drizzle-kit push";
211
211
  }
212
212
 
213
- if (existsSync(join(projectRoot, "bunfig.toml")) || existsSync(join(projectRoot, "bun.lockb"))) {
214
- const triggerFile = existsSync(join(projectRoot, "bunfig.toml")) ? "bunfig.toml" : "bun.lockb";
213
+ if (existsSync(join(projectRoot, "bunfig.toml")) || existsSync(join(projectRoot, "bun.lockb")) || existsSync(join(projectRoot, "bun.lock"))) {
214
+ const triggerFile = existsSync(join(projectRoot, "bunfig.toml"))
215
+ ? "bunfig.toml"
216
+ : existsSync(join(projectRoot, "bun.lockb"))
217
+ ? "bun.lockb"
218
+ : "bun.lock";
215
219
  return { ...container, stack: "bun", setupCmd, testCmd: "bun test", buildCmd: "bun run build", triggerFile };
216
220
  }
217
221
  if (existsSync(join(projectRoot, "deno.json")) || existsSync(join(projectRoot, "deno.jsonc"))) {
@@ -219,20 +223,38 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
219
223
  return { ...container, stack: "deno", setupCmd, testCmd: "deno test", buildCmd: "deno task build", triggerFile };
220
224
  }
221
225
  if (existsSync(join(projectRoot, "package.json"))) {
222
- let testScript = "npm test";
223
- let buildScript = "npm run build";
226
+ const hasPnpm = existsSync(join(projectRoot, "pnpm-lock.yaml"));
227
+ const hasYarn = existsSync(join(projectRoot, "yarn.lock"));
228
+ const hasBun = existsSync(join(projectRoot, "bun.lock")) || existsSync(join(projectRoot, "bun.lockb"));
229
+ const pm = hasPnpm ? "pnpm" : hasYarn ? "yarn" : hasBun ? "bun" : "npm";
230
+ const triggerFile = hasPnpm
231
+ ? "pnpm-lock.yaml"
232
+ : hasYarn
233
+ ? "yarn.lock"
234
+ : hasBun
235
+ ? (existsSync(join(projectRoot, "bun.lock")) ? "bun.lock" : "bun.lockb")
236
+ : "package.json";
237
+
238
+ let testScript = "";
239
+ let buildScript = "";
224
240
  try {
225
241
  const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
226
242
  if (pkg && typeof pkg === "object") {
227
243
  const scripts = pkg.scripts || {};
228
- testScript = scripts.test ? "npm test" : "npm test";
229
- buildScript = scripts.build ? "npm run build" : "";
244
+ if (scripts.test) {
245
+ testScript = pm === "yarn" ? "yarn test" : `${pm} test`;
246
+ } else if (pm === "bun") {
247
+ testScript = "bun test";
248
+ }
249
+ if (scripts.build) {
250
+ buildScript = pm === "yarn" ? "yarn build" : `${pm} run build`;
251
+ }
230
252
  }
231
253
  } catch (_) {
232
- testScript = "npm test";
233
- buildScript = "npm run build";
254
+ testScript = "";
255
+ buildScript = "";
234
256
  }
235
- return { ...container, stack: "node", setupCmd, testCmd: testScript, buildCmd: buildScript, triggerFile: "package.json" };
257
+ return { ...container, stack: pm === "npm" ? "node" : pm, setupCmd, testCmd: testScript, buildCmd: buildScript, triggerFile };
236
258
  }
237
259
 
238
260
  // 7. .NET / C# / F# / PHP / Python root file extension fallback
@@ -1,8 +1,28 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { spawnSync } from "node:child_process";
3
4
  import { detectStack, parseYaml } from "./config.mjs";
4
5
  import { runCmd } from "./git.mjs";
5
6
 
7
+ /**
8
+ * Check if a command-line binary is available on the system PATH.
9
+ * @param {string} binName
10
+ * @returns {boolean}
11
+ */
12
+ export function hasBinary(binName) {
13
+ if (!binName || typeof binName !== "string") return false;
14
+ const bin = binName.trim().split(/\s+/)[0];
15
+ if (!bin) return false;
16
+ try {
17
+ const isWin = process.platform === "win32";
18
+ const checker = isWin ? "where" : "which";
19
+ const res = spawnSync(checker, [bin], { stdio: "ignore" });
20
+ return res.status === 0;
21
+ } catch (_) {
22
+ return false;
23
+ }
24
+ }
25
+
6
26
  /**
7
27
  * Inspect repository topology, workspace structure, and manifests to infer candidate verification oracles.
8
28
  * @param {string} [root=process.cwd()]
@@ -37,18 +57,24 @@ export function detectStackOracles(root = process.cwd()) {
37
57
 
38
58
  const packages = [];
39
59
 
40
- // Check Node.json package.json scripts
60
+ // Check Node.js package.json scripts
41
61
  const pkgPath = join(root, "package.json");
42
62
  if (existsSync(pkgPath)) {
43
63
  try {
44
64
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
45
65
  const scripts = pkg.scripts || {};
66
+ const hasPnpm = existsSync(join(root, "pnpm-lock.yaml")) || existsSync(join(root, "pnpm-workspace.yaml"));
67
+ const hasYarn = existsSync(join(root, "yarn.lock"));
68
+ const hasBun = existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bunfig.toml"));
69
+ const pm = hasPnpm ? "pnpm" : hasYarn ? "yarn" : hasBun ? "bun" : "npm";
46
70
 
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";
71
+ if (scripts.test) candidates.testCmd = candidates.testCmd || (pm === "yarn" ? "yarn test" : `${pm} test`);
72
+ if (scripts.build) candidates.buildCmd = candidates.buildCmd || (pm === "yarn" ? "yarn build" : `${pm} run build`);
73
+ if (scripts.lint) candidates.lintCmd = pm === "yarn" ? "yarn lint" : `${pm} run lint`;
50
74
  if (scripts.typecheck || scripts["type-check"]) {
51
- candidates.typecheckCmd = scripts.typecheck ? "npm run typecheck" : "npm run type-check";
75
+ candidates.typecheckCmd = scripts.typecheck
76
+ ? (pm === "yarn" ? "yarn typecheck" : `${pm} run typecheck`)
77
+ : (pm === "yarn" ? "yarn type-check" : `${pm} run type-check`);
52
78
  } else if (existsSync(join(root, "tsconfig.json"))) {
53
79
  candidates.typecheckCmd = "npx tsc --noEmit";
54
80
  }
@@ -69,7 +95,9 @@ export function detectStackOracles(root = process.cwd()) {
69
95
  if (stack.includes("go") || existsSync(join(root, "go.mod"))) {
70
96
  candidates.testCmd = candidates.testCmd || "go test ./...";
71
97
  candidates.buildCmd = candidates.buildCmd || "go build ./...";
72
- candidates.lintCmd = candidates.lintCmd || "golangci-lint run";
98
+ if (hasBinary("golangci-lint")) {
99
+ candidates.lintCmd = candidates.lintCmd || "golangci-lint run";
100
+ }
73
101
  candidates.typecheckCmd = candidates.typecheckCmd || "go vet ./...";
74
102
  }
75
103
 
@@ -77,8 +105,14 @@ export function detectStackOracles(root = process.cwd()) {
77
105
  if (stack.includes("pytest") || existsSync(join(root, "pyproject.toml")) || existsSync(join(root, "requirements.txt"))) {
78
106
  candidates.testCmd = candidates.testCmd || "pytest";
79
107
  candidates.buildCmd = candidates.buildCmd || "";
80
- candidates.lintCmd = candidates.lintCmd || "flake8 .";
81
- candidates.typecheckCmd = candidates.typecheckCmd || "mypy .";
108
+ if (hasBinary("flake8")) {
109
+ candidates.lintCmd = candidates.lintCmd || "flake8 .";
110
+ } else if (hasBinary("ruff")) {
111
+ candidates.lintCmd = candidates.lintCmd || "ruff check .";
112
+ }
113
+ if (hasBinary("mypy")) {
114
+ candidates.typecheckCmd = candidates.typecheckCmd || "mypy .";
115
+ }
82
116
  }
83
117
 
84
118
  // Discover subpackages if monorepo
@@ -280,14 +280,15 @@ export async function runTaskCreateWizard(root = process.cwd(), options = {}) {
280
280
  if (useTodo) {
281
281
  const selectedTodo = await select(
282
282
  todos.slice(0, 10).map((t) => ({
283
- label: `[${t.type}] ${t.text} (${t.file}:${t.line})`,
283
+ label: `[${t.type || t.tag || "TODO"}] ${t.text} (${t.file}:${t.line})`,
284
284
  value: t,
285
285
  })),
286
286
  "Select TODO Candidate",
287
287
  options
288
288
  );
289
- title = `Fix ${selectedTodo.type}: ${selectedTodo.text.slice(0, 40)}`;
290
- promptText = `Resolve ${selectedTodo.type} in ${selectedTodo.file} at line ${selectedTodo.line}: ${selectedTodo.text}`;
289
+ const todoKind = selectedTodo.type || selectedTodo.tag || "TODO";
290
+ title = `Fix ${todoKind}: ${selectedTodo.text.slice(0, 40)}`;
291
+ promptText = `Resolve ${todoKind} in ${selectedTodo.file} at line ${selectedTodo.line}: ${selectedTodo.text}`;
291
292
  }
292
293
  }
293
294