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/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
+
@@ -252,7 +252,7 @@ export const COMMAND_REGISTRY = [
252
252
  id: "init",
253
253
  path: ["init"],
254
254
  title: "init",
255
- description: "Configure Stack Oracle and Jules provider manifests",
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: "Force overwrite existing config and assets (-f)" },
276
- { name: "dry-run", type: "boolean", description: "Preview plan without writing files (-d)" },
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: "fix",
944
- path: ["fix"],
945
- title: "fix",
946
- description: "Auto-repair from piped terminal logs or error trace (npm test 2>&1 | agentctl fix)",
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 fix",
955
- "agentctl fix --file ./fail.log",
956
- 'agentctl fix --input "Error: boom" --task --json',
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)" },
@@ -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 agent role markdown prompt from .agent/prompts/
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
- const promptsDir = join(root, ".agent", "prompts");
161
- if (!existsSync(promptsDir)) return null;
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
- } catch (_) {}
197
+ } catch (_) {}
198
+ }
192
199
  return null;
193
200
  }