jules-orchestrator-kit 0.74.0 → 0.75.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/git.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
+ import { killProcessTree } from "./process-tree.mjs";
2
3
  import { readFileSync, existsSync, statSync, lstatSync, readlinkSync, mkdtempSync, rmSync, symlinkSync } from "node:fs";
3
4
  import { join, delimiter } from "node:path";
4
5
  import { tmpdir } from "node:os";
@@ -215,6 +216,13 @@ export function runCmd(command, opts = {}) {
215
216
  throw new GateError("Empty command provided");
216
217
  }
217
218
 
219
+ // On POSIX, detach so the child becomes its own process-group leader. Then a
220
+ // timed-out kill can signal the whole group (`kill(-pid)`) and reap workers
221
+ // spawned by the command. Windows omits detached: taskkill /T walks the tree
222
+ // without a separate group, and detached consoles behave differently under
223
+ // cmd.exe / CreateProcess.
224
+ const posixDetach = process.platform !== "win32";
225
+
218
226
  try {
219
227
  const ret = useShell
220
228
  ? spawnSync(shellCmd, {
@@ -225,6 +233,7 @@ export function runCmd(command, opts = {}) {
225
233
  env: childEnv,
226
234
  timeout,
227
235
  maxBuffer,
236
+ ...(posixDetach ? { detached: true } : {}),
228
237
  })
229
238
  : spawnSync(winSpawn ? winSpawn.file : binary, winSpawn ? winSpawn.args : args, {
230
239
  cwd,
@@ -235,9 +244,15 @@ export function runCmd(command, opts = {}) {
235
244
  env: childEnv,
236
245
  timeout,
237
246
  maxBuffer,
247
+ ...(posixDetach ? { detached: true } : {}),
238
248
  });
239
249
 
240
250
  const isTimeout = Boolean(ret.error && (ret.error.code === "ETIMEDOUT" || (ret.signal === "SIGTERM" && ret.error.killed)));
251
+ // Node's spawnSync timeout kills only the direct child; reap the tree so
252
+ // background workers do not linger (ROADMAP_V1 reliability item).
253
+ if (isTimeout && ret.pid) {
254
+ killProcessTree(ret.pid);
255
+ }
241
256
  const isNobufs = Boolean(ret.error && (ret.error.code === "ENOBUFS" || (ret.error.message && ret.error.message.includes("maxBuffer"))));
242
257
 
243
258
  const status = typeof ret.status === "number" ? ret.status : (isTimeout ? 124 : 1);
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Deterministic migration of supported 0.x config and task-envelope state to
3
+ * canonical v1 shapes (`.agent/config.yml`, `agentctl.task/v1` frontmatter).
4
+ *
5
+ * Closes the `agentctl migrate` CLI gap from docs/v1-surface-inventory.md / #38.
6
+ */
7
+
8
+ import { existsSync, readFileSync, readdirSync, rmSync, mkdirSync } from "node:fs";
9
+ import { join, dirname } from "node:path";
10
+ import { parseYaml, yamlScalar } from "./config.mjs";
11
+ import {
12
+ parseTaskFrontmatter,
13
+ serializeTaskFrontmatter,
14
+ parseEnvelopeHeader,
15
+ } from "./envelope.mjs";
16
+ import { safeAtomicWrite } from "./fs-atomic.mjs";
17
+
18
+ /** Top-level 0.x verify command keys → nested `verify.*` fields. */
19
+ export const LEGACY_VERIFY_FIELDS = Object.freeze({
20
+ test_cmd: "test",
21
+ setup_cmd: "setup",
22
+ lint_cmd: "lint",
23
+ build_cmd: "build",
24
+ fuzz_cmd: "fuzz",
25
+ invariant_cmd: "invariant",
26
+ e2e_cmd: "e2e",
27
+ teardown_cmd: "teardown",
28
+ timeout_ms: "timeout_ms",
29
+ });
30
+
31
+ const CANONICAL_MESSAGE = "Repository is already in canonical v1 format.";
32
+
33
+ /**
34
+ * @param {unknown} value
35
+ * @param {number} indent
36
+ * @returns {string}
37
+ */
38
+ function serializeYamlValue(value, indent = 0) {
39
+ const pad = " ".repeat(indent);
40
+ if (value === null || value === undefined) return "";
41
+ if (Array.isArray(value)) {
42
+ if (value.length === 0) return "[]";
43
+ return (
44
+ "\n" +
45
+ value
46
+ .map((item) => {
47
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
48
+ const entries = Object.entries(item);
49
+ if (entries.length === 0) return `${pad}- {}`;
50
+ const [firstKey, firstVal] = entries[0];
51
+ const firstLine =
52
+ typeof firstVal === "object" && firstVal !== null
53
+ ? `${pad}- ${firstKey}:${serializeYamlValue(firstVal, indent + 2)}`
54
+ : `${pad}- ${firstKey}: ${yamlScalar(firstVal)}`;
55
+ const rest = entries.slice(1).map(([k, v]) => {
56
+ if (typeof v === "object" && v !== null) {
57
+ return `${pad} ${k}:${serializeYamlValue(v, indent + 2)}`;
58
+ }
59
+ if (typeof v === "boolean" || typeof v === "number") return `${pad} ${k}: ${v}`;
60
+ return `${pad} ${k}: ${yamlScalar(v)}`;
61
+ });
62
+ return [firstLine, ...rest].join("\n");
63
+ }
64
+ return `${pad}- ${yamlScalar(item)}`;
65
+ })
66
+ .join("\n")
67
+ );
68
+ }
69
+ if (typeof value === "object") {
70
+ const entries = Object.entries(value);
71
+ if (entries.length === 0) return "{}";
72
+ return (
73
+ "\n" +
74
+ entries
75
+ .map(([k, v]) => {
76
+ if (v !== null && typeof v === "object") {
77
+ const nested = serializeYamlValue(v, indent + 1);
78
+ if (nested === "[]" || nested === "{}") return `${pad}${k}: ${nested}`;
79
+ return `${pad}${k}:${nested}`;
80
+ }
81
+ if (typeof v === "boolean" || typeof v === "number") return `${pad}${k}: ${v}`;
82
+ return `${pad}${k}: ${yamlScalar(v)}`;
83
+ })
84
+ .join("\n")
85
+ );
86
+ }
87
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
88
+ return yamlScalar(value);
89
+ }
90
+
91
+ /**
92
+ * Serialize a plain config object to kit-style YAML.
93
+ * @param {Record<string, unknown>} obj
94
+ * @param {string} [headerComment]
95
+ * @returns {string}
96
+ */
97
+ export function serializeConfigYaml(obj, headerComment = "# Agent Orchestrator Kit Config") {
98
+ const lines = [headerComment];
99
+ for (const [key, value] of Object.entries(obj)) {
100
+ if (value === undefined) continue;
101
+ if (value !== null && typeof value === "object") {
102
+ const nested = serializeYamlValue(value, 1);
103
+ if (nested === "[]" || nested === "{}") {
104
+ lines.push(`${key}: ${nested}`);
105
+ } else {
106
+ lines.push(`${key}:${nested}`);
107
+ }
108
+ } else if (typeof value === "boolean" || typeof value === "number") {
109
+ lines.push(`${key}: ${value}`);
110
+ } else {
111
+ lines.push(`${key}: ${yamlScalar(value)}`);
112
+ }
113
+ }
114
+ return lines.join("\n").replace(/\n+$/, "") + "\n";
115
+ }
116
+
117
+ /**
118
+ * Lift legacy top-level `*_cmd` / `timeout_ms` keys into `verify: { ... }`.
119
+ *
120
+ * @param {Record<string, any>} parsed
121
+ * @returns {{ obj: Record<string, any>, changes: string[], lifted: boolean }}
122
+ */
123
+ export function normalizeLegacyVerifyFields(parsed) {
124
+ const obj = { ...parsed };
125
+ const changes = [];
126
+ const verify = { ...(obj.verify && typeof obj.verify === "object" ? obj.verify : {}) };
127
+ let lifted = false;
128
+
129
+ for (const [legacyKey, verifyKey] of Object.entries(LEGACY_VERIFY_FIELDS)) {
130
+ if (!Object.prototype.hasOwnProperty.call(obj, legacyKey)) continue;
131
+ const legacyVal = obj[legacyKey];
132
+ const existing = verify[verifyKey];
133
+ const existingEmpty = existing === undefined || existing === null || existing === "";
134
+ if (existingEmpty && legacyVal !== undefined && legacyVal !== null && legacyVal !== "") {
135
+ verify[verifyKey] = legacyVal;
136
+ changes.push(`lifted ${legacyKey} → verify.${verifyKey}`);
137
+ } else {
138
+ changes.push(`removed legacy ${legacyKey}`);
139
+ }
140
+ delete obj[legacyKey];
141
+ lifted = true;
142
+ }
143
+
144
+ if (lifted) {
145
+ obj.verify = verify;
146
+ }
147
+
148
+ if (obj.version === 2 || obj.version === "2") {
149
+ obj.version = 1;
150
+ changes.push("normalized version → 1");
151
+ lifted = true;
152
+ }
153
+
154
+ return { obj, changes, lifted };
155
+ }
156
+
157
+ /**
158
+ * Build a canonical config object from a parsed jules.yml (or mixed legacy) document.
159
+ * @param {Record<string, any>} parsed
160
+ * @returns {{ obj: Record<string, any>, changes: string[] }}
161
+ */
162
+ function toCanonicalConfig(parsed) {
163
+ const { obj: normalized, changes } = normalizeLegacyVerifyFields(parsed);
164
+ const verify = {
165
+ ...(normalized.verify && typeof normalized.verify === "object" ? normalized.verify : {}),
166
+ };
167
+
168
+ const out = {
169
+ version: 1,
170
+ provider: normalized.provider || "jules",
171
+ tier: normalized.tier || "free",
172
+ base_branch: normalized.base_branch || normalized.baseBranch || "main",
173
+ branch_prefix: normalized.branch_prefix || normalized.branchPrefix || "agent/",
174
+ };
175
+
176
+ if (normalized.limits && typeof normalized.limits === "object") {
177
+ out.limits = normalized.limits;
178
+ }
179
+
180
+ const verifyOut = {
181
+ profile: verify.profile || "standard",
182
+ scope: verify.scope || "global",
183
+ };
184
+ for (const key of [
185
+ "setup",
186
+ "test",
187
+ "lint",
188
+ "build",
189
+ "typecheck",
190
+ "fuzz",
191
+ "invariant",
192
+ "e2e",
193
+ "teardown",
194
+ "timeout_ms",
195
+ ]) {
196
+ if (verify[key] !== undefined && verify[key] !== null && verify[key] !== "") {
197
+ verifyOut[key] = verify[key];
198
+ }
199
+ }
200
+ if (verify.timeout_ms === undefined && verify.timeoutMs === undefined) {
201
+ verifyOut.timeout_ms = 300000;
202
+ } else if (verify.timeoutMs !== undefined && verify.timeout_ms === undefined) {
203
+ verifyOut.timeout_ms = verify.timeoutMs;
204
+ }
205
+ if (verify.policy) verifyOut.policy = verify.policy;
206
+ out.verify = verifyOut;
207
+
208
+ if (Array.isArray(normalized.presets)) out.presets = normalized.presets;
209
+
210
+ if (Array.isArray(normalized.forbidden_paths)) out.forbidden_paths = normalized.forbidden_paths;
211
+ else if (Array.isArray(normalized.scope?.deny)) out.forbidden_paths = normalized.scope.deny;
212
+ if (Array.isArray(normalized.allow_paths)) out.allow_paths = normalized.allow_paths;
213
+ else if (Array.isArray(normalized.scope?.allow)) out.allow_paths = normalized.scope.allow;
214
+
215
+ const reserved = new Set([
216
+ "version",
217
+ "provider",
218
+ "tier",
219
+ "base_branch",
220
+ "baseBranch",
221
+ "branch_prefix",
222
+ "branchPrefix",
223
+ "limits",
224
+ "verify",
225
+ "presets",
226
+ "forbidden_paths",
227
+ "allow_paths",
228
+ "scope",
229
+ ...Object.keys(LEGACY_VERIFY_FIELDS),
230
+ ]);
231
+ for (const [k, v] of Object.entries(normalized)) {
232
+ if (reserved.has(k) || out[k] !== undefined) continue;
233
+ out[k] = v;
234
+ }
235
+
236
+ changes.unshift("migrated legacy manifest shape to canonical config.yml");
237
+ return { obj: out, changes };
238
+ }
239
+
240
+ /**
241
+ * @param {string} content
242
+ * @returns {{ needs: boolean, reason?: string }}
243
+ */
244
+ export function taskNeedsMigration(content) {
245
+ const text = String(content || "");
246
+ if (/JULES_TASK_ENVELOPE/.test(text)) {
247
+ return { needs: true, reason: "legacy JULES_TASK_ENVELOPE marker" };
248
+ }
249
+ const fm = parseTaskFrontmatter(text);
250
+ if (!fm) return { needs: false };
251
+ const version = fm.metadata?.version || fm.metadata?.apiVersion;
252
+ if (version !== "agentctl.task/v1") {
253
+ return { needs: true, reason: "missing or non-v1 version field" };
254
+ }
255
+ return { needs: false };
256
+ }
257
+
258
+ /**
259
+ * Upgrade a task file body to `agentctl.task/v1` frontmatter.
260
+ * @param {string} content
261
+ * @returns {{ content: string, changes: string[] } | null}
262
+ */
263
+ export function upgradeTaskContent(content) {
264
+ const check = taskNeedsMigration(content);
265
+ if (!check.needs) return null;
266
+
267
+ const header = parseEnvelopeHeader(content);
268
+ if (!header) return null;
269
+
270
+ const fm = parseTaskFrontmatter(content);
271
+ let body;
272
+ const changes = [];
273
+
274
+ if (/JULES_TASK_ENVELOPE/.test(content)) {
275
+ changes.push("converted JULES_TASK_ENVELOPE to agentctl.task/v1 frontmatter");
276
+ if (fm) {
277
+ body = fm.body;
278
+ } else {
279
+ body = content.replace(/<!--\s*JULES_TASK_ENVELOPE:\s*{[\s\S]*?}\s*-->\s*/, "");
280
+ }
281
+ } else {
282
+ changes.push("set version: agentctl.task/v1");
283
+ body = fm ? fm.body : content;
284
+ }
285
+
286
+ const serialized = serializeTaskFrontmatter({
287
+ kind: header.kind || "Task",
288
+ version: "agentctl.task/v1",
289
+ id: header.id || header.taskId,
290
+ title: header.title,
291
+ role: header.role,
292
+ tier: header.tier,
293
+ base_commit: header.base_commit || header.baseCommit,
294
+ risk: header.risk,
295
+ circuitBreaker: header.circuitBreaker,
296
+ dependsOn: header.dependsOn,
297
+ scope: header.scope,
298
+ verification: header.verification,
299
+ verifyCmd: header.verifyCmd,
300
+ invariants: header.invariants,
301
+ mcp_directives: header.mcp_directives,
302
+ flags: header.flags,
303
+ });
304
+
305
+ const cleanedBody = String(body || "").replace(/^\r?\n/, "");
306
+ return {
307
+ content: `${serialized}\n${cleanedBody}`,
308
+ changes,
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Plan migrations without writing disk.
314
+ *
315
+ * @param {string} root
316
+ * @param {object} [_options]
317
+ * @returns {{
318
+ * ok: boolean,
319
+ * migrated: Array<{ type: string, path: string, changes: string[], from?: string, content?: string, remove?: string[] }>,
320
+ * warnings: string[],
321
+ * message?: string,
322
+ * }}
323
+ */
324
+ export function planMigration(root, _options = {}) {
325
+ const migrated = [];
326
+ const warnings = [];
327
+
328
+ const agentDir = join(root, ".agent");
329
+ const configPath = join(agentDir, "config.yml");
330
+ const julesPath = join(agentDir, "jules.yml");
331
+ const hasConfig = existsSync(configPath);
332
+ const hasJules = existsSync(julesPath);
333
+
334
+ if (hasJules && hasConfig) {
335
+ warnings.push(
336
+ "Both .agent/jules.yml and .agent/config.yml exist; leaving jules.yml in place (will not overwrite config.yml). Remove .agent/jules.yml manually after confirming config.yml is authoritative."
337
+ );
338
+ }
339
+
340
+ if (hasJules && !hasConfig) {
341
+ let raw = "";
342
+ try {
343
+ raw = readFileSync(julesPath, "utf-8");
344
+ } catch (err) {
345
+ warnings.push(`Could not read .agent/jules.yml: ${err.message}`);
346
+ }
347
+ if (raw) {
348
+ const parsed = parseYaml(raw) || {};
349
+ const { obj, changes } = toCanonicalConfig(parsed);
350
+ const content = serializeConfigYaml(
351
+ obj,
352
+ "# Agent Orchestrator Kit Config (migrated from .agent/jules.yml)"
353
+ );
354
+ migrated.push({
355
+ type: "config",
356
+ path: ".agent/config.yml",
357
+ from: ".agent/jules.yml",
358
+ changes: [
359
+ "rename .agent/jules.yml → .agent/config.yml",
360
+ ...changes.filter((c) => !c.startsWith("migrated legacy")),
361
+ "remove .agent/jules.yml",
362
+ ],
363
+ content,
364
+ remove: [".agent/jules.yml"],
365
+ });
366
+ }
367
+ }
368
+
369
+ if (hasConfig) {
370
+ let raw = "";
371
+ try {
372
+ raw = readFileSync(configPath, "utf-8");
373
+ } catch (err) {
374
+ warnings.push(`Could not read .agent/config.yml: ${err.message}`);
375
+ }
376
+ if (raw) {
377
+ const parsed = parseYaml(raw) || {};
378
+ const { obj, changes, lifted } = normalizeLegacyVerifyFields(parsed);
379
+ if (lifted && changes.length > 0) {
380
+ const content = serializeConfigYaml(
381
+ obj,
382
+ "# Agent Orchestrator Kit Config (legacy verify fields normalized)"
383
+ );
384
+ migrated.push({
385
+ type: "config",
386
+ path: ".agent/config.yml",
387
+ changes,
388
+ content,
389
+ });
390
+ }
391
+ }
392
+ }
393
+
394
+ const queueDir = join(agentDir, "jules-queue");
395
+ if (existsSync(queueDir)) {
396
+ let entries = [];
397
+ try {
398
+ entries = readdirSync(queueDir);
399
+ } catch (_) {
400
+ entries = [];
401
+ }
402
+ for (const name of entries.sort()) {
403
+ if (!name.endsWith(".md") || name === "README.md") continue;
404
+ const rel = `.agent/jules-queue/${name}`;
405
+ const abs = join(queueDir, name);
406
+ let raw = "";
407
+ try {
408
+ raw = readFileSync(abs, "utf-8");
409
+ } catch (err) {
410
+ warnings.push(`Could not read ${rel}: ${err.message}`);
411
+ continue;
412
+ }
413
+ const upgraded = upgradeTaskContent(raw);
414
+ if (upgraded) {
415
+ migrated.push({
416
+ type: "task",
417
+ path: rel,
418
+ changes: upgraded.changes,
419
+ content: upgraded.content,
420
+ });
421
+ }
422
+ }
423
+ }
424
+
425
+ if (migrated.length === 0 && warnings.length === 0) {
426
+ return { ok: true, migrated: [], warnings: [], message: CANONICAL_MESSAGE };
427
+ }
428
+
429
+ return { ok: true, migrated, warnings };
430
+ }
431
+
432
+ /**
433
+ * Apply a migration plan to disk (or return the plan when `dryRun` is set).
434
+ *
435
+ * @param {string} root
436
+ * @param {{ dryRun?: boolean }} [options]
437
+ * @returns {{
438
+ * ok: boolean,
439
+ * dryRun: boolean,
440
+ * migrated: Array<{ type: string, path: string, changes: string[] }>,
441
+ * warnings: string[],
442
+ * message?: string,
443
+ * }}
444
+ */
445
+ export function executeMigration(root, options = {}) {
446
+ const dryRun = Boolean(options.dryRun);
447
+ const plan = planMigration(root, options);
448
+
449
+ if (dryRun) {
450
+ return {
451
+ ok: true,
452
+ dryRun: true,
453
+ migrated: plan.migrated.map(({ type, path, changes }) => ({ type, path, changes })),
454
+ warnings: plan.warnings,
455
+ ...(plan.message ? { message: plan.message } : {}),
456
+ };
457
+ }
458
+
459
+ if (plan.migrated.length === 0) {
460
+ return {
461
+ ok: true,
462
+ dryRun: false,
463
+ migrated: [],
464
+ warnings: plan.warnings,
465
+ ...(plan.message || plan.warnings.length === 0
466
+ ? { message: plan.message || CANONICAL_MESSAGE }
467
+ : {}),
468
+ };
469
+ }
470
+
471
+ const applied = [];
472
+ for (const item of plan.migrated) {
473
+ const abs = join(root, item.path);
474
+ if (item.content !== undefined) {
475
+ mkdirSync(dirname(abs), { recursive: true });
476
+ safeAtomicWrite(abs, item.content);
477
+ }
478
+ if (Array.isArray(item.remove)) {
479
+ for (const rel of item.remove) {
480
+ const removeAbs = join(root, rel);
481
+ if (existsSync(removeAbs)) rmSync(removeAbs, { force: true });
482
+ }
483
+ }
484
+ applied.push({ type: item.type, path: item.path, changes: item.changes });
485
+ }
486
+
487
+ return {
488
+ ok: true,
489
+ dryRun: false,
490
+ migrated: applied,
491
+ warnings: plan.warnings,
492
+ };
493
+ }
@@ -145,7 +145,7 @@ export const COMMAND_REGISTRY = [
145
145
  id: "task",
146
146
  path: ["task"],
147
147
  title: "task",
148
- description: "Manage task envelopes: create, template, or optimize prompts (see task create | task template | task optimize)",
148
+ description: "Manage task envelopes: create, template, optimize, or validate (see task create | task template | task optimize | task validate)",
149
149
  category: "Create",
150
150
  mutates: true,
151
151
  risk: "moderate",
@@ -193,6 +193,7 @@ export const COMMAND_REGISTRY = [
193
193
  { name: "no-interactive", type: "boolean", description: "Alias for --non-interactive" },
194
194
  { name: "yes", type: "boolean", description: "Accept default values non-interactively (-y)" },
195
195
  { name: "dry-run", type: "boolean", description: "Simulate task envelope creation without queueing (-d)" },
196
+ { name: "allow-protected", type: "boolean", description: "Permit task creation on branches modifying protected paths" },
196
197
  { name: "json", type: "boolean", description: "Output structured JSON envelope (-j)" },
197
198
  ],
198
199
  },
@@ -236,7 +237,7 @@ export const COMMAND_REGISTRY = [
236
237
  'agentctl task optimize --file prompt.txt --json',
237
238
  ],
238
239
  flags: [
239
- { name: "fix", type: "boolean", description: "Synthesize optimized markdown task envelope (-f)" },
240
+ { name: "fix", type: "boolean", description: "Synthesize optimized markdown task prompt (-f)" },
240
241
  { name: "prompt", type: "string", description: "Task prompt text to score (-p)" },
241
242
  { name: "prompt-file", type: "string", description: "Read prompt from file" },
242
243
  { name: "file", type: "string", description: "Alias for --prompt-file: path to text file containing task prompt" },
@@ -248,6 +249,25 @@ export const COMMAND_REGISTRY = [
248
249
  { name: "json", type: "boolean", description: "Output structured JSON prompt evaluation (-j)" },
249
250
  ],
250
251
  },
252
+ {
253
+ id: "task-validate",
254
+ path: ["task", "validate"],
255
+ title: "task validate",
256
+ description: "Validate a task envelope markdown or JSON file without dispatching",
257
+ category: "Inspect",
258
+ mutates: false,
259
+ risk: "low",
260
+ interactive: "never",
261
+ requiresRepository: false,
262
+ shortcuts: [],
263
+ examples: [
264
+ "agentctl task validate .agent/jules-queue/TASK-001.md",
265
+ "agentctl task validate task.json --json",
266
+ ],
267
+ flags: [
268
+ { name: "json", type: "boolean", description: "Output machine-readable validation result" },
269
+ ],
270
+ },
251
271
  {
252
272
  id: "init",
253
273
  path: ["init"],
@@ -277,6 +297,51 @@ export const COMMAND_REGISTRY = [
277
297
  { name: "json", type: "boolean", description: "Output structured JSON manifest (-j)" },
278
298
  ],
279
299
  },
300
+ {
301
+ id: "uninstall",
302
+ path: ["uninstall"],
303
+ title: "uninstall",
304
+ description: "Remove kit-owned configuration and gitignore entries from the repository",
305
+ category: "Configure",
306
+ mutates: true,
307
+ risk: "low",
308
+ interactive: "never",
309
+ requiresRepository: true,
310
+ shortcuts: [],
311
+ examples: [
312
+ "agentctl uninstall --dry-run",
313
+ "agentctl uninstall --yes",
314
+ "agentctl uninstall --json",
315
+ ],
316
+ flags: [
317
+ { name: "dry-run", type: "boolean", description: "Report files that would be removed without modifying disk (-d)" },
318
+ { name: "yes", type: "boolean", description: "Confirm uninstallation without prompting (-y)" },
319
+ { name: "force", type: "boolean", description: "Remove legacy scaffold directories if present (.agent/rules, .agent/prompts, .agent/workflows) (-f)" },
320
+ { name: "json", type: "boolean", description: "Output machine-readable removal plan or outcome (-j)" },
321
+ ],
322
+ },
323
+ {
324
+ id: "migrate",
325
+ path: ["migrate"],
326
+ title: "migrate",
327
+ description: "Migrate legacy 0.x config and task envelopes to canonical v1 format",
328
+ category: "Configure",
329
+ mutates: true,
330
+ risk: "low",
331
+ interactive: "never",
332
+ requiresRepository: true,
333
+ shortcuts: [],
334
+ examples: [
335
+ "agentctl migrate --dry-run",
336
+ "agentctl migrate --yes",
337
+ "agentctl migrate --yes --json",
338
+ ],
339
+ flags: [
340
+ { name: "dry-run", type: "boolean", description: "Report planned migrations without modifying disk (-d)" },
341
+ { name: "yes", type: "boolean", description: "Confirm migration without prompting (-y)" },
342
+ { name: "json", type: "boolean", description: "Output machine-readable migration plan or outcome (-j)" },
343
+ ],
344
+ },
280
345
  {
281
346
  id: "dashboard",
282
347
  path: ["dashboard"],
@@ -298,6 +363,23 @@ export const COMMAND_REGISTRY = [
298
363
  { name: "host", type: "string", description: "Bind host address (default 127.0.0.1)" },
299
364
  ],
300
365
  },
366
+ {
367
+ id: "menu",
368
+ path: ["menu"],
369
+ title: "menu",
370
+ description: "Launch interactive terminal navigation hub",
371
+ category: "Inspect",
372
+ mutates: false,
373
+ risk: "low",
374
+ interactive: "always",
375
+ requiresRepository: true,
376
+ shortcuts: ["tui", "ui"],
377
+ examples: [
378
+ "agentctl menu",
379
+ "agentctl tui",
380
+ ],
381
+ flags: [],
382
+ },
301
383
  {
302
384
  id: "budget",
303
385
  path: ["budget"],
@@ -724,6 +806,7 @@ export const COMMAND_REGISTRY = [
724
806
  'agentctl dispatch --prompt "Add retry handling to src/webhook.js"',
725
807
  'agentctl dispatch -p "Fix type errors" --role types --tier fast',
726
808
  'agentctl dispatch --prompt-file task.md --dry-run',
809
+ 'agentctl dispatch --prompt "Add feature" --check-premise --goal-check "node scripts/check-feature.mjs"',
727
810
  ],
728
811
  flags: [
729
812
  { name: "title", type: "string", description: "Task title (-t)" },
@@ -731,7 +814,8 @@ export const COMMAND_REGISTRY = [
731
814
  { name: "prompt-file", type: "string", description: "Read prompt from file (-f)" },
732
815
  { name: "role", type: "string", description: "Specialist role (auditor, performance, security, hygiene, resilience, types, debugger, testing, e2e, database, docs, a11y) (-r)" },
733
816
  { name: "tier", type: "string", description: "Execution tier override (fast | complex)" },
734
- { name: "check-premise", type: "boolean", description: "Verify premise locally before dispatching" },
817
+ { name: "check-premise", type: "boolean", description: "Skip only when an explicit goal check proves the task complete" },
818
+ { name: "goal-check", type: "string", description: "Objective-specific command for --check-premise; distinct from --verify-cmd" },
735
819
  { name: "idempotent", type: "boolean", description: "Alias for --check-premise" },
736
820
  { name: "author", type: "string", description: "Attribution author for the dispatch" },
737
821
  { name: "verify-cmd", type: "string", description: "Verification command override (-v)" },
@@ -890,7 +974,7 @@ export const COMMAND_REGISTRY = [
890
974
  id: "lock",
891
975
  path: ["lock"],
892
976
  title: "lock",
893
- description: "Multi-agent coordination locks: acquire, release, or view file status",
977
+ description: "Multi-agent coordination locks: acquire, release, reap, or view file status",
894
978
  category: "Operate",
895
979
  mutates: true,
896
980
  risk: "low",
@@ -902,10 +986,13 @@ export const COMMAND_REGISTRY = [
902
986
  "agentctl lock acquire agent-1 task-1 src/main.js",
903
987
  "agentctl lock acquire agent-1 task-1 src/main.js --ttl 60",
904
988
  "agentctl lock release task-1",
989
+ "agentctl lock reap",
990
+ "agentctl lock reap --dry-run",
905
991
  ],
906
992
  flags: [
907
993
  { name: "ttl", type: "string", description: "Lease duration in minutes for acquire (default: 120)" },
908
994
  { name: "pid", type: "string", description: "Bind the lock to a process id instead of a time lease" },
995
+ { name: "dry-run", type: "boolean", short: "d", description: "Report stale locks without unlinking them" },
909
996
  { name: "json", type: "boolean", description: "Output structured JSON result (-j)" },
910
997
  ],
911
998
  },