jules-orchestrator-kit 0.38.0 → 0.38.2

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.
Files changed (44) hide show
  1. package/.agent/rules/jules-protocol.md +3 -0
  2. package/JULES_RULES_TEMPLATE.md +3 -1
  3. package/README.md +163 -307
  4. package/bin/agentctl.mjs +177 -7
  5. package/bin/init.js +8 -8
  6. package/index.mjs +14 -2
  7. package/package.json +1 -1
  8. package/scripts/command-resolver.mjs +1 -2
  9. package/scripts/doc-sync-check.mjs +36 -1
  10. package/scripts/jules-create.mjs +1 -1
  11. package/scripts/jules-dispatch.mjs +1 -2
  12. package/scripts/jules-nightly.mjs +2 -2
  13. package/scripts/jules-patch.mjs +2 -2
  14. package/scripts/jules-queue-runner.mjs +1 -1
  15. package/scripts/jules-scan-todos.mjs +1 -1
  16. package/scripts/jules-self-audit.mjs +1 -2
  17. package/scripts/jules-status.mjs +1 -1
  18. package/scripts/release.mjs +55 -0
  19. package/scripts/run-tests.mjs +15 -1
  20. package/scripts/utils.mjs +14 -9
  21. package/src/asset-integrity.mjs +76 -0
  22. package/src/asset_integrity.mjs +1 -76
  23. package/src/config.mjs +42 -23
  24. package/src/dag-engine.mjs +44 -6
  25. package/src/engine.mjs +18 -11
  26. package/src/execution-envelope.mjs +111 -0
  27. package/src/execution_envelope.mjs +1 -111
  28. package/src/git.mjs +6 -3
  29. package/src/merge-blocks.mjs +178 -1
  30. package/src/ops/command-registry.mjs +25 -0
  31. package/src/ops/handover.mjs +411 -0
  32. package/src/ops/ide-scaffold.mjs +3 -3
  33. package/src/ops/next-step.mjs +6 -2
  34. package/src/prompt-guard.mjs +34 -3
  35. package/src/provider.mjs +191 -86
  36. package/src/role-resolver.mjs +31 -0
  37. package/src/rules-budget.mjs +179 -0
  38. package/src/rules_budget.mjs +1 -179
  39. package/src/security.mjs +98 -56
  40. package/src/state.mjs +11 -6
  41. package/src/task-optimizer.mjs +10 -2
  42. package/src/tui.mjs +1 -1
  43. package/src/webhook.mjs +2 -2
  44. package/src/wizard-task.mjs +4 -30
@@ -279,7 +279,27 @@ export function mergeBlocks3Way(baseText = "", oursText = "", theirsText = "", l
279
279
  }
280
280
  }
281
281
 
282
- const mergedText = outputBlocks.join("\n\n");
282
+ let mergedText = outputBlocks.join("\n\n");
283
+
284
+ if (conflicts > 0) {
285
+ if (lang === "json") {
286
+ try {
287
+ const autoResolved = resolveJsonConflict(mergedText);
288
+ if (autoResolved && !autoResolved.includes("<<<<<<<")) {
289
+ mergedText = autoResolved;
290
+ conflicts = 0;
291
+ }
292
+ } catch (_) {}
293
+ } else if (lang === "md" || lang === "markdown") {
294
+ try {
295
+ const autoResolved = resolveMarkdownConflict(mergedText);
296
+ if (autoResolved && !autoResolved.includes("<<<<<<<")) {
297
+ mergedText = autoResolved;
298
+ conflicts = 0;
299
+ }
300
+ } catch (_) {}
301
+ }
302
+ }
283
303
 
284
304
  return {
285
305
  mergedText,
@@ -290,3 +310,160 @@ export function mergeBlocks3Way(baseText = "", oursText = "", theirsText = "", l
290
310
  },
291
311
  };
292
312
  }
