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/CHANGELOG.md +13 -0
- package/README.md +2 -2
- package/bin/agentctl.mjs +372 -20
- package/package.json +1 -1
- package/scripts/ci-scope-guard.mjs +30 -7
- package/src/bidi-guard.mjs +190 -15
- package/src/config.mjs +40 -1
- package/src/engine.mjs +184 -47
- package/src/git.mjs +15 -0
- package/src/migrate.mjs +493 -0
- package/src/ops/command-registry.mjs +91 -4
- package/src/ops/doctor-registry.mjs +184 -32
- package/src/ops/pr-harvest.mjs +22 -2
- package/src/ops/toolchain-diagnostics.mjs +253 -0
- package/src/ops/tui-menu.mjs +192 -0
- package/src/process-tree.mjs +146 -0
- package/src/scaffold.mjs +49 -1
- package/src/scope-guard.mjs +73 -5
- package/src/secret-scanner.mjs +1 -1
- package/src/security.mjs +37 -30
- package/src/self-audit.mjs +237 -3
- package/src/state.mjs +100 -4
- package/src/uninstall.mjs +227 -0
- package/src/wizard-init.mjs +11 -0
- package/src/wizard-task.mjs +8 -2
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);
|
package/src/migrate.mjs
ADDED
|
@@ -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
|
|
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
|
|
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: "
|
|
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
|
},
|