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
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Self-audit gatekeeper: runs the 4-phase safety gate against the working
3
+ * tree or base branch, plus the ledger/worktree/config audits around it.
4
+ *
5
+ * This is the home of the logic that used to live in
6
+ * `scripts/jules-self-audit.mjs`; that file is now a minimal CLI entry point.
7
+ */
8
+
9
+ import { existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { gate } from "./engine.mjs";
12
+ import { loadConfig, parseYaml } from "./config.mjs";
13
+ import { matchesGlob } from "./security.mjs";
14
+
15
+ /** Convenience wrapper kept for the historical `matchGlob` name. */
16
+ export function matchGlob(filepath, globPattern) {
17
+ return matchesGlob(filepath, globPattern);
18
+ }
19
+
20
+ export const EXECUTION_CONFIG_FILES = ["package.json", "tsconfig.json", "Cargo.toml", "go.mod", "pyproject.toml", "vite.config.ts", "jest.config.js", ".npmrc"];
21
+ export const RESTRICTED_AGENT_FILES = ["AGENTS.md", "JULES_RULES_TEMPLATE.md"];
22
+ export const COMMAND_DEFINING_FILES = ["package.json", "Cargo.toml", "go.mod", "pyproject.toml", ".agent/jules.yml", ".agent/config.yml"];
23
+
24
+ export function loadForbiddenPatterns(configContent = "") {
25
+ const defaults = ["scripts/jules-*", ".agent/jules.yml", ".github/**", "**/*.pem"];
26
+ if (!configContent) return defaults;
27
+ const parsed = parseYaml(configContent);
28
+ const userPaths = Array.isArray(parsed.forbidden_paths) ? parsed.forbidden_paths : [];
29
+ return Array.from(new Set([...defaults, ...userPaths]));
30
+ }
31
+
32
+ export function loadAllowedPatterns(configContent = "") {
33
+ if (!configContent) return [];
34
+ const parsed = parseYaml(configContent);
35
+ return Array.isArray(parsed.allow_paths) ? parsed.allow_paths : [];
36
+ }
37
+
38
+ export function validateJulesConfig(_configContent = "", jsonGuardrailsContent = "") {
39
+ const errors = [];
40
+ if (jsonGuardrailsContent) {
41
+ try {
42
+ const parsed = JSON.parse(jsonGuardrailsContent);
43
+ if (Array.isArray(parsed.rules)) {
44
+ for (const rule of parsed.rules) {
45
+ try {
46
+ new RegExp(rule.trigger, "i");
47
+ } catch (err) {
48
+ errors.push(`Invalid RegExp trigger: ${err.message}`);
49
+ }
50
+ }
51
+ }
52
+ } catch (err) {
53
+ errors.push(`JSON Parse Error: ${err.message}`);
54
+ }
55
+ }
56
+ return { ok: errors.length === 0, errors };
57
+ }
58
+
59
+ export function parseAndCleanStderr(str = "") {
60
+ if (typeof str !== "string") return "";
61
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\u001b\[[0-9;]*m/g, "");
62
+ }
63
+
64
+ export function getOodaStateFile(mergeBase = "main") {
65
+ return `.agent/state/ooda-${mergeBase}.json`;
66
+ }
67
+
68
+ /**
69
+ * Audit ledger states and verify integrity of ledger/state files.
70
+ */
71
+ export function auditLedgers(opts = {}) {
72
+ const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
73
+ const stateDir = join(root, ".agent", "state");
74
+ const exists = existsSync(stateDir);
75
+ return { ok: true, stateDir, exists };
76
+ }
77
+
78
+ /**
79
+ * Audit git worktrees and active workspace boundaries.
80
+ */
81
+ export function auditWorktrees(opts = {}) {
82
+ const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
83
+ const worktreeDir = join(root, ".agent", "worktrees");
84
+ const exists = existsSync(worktreeDir);
85
+ return { ok: true, worktreeDir, exists };
86
+ }
87
+
88
+ /**
89
+ * Audit gate rules (scope, payload governor, secret scanning, verification suite).
90
+ */
91
+ export async function auditGates(opts = {}) {
92
+ const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
93
+ const config = opts.config || loadConfig(root);
94
+ const base = opts.base || process.env.BASE_BRANCH || config.baseBranch || "main";
95
+
96
+ return await gate({
97
+ root,
98
+ config,
99
+ base,
100
+ fix: process.env.ALLOW_AUTO_REPAIR === "true",
101
+ allowProtected: process.env.JULES_ALLOW_COMMAND_FILE_CHANGES === "true",
102
+ allowTestChanges: process.env.JULES_ALLOW_TEST_CHANGES || opts.allowTestChanges,
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Main self-audit entrypoint executing validation passes.
108
+ */
109
+ export async function runSelfAudit(opts = {}) {
110
+ const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
111
+ const config = loadConfig(root);
112
+ const base = opts.base || process.env.BASE_BRANCH || config.baseBranch || "main";
113
+
114
+ const optsWithContext = { ...opts, root, config, base };
115
+
116
+ const ledgersResult = auditLedgers(optsWithContext);
117
+ const worktreesResult = auditWorktrees(optsWithContext);
118
+ const gatesResult = await auditGates(optsWithContext);
119
+
120
+ if (!gatesResult.ok) {
121
+ return gatesResult;
122
+ }
123
+
124
+ return {
125
+ ...gatesResult,
126
+ audits: {
127
+ ledgers: ledgersResult,
128
+ worktrees: worktreesResult,
129
+ gates: gatesResult,
130
+ },
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Preflight sandbox probe. Retained as a compatibility surface: the sandbox
136
+ * it once wrapped is now executed by the gate itself, so it reports ok.
137
+ */
138
+ export async function runPreflightSandbox() {
139
+ return { ok: true };
140
+ }
@@ -219,6 +219,35 @@ export async function extractSessionPatch(sessionId, opts = {}) {
219
219
  * @param {object} opts
220
220
  * @returns {Promise<{ ok: boolean, patchApplied: boolean, checkPassed: boolean, patch: string, files: string[], error?: string }>}
221
221
  */
222
+ /**
223
+ * Fetch a session patch with argument validation at the boundary.
224
+ * (Moved out of scripts/jules-patch.mjs, which keeps a CLI entry point.)
225
+ */
226
+ export async function fetchSessionPatch(sessionId, options = {}) {
227
+ if (!sessionId || typeof sessionId !== "string") {
228
+ throw new Error("Session ID is required.");
229
+ }
230
+ return await extractSessionPatch(sessionId, options);
231
+ }
232
+
233
+ /**
234
+ * Maps a Jules session state to the coarse bucket the status CLI reports.
235
+ * (Moved out of scripts/jules-status.mjs, which keeps a CLI entry point.)
236
+ */
237
+ export function categorizeTaskStatus(statusStr = "") {
238
+ const s = String(statusStr).toUpperCase();
239
+ if (s === "AWAITING_PLAN_APPROVAL" || s === "AWAITING_USER_FEEDBACK") {
240
+ return "action_required";
241
+ }
242
+ if (s === "IN_PROGRESS" || s === "DISPATCHED") {
243
+ return "in_progress";
244
+ }
245
+ if (s === "COMPLETED" || s === "FAILED") {
246
+ return "completed";
247
+ }
248
+ return "unknown";
249
+ }
250
+
222
251
  export async function applySessionPatch(sessionId, opts = {}) {
223
252
  const root = opts.root || resolveRoot();
224
253
  const res = await extractSessionPatch(sessionId, { ...opts, root });
package/src/stability.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { resolveVerify } from "./config.mjs";
3
- import { computeOscillation } from "./flaky-ledger.mjs";
3
+ import { computeOscillation, recordVerifyRun } from "./flaky-ledger.mjs";
4
4
 
5
5
  /**
6
6
  * Executes a test command repeatedly to probe for race conditions, non-deterministic timers, or test flakiness.
@@ -11,6 +11,7 @@ import { computeOscillation } from "./flaky-ledger.mjs";
11
11
  * @param {number} [options.repeat=5] - Number of consecutive iterations to execute
12
12
  * @param {number} [options.minPassRate=1.0] - Required minimum pass rate (0.0 - 1.0)
13
13
  * @param {number} [options.timeoutMs=30000] - Timeout per iteration in milliseconds
14
+ * @param {boolean} [options.record=true] - Whether to record run outcomes to .agent/state/flaky.jsonl
14
15
  * @returns {object} { ok, repeat, passes, failures, passRate, oscillation, runs, durationMs, summary }
15
16
  */
16
17
  export function runStabilityProbe(testCmd, options = {}) {
@@ -19,6 +20,7 @@ export function runStabilityProbe(testCmd, options = {}) {
19
20
  const repeat = typeof options.repeat === "number" && options.repeat > 0 ? options.repeat : 5;
20
21
  const minPassRate = typeof options.minPassRate === "number" ? options.minPassRate : 1.0;
21
22
  const timeoutMs = options.timeoutMs || 30000;
23
+ const shouldRecord = options.record !== false;
22
24
 
23
25
  const runs = [];
24
26
  let passes = 0;
@@ -57,6 +59,11 @@ export function runStabilityProbe(testCmd, options = {}) {
57
59
  }
58
60
 
59
61
  const duration = Date.now() - runStart;
62
+ if (shouldRecord) {
63
+ const fingerprint = !pass && stderr ? stderr.trim().slice(0, 200) : null;
64
+ recordVerifyRun(root, cmd, pass, fingerprint, duration);
65
+ }
66
+
60
67
  runs.push({
61
68
  iteration: i,
62
69
  pass,
@@ -221,7 +221,10 @@ test("every source file parses", () => {
221
221
  }
222
222
 
223
223
  export function detectEdgeRuntime(projectRoot = process.cwd()) {
224
- const isCloudflare = existsSync(join(projectRoot, "wrangler.toml")) || existsSync(join(projectRoot, "wrangler.json"));
224
+ const isCloudflare =
225
+ existsSync(join(projectRoot, "wrangler.toml")) ||
226
+ existsSync(join(projectRoot, "wrangler.json")) ||
227
+ existsSync(join(projectRoot, "wrangler.jsonc"));
225
228
  const isDeno = existsSync(join(projectRoot, "deno.json")) || existsSync(join(projectRoot, "deno.jsonc"));
226
229
  const isNetlify = existsSync(join(projectRoot, "netlify.toml"));
227
230
 
@@ -441,7 +444,7 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
441
444
 
442
445
  // 5. Python / Django / Elixir / Ruby / Java
443
446
  if (existsSync(join(projectRoot, "manage.py"))) {
444
- return { ...container, stack: "django", testCmd: "python manage.py test --keepdb", buildCmd: "python manage.py check", triggerFile: "manage.py" };
447
+ return { ...container, stack: "django", testCmd: `${pythonBin()} manage.py test --keepdb`, buildCmd: `${pythonBin()} manage.py check`, triggerFile: "manage.py" };
445
448
  }
446
449
  if (existsSync(join(projectRoot, "pyproject.toml")) || existsSync(join(projectRoot, "requirements.txt")) || existsSync(join(projectRoot, "setup.py"))) {
447
450
  const triggerFile = existsSync(join(projectRoot, "pyproject.toml")) ? "pyproject.toml" : existsSync(join(projectRoot, "requirements.txt")) ? "requirements.txt" : "setup.py";
package/src/state.mjs CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  fsyncSync,
11
11
  unlinkSync,
12
12
  lstatSync,
13
+ statSync,
13
14
  } from "node:fs";
14
15
  import { join, basename } from "node:path";
15
16
  import { createHash, randomUUID } from "node:crypto";
@@ -57,6 +58,32 @@ export function getDailyLedgerPath(rootOrOpts = resolveRoot()) {
57
58
  return join(getStateDir(root), `ledger-${dateStr}.jsonl`);
58
59
  }
59
60
 
61
+ /**
62
+ * Delete files in a ledger/state directory older than `retentionDays`.
63
+ *
64
+ * Moved out of the deleted scripts/utils.mjs shim; the ledger rotation policy
65
+ * lives with the ledger.
66
+ *
67
+ * @param {string} stateDir
68
+ * @param {number} [retentionDays=30]
69
+ */
70
+ export function pruneOldLedgers(stateDir, retentionDays = 30) {
71
+ if (!existsSync(stateDir)) return;
72
+ const cutoff = Date.now() - retentionDays * 86400 * 1000;
73
+ try {
74
+ const files = readdirSync(stateDir);
75
+ for (const f of files) {
76
+ const full = join(stateDir, f);
77
+ try {
78
+ const stat = statSync(full);
79
+ if (stat.isFile() && stat.mtimeMs < cutoff) {
80
+ unlinkSync(full);
81
+ }
82
+ } catch (_) {}
83
+ }
84
+ } catch (_) {}
85
+ }
86
+
60
87
  /**
61
88
  * How far back a task still counts against the allowance.
62
89
  *
@@ -351,6 +378,24 @@ export function checkDailyBudget(arg1 = resolveRoot(), arg2 = 300, opts = {}) {
351
378
  }
352
379
  }
353
380
 
381
+ /**
382
+ * Reserve one unit of the daily budget, nameably.
383
+ *
384
+ * @param {number} [maxSessions=300]
385
+ * @param {string} [taskKey=""]
386
+ * @param {string} [root] - Ledger root. Defaults to the git toplevel. Pass an
387
+ * explicit root to keep callers (notably tests) off the operator's real ledger.
388
+ */
389
+ export function reserveDailyBudget(maxSessions = 300, taskKey = "", root = resolveRoot()) {
390
+ // The id is what makes the reservation releasable. Written without one, a
391
+ // reservation counted against the day and no rollback, commit or reconcile
392
+ // could ever name it again — it stayed charged until the ledger rotated.
393
+ const reservationId = `res-${Date.now()}-${randomUUID().slice(0, 8)}`;
394
+ appendLedger({ event: "budget_reserved", reservationId, key: taskKey }, root);
395
+ const check = checkDailyBudget(root, maxSessions);
396
+ return { ok: check.ok, used: check.used, budget: maxSessions, reservationId };
397
+ }
398
+
354
399
  export class BudgetError extends Error {
355
400
  constructor(message) {
356
401
  super(message);
package/src/swarm.mjs ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Swarm session audit & reservation sync manifest.
3
+ *
4
+ * Extracted from the deleted `scripts/utils.mjs` shim. These four functions
5
+ * are the SDK-level surface index.mjs re-exports (extractPrUrls,
6
+ * auditSessions, buildSyncManifest, pushReservationManifest): parsing PR
7
+ * links out of session outputs, bucketing sessions by staleness, and writing
8
+ * the swarm reservation manifest the merge gate reads.
9
+ */
10
+
11
+ import { writeFileSync, existsSync, mkdirSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ export function extractPrUrls(outputs = []) {
15
+ const prs = [];
16
+ const regex = /https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g;
17
+ for (const item of outputs) {
18
+ const text = typeof item === "string" ? item : item?.link || JSON.stringify(item);
19
+ const matches = text.match(regex);
20
+ if (matches) {
21
+ for (const m of matches) {
22
+ if (!prs.includes(m)) prs.push(m);
23
+ }
24
+ }
25
+ }
26
+ return prs;
27
+ }
28
+
29
+ export function auditSessions(sessions = [], opts = {}) {
30
+ const staleHoursThreshold = opts.staleHoursThreshold || 24;
31
+ const now = Date.now();
32
+ const merged = [];
33
+ const active = [];
34
+ const stale = [];
35
+
36
+ for (const s of sessions) {
37
+ if (s.state === "MERGED") {
38
+ merged.push(s);
39
+ } else {
40
+ const updatedMs = s.updateTime ? new Date(s.updateTime).getTime() : now;
41
+ const ageHours = (now - updatedMs) / (1000 * 60 * 60);
42
+ if (ageHours > staleHoursThreshold) {
43
+ stale.push(s);
44
+ } else {
45
+ active.push(s);
46
+ }
47
+ }
48
+ }
49
+
50
+ return { merged, active, stale };
51
+ }
52
+
53
+ export function buildSyncManifest(tasks = []) {
54
+ const reservations = tasks.map((t) => ({ id: t.id, title: t.title, scope: t.scope }));
55
+ return {
56
+ version: 1,
57
+ totalTasks: tasks.length,
58
+ reservations,
59
+ };
60
+ }
61
+
62
+ export async function pushReservationManifest(manifest, projectRoot = process.cwd()) {
63
+ const isDry = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
64
+ const isRemote = process.env.JULES_SWARM_REMOTE_PUSH === "true";
65
+
66
+ const agentDir = join(projectRoot, ".agent");
67
+ if (!existsSync(agentDir)) {
68
+ try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
69
+ }
70
+ const manifestPath = join(agentDir, "sync-manifest.json");
71
+ try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
72
+
73
+ if (isDry) return { status: "DRY_RUN", path: manifestPath };
74
+ if (isRemote) return { status: "PUSHED", path: manifestPath };
75
+ return { status: "SAVED_LOCAL", path: manifestPath };
76
+ }
@@ -4,6 +4,7 @@ import { checkScope } from "./security.mjs";
4
4
  import { detectStackOracles } from "./wizard-oracle.mjs";
5
5
  import { loadConfig } from "./config.mjs";
6
6
  import { sanitizePromptVocabulary } from "./prompt-guard.mjs";
7
+ import { buildGuardrailFooter } from "./wizard-task.mjs";
7
8
 
8
9
  /**
9
10
  * Calculates Levenshtein distance between two strings.
@@ -350,7 +351,7 @@ export function optimizeTaskPrompt(promptText, options = {}) {
350
351
  const isWeb = Boolean(options.web || (analysis.webIntent && analysis.webIntent.isWeb));
351
352
  const includeExplorationBudget = options.explorationBudget !== false;
352
353
  const includeCriticGuidance = options.criticGuidance !== false;
353
- const verifyCmd = analysis.oracle.command || options.verifyCmd || "npm test";
354
+ const verifyCmd = analysis.oracle.command || options.verifyCmd || null;
354
355
 
355
356
  // Construct structured Markdown envelope
356
357
  const lines = [];
@@ -359,16 +360,24 @@ export function optimizeTaskPrompt(promptText, options = {}) {
359
360
 
360
361
  if (includeExplorationBudget) {
361
362
  lines.push("## Google Labs Exploration Budget Protocol (3-Phase Discovery)");
362
- lines.push("To maximize diagnostic accuracy (Hit@5 57%), execute this task in 3 distinct phases:");
363
+ lines.push("Use deep planning mode. execute this task in 3 distinct phases (Discovery, Test Formulation, Surgical Implementation):");
363
364
  lines.push("1. **PHASE 1: DISCOVERY & SYMBOL TRACING (Stay Silent, Write NO Code)**");
364
365
  lines.push(" - Read target source files, definitions, and dependent call sites.");
365
366
  lines.push(" - Formulate diagnostic hypothesis and verify exact symbol signatures before making edits.");
366
367
  lines.push("2. **PHASE 2: ORACLE FORMULATION**");
367
- lines.push(` - Execute baseline verification: \`${verifyCmd}\`.`);
368
+ if (verifyCmd) {
369
+ lines.push(` - Execute baseline verification: \`${verifyCmd}\`.`);
370
+ } else {
371
+ lines.push(" - Execute baseline verification: (None specified - define verification command).");
372
+ }
368
373
  lines.push(" - Identify specific test assertions, benchmarks, or status codes to satisfy.");
369
374
  lines.push("3. **PHASE 3: SURGICAL IMPLEMENTATION & VERIFICATION**");
370
375
  lines.push(" - Apply minimal, zero-bloat code modifications.");
371
- lines.push(` - Execute \`${verifyCmd}\` and verify 100% clean exit code 0.`);
376
+ if (verifyCmd) {
377
+ lines.push(` - Execute \`${verifyCmd}\` and verify 100% clean exit code 0.`);
378
+ } else {
379
+ lines.push(" - Execute automated verification suite and verify 100% clean exit code 0.");
380
+ }
372
381
  lines.push("");
373
382
  }
374
383
 
@@ -376,6 +385,8 @@ export function optimizeTaskPrompt(promptText, options = {}) {
376
385
  lines.push(`- **Goal**: ${promptBody}`);
377
386
  if (analysis.oracle.command) {
378
387
  lines.push(`- **Verification Command**: \`${analysis.oracle.command}\` (Must pass cleanly with exit code 0)`);
388
+ } else {
389
+ lines.push("- **Verification Command**: (None) - WARNING: Missing test oracle.");
379
390
  }
380
391
  lines.push("- **Falsifiability Criteria**: Zero deleted tests, zero weakened assertions, zero lint errors.");
381
392
  lines.push("");
@@ -401,10 +412,26 @@ export function optimizeTaskPrompt(promptText, options = {}) {
401
412
  lines.push("");
402
413
  }
403
414
 
415
+ const root = options.rootDir || process.cwd();
416
+ let cfg = options.config;
417
+ if (!cfg) {
418
+ try {
419
+ cfg = loadConfig(root);
420
+ } catch (_) {
421
+ cfg = {};
422
+ }
423
+ }
424
+
404
425
  lines.push("## Standard Guardrails");
405
- lines.push("- Do NOT modify package.json, lockfiles, or .github/ infrastructure files.");
406
- lines.push("- Diff Payload Governor: Keep total diff payload under 75 KB (\`git diff | wc -c\`).");
407
- lines.push(`- Verify before finishing: Execute \`${verifyCmd}\` and confirm zero errors.`);
426
+ const rawFooter = buildGuardrailFooter(cfg, {
427
+ baseBranch: options.baseBranch,
428
+ diffKb: options.diffKb,
429
+ }).trim();
430
+ const cleanedFooter = rawFooter.replace(/^---\s*\n?HARD CONSTRAINTS:\s*\n?/, "").trim();
431
+ lines.push(cleanedFooter);
432
+ if (verifyCmd) {
433
+ lines.push(`- Verify before finishing: Execute \`${verifyCmd}\` and confirm zero errors.`);
434
+ }
408
435
 
409
436
  const optimizedPrompt = lines.join("\n");
410
437
 
package/src/telemetry.mjs CHANGED
@@ -226,6 +226,29 @@ export function appendTelemetry(rootOrOpts = resolveRoot(), kind = "event", fiel
226
226
  });
227
227
  }
228
228
 
229
+ /**
230
+ * Best-effort companion to {@link appendTelemetry}: records the event when it
231
+ * can, and returns null instead of throwing when it cannot.
232
+ *
233
+ * Telemetry is diagnostic data. A gate verdict must never hinge on the mutex
234
+ * or disk write that logs how the verdict came about, so the safety policy
235
+ * "telemetry failures are swallowed" lives here, in the module that owns
236
+ * appending, instead of being re-implemented by each caller that needs it
237
+ * (src/engine.mjs carried its own copy of this wrapper).
238
+ *
239
+ * @param {string} [root]
240
+ * @param {string} kind
241
+ * @param {Object} [fields={}]
242
+ * @returns {Object|null} Appended telemetry record, or null if it was not written
243
+ */
244
+ export function appendTelemetryBestEffort(root = resolveRoot(), kind = "event", fields = {}) {
245
+ try {
246
+ return appendTelemetry(root, kind, fields);
247
+ } catch (_) {
248
+ return null;
249
+ }
250
+ }
251
+
229
252
  /**
230
253
  * Days of telemetry kept on disk.
231
254
  *