jules-orchestrator-kit 0.73.0 → 0.74.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.73.0",
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.",
3
+ "version": "0.74.0",
4
+ "description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for autonomous coding agents \u2014 Google Jules, Claude Code, Codex and Gemini CLI.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/FullThrottle83/jules-orchestrator-kit.git"
@@ -33,9 +33,7 @@
33
33
  ".agent/rules/",
34
34
  ".agent/prompts/",
35
35
  ".agent/workflows/",
36
- "CHANGELOG.md",
37
- "ROADMAP_V1.md",
38
- "AGENTS.md"
36
+ "CHANGELOG.md"
39
37
  ],
40
38
  "scripts": {
41
39
  "init": "node bin/init.js",
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFileSync, existsSync } from "node:fs";
4
- import { validateEnvelope } from "../src/envelope.mjs";
4
+ import { validateEnvelope, parseEnvelopeHeader } from "../src/envelope.mjs";
5
5
 
6
6
  const args = process.argv.slice(2);
7
7
  const envelopeFile = args[0];
8
8
 
9
9
  if (!envelopeFile) {
10
- console.log("Usage: node scripts/validate-envelope.mjs <path-to-envelope.json>");
10
+ console.log("Usage: node scripts/validate-envelope.mjs <path-to-envelope.json|path-to-task.md>");
11
11
  process.exit(1);
12
12
  }
13
13
 