313
+
314
+ /**
315
+ * Deep-merges two parsed JSON values.
316
+ * - Objects: merged recursively; HEAD (ours) wins on scalar conflicts.
317
+ * - Arrays: concatenated with primitive deduplication and deep object equality check.
318
+ * - Primitives: HEAD (ours) wins unless null/undefined.
319
+ */
320
+ export function deepMergeJson(head, dev) {
321
+ if (head === null || head === undefined) return dev;
322
+ if (dev === null || dev === undefined) return head;
323
+
324
+ if (Array.isArray(head) && Array.isArray(dev)) {
325
+ const combined = [...head];
326
+ for (const item of dev) {
327
+ const isPrimitive = typeof item !== "object" || item === null;
328
+ if (isPrimitive) {
329
+ if (!combined.includes(item)) combined.push(item);
330
+ } else {
331
+ const itemStr = JSON.stringify(item);
332
+ const exists = combined.some((c) => JSON.stringify(c) === itemStr);
333
+ if (!exists) combined.push(item);
334
+ }
335
+ }
336
+ return combined;
337
+ }
338
+
339
+ if (typeof head === "object" && typeof dev === "object" && !Array.isArray(head) && !Array.isArray(dev)) {
340
+ const result = { ...dev };
341
+ for (const key of Object.keys(head)) {
342
+ if (key in result) {
343
+ result[key] = deepMergeJson(head[key], result[key]);
344
+ } else {
345
+ result[key] = head[key];
346
+ }
347
+ }
348
+ return result;
349
+ }
350
+
351
+ // Scalar conflict: HEAD (ours) wins
352
+ return head;
353
+ }
354
+
355
+ /**
356
+ * Automatically resolves merge conflicts in JSON files.
357
+ * Extracts <<<<<<< / ======= / >>>>>>> markers, parses both branches, deep-merges them,
358
+ * and returns the formatted JSON string.
359
+ *
360
+ * @param {string} content - Raw conflict-marked file content
361
+ * @returns {string} Clean, resolved JSON string (or original content if unresolvable)
362
+ */
363
+ export function resolveJsonConflict(content) {
364
+ if (typeof content !== "string") return content;
365
+ if (!content.includes("<<<<<<<") || !content.includes("=======") || !content.includes(">>>>>>>")) {
366
+ return content;
367
+ }
368
+
369
+ // Check if the whole file or a segment is marked as conflict
370
+ const fullConflictRegex = /^<{7}.*?\r?\n([\s\S]*?)\r?\n={7}\r?\n([\s\S]*?)\r?\n>{7}.*?$/m;
371
+ const match = content.match(fullConflictRegex);
372
+
373
+ if (match) {
374
+ const headRaw = match[1].trim();
375
+ const devRaw = match[2].trim();
376
+
377
+ try {
378
+ const headJson = JSON.parse(headRaw);
379
+ const devJson = JSON.parse(devRaw);
380
+ const merged = deepMergeJson(headJson, devJson);
381
+ return JSON.stringify(merged, null, 2) + "\n";
382
+ } catch (_) {
383
+ // If individual branches are partial JSON snippets, fall through to block resolution
384
+ }
385
+ }
386
+
387
+ // Multi-block / partial JSON conflict resolution
388
+ const conflictBlockRegex = /<{7}[^\n]*\r?\n([\s\S]*?)\r?\n={7}\r?\n([\s\S]*?)\r?\n>{7}[^\n]*/g;
389
+ const resolved = content.replace(conflictBlockRegex, (_, headBlock, devBlock) => {
390
+ try {
391
+ const h = JSON.parse(`{${headBlock}}`);
392
+ const d = JSON.parse(`{${devBlock}}`);
393
+ const m = deepMergeJson(h, d);
394
+ const inner = JSON.stringify(m, null, 2).replace(/^\{\s*\n?/, "").replace(/\n?\s*\}$/, "");
395
+ return inner;
396
+ } catch (_) {
397
+ // Fallback: take head (ours)
398
+ return headBlock.trim();
399
+ }
400
+ });
401
+
402
+ return resolved;
403
+ }
404
+
405
+ /**
406
+ * Automatically resolves merge conflicts in Markdown files (changelogs, notes, docs).
407
+ * Removes conflict markers, deduplicates repeated items, and concatenates updates.
408
+ *
409
+ * @param {string} content - Raw conflict-marked markdown content
410
+ * @returns {string} Resolved Markdown text
411
+ */
412
+ export function resolveMarkdownConflict(content) {
413
+ if (typeof content !== "string") return content;
414
+ if (!content.includes("<<<<<<<") || !content.includes("=======") || !content.includes(">>>>>>>")) {
415
+ return content;
416
+ }
417
+
418
+ const lines = content.split(/\r?\n/);
419
+ const result = [];
420
+ let inConflict = false;
421
+ let headBuffer = [];
422
+ let devBuffer = [];
423
+ let section = null; // 'head' | 'dev'
424
+
425
+ for (let i = 0; i < lines.length; i++) {
426
+ const line = lines[i];
427
+
428
+ if (line.startsWith("<<<<<<<")) {
429
+ inConflict = true;
430
+ section = "head";
431
+ headBuffer = [];
432
+ devBuffer = [];
433
+ continue;
434
+ }
435
+
436
+ if (inConflict && line.startsWith("=======")) {
437
+ section = "dev";
438
+ continue;
439
+ }
440
+
441
+ if (inConflict && line.startsWith(">>>>>>>")) {
442
+ inConflict = false;
443
+ section = null;
444
+
445
+ // Concatenate both sides while deduplicating identical lines
446
+ const seen = new Set();
447
+ for (const hLine of headBuffer) {
448
+ result.push(hLine);
449
+ if (hLine.trim()) seen.add(hLine.trim());
450
+ }
451
+ for (const dLine of devBuffer) {
452
+ if (!seen.has(dLine.trim())) {
453
+ result.push(dLine);
454
+ if (dLine.trim()) seen.add(dLine.trim());
455
+ }
456
+ }
457
+ continue;
458
+ }
459
+
460
+ if (inConflict) {
461
+ if (section === "head") headBuffer.push(line);
462
+ else if (section === "dev") devBuffer.push(line);
463
+ } else {
464
+ result.push(line);
465
+ }
466
+ }
467
+
468
+ return result.join("\n");
469
+ }
@@ -276,6 +276,31 @@ export const COMMAND_REGISTRY = [
276
276
  { name: "json", type: "boolean", description: "Output JSON structured response" },
277
277
  ],
