jules-orchestrator-kit 0.73.1 → 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/CHANGELOG.md +10 -0
- package/README.md +25 -24
- package/bin/agentctl.mjs +96 -43
- package/bin/init.js +10 -13
- package/package.json +1 -1
- package/scripts/validate-envelope.mjs +23 -4
- package/src/dag-engine.mjs +14 -14
- package/src/engine.mjs +1 -0
- package/src/envelope.mjs +414 -13
- package/src/ops/command-registry.mjs +11 -17
- package/src/role-resolver.mjs +23 -16
- package/src/scaffold.mjs +69 -2
- package/src/wizard-init.mjs +16 -7
- package/src/wizard-task.mjs +123 -5
package/src/envelope.mjs
CHANGED
|
@@ -24,12 +24,15 @@ export function validateEnvelope(envelope = {}, opts = {}) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// 1. Mandatory Fields
|
|
27
|
-
|
|
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
|
-
|
|
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(
|
|
106
|
+
const scopeCheck = checkScope(allowPaths, scope, { allowProtected });
|
|
92
107
|
if (!scopeCheck.ok) {
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
307
|
+
* @returns {{ metadata: Record<string, any>, body: string, rawFrontmatter: string } | null}
|
|
124
308
|
*/
|
|
125
|
-
export function
|
|
126
|
-
if (!content) return null;
|
|
127
|
-
const match =
|
|
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
|
-
|
|
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
|
+
|
|
@@ -252,7 +252,7 @@ export const COMMAND_REGISTRY = [
|
|
|
252
252
|
id: "init",
|
|
253
253
|
path: ["init"],
|
|
254
254
|
title: "init",
|
|
255
|
-
description: "
|
|
255
|
+
description: "Write the minimal canonical project config and runtime ignore entries",
|
|
256
256
|
category: "Configure",
|
|
257
257
|
mutates: true,
|
|
258
258
|
risk: "moderate",
|
|
@@ -272,8 +272,8 @@ export const COMMAND_REGISTRY = [
|
|
|
272
272
|
{ name: "non-interactive", type: "boolean", description: "Run non-interactively with defaults" },
|
|
273
273
|
{ name: "no-interactive", type: "boolean", description: "Alias for --non-interactive" },
|
|
274
274
|
{ name: "yes", type: "boolean", description: "Accept auto-detected Stack Oracle defaults (-y)" },
|
|
275
|
-
{ name: "force", type: "boolean", description: "
|
|
276
|
-
{ name: "dry-run", type: "boolean", description: "
|
|
275
|
+
{ name: "force", type: "boolean", description: "0.x compatibility: also write the legacy Jules manifest and full scaffold (-f)" },
|
|
276
|
+
{ name: "dry-run", type: "boolean", description: "List exact files that would be written without changing the repository (-d)" },
|
|
277
277
|
{ name: "json", type: "boolean", description: "Output structured JSON manifest (-j)" },
|
|
278
278
|
],
|
|
279
279
|
},
|
|
@@ -522,7 +522,6 @@ export const COMMAND_REGISTRY = [
|
|
|
522
522
|
examples: [
|
|
523
523
|
"agentctl gate",
|
|
524
524
|
"agentctl gate --mode working-tree",
|
|
525
|
-
"agentctl gate --fix",
|
|
526
525
|
"agentctl gate --json",
|
|
527
526
|
],
|
|
528
527
|
flags: [
|
|
@@ -531,7 +530,6 @@ export const COMMAND_REGISTRY = [
|
|
|
531
530
|
{ name: "working-tree", type: "boolean", description: "Evaluate the working tree (default mode)" },
|
|
532
531
|
{ name: "staged", type: "boolean", description: "Evaluate staged changes" },
|
|
533
532
|
{ name: "committed", type: "boolean", description: "Evaluate committed changes" },
|
|
534
|
-
{ name: "fix", type: "boolean", description: "Trigger automated OODA self-repair loop on failure" },
|
|
535
533
|
{ name: "allow-protected", type: "boolean", description: "Bypass protected path checks for authorized maintainers" },
|
|
536
534
|
{ name: "allow-unreadable-tests", type: "boolean", description: "Permit unreadable test dialects for this run" },
|
|
537
535
|
{ name: "allow-test-modifications", type: "boolean", description: "Waive every test-tampering check for this run" },
|
|
@@ -557,7 +555,6 @@ export const COMMAND_REGISTRY = [
|
|
|
557
555
|
"agentctl check",
|
|
558
556
|
"agentctl check --base main --strict-locks",
|
|
559
557
|
"agentctl gate --strict-locks # canonical spelling; check is an alias",
|
|
560
|
-
"agentctl check --fix",
|
|
561
558
|
"agentctl check --json",
|
|
562
559
|
],
|
|
563
560
|
flags: [
|
|
@@ -566,7 +563,6 @@ export const COMMAND_REGISTRY = [
|
|
|
566
563
|
{ name: "working-tree", type: "boolean", description: "Evaluate the working tree (default mode)" },
|
|
567
564
|
{ name: "staged", type: "boolean", description: "Evaluate staged changes" },
|
|
568
565
|
{ name: "committed", type: "boolean", description: "Evaluate committed changes" },
|
|
569
|
-
{ name: "fix", type: "boolean", description: "Trigger automated OODA self-repair loop on failure" },
|
|
570
566
|
{ name: "allow-protected", type: "boolean", description: "Bypass protected path checks for authorized maintainers" },
|
|
571
567
|
{ name: "allow-unreadable-tests", type: "boolean", description: "Permit unreadable test dialects for this run" },
|
|
572
568
|
{ name: "allow-test-modifications", type: "boolean", description: "Waive every test-tampering check for this run" },
|
|
@@ -591,7 +587,6 @@ export const COMMAND_REGISTRY = [
|
|
|
591
587
|
examples: [
|
|
592
588
|
"agentctl audit",
|
|
593
589
|
"agentctl audit --mode working-tree",
|
|
594
|
-
"agentctl audit --fix",
|
|
595
590
|
"agentctl audit --json",
|
|
596
591
|
],
|
|
597
592
|
flags: [
|
|
@@ -600,7 +595,6 @@ export const COMMAND_REGISTRY = [
|
|
|
600
595
|
{ name: "working-tree", type: "boolean", description: "Evaluate the working tree (default mode)" },
|
|
601
596
|
{ name: "staged", type: "boolean", description: "Evaluate staged changes" },
|
|
602
597
|
{ name: "committed", type: "boolean", description: "Evaluate committed changes" },
|
|
603
|
-
{ name: "fix", type: "boolean", description: "Trigger automated OODA self-repair loop on failure" },
|
|
604
598
|
{ name: "allow-protected", type: "boolean", description: "Bypass protected path checks for authorized maintainers" },
|
|
605
599
|
{ name: "allow-unreadable-tests", type: "boolean", description: "Permit unreadable test dialects for this run" },
|
|
606
600
|
{ name: "allow-test-modifications", type: "boolean", description: "Waive every test-tampering check for this run" },
|
|
@@ -940,20 +934,20 @@ export const COMMAND_REGISTRY = [
|
|
|
940
934
|
],
|
|
941
935
|
},
|
|
942
936
|
{
|
|
943
|
-
id: "
|
|
944
|
-
path: ["
|
|
945
|
-
title: "
|
|
946
|
-
description: "
|
|
937
|
+
id: "repair",
|
|
938
|
+
path: ["repair"],
|
|
939
|
+
title: "repair",
|
|
940
|
+
description: "Explicitly repair from piped terminal logs or an error trace; `fix` remains a 0.x compatibility alias",
|
|
947
941
|
category: "Repair",
|
|
948
942
|
mutates: true,
|
|
949
943
|
risk: "moderate",
|
|
950
944
|
interactive: "never",
|
|
951
945
|
requiresRepository: true,
|
|
952
|
-
shortcuts: [],
|
|
946
|
+
shortcuts: ["fix"],
|
|
953
947
|
examples: [
|
|
954
|
-
"npm test 2>&1 | agentctl
|
|
955
|
-
"agentctl
|
|
956
|
-
'agentctl
|
|
948
|
+
"npm test 2>&1 | agentctl repair",
|
|
949
|
+
"agentctl repair --file ./fail.log",
|
|
950
|
+
'agentctl repair --input "Error: boom" --task --json',
|
|
957
951
|
],
|
|
958
952
|
flags: [
|
|
959
953
|
{ name: "input", type: "string", description: "Inline error log text (-i)" },
|
package/src/role-resolver.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { loadConfig } from "./config.mjs";
|
|
4
5
|
|
|
6
|
+
const SHIPPED_PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", ".agent", "prompts");
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Placeholders a role prompt may use in place of a hardcoded command.
|
|
7
10
|
*
|
|
@@ -125,7 +128,8 @@ export function hydrateRolePrompt(content = "", config = {}) {
|
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
/**
|
|
128
|
-
* Resolves specialist
|
|
131
|
+
* Resolves a specialist role from a repository-local prompt override first,
|
|
132
|
+
* then from the canonical prompts shipped with the package.
|
|
129
133
|
* @param {string} [root=process.cwd()]
|
|
130
134
|
* @param {string} [roleName=""]
|
|
131
135
|
* @param {object} [opts] - `{ config }` to avoid re-reading .agent/config.yml.
|
|
@@ -157,19 +161,22 @@ export function resolveRolePrompt(root = process.cwd(), roleName = "", opts = {}
|
|
|
157
161
|
candidates.push(directAlias);
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
// A repository-local prompt is an explicit override. When no local
|
|
165
|
+
// prompt exists, resolve the canonical role from the package payload instead
|
|
166
|
+
// of requiring init to copy static role files into every target repository.
|
|
167
|
+
for (const promptsDir of [join(root, ".agent", "prompts"), SHIPPED_PROMPTS_DIR]) {
|
|
168
|
+
if (!existsSync(promptsDir)) continue;
|
|
169
|
+
try {
|
|
170
|
+
const files = readdirSync(promptsDir);
|
|
171
|
+
let matched = null;
|
|
172
|
+
for (const cand of candidates) {
|
|
173
|
+
matched = files.find(
|
|
174
|
+
(f) => f.toLowerCase() === `${cand}.md` || f.toLowerCase() === cand
|
|
175
|
+
);
|
|
176
|
+
if (matched) break;
|
|
177
|
+
}
|
|
178
|
+
if (!matched) continue;
|
|
162
179
|
|
|
163
|
-
try {
|
|
164
|
-
const files = readdirSync(promptsDir);
|
|
165
|
-
let matched = null;
|
|
166
|
-
for (const cand of candidates) {
|
|
167
|
-
matched = files.find(
|
|
168
|
-
(f) => f.toLowerCase() === `${cand}.md` || f.toLowerCase() === cand
|
|
169
|
-
);
|
|
170
|
-
if (matched) break;
|
|
171
|
-
}
|
|
172
|
-
if (matched) {
|
|
173
180
|
const fullPath = join(promptsDir, matched);
|
|
174
181
|
const raw = readFileSync(fullPath, "utf-8").trim();
|
|
175
182
|
|
|
@@ -187,7 +194,7 @@ export function resolveRolePrompt(root = process.cwd(), roleName = "", opts = {}
|
|
|
187
194
|
path: fullPath,
|
|
188
195
|
content: hydrateRolePrompt(raw, config),
|
|
189
196
|
};
|
|
190
|
-
}
|
|
191
|
-
}
|
|
197
|
+
} catch (_) {}
|
|
198
|
+
}
|
|
192
199
|
return null;
|
|
193
200
|
}
|