@@ -18,7 +18,26 @@ if (!existsSync(envelopeFile)) {
18
18
 
19
19
  try {
20
20
  const content = readFileSync(envelopeFile, "utf-8");
21
- const payload = JSON.parse(content);
21
+ let payload = null;
22
+ if (envelopeFile.endsWith(".json")) {
23
+ payload = JSON.parse(content);
24
+ } else {
25
+ payload = parseEnvelopeHeader(content);
26
+ if (!payload) {
27
+ try {
28
+ payload = JSON.parse(content);
29
+ } catch {
30
+ payload = null;
31
+ }
32
+ }
33
+ }
34
+
35
+ if (!payload || typeof payload !== "object") {
36
+ console.error("❌ TASK ENVELOPE PREMISE VALIDATION FAILED:");
37
+ console.error(" - File does not contain a valid JSON payload or task envelope frontmatter.");
38
+ process.exit(1);
39
+ }
40
+
22
41
  const res = validateEnvelope(payload);
23
42
 
24
43
  if (!res.ok) {
@@ -39,6 +58,6 @@ try {
39
58
  console.log("✅ Task envelope validated successfully.");
40
59
  process.exit(0);
41
60
  } catch (err) {
42
- console.error(`❌ Invalid JSON or execution error: ${err.message}`);
61
+ console.error(`❌ Invalid envelope or execution error: ${err.message}`);
43
62
  process.exit(1);
44
63
  }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFileSync, existsSync } from "node:fs";
3
3
  import { appendTelemetry } from "./telemetry.mjs";
4
+ import { parseEnvelopeHeader } from "./envelope.mjs";
4
5
 
5
6
  /**
6
7
  * Custom error thrown when a circular dependency is detected in the DAG.
@@ -495,20 +496,19 @@ export async function executeQueueDag(root = process.cwd(), options = {}) {
495
496
 
496
497
  let targetFiles = [];
497
498
 
498
- // Check envelope header
499
- const match = content.match(/<!--\s*JULES_TASK_ENVELOPE:\s*({[\s\S]*?})\s*-->/);
500
- if (match) {
501
- try {
502
- const meta = JSON.parse(match[1]);
503
- if (meta.id) taskId = meta.id;
504
- if (meta.title) title = meta.title;
505
- if (meta.role) role = meta.role;
506
- if (meta.tier) tier = meta.tier;
507
- if (Array.isArray(meta.targetFiles)) targetFiles = meta.targetFiles;
508
- else if (Array.isArray(meta.referenced_paths)) targetFiles = meta.referenced_paths;
509
- if (Array.isArray(meta.dependsOn)) dependsOn = meta.dependsOn;
510
- else if (typeof meta.dependsOn === "string") dependsOn = meta.dependsOn.split(",").map((s) => s.trim()).filter(Boolean);
511
- } catch (_) {}
499
+ // Check envelope header (supports agentctl.task/v1 YAML frontmatter and legacy HTML comment)
500
+ const header = parseEnvelopeHeader(content);
501
+ if (header) {
502
+ if (header.id || header.taskId) taskId = header.id || header.taskId;
503
+ if (header.title) title = header.title;
504
+ if (header.role) role = header.role;
505
+ if (header.tier) tier = header.tier;
506
+ if (Array.isArray(header.targetFiles)) targetFiles = header.targetFiles;
507
+ else if (Array.isArray(header.referenced_paths)) targetFiles = header.referenced_paths;
508
+ else if (Array.isArray(header.allowed_paths)) targetFiles = header.allowed_paths;
509
+ else if (header.scope && Array.isArray(header.scope.allow)) targetFiles = header.scope.allow;
510
+ if (Array.isArray(header.dependsOn)) dependsOn = header.dependsOn;
511
+ else if (typeof header.dependsOn === "string") dependsOn = header.dependsOn.split(",").map((s) => s.trim()).filter(Boolean);
512
512
  } else if (file.endsWith(".json")) {
513
513
  try {
514
514
  const parsed = JSON.parse(content);
package/src/engine.mjs CHANGED
@@ -143,6 +143,7 @@ export function fingerprintFailureState(failure = {}, root = process.cwd()) {
143
143
  * FAILS CLOSED ON GIT OR CONFIG ERRORS.
144
144
  */
145
145
  export async function gate(opts = {}) {
146
+ opts = opts && typeof opts === "object" ? { ...opts, fix: false } : { fix: false };
146
147
  const root = opts.root || process.cwd();
147
148
  const mode = opts.mode || (opts.workingTree ? "working-tree" : (process.env.JULES_GATE_MODE || "working-tree"));
148
149
  const config = opts.config || (mode === "working-tree" ? loadConfig(root) : null);
@@ -999,7 +1000,7 @@ export async function checkTaskPremise(task = {}, opts = {}) {
999
1000
 
1000
1001
  /**
1001
1002
  * Session states the API documents as final. The full `SessionState` enum is
1002
- * transcribed in `docs/jules-quality-plan.md`.
1003
+ * transcribed in `docs/archive/jules-quality-plan.md`.
1003
1004
  */
1004
1005
  export const TERMINAL_SESSION_STATES = new Set(["COMPLETED", "FAILED"]);
1005
1006
 
package/src/envelope.mjs CHANGED
@@ -24,12 +24,15 @@ export function validateEnvelope(envelope = {}, opts = {}) {
24
24
  }
25
25
 
26
26
  // 1. Mandatory Fields
27
- if (!envelope.intent || typeof envelope.intent !== "string" || !envelope.intent.trim()) {
27
+ const rawIntent = (envelope.intent && typeof envelope.intent === "object")
28
+ ? envelope.intent.outcome
29
+ : (envelope.intent || envelope.title || envelope.prompt);
30
+ if (!rawIntent || typeof rawIntent !== "string" || !rawIntent.trim()) {
28
31
  errors.push("Envelope missing required 'intent' string.");
29
32
  }
30
33
 
31
34
  // 2. Base Commit & Freshness Check
32
- const baseCommit = envelope.base_commit || "main";
35
+ const baseCommit = envelope.base_commit || envelope.baseCommit || envelope.base_sha || envelope.baseSha || "main";
33
36
  let resolvedBase = null;
34
37
  try {
35
38
  resolvedBase = resolveBase(root, baseCommit);
@@ -86,19 +89,51 @@ export function validateEnvelope(envelope = {}, opts = {}) {
86
89
  }
87
90
 
88
91
  // 4. Allowed Paths & Protected Scope Check
89
- if (Array.isArray(envelope.allowed_paths) && envelope.allowed_paths.length > 0) {
92
+ const riskObj = envelope.risk && typeof envelope.risk === "object" ? envelope.risk : {};
93
+ const rawLane = riskObj.lane || envelope.risk_lane || envelope.riskLane;
94
+ const riskLane = rawLane ? String(rawLane).toLowerCase() : (envelope.flags?.requirePlanApproval ? "amber" : "green");
95
+ const allowProtected = opts.allowProtected ?? (riskLane === "amber" && Boolean(envelope.flags?.requirePlanApproval || riskObj.require_plan_approval));
96
+
97
+ if (riskLane === "red") {
98
+ errors.push("Red-lane tasks are outside autonomous agent delegation boundaries (unsupervised high-impact, custom crypto, un-oracleable).");
99
+ } else if (riskLane === "orange") {
100
+ warnings.push("Orange-lane task: candidate generation only. Execution must be conducted outside Jules via protected control plane.");
101
+ }
102
+
103
+ const allowPaths = envelope.allowed_paths || envelope.scope?.allow;
104
+ if (Array.isArray(allowPaths) && allowPaths.length > 0) {
90
105
  const scope = normalizeScope(opts.scopeConfig || {});
91
- const scopeCheck = checkScope(envelope.allowed_paths, scope);
106
+ const scopeCheck = checkScope(allowPaths, scope, { allowProtected });
92
107
  if (!scopeCheck.ok) {
93
- errors.push(`Allowed paths violate protected scope: ${scopeCheck.violations.map(v => `${v.file} (${v.reason})`).join(", ")}`);
108
+ const hasProtectedViolation = scopeCheck.violations.some((v) => v.rule === "protect");
109
+ if (hasProtectedViolation && !allowProtected) {
110
+ errors.push(
111
+ `Allowed paths violate protected scope: ${scopeCheck.violations.map((v) => `${v.file} (${v.reason})`).join(", ")}. Modifying protected paths requires risk.lane 'amber' and requirePlanApproval: true.`
112
+ );
113
+ } else {
114
+ errors.push(`Allowed paths violate protected scope: ${scopeCheck.violations.map((v) => `${v.file} (${v.reason})`).join(", ")}`);
115
+ }
94
116
  }
95
117
  }
96
118
 
97
- // 5. Acceptance Criteria
119
+ // 5. Acceptance Criteria & Verification Commands
98
120
  if (envelope.acceptance_criteria !== undefined) {
99
121
  if (!Array.isArray(envelope.acceptance_criteria) || envelope.acceptance_criteria.length === 0) {
100
122
  errors.push("acceptance_criteria must be a non-empty array of strings when provided.");
101
123
  }
124
+ } else if (envelope.verification?.commands !== undefined) {
125
+ if (!Array.isArray(envelope.verification.commands) || envelope.verification.commands.length === 0) {
126
+ errors.push("verification.commands must be a non-empty array of strings when provided.");
127
+ }
128
+ }
129
+
130
+ if (envelope.verification && typeof envelope.verification === "object") {
131
+ if (envelope.verification.require_nonzero_test_count !== undefined && typeof envelope.verification.require_nonzero_test_count !== "boolean") {
132
+ errors.push("verification.require_nonzero_test_count must be a boolean when provided.");
133
+ }
134
+ if (envelope.verification.trusted_base !== undefined && typeof envelope.verification.trusted_base !== "string") {
135
+ errors.push("verification.trusted_base must be a string when provided.");
136
+ }
102
137
  }
103
138
 
104
139
  // 6. Concurrency Group Check
@@ -108,8 +143,49 @@ export function validateEnvelope(envelope = {}, opts = {}) {
108
143
  }
109
144
  }
110
145
 
111
- return {
146
+ // 7. Invariants Check
147
+ if (envelope.invariants !== undefined && !Array.isArray(envelope.invariants)) {
148
+ errors.push("invariants must be an array of strings when provided.");
149
+ }
150
+
151
+ // 8. MCP Directives Check
152
+ if (envelope.mcp_directives !== undefined && !Array.isArray(envelope.mcp_directives)) {
153
+ errors.push("mcp_directives must be an array of strings when provided.");
154
+ }
155
+
156
+ // 9. Risk Object Check
157
+ if (envelope.risk !== undefined) {
158
+ if (typeof envelope.risk !== "object" || envelope.risk === null) {
159
+ errors.push("risk must be an object when provided.");
160
+ } else {
161
+ if (envelope.risk.lane !== undefined && !["green", "amber", "orange", "red"].includes(String(envelope.risk.lane).toLowerCase())) {
162
+ errors.push("risk.lane must be one of 'green', 'amber', 'orange', 'red' when provided.");
163
+ }
164
+ if (envelope.risk.require_plan_approval !== undefined && typeof envelope.risk.require_plan_approval !== "boolean") {
165
+ errors.push("risk.require_plan_approval must be a boolean when provided.");
166
+ }
167
+ }
168
+ }
112
169
 
170
+ // 10. Circuit Breaker Check
171
+ const cb = envelope.circuit_breaker || envelope.circuitBreaker;
172
+ if (cb !== undefined) {
173
+ if (typeof cb !== "object" || cb === null) {
174
+ errors.push("circuitBreaker must be an object when provided.");
175
+ } else {
176
+ if (cb.max_attempts !== undefined && !Number.isInteger(Number(cb.max_attempts))) {
177
+ errors.push("circuitBreaker.max_attempts must be an integer when provided.");
178
+ }
179
+ if (cb.max_diff_lines !== undefined && !Number.isInteger(Number(cb.max_diff_lines))) {
180
+ errors.push("circuitBreaker.max_diff_lines must be an integer when provided.");
181
+ }
182
+ if (cb.stop_if_same_failure_repeats !== undefined && typeof cb.stop_if_same_failure_repeats !== "boolean") {
183
+ errors.push("circuitBreaker.stop_if_same_failure_repeats must be a boolean when provided.");
184
+ }
185
+ }
186
+ }
187
+
188
+ return {
113
189
  ok: errors.length === 0,
114
190
  code: errors.length === 0 ? 0 : 1,
115
191
  errors,
@@ -118,18 +194,343 @@ export function validateEnvelope(envelope = {}, opts = {}) {
118
194
  }
119
195
 
120
196
  /**
121
- * Parse JSON metadata payload embedded in HTML comment JULES_TASK_ENVELOPE.
197
+ * Parses a subset of YAML suitable for task envelopes without third-party dependencies.
198
+ * Handles mappings, sequence lists, inline lists, quoted strings, numbers, and booleans.
199
+ *
200
+ * @param {string} yamlText
201
+ * @returns {Record<string, any>}
202
+ */
203
+ export function parseYamlSubset(yamlText) {
204
+ if (!yamlText || typeof yamlText !== "string") return {};
205
+ const lines = yamlText.split(/\r?\n/);
206
+ const root = {};
207
+ const stack = [{ indent: -1, obj: root, key: null }];
208
+
209
+ function parseVal(raw) {
210
+ const trimmed = raw.trim();
211
+ if (trimmed === "") return "";
212
+ if (trimmed === "true") return true;
213
+ if (trimmed === "false") return false;
214
+ if (trimmed === "null" || trimmed === "~") return null;
215
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
216
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
217
+ return trimmed.slice(1, -1);
218
+ }
219
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
220
+ const inner = trimmed.slice(1, -1).trim();
221
+ if (!inner) return [];
222
+ return inner.split(",").map((s) => parseVal(s.trim()));
223
+ }
224
+ return trimmed;
225
+ }
226
+
227
+ for (let i = 0; i < lines.length; i++) {
228
+ const rawLine = lines[i];
229
+ let line = rawLine;
230
+ const commentIdx = line.indexOf("#");
231
+ if (commentIdx !== -1) {
232
+ const before = line.slice(0, commentIdx);
233
+ const singleQuotes = (before.match(/'/g) || []).length;
234
+ const doubleQuotes = (before.match(/"/g) || []).length;
235
+ if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {
236
+ line = before;
237
+ }
238
+ }
239
+ if (!line.trim()) continue;
240
+
241
+ const indent = line.search(/\S/);
242
+ const content = line.trim();
243
+
244
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
245
+ stack.pop();
246
+ }
247
+
248
+ const current = stack[stack.length - 1];
249
+
250
+ if (content.startsWith("- ")) {
251
+ const itemVal = parseVal(content.slice(2));
252
+ let targetArray;
253
+ if (Array.isArray(current.obj)) {
254
+ targetArray = current.obj;
255
+ } else if (current.key && current.obj && Array.isArray(current.obj[current.key])) {
256
+ targetArray = current.obj[current.key];
257
+ } else if (current.key && current.obj) {
258
+ current.obj[current.key] = [];
259
+ targetArray = current.obj[current.key];
260
+ }
261
+ if (targetArray) {
262
+ targetArray.push(itemVal);
263
+ }
264
+ continue;
265
+ }
266
+
267
+ const colonIdx = content.indexOf(":");
268
+ if (colonIdx !== -1) {
269
+ const key = content.slice(0, colonIdx).trim();
270
+ const valStr = content.slice(colonIdx + 1).trim();
271
+
272
+ if (valStr === "") {
273
+ let isNextArray = false;
274
+ for (let j = i + 1; j < lines.length; j++) {
275
+ const nextTrimmed = lines[j].trim();
276
+ if (!nextTrimmed || nextTrimmed.startsWith("#")) continue;
277
+ if (nextTrimmed.startsWith("- ")) isNextArray = true;
278
+ break;
279
+ }
280
+ const container = isNextArray ? [] : {};
281
+ if (Array.isArray(current.obj)) {
282
+ current.obj.push({ [key]: container });
283
+ } else {
284
+ current.obj[key] = container;
285
+ }
286
+ stack.push({ indent, obj: container, key });
287
+ } else {
288
+ const val = parseVal(valStr);
289
+ if (Array.isArray(current.obj)) {
290
+ current.obj.push({ [key]: val });
291
+ } else {
292
+ current.obj[key] = val;
293
+ }
294
+ current.key = key;
295
+ }
296
+ }
297
+ }
298
+
299
+ return root;
300
+ }
301
+
302
+ /**
303
+ * Extracts and parses YAML frontmatter from markdown task files.
304
+ * Supports standard `--- ... ---` envelopes (including `kind: Task`, `version: agentctl.task/v1`).
305
+ *
122
306
  * @param {string} content
123
- * @returns {Record<string, any> | null}
307
+ * @returns {{ metadata: Record<string, any>, body: string, rawFrontmatter: string } | null}
124
308
  */
125
- export function parseEnvelopeHeader(content) {
126
- if (!content) return null;
127
- const match = String(content).match(/<!--\s*JULES_TASK_ENVELOPE:\s*({[\s\S]*?})\s*-->/);
309
+ export function parseTaskFrontmatter(content) {
310
+ if (!content || typeof content !== "string") return null;
311
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/);
128
312
  if (!match) return null;
129
313
  try {
130
- return JSON.parse(match[1]);
314
+ const parsed = parseYamlSubset(match[1]);
315
+ if (!parsed || typeof parsed !== "object" || Object.keys(parsed).length === 0) {
316
+ return null;
317
+ }
318
+ const body = content.slice(match[0].length);
319
+ return {
320
+ metadata: parsed,
321
+ body,
322
+ rawFrontmatter: match[1],
323
+ };
131
324
  } catch {
132
325
  return null;
133
326
  }
134
327
  }
135
328
 
329
+ /**
330
+ * Serializes task metadata into a canonical `agentctl.task/v1` YAML frontmatter block.
331
+ *
332
+ * @param {Record<string, any>} meta
333
+ * @returns {string}
334
+ */
335
+ export function serializeTaskFrontmatter(meta = {}) {
336
+ const lines = [
337
+ "---",
338
+ `kind: ${meta.kind || "Task"}`,
339
+ `version: ${meta.version || "agentctl.task/v1"}`,
340
+ ];
341
+ if (meta.id) lines.push(`id: ${meta.id}`);
342
+ if (meta.title) lines.push(`title: ${meta.title}`);
343
+ if (meta.role) lines.push(`role: ${meta.role}`);
344
+ if (meta.tier) lines.push(`tier: ${meta.tier}`);
345
+ const base = meta.baseCommit || meta.base_commit || meta.baseSha || meta.base_sha;
346
+ if (base) lines.push(`base_commit: ${base}`);
347
+
348
+ if (meta.risk && typeof meta.risk === "object") {
349
+ lines.push("risk:");
350
+ if (meta.risk.lane) lines.push(` lane: ${meta.risk.lane}`);
351
+ if (meta.risk.require_plan_approval !== undefined) {
352
+ lines.push(` require_plan_approval: ${Boolean(meta.risk.require_plan_approval)}`);
353
+ }
354
+ }
355
+
356
+ const cb = meta.circuitBreaker || meta.circuit_breaker;
357
+ if (cb && typeof cb === "object") {
358
+ lines.push("circuitBreaker:");
359
+ if (cb.max_attempts !== undefined) lines.push(` max_attempts: ${cb.max_attempts}`);
360
+ if (cb.max_diff_lines !== undefined) lines.push(` max_diff_lines: ${cb.max_diff_lines}`);
361
+ if (cb.stop_if_same_failure_repeats !== undefined) lines.push(` stop_if_same_failure_repeats: ${cb.stop_if_same_failure_repeats}`);
362
+ }
363
+
364
+ if (Array.isArray(meta.dependsOn) && meta.dependsOn.length > 0) {
365
+ lines.push("dependsOn:");
366
+ for (const dep of meta.dependsOn) {
367
+ lines.push(` - ${dep}`);
368
+ }
369
+ }
370
+
371
+ const allowPaths = meta.scope?.allow || meta.allowed_paths || meta.targetFiles || [];
372
+ const denyPaths = meta.scope?.deny || meta.forbiddenPaths || meta.denyPaths || [];
373
+
374
+ lines.push("scope:");
375
+ lines.push(" allow:");
376
+ if (Array.isArray(allowPaths) && allowPaths.length > 0) {
377
+ for (const p of allowPaths) lines.push(` - ${p}`);
378
+ }
379
+ lines.push(" deny:");
380
+ if (Array.isArray(denyPaths) && denyPaths.length > 0) {
381
+ for (const p of denyPaths) lines.push(` - ${p}`);
382
+ }
383
+
384
+ const commands = meta.verification?.commands || (meta.verifyCmd ? [meta.verifyCmd] : []);
385
+ const requireNonzero = meta.verification?.require_nonzero_test_count;
386
+ const trustedBase = meta.verification?.trusted_base;
387
+ if ((Array.isArray(commands) && commands.length > 0) || requireNonzero !== undefined || trustedBase) {
388
+ lines.push("verification:");
389
+ if (Array.isArray(commands) && commands.length > 0) {
390
+ lines.push(" commands:");
391
+ for (const cmd of commands) lines.push(` - ${cmd}`);
392
+ }
393
+ if (requireNonzero !== undefined) {
394
+ lines.push(` require_nonzero_test_count: ${requireNonzero}`);
395
+ }
396
+ if (trustedBase) {
397
+ lines.push(` trusted_base: ${trustedBase}`);
398
+ }
399
+ }
400
+
401
+ const invariants = Array.isArray(meta.invariants) ? meta.invariants : (Array.isArray(meta.constraints) ? meta.constraints : []);
402
+ if (invariants.length > 0) {
403
+ lines.push("invariants:");
404
+ for (const inv of invariants) lines.push(` - ${inv}`);
405
+ }
406
+
407
+ const mcpList = meta.mcp_directives || meta.mcpDirectives;
408
+ if (Array.isArray(mcpList) && mcpList.length > 0) {
409
+ lines.push("mcp_directives:");
410
+ for (const d of mcpList) lines.push(` - ${d}`);
411
+ }
412
+
413
+ if (meta.flags && typeof meta.flags === "object") {
414
+ lines.push("flags:");
415
+ for (const [k, v] of Object.entries(meta.flags)) {
416
+ lines.push(` ${k}: ${v}`);
417
+ }
418
+ }
419
+
420
+ lines.push("---");
421
+ return lines.join("\n");
422
+ }
423
+
424
+ /**
425
+ * Parse metadata payload from either `agentctl.task/v1` frontmatter or HTML comment JULES_TASK_ENVELOPE.
426
+ *
427
+ * @param {string} content
428
+ * @returns {Record<string, any> | null}
429
+ */
430
+ export function parseEnvelopeHeader(content) {
431
+ if (!content) return null;
432
+ const str = String(content);
433
+
434
+ // 1. Check for YAML frontmatter (agentctl.task/v1 or standard frontmatter)
435
+ const frontmatter = parseTaskFrontmatter(str);
436
+ if (frontmatter && frontmatter.metadata) {
437
+ const m = frontmatter.metadata;
438
+ let verifyCmd = m.verifyCmd || m.verify;
439
+ if (!verifyCmd && m.verification) {
440
+ if (Array.isArray(m.verification.commands) && m.verification.commands.length > 0) {
441
+ verifyCmd = m.verification.commands[0];
442
+ } else if (typeof m.verification.commands === "string") {
443
+ verifyCmd = m.verification.commands;
444
+ } else if (typeof m.verification === "string") {
445
+ verifyCmd = m.verification;
446
+ }
447
+ }
448
+
449
+ const scope = m.scope || {};
450
+ const allow = Array.isArray(scope.allow) ? scope.allow : (Array.isArray(m.allowed_paths) ? m.allowed_paths : (Array.isArray(m.targetFiles) ? m.targetFiles : []));
451
+ const deny = Array.isArray(scope.deny) ? scope.deny : (Array.isArray(m.forbiddenPaths) ? m.forbiddenPaths : (Array.isArray(m.denyPaths) ? m.denyPaths : []));
452
+
453
+ const flags = m.flags || {};
454
+ if (m.autoPr !== undefined) flags.autoPr = Boolean(m.autoPr);
455
+ if (m.requirePlanApproval !== undefined) flags.requirePlanApproval = Boolean(m.requirePlanApproval);
456
+ if (m.repoless !== undefined) flags.repoless = Boolean(m.repoless);
457
+
458
+ const risk = m.risk && typeof m.risk === "object" ? m.risk : {};
459
+ const rawLane = risk.lane || m.risk_lane || m.riskLane;
460
+ const riskLane = rawLane ? String(rawLane).toLowerCase() : (flags.requirePlanApproval ? "amber" : "green");
461
+ if (risk.require_plan_approval !== undefined) {
462
+ flags.requirePlanApproval = Boolean(risk.require_plan_approval);
463
+ } else if (risk.requirePlanApproval !== undefined) {
464
+ flags.requirePlanApproval = Boolean(risk.requirePlanApproval);
465
+ } else if (riskLane === "amber" && flags.requirePlanApproval === undefined) {
466
+ flags.requirePlanApproval = true;
467
+ }
468
+
469
+ const circuitBreaker = m.circuitBreaker || m.circuit_breaker;
470
+ const invariants = Array.isArray(m.invariants) ? m.invariants : (Array.isArray(m.constraints) ? m.constraints : []);
471
+ const mcpDirectives = m.mcp_directives || m.mcpDirectives;
472
+
473
+ const metaObj = m.metadata && typeof m.metadata === "object" ? m.metadata : {};
474
+ const taskId = m.id || m.taskId || metaObj.id || metaObj.taskId;
475
+ const title = m.title || metaObj.title || (typeof m.intent === "string" ? m.intent : m.intent?.outcome);
476
+
477
+ let verification = m.verification;
478
+ if (typeof verification === "string") {
479
+ verification = { commands: [verification] };
480
+ } else if (Array.isArray(verification)) {
481
+ verification = { commands: verification };
482
+ } else if (verification && typeof verification === "object") {
483
+ if (typeof verification.commands === "string") {
484
+ verification = { ...verification, commands: [verification.commands] };
485
+ }
486
+ } else if (verifyCmd) {
487
+ verification = { commands: [verifyCmd] };
488
+ }
489
+
490
+ return {
491
+ version: m.version || m.apiVersion || 1,
492
+ kind: m.kind || "Task",
493
+ id: taskId,
494
+ taskId,
495
+ title,
496
+ intent: m.intent,
497
+ role: m.role || metaObj.role,
498
+ tier: m.tier || metaObj.tier,
499
+ risk: {
500
+ lane: riskLane,
501
+ require_plan_approval: Boolean(flags.requirePlanApproval),
502
+ },
503
+ circuitBreaker,
504
+ dependsOn: m.dependsOn,
505
+ flags,
506
+ verifyCmd,
507
+ verification,
508
+ scope: {
509
+ allow,
510
+ deny,
511
+ },
512
+ targetFiles: allow,
513
+ allowed_paths: allow,
514
+ forbiddenPaths: deny,
515
+ invariants,
516
+ constraints: invariants,
517
+ mcp_directives: Array.isArray(mcpDirectives) ? mcpDirectives : (typeof mcpDirectives === "string" ? [mcpDirectives] : []),
518
+ baseCommit: m.base_commit || m.base_sha || m.baseCommit || m.baseRef,
519
+ base_commit: m.base_commit || m.base_sha || m.baseCommit || m.baseRef,
520
+ promptBody: frontmatter.body,
521
+ };
522
+ }
523
+
524
+ // 2. Legacy HTML comment fallback
525
+ const match = str.match(/<!--\s*JULES_TASK_ENVELOPE:\s*({[\s\S]*?})\s*-->/);
526
+ if (match) {
527
+ try {
528
+ return JSON.parse(match[1]);
529
+ } catch {
530
+ return null;
531
+ }
532
+ }
533
+
534
+ return null;
535
+ }
536
+