278
278
  },
279
+ {
280
+ id: "handover",
281
+ path: ["handover"],
282
+ title: "handover",
283
+ description: "Inspect or generate Baton Pass session handover envelopes",
284
+ category: "Operate",
285
+ mutates: true,
286
+ risk: "low",
287
+ interactive: "never",
288
+ requiresRepository: true,
289
+ shortcuts: ["ho"],
290
+ examples: [
291
+ "agentctl handover list",
292
+ "agentctl handover show sess-123",
293
+ 'agentctl handover create sess-123 --intent "Refactor auth" --status rolled-back',
294
+ ],
295
+ flags: [
296
+ { name: "intent", type: "string", description: "Task intent or goal description" },
297
+ { name: "status", type: "string", description: "Handover status (aborted, rolled-back, escalated, failed)" },
298
+ { name: "landmines", type: "string", description: "Obstacles or error summary" },
299
+ { name: "next-steps", type: "string", description: "Actionable next steps for successor agent" },
300
+ { name: "json", type: "boolean", description: "Output JSON structured response" },
301
+ { name: "limit", type: "string", description: "Maximum handovers to list" },
302
+ ],
303
+ },
279
304
  ];
280
305
 
281
306
  /**
@@ -0,0 +1,411 @@
1
+ import {
2
+ readFileSync,
3
+ mkdirSync,
4
+ existsSync,
5
+ readdirSync,
6
+ statSync,
7
+ unlinkSync,
8
+ renameSync,
9
+ openSync,
10
+ writeSync,
11
+ fsyncSync,
12
+ closeSync,
13
+ } from "node:fs";
14
+ import { join, resolve, relative, isAbsolute, basename } from "node:path";
15
+ import { randomUUID } from "node:crypto";
16
+ import { redactSecrets } from "../security.mjs";
17
+ import { resolveRoot } from "../config.mjs";
18
+
19
+ export class HandoverError extends Error {
20
+ constructor(message, opts = {}) {
21
+ super(message);
22
+ this.name = "HandoverError";
23
+ this.code = opts.code || 1;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * @typedef {"aborted" | "escalated" | "rolled-back" | "quarantined" | "completed" | "failed"} HandoverStatus
29
+ *
30
+ * @typedef {Object} HandoverPayload
31
+ * @property {string} sessionId
32
+ * @property {HandoverStatus} [status]
33
+ * @property {string} [intent]
34
+ * @property {string[] | string} [completed]
35
+ * @property {string[] | string} [assumptions]
36
+ * @property {string[] | string} [landmines]
37
+ * @property {string[] | string} [nextSteps]
38
+ * @property {string[]} [referencedPaths]
39
+ * @property {string} [branch]
40
+ * @property {string} [headSha]
41
+ * @property {string} [diffSummary]
42
+ * @property {Record<string, any>} [metadata]
43
+ */
44
+
45
+ /**
46
+ * Ensures handover storage directory exists.
47
+ * @param {string} root
48
+ * @returns {string} Absolute path to .agent/handovers directory
49
+ */
50
+ export function getHandoverDir(root = resolveRoot()) {
51
+ const dir = join(root, ".agent", "handovers");
52
+ try {
53
+ mkdirSync(dir, { recursive: true });
54
+ } catch (_) {}
55
+ return dir;
56
+ }
57
+
58
+ function assertSessionId(sessionId) {
59
+ if (typeof sessionId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(sessionId)) {
60
+ throw new HandoverError(`Invalid handover session id: "${sessionId}"`);
61
+ }
62
+ }
63
+
64
+ function toItemArray(value) {
65
+ if (!value) return [];
66
+ if (Array.isArray(value)) {
67
+ return value.map((item) => String(item).trim()).filter(Boolean);
68
+ }
69
+ return String(value)
70
+ .split(/\r?\n/)
71
+ .map((line) => line.replace(/^[-*•]\s+/, "").trim())
72
+ .filter(Boolean);
73
+ }
74
+
75
+ function sanitizeText(value) {
76
+ if (!value) return "";
77
+ return redactSecrets(String(value).trim());
78
+ }
79
+
80
+ function writeFileAtomically(filePath, content) {
81
+ const tmpPath = `${filePath}.${randomUUID()}.tmp`;
82
+ let fd;
83
+ try {
84
+ fd = openSync(tmpPath, "wx", 0o600);
85
+ writeSync(fd, content, "utf-8");
86
+ fsyncSync(fd);
87
+ closeSync(fd);
88
+ fd = undefined;
89
+ renameSync(tmpPath, filePath);
90
+ } catch (err) {
91
+ if (fd !== undefined) {
92
+ try { closeSync(fd); } catch (_) {}
93
+ }
94
+ try { unlinkSync(tmpPath); } catch (_) {}
95
+ throw err;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Creates and persists a Baton Pass handover manifest in .agent/handovers/YYYY-MM-DD-[sessionId].md.
101
+ *
102
+ * @param {string} root - Project root directory
103
+ * @param {HandoverPayload} data - Handover details
104
+ * @param {Object} [options]
105
+ * @param {number} [options.maxRetention=20] - Max handovers to retain
106
+ * @returns {{ sessionId: string, filePath: string, createdAt: string, status: string, markdown: string }}
107
+ */
108
+ export function createHandover(root = resolveRoot(), data = {}, options = {}) {
109
+ const handoverDir = getHandoverDir(root);
110
+ const sessionId = data.sessionId || `session-${Date.now()}`;
111
+ assertSessionId(sessionId);
112
+
113
+ const createdAt = data.createdAt || new Date().toISOString();
114
+ const datePrefix = createdAt.substring(0, 10);
115
+ const status = data.status || "aborted";
116
+ const branch = sanitizeText(data.branch || "");
117
+ const headSha = sanitizeText(data.headSha || "");
118
+
119
+ const intent = sanitizeText(data.intent || "");
120
+ const completed = toItemArray(data.completed).map(sanitizeText);
121
+ const assumptions = toItemArray(data.assumptions).map(sanitizeText);
122
+ const landmines = toItemArray(data.landmines).map(sanitizeText);
123
+ const nextSteps = toItemArray(data.nextSteps).map(sanitizeText);
124
+ const referencedPaths = (Array.isArray(data.referencedPaths) ? data.referencedPaths : [])
125
+ .map((p) => sanitizeText(p))
126
+ .filter(Boolean);
127
+ const diffSummary = sanitizeText(data.diffSummary || "");
128
+
129
+ const fileName = `${datePrefix}-${sessionId}.md`;
130
+ const filePath = join(handoverDir, fileName);
131
+
132
+ // Build YAML frontmatter + structured markdown
133
+ const frontmatterLines = [
134
+ "---",
135
+ "schema: agentctl/handover-v1",
136
+ `session_id: "${sessionId}"`,
137
+ `created_at: "${createdAt}"`,
138
+ `status: "${status}"`,
139
+ ];
140
+ if (branch) frontmatterLines.push(`branch: "${branch}"`);
141
+ if (headSha) frontmatterLines.push(`head_sha: "${headSha}"`);
142
+ if (referencedPaths.length > 0) {
143
+ frontmatterLines.push("referenced_paths:");
144
+ for (const p of referencedPaths) {
145
+ frontmatterLines.push(` - "${p}"`);
146
+ }
147
+ }
148
+ frontmatterLines.push("---", "");
149
+
150
+ const bodySections = [
151
+ `# Agent Handover: ${sessionId}`,
152
+ "",
153
+ `**Status:** \`${status}\` `,
154
+ `**Recorded At:** \`${createdAt}\` `,
155
+ branch ? `**Branch:** \`${branch}\` ` : "",
156
+ headSha ? `**Commit HEAD:** \`${headSha}\` ` : "",
157
+ "",
158
+ "## Intent",
159
+ intent ? intent : "*(No specific intent recorded)*",
160
+ "",
161
+ "## Completed Work",
162
+ completed.length > 0 ? completed.map((c) => `- ${c}`).join("\n") : "*(No completed steps recorded)*",
163
+ "",
164
+ "## Validated Assumptions",
165
+ assumptions.length > 0 ? assumptions.map((a) => `- ${a}`).join("\n") : "*(No explicit assumptions noted)*",
166
+ "",
167
+ "## Landmines & Failures",
168
+ landmines.length > 0 ? landmines.map((l) => `- ${l}`).join("\n") : "*(No blockers or landmines reported)*",
169
+ "",
170
+ "## Recommended Next Steps",
171
+ nextSteps.length > 0 ? nextSteps.map((s) => `- ${s}`).join("\n") : "*(No immediate next steps specified)*",
172
+ ].filter(Boolean);
173
+
174
+ if (referencedPaths.length > 0) {
175
+ bodySections.push(
176
+ "",
177
+ "## Referenced Paths",
178
+ ...referencedPaths.map((p) => `- \`${p}\``)
179
+ );
180
+ }
181
+
182
+ if (diffSummary) {
183
+ bodySections.push(
184
+ "",
185
+ "## Diff Summary",
186
+ "```text",
187
+ diffSummary,
188
+ "```"
189
+ );
190
+ }
191
+
192
+ bodySections.push("");
193
+ const fullContent = frontmatterLines.join("\n") + bodySections.join("\n");
194
+
195
+ writeFileAtomically(filePath, fullContent);
196
+
197
+ const retention = typeof options.maxRetention === "number" ? options.maxRetention : 20;
198
+ if (retention > 0) {
199
+ pruneHandovers(root, retention);
200
+ }
201
+
202
+ return {
203
+ sessionId,
204
+ filePath,
205
+ createdAt,
206
+ status,
207
+ markdown: fullContent,
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Loads and parses a handover file by sessionId or filepath.
213
+ *
214
+ * @param {string} root
215
+ * @param {string} sessionIdOrPath
216
+ * @returns {Object} Structured handover object
217
+ */
218
+ export function loadHandover(root = resolveRoot(), sessionIdOrPath = "") {
219
+ const handoverDir = resolve(root, ".agent", "handovers");
220
+ let targetPath;
221
+
222
+ if (!sessionIdOrPath || typeof sessionIdOrPath !== "string") {
223
+ throw new HandoverError("Handover session id or path is required");
224
+ }
225
+
226
+ if (sessionIdOrPath.endsWith(".md")) {
227
+ targetPath = isAbsolute(sessionIdOrPath) ? resolve(sessionIdOrPath) : resolve(root, sessionIdOrPath);
228
+ const rel = relative(handoverDir, targetPath);
229
+ if (rel.startsWith("..") || isAbsolute(rel)) {
230
+ throw new HandoverError(`Handover path escapes handovers directory: ${sessionIdOrPath}`);
231
+ }
232
+ } else {
233
+ assertSessionId(sessionIdOrPath);
234
+ // Find matching file in directory with date prefix
235
+ const files = existsSync(handoverDir) ? readdirSync(handoverDir).filter((f) => f.endsWith(".md")) : [];
236
+ const match = files.find((f) => f === `${sessionIdOrPath}.md` || f.endsWith(`-${sessionIdOrPath}.md`));
237
+ if (!match) {
238
+ throw new HandoverError(`Handover file not found for session "${sessionIdOrPath}"`);
239
+ }
240
+ targetPath = join(handoverDir, match);
241
+ }
242
+
243
+ if (!existsSync(targetPath)) {
244
+ throw new HandoverError(`Handover file not found: ${targetPath}`);
245
+ }
246
+
247
+ const raw = readFileSync(targetPath, "utf-8");
248
+
249
+ // Parse YAML Frontmatter
250
+ let frontmatter = {};
251
+ let markdownBody = raw;
252
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
253
+ if (fmMatch) {
254
+ const fmLines = fmMatch[1].split(/\r?\n/);
255
+ markdownBody = fmMatch[2];
256
+ for (const line of fmLines) {
257
+ const kv = line.match(/^([a-z_]+):\s*(?:"(.*)"|(.*))$/);
258
+ if (kv) {
259
+ frontmatter[kv[1]] = (kv[2] !== undefined ? kv[2] : kv[3] || "").trim();
260
+ }
261
+ }
262
+ }
263
+
264
+ // Parse markdown sections
265
+ function extractSection(title) {
266
+ const regex = new RegExp(`## ${title}\\r?\\n([\\s\\S]*?)(?=\\r?\\n## |$)`, "i");
267
+ const m = markdownBody.match(regex);
268
+ if (!m) return "";
269
+ return m[1].trim();
270
+ }
271
+
272
+ function extractListSection(title) {
273
+ const section = extractSection(title);
274
+ if (!section || section.startsWith("*(")) return [];
275
+ return section
276
+ .split(/\r?\n/)
277
+ .map((line) => line.replace(/^[-*•]\s+/, "").replace(/^`|`$/g, "").trim())
278
+ .filter(Boolean);
279
+ }
280
+
281
+ const intentRaw = extractSection("Intent");
282
+ const intent = intentRaw.startsWith("*(") ? "" : intentRaw;
283
+
284
+ return {
285
+ schema: frontmatter.schema || "agentctl/handover-v1",
286
+ sessionId: frontmatter.session_id || basename(targetPath, ".md"),
287
+ createdAt: frontmatter.created_at || new Date(statSync(targetPath).mtimeMs).toISOString(),
288
+ status: frontmatter.status || "aborted",
289
+ branch: frontmatter.branch || "",
290
+ headSha: frontmatter.head_sha || "",
291
+ referencedPaths: extractListSection("Referenced Paths"),
292
+ intent,
293
+ completed: extractListSection("Completed Work"),
294
+ assumptions: extractListSection("Validated Assumptions"),
295
+ landmines: extractListSection("Landmines & Failures"),
296
+ nextSteps: extractListSection("Recommended Next Steps"),
297
+ rawMarkdown: raw,
298
+ filePath: targetPath,
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Lists all handovers in .agent/handovers/ ordered by creation timestamp descending.
304
+ *
305
+ * @param {string} root
306
+ * @returns {Array<{ sessionId: string, status: string, createdAt: string, filePath: string, intent: string }>}
307
+ */
308
+ export function listHandovers(root = resolveRoot()) {
309
+ const handoverDir = getHandoverDir(root);
310
+ if (!existsSync(handoverDir)) return [];
311
+
312
+ const files = readdirSync(handoverDir).filter((f) => f.endsWith(".md"));
313
+ const items = [];
314
+
315
+ for (const file of files) {
316
+ const filePath = join(handoverDir, file);
317
+ try {
318
+ const parsed = loadHandover(root, filePath);
319
+ items.push({
320
+ sessionId: parsed.sessionId,
321
+ status: parsed.status,
322
+ createdAt: parsed.createdAt,
323
+ filePath,
324
+ intent: parsed.intent,
325
+ });
326
+ } catch (_) {}
327
+ }
328
+
329
+ items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
330
+ return items;
331
+ }
332
+
333
+ /**
334
+ * Prunes handovers keeping only the most recent N files.
335
+ *
336
+ * @param {string} root
337
+ * @param {number} [maxRetention=20]
338
+ * @returns {number} Count of pruned files
339
+ */
340
+ export function pruneHandovers(root = resolveRoot(), maxRetention = 20) {
341
+ const list = listHandovers(root);
342
+ if (list.length <= maxRetention) return 0;
343
+
344
+ const toRemove = list.slice(maxRetention);
345
+ let prunedCount = 0;
346
+
347
+ for (const item of toRemove) {
348
+ try {
349
+ if (existsSync(item.filePath)) {
350
+ unlinkSync(item.filePath);
351
+ prunedCount++;
352
+ }
353
+ } catch (_) {}
354
+ }
355
+
356
+ return prunedCount;
357
+ }
358
+
359
+ /**
360
+ * Formats a handover object into a compact, token-efficient prompt context block.
361
+ *
362
+ * @param {Object} handover - Parsed handover object
363
+ * @returns {string} Formatted [SESSION_HANDOVER_CONTEXT] string
364
+ */
365
+ export function formatHandoverPromptContext(handover) {
366
+ if (!handover || typeof handover !== "object") return "";
367
+
368
+ const lines = [
369
+ "[SESSION_HANDOVER_CONTEXT]",
370
+ `Previous Session: ${handover.sessionId || "unknown"} (Status: ${handover.status || "aborted"}, Created: ${handover.createdAt || "N/A"})`,
371
+ ];
372
+
373
+ if (handover.intent) {
374
+ lines.push(`Prior Intent: ${handover.intent}`);
375
+ }
376
+
377
+ if (Array.isArray(handover.completed) && handover.completed.length > 0) {
378
+ lines.push("Completed Progress:");
379
+ for (const c of handover.completed) {
380
+ lines.push(` - ${c}`);
381
+ }
382
+ }
383
+
384
+ if (Array.isArray(handover.assumptions) && handover.assumptions.length > 0) {
385
+ lines.push("Validated Assumptions:");
386
+ for (const a of handover.assumptions) {
387
+ lines.push(` - ${a}`);
388
+ }
389
+ }
390
+
391
+ if (Array.isArray(handover.landmines) && handover.landmines.length > 0) {
392
+ lines.push("Obstacles & Landmines:");
393
+ for (const l of handover.landmines) {
394
+ lines.push(` - ${l}`);
395
+ }
396
+ }
397
+
398
+ if (Array.isArray(handover.nextSteps) && handover.nextSteps.length > 0) {
399
+ lines.push("Recommended Next Steps:");
400
+ for (const s of handover.nextSteps) {
401
+ lines.push(` - ${s}`);
402
+ }
403
+ }
404
+
405
+ if (Array.isArray(handover.referencedPaths) && handover.referencedPaths.length > 0) {
406
+ lines.push(`Referenced Files: ${handover.referencedPaths.join(", ")}`);
407
+ }
408
+
409
+ lines.push("[/SESSION_HANDOVER_CONTEXT]");
410
+ return lines.join("\n");
411
+ }
@@ -75,21 +75,21 @@ export function scaffoldIdeConfig(target = "all", options = {}) {
75
75
  {
76
76
  label: "Jules: Run System Doctor",
77
77
  type: "shell",
78
- command: "npx agentctl doctor",
78
+ command: "npx jules-orchestrator-kit doctor",
79
79
  group: "test",
80
80
  problemMatcher: [],
81
81
  },
82
82
  {
83
83
  label: "Jules: Create Task Envelope",
84
84
  type: "shell",
85
- command: "npx agentctl task create",
85
+ command: "npx jules-orchestrator-kit task create",
86
86
  group: "build",
87
87
  problemMatcher: [],
88
88
  },
89
89
  {
90
90
  label: "Jules: 1-Click Rollback Checkpoint",
91
91
  type: "shell",
92
- command: "npx agentctl rollback --latest",
92
+ command: "npx jules-orchestrator-kit rollback --latest",
93
93
  group: "none",
94
94
  problemMatcher: [],
95
95
  },
@@ -62,8 +62,12 @@ export function resolveNextStep(root, env = process.env) {
62
62
  };
63
63
  }
64
64
 
65
- const queueDir = join(root, ".agent", "queue");
66
- const queued = existsSync(queueDir) ? readdirSync(queueDir).filter((f) => f.endsWith(".json") || f.endsWith(".yml")).length : 0;
65
+ const primaryQueueDir = join(root, ".agent", "jules-queue");
66
+ const fallbackQueueDir = join(root, ".agent", "queue");
67
+ const queueDir = existsSync(primaryQueueDir) ? primaryQueueDir : fallbackQueueDir;
68
+ const queued = existsSync(queueDir)
69
+ ? readdirSync(queueDir).filter((f) => f.endsWith(".md") || f.endsWith(".json") || f.endsWith(".yml")).length
70
+ : 0;
67
71
  if (queued > 0) {
68
72
  return {
69
73
  id: "queue",