jules-orchestrator-kit 0.32.3 → 0.32.5

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.
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "node:fs";
2
- import { join, sep } from "node:path";
2
+ import { join, relative } from "node:path";
3
3
 
4
4
  /**
5
5
  * Generate a zero-dependency smoke test file using node:test for untested JS/Generic repos.
@@ -155,6 +155,31 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
155
155
  return { ...container, stack: "make", testCmd: "make test", buildCmd: "make build", triggerFile: "Makefile" };
156
156
  }
157
157
 
158
+ // 4b. Web3 / Solidity / Foundry & Hardhat
159
+ if (existsSync(join(projectRoot, "foundry.toml")) || existsSync(join(projectRoot, "remappings.txt"))) {
160
+ const triggerFile = existsSync(join(projectRoot, "foundry.toml")) ? "foundry.toml" : "remappings.txt";
161
+ return {
162
+ ...container,
163
+ stack: "foundry",
164
+ testCmd: "forge test --offline",
165
+ buildCmd: "forge build --offline",
166
+ fmtCmd: "forge fmt --check",
167
+ fuzzCmd: "forge test --offline --match-test testFuzz",
168
+ invariantCmd: "forge test --offline --match-test invariant",
169
+ triggerFile,
170
+ };
171
+ }
172
+ if (existsSync(join(projectRoot, "hardhat.config.js")) || existsSync(join(projectRoot, "hardhat.config.ts"))) {
173
+ const triggerFile = existsSync(join(projectRoot, "hardhat.config.ts")) ? "hardhat.config.ts" : "hardhat.config.js";
174
+ return {
175
+ ...container,
176
+ stack: "hardhat",
177
+ testCmd: "npx hardhat test",
178
+ buildCmd: "npx hardhat compile",
179
+ triggerFile,
180
+ };
181
+ }
182
+
158
183
  // 5. Python / Django / Elixir / Ruby / Java
159
184
  if (existsSync(join(projectRoot, "manage.py"))) {
160
185
  return { ...container, stack: "django", testCmd: "python manage.py test --keepdb", buildCmd: "python manage.py check", triggerFile: "manage.py" };
@@ -230,42 +255,366 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
230
255
  return { ...container, stack: "unknown", testCmd: "", buildCmd: "", triggerFile: null };
231
256
  }
232
257
 
258
+ /**
259
+ * Normalizes file paths to POSIX slashes.
260
+ */
261
+ function toPosixPath(p = "") {
262
+ return p.replace(/\\/g, "/");
263
+ }
264
+
265
+ /**
266
+ * Finds the nearest subproject root directory for a given relative file path.
267
+ */
268
+ export function findSubprojectRoot(fileRelPath = "", root = process.cwd()) {
269
+ const normalized = toPosixPath(fileRelPath);
270
+ const parts = normalized.split("/");
271
+ if (parts.length <= 1) return ".";
272
+
273
+ const manifestFiles = [
274
+ "package.json",
275
+ "foundry.toml",
276
+ "hardhat.config.js",
277
+ "hardhat.config.ts",
278
+ "Cargo.toml",
279
+ "go.mod",
280
+ "pyproject.toml",
281
+ "setup.py",
282
+ "requirements.txt",
283
+ "composer.json",
284
+ "pom.xml",
285
+ "build.gradle",
286
+ ];
287
+
288
+ // Walk up from parent directory of file to top-level folder
289
+ for (let i = parts.length - 1; i >= 1; i--) {
290
+ const candidateDir = parts.slice(0, i).join("/");
291
+ const fullCandidate = join(root, candidateDir);
292
+ if (existsSync(fullCandidate)) {
293
+ try {
294
+ const entries = readdirSync(fullCandidate);
295
+ const hasManifest = entries.some(
296
+ (e) => manifestFiles.includes(e) || e.endsWith(".csproj") || e.endsWith(".fsproj")
297
+ );
298
+ if (hasManifest) {
299
+ return candidateDir;
300
+ }
301
+ } catch (_) {}
302
+ }
303
+ }
304
+
305
+ return ".";
306
+ }
307
+
308
+ /**
309
+ * Detects illegal cross-package imports escaping subproject boundaries in monorepos.
310
+ */
311
+ export function detectCrossPackageBoundaryViolations(changedFiles = [], root = process.cwd(), options = {}) {
312
+ const violations = [];
313
+ if (!changedFiles || changedFiles.length === 0) return violations;
314
+
315
+ const fileContents = options.fileContents || {};
316
+
317
+ // Regex patterns for relative imports across languages
318
+ const jsImportRegex = /(?:import\s+(?:[\w*\s{},]*\s+from\s+)?|export\s+(?:[\w*\s{},]*\s+from\s+)?|require\s*\(\s*|import\s*\(\s*)['"](\.[^'"]+)['"]/g;
319
+ const goImportRegex = /(?:^|\n)\s*(?:import\s+['"](\.[^'"]+)['"]|import\s*\([\s\S]*?['"](\.[^'"]+)['"][\s\S]*?\))/g;
320
+ const rustPathRegex = /#\[path\s*=\s*['"](\.[^'"]+)['"]\]/g;
321
+ const pyRelImportRegex = /(?:^|\n)\s*from\s+(\.{2,}\w*)\s+import/g;
322
+
323
+ for (const file of changedFiles) {
324
+ const posixFile = toPosixPath(file);
325
+ let content = fileContents[file] || fileContents[posixFile];
326
+
327
+ if (!content) {
328
+ const fullPath = join(root, file);
329
+ if (existsSync(fullPath)) {
330
+ try {
331
+ content = readFileSync(fullPath, "utf-8");
332
+ } catch (_) {}
333
+ }
334
+ }
335
+
336
+ if (!content || typeof content !== "string") continue;
337
+
338
+ const subproject = findSubprojectRoot(posixFile, root);
339
+ if (subproject === "." && !options.enforceRootBoundary) {
340
+ continue;
341
+ }
342
+
343
+ const fileDir = posixFile.includes("/") ? posixFile.substring(0, posixFile.lastIndexOf("/")) : ".";
344
+ const ext = posixFile.includes(".") ? posixFile.substring(posixFile.lastIndexOf(".")).toLowerCase() : "";
345
+ const isJsTs = [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".svelte", ".vue"].includes(ext) || !ext;
346
+ const isGo = ext === ".go";
347
+ const isRust = ext === ".rs";
348
+ const isPython = ext === ".py";
349
+
350
+ const lines = content.split("\n");
351
+
352
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
353
+ const line = lines[lineIdx];
354
+
355
+ // 1. JS / TS imports
356
+ if (isJsTs) {
357
+ let match;
358
+ jsImportRegex.lastIndex = 0;
359
+ while ((match = jsImportRegex.exec(line)) !== null) {
360
+ const relTarget = match[1];
361
+ if (relTarget.startsWith("..")) {
362
+ const resolved = toPosixPath(join(fileDir, relTarget));
363
+ const relToSubproject = toPosixPath(relative(subproject, resolved));
364
+ if (relToSubproject.startsWith("..") || resolved === "." || (subproject !== "." && !resolved.startsWith(subproject + "/"))) {
365
+ violations.push({
366
+ file: posixFile,
367
+ subproject,
368
+ importTarget: relTarget,
369
+ resolvedTarget: resolved,
370
+ line: lineIdx + 1,
371
+ language: "javascript/typescript",
372
+ reason: `Cross-Package Boundary Violation: File "${posixFile}" in package "${subproject}" illegally imports "${relTarget}" (resolves to "${resolved}"), escaping its package boundary. Declare a workspace dependency instead of a relative cross-package file import.`,
373
+ });
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ // 2. Go relative imports escaping module
380
+ if (isGo) {
381
+ let match;
382
+ goImportRegex.lastIndex = 0;
383
+ while ((match = goImportRegex.exec(line)) !== null) {
384
+ const relTarget = match[1] || match[2];
385
+ if (relTarget && relTarget.startsWith("..")) {
386
+ const resolved = toPosixPath(join(fileDir, relTarget));
387
+ const relToSubproject = toPosixPath(relative(subproject, resolved));
388
+ if (relToSubproject.startsWith("..")) {
389
+ violations.push({
390
+ file: posixFile,
391
+ subproject,
392
+ importTarget: relTarget,
393
+ resolvedTarget: resolved,
394
+ line: lineIdx + 1,
395
+ language: "go",
396
+ reason: `Cross-Module Boundary Violation: Go file "${posixFile}" in module "${subproject}" illegally imports relative path "${relTarget}" escaping module root.`,
397
+ });
398
+ }
399
+ }
400
+ }
401
+ }
402
+
403
+ // 3. Rust path attributes escaping crate
404
+ if (isRust) {
405
+ let match;
406
+ rustPathRegex.lastIndex = 0;
407
+ while ((match = rustPathRegex.exec(line)) !== null) {
408
+ const relTarget = match[1];
409
+ if (relTarget && relTarget.startsWith("..")) {
410
+ const resolved = toPosixPath(join(fileDir, relTarget));
411
+ const relToSubproject = toPosixPath(relative(subproject, resolved));
412
+ if (relToSubproject.startsWith("..")) {
413
+ violations.push({
414
+ file: posixFile,
415
+ subproject,
416
+ importTarget: relTarget,
417
+ resolvedTarget: resolved,
418
+ line: lineIdx + 1,
419
+ language: "rust",
420
+ reason: `Cross-Crate Boundary Violation: Rust file "${posixFile}" in crate "${subproject}" uses path attribute "${relTarget}" escaping crate root.`,
421
+ });
422
+ }
423
+ }
424
+ }
425
+ }
426
+
427
+ // 4. Python relative imports escaping package
428
+ if (isPython) {
429
+ let match;
430
+ pyRelImportRegex.lastIndex = 0;
431
+ while ((match = pyRelImportRegex.exec(line)) !== null) {
432
+ const dots = match[1];
433
+ const dotCount = (dots.match(/\./g) || []).length;
434
+ // Count depth from subproject to fileDir
435
+ const depth = fileDir === subproject ? 0 : fileDir.slice(subproject.length + 1).split("/").length;
436
+ if (dotCount > depth + 1) {
437
+ violations.push({
438
+ file: posixFile,
439
+ subproject,
440
+ importTarget: dots,
441
+ resolvedTarget: "<parent-package>",
442
+ line: lineIdx + 1,
443
+ language: "python",
444
+ reason: `Cross-Package Boundary Violation: Python file "${posixFile}" in package "${subproject}" uses relative import with ${dotCount} dots, escaping package root.`,
445
+ });
446
+ }
447
+ }
448
+ }
449
+ }
450
+ }
451
+
452
+ return violations;
453
+ }
454
+
455
+ /**
456
+ * Discovers monorepo packages and detects circular dependencies.
457
+ */
458
+ export function detectCircularDependencies(root = process.cwd()) {
459
+ const packageMap = new Map(); // name -> { path, deps: [] }
460
+ const searchDirs = ["packages", "apps", "crates", "services", "libs", "modules", "backend", "frontend", "cli", "."];
461
+
462
+ for (const dir of searchDirs) {
463
+ const fullDir = join(root, dir);
464
+ if (!existsSync(fullDir)) continue;
465
+
466
+ const subDirs = dir === "." ? ["."] : [];
467
+ try {
468
+ if (dir !== ".") {
469
+ const entries = readdirSync(fullDir, { withFileTypes: true });
470
+ for (const e of entries) {
471
+ if (e.isDirectory()) subDirs.push(`${dir}/${e.name}`);
472
+ }
473
+ }
474
+ } catch (_) {}
475
+
476
+ for (const subDir of subDirs) {
477
+ const fullSubDir = join(root, subDir);
478
+ // 1. JS/TS package.json
479
+ const pkgJsonPath = join(fullSubDir, "package.json");
480
+ if (existsSync(pkgJsonPath)) {
481
+ try {
482
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
483
+ if (pkg.name) {
484
+ const allDeps = {
485
+ ...(pkg.dependencies || {}),
486
+ ...(pkg.devDependencies || {}),
487
+ ...(pkg.peerDependencies || {}),
488
+ };
489
+ packageMap.set(pkg.name, {
490
+ name: pkg.name,
491
+ path: subDir,
492
+ type: "npm",
493
+ deps: Object.keys(allDeps),
494
+ });
495
+ }
496
+ } catch (_) {}
497
+ }
498
+
499
+ // 2. Rust Cargo.toml
500
+ const cargoPath = join(fullSubDir, "Cargo.toml");
501
+ if (existsSync(cargoPath)) {
502
+ try {
503
+ const content = readFileSync(cargoPath, "utf-8");
504
+ const nameMatch = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
505
+ if (nameMatch) {
506
+ const crateName = nameMatch[1];
507
+ const deps = [];
508
+ const depMatches = content.matchAll(/([a-zA-Z0-9_-]+)\s*=\s*\{[^}]*?path\s*=\s*["']([^"']+)["']/g);
509
+ for (const dm of depMatches) {
510
+ deps.push(dm[1]);
511
+ }
512
+ packageMap.set(crateName, {
513
+ name: crateName,
514
+ path: subDir,
515
+ type: "cargo",
516
+ deps,
517
+ });
518
+ }
519
+ } catch (_) {}
520
+ }
521
+ }
522
+ }
523
+
524
+ // Filter dependencies to only workspace internal packages
525
+ const knownNames = new Set(packageMap.keys());
526
+ const graph = new Map();
527
+ for (const [name, info] of packageMap.entries()) {
528
+ graph.set(name, info.deps.filter((d) => knownNames.has(d)));
529
+ }
530
+
531
+ // Cycle detection via DFS
532
+ const cycles = [];
533
+ const visited = new Set();
534
+ const recStack = new Set();
535
+ const path = [];
536
+
537
+ function dfs(node) {
538
+ visited.add(node);
539
+ recStack.add(node);
540
+ path.push(node);
541
+
542
+ const neighbors = graph.get(node) || [];
543
+ for (const neighbor of neighbors) {
544
+ if (!visited.has(neighbor)) {
545
+ dfs(neighbor);
546
+ } else if (recStack.has(neighbor)) {
547
+ const cycleStartIndex = path.indexOf(neighbor);
548
+ const cycle = path.slice(cycleStartIndex).concat(neighbor);
549
+ cycles.push(cycle.join(" -> "));
550
+ }
551
+ }
552
+
553
+ path.pop();
554
+ recStack.delete(node);
555
+ }
556
+
557
+ for (const node of graph.keys()) {
558
+ if (!visited.has(node)) {
559
+ dfs(node);
560
+ }
561
+ }
562
+
563
+ return {
564
+ hasCycles: cycles.length > 0,
565
+ cycles,
566
+ packages: Array.from(packageMap.values()),
567
+ };
568
+ }
569
+
233
570
  /**
234
571
  * Resolves workspace boundaries in polyglot monorepos by mapping changed files to subprojects.
235
572
  */
236
- export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd()) {
573
+ export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd(), options = {}) {
237
574
  const rootStack = detectPolyglotStack(root);
575
+ const boundaryViolations = detectCrossPackageBoundaryViolations(changedFiles, root, options);
576
+ const circular = detectCircularDependencies(root);
577
+
238
578
  if (!changedFiles || changedFiles.length === 0) {
239
- return { isMonorepo: false, globalFallback: false, projects: [{ path: ".", ...rootStack }], testCmd: rootStack.testCmd, buildCmd: rootStack.buildCmd };
579
+ return {
580
+ isMonorepo: circular.packages.length > 1,
581
+ globalFallback: false,
582
+ projects: [{ path: ".", ...rootStack }],
583
+ testCmd: rootStack.testCmd,
584
+ buildCmd: rootStack.buildCmd,
585
+ boundaryViolations,
586
+ circularDependencies: circular.cycles,
587
+ };
240
588
  }
241
589
 
242
590
  const sharedTriggers = new Set(["openapi.yaml", "openapi.json", "schema.graphql", "docker-compose.yml", "Makefile", "turbo.json", "pnpm-workspace.yaml", "nx.json"]);
243
591
  for (const file of changedFiles) {
244
- const baseName = file.split(sep).pop();
592
+ const baseName = toPosixPath(file).split("/").pop();
245
593
  if (sharedTriggers.has(baseName) || file.startsWith(".agent/") || file.startsWith(".github/")) {
246
- return { isMonorepo: true, globalFallback: true, projects: [{ path: ".", ...rootStack }], testCmd: rootStack.testCmd, buildCmd: rootStack.buildCmd };
594
+ return {
595
+ isMonorepo: true,
596
+ globalFallback: true,
597
+ projects: [{ path: ".", ...rootStack }],
598
+ testCmd: rootStack.testCmd,
599
+ buildCmd: rootStack.buildCmd,
600
+ boundaryViolations,
601
+ circularDependencies: circular.cycles,
602
+ };
247
603
  }
248
604
  }
249
605
 
250
606
  const projectMap = new Map();
251
607
  for (const file of changedFiles) {
252
- const parts = file.split(sep);
253
- let resolved = false;
254
-
255
- for (let i = parts.length - 1; i >= 1; i--) {
256
- const subDir = parts.slice(0, i).join("/");
608
+ const subDir = findSubprojectRoot(file, root);
609
+ if (subDir !== ".") {
257
610
  const subStack = detectPolyglotStack(join(root, subDir));
258
- if (subStack.stack !== "unknown" && subStack.triggerFile) {
259
- if (!projectMap.has(subDir)) {
260
- projectMap.set(subDir, { path: subDir, ...subStack });
261
- }
262
- resolved = true;
263
- break;
611
+ if (!projectMap.has(subDir)) {
612
+ projectMap.set(subDir, { path: subDir, ...subStack });
613
+ }
614
+ } else {
615
+ if (!projectMap.has(".")) {
616
+ projectMap.set(".", { path: ".", ...rootStack });
264
617
  }
265
- }
266
-
267
- if (!resolved && !projectMap.has(".")) {
268
- projectMap.set(".", { path: ".", ...rootStack });
269
618
  }
270
619
  }
271
620
 
@@ -278,11 +627,13 @@ export function resolveWorkspaceBoundary(changedFiles = [], root = process.cwd()
278
627
  .map((p) => (p.path === "." ? p.buildCmd : `(cd ${p.path} && ${p.buildCmd})`));
279
628
 
280
629
  return {
281
- isMonorepo: projects.length > 1,
630
+ isMonorepo: projects.length > 1 || circular.packages.length > 1,
282
631
  globalFallback: false,
283
632
  projects,
284
633
  testCmd: testCmds.join(" && ") || rootStack.testCmd,
285
634
  buildCmd: buildCmds.join(" && ") || rootStack.buildCmd,
635
+ boundaryViolations,
636
+ circularDependencies: circular.cycles,
286
637
  };
287
638
  }
288
639
 
@@ -1,5 +1,5 @@
1
- import { writeFileSync, mkdirSync, existsSync } from "node:fs";
2
- import { resolve } from "node:path";
1
+ import { writeFileSync, readFileSync, readdirSync, mkdirSync, existsSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
3
  import { loadConfig } from "./config.mjs";
4
4
  import { gate } from "./engine.mjs";
5
5
  import { scanDiff, shannonEntropy } from "./security.mjs";
@@ -21,9 +21,38 @@ HARD CONSTRAINTS:
21
21
 
22
22
  const TRIVIAL_ORACLES = new Set(["true", "echo", ":", "false", "exit 0", "exit 1", "echo ok"]);
23
23
 
24
+ /**
25
+ * Resolves specialist agent role markdown prompt from .agent/prompts/
26
+ * @param {string} [root=process.cwd()]
27
+ * @param {string} [roleName=""]
28
+ * @returns {{ role: string, path: string, content: string } | null}
29
+ */
30
+ export function resolveRolePrompt(root = process.cwd(), roleName = "") {
31
+ if (!roleName || typeof roleName !== "string") return null;
32
+ const cleanName = roleName.trim().toLowerCase();
33
+ const promptsDir = join(root, ".agent", "prompts");
34
+ if (!existsSync(promptsDir)) return null;
35
+
36
+ try {
37
+ const files = readdirSync(promptsDir);
38
+ const matched = files.find(
39
+ (f) => f.toLowerCase() === `${cleanName}.md` || f.toLowerCase() === cleanName
40
+ );
41
+ if (matched) {
42
+ const fullPath = join(promptsDir, matched);
43
+ return {
44
+ role: matched.replace(/\.md$/i, ""),
45
+ path: fullPath,
46
+ content: readFileSync(fullPath, "utf-8").trim(),
47
+ };
48
+ }
49
+ } catch (_) {}
50
+ return null;
51
+ }
52
+
24
53
  /**
25
54
  * Pure planning core for task creation & envelope synthesis.
26
- * Supports web task templates, exploration budgets, and critic agent steering.
55
+ * Supports web task templates, specialist roles, exploration budgets, and critic agent steering.
27
56
  * @param {string} root
28
57
  * @param {object} inputObj
29
58
  * @returns {{
@@ -33,6 +62,8 @@ const TRIVIAL_ORACLES = new Set(["true", "echo", ":", "false", "exit 0", "exit 1
33
62
  * prompt: string,
34
63
  * fullPrompt: string,
35
64
  * verifyCmd: string,
65
+ * role?: string,
66
+ * dependsOn?: string[],
36
67
  * flags: { autoPr: boolean, requirePlanApproval: boolean, repoless: boolean, startingBranch: string },
37
68
  * secretFindings: Array<any>,
38
69
  * taskFileContent: string
@@ -45,7 +76,22 @@ export function planTaskCreate(root = process.cwd(), inputObj = {}) {
45
76
  let rawPrompt = inputObj.prompt || "";
46
77
  let verifyCmd = (inputObj.verifyCmd || config.verify.test || config.verify.build || "").trim();
47
78
 
48
- // 0. Process Template if specified
79
+ // 0a. Process Specialist Role if specified
80
+ let resolvedRole = null;
81
+ if (inputObj.role) {
82
+ resolvedRole = resolveRolePrompt(root, inputObj.role);
83
+ if (!resolvedRole) {
84
+ throw new Error(
85
+ `Unknown agent role '${inputObj.role}'. Expected matching prompt file in .agent/prompts/ (e.g. Overseer, Bolt, Sentinel, Janitor).`
86
+ );
87
+ }
88
+ rawPrompt = `${resolvedRole.content}\n\n${rawPrompt}`.trim();
89
+ if (!title || title === "Agent Task") {
90
+ title = `${resolvedRole.role}: ${inputObj.title || "Specialist Task"}`;
91
+ }
92
+ }
93
+
94
+ // 0b. Process Template if specified
49
95
  if (inputObj.template) {
50
96
  const tpl = getWebTemplate(inputObj.template);
51
97
  if (!tpl) {
@@ -57,7 +103,7 @@ export function planTaskCreate(root = process.cwd(), inputObj = {}) {
57
103
  criticGuidance: inputObj.criticGuidance
58
104
  });
59
105
  rawPrompt = rawPrompt ? `${rawPrompt}\n\n${synthesized.prompt}` : synthesized.fullEnvelope;
60
- if (!title) title = synthesized.title;
106
+ if (!title || title === "Agent Task") title = synthesized.title;
61
107
  if (!verifyCmd) verifyCmd = synthesized.verifyCmd;
62
108
  }
63
109
 
@@ -123,12 +169,24 @@ ${GUARDRAIL_FOOTER}`;
123
169
 
124
170
  const promptAnalysis = scorePromptFalsifiability(rawPrompt, { rootDir: root, verifyCmd });
125
171
 
172
+ const rawDepends = inputObj.dependsOn || inputObj.depends;
173
+ const dependsOn = Array.isArray(rawDepends)
174
+ ? rawDepends
175
+ : typeof rawDepends === "string"
176
+ ? rawDepends.split(",").map((s) => s.trim()).filter(Boolean)
177
+ : undefined;
178
+
179
+ const tier = inputObj.tier === "fast" || inputObj.tier === "complex" ? inputObj.tier : undefined;
180
+
126
181
  const envelopeMetadata = {
127
182
  version: 1,
128
183
  id: taskId,
129
184
  title,
130
185
  flags,
131
186
  verifyCmd,
187
+ role: resolvedRole ? resolvedRole.role : (inputObj.role || undefined),
188
+ dependsOn,
189
+ tier,
132
190
  falsifiabilityScore: promptAnalysis.score,
133
191
  grade: promptAnalysis.grade,
134
192
  };
@@ -148,6 +206,9 @@ ${fullPrompt}
148
206
  prompt: rawPrompt,
149
207
  fullPrompt,
150
208
  verifyCmd,
209
+ role: envelopeMetadata.role,
210
+ dependsOn,
211
+ tier,
151
212
  flags,
152
213
  secretFindings,
153
214
  promptAnalysis,