jules-orchestrator-kit 0.52.8 → 0.54.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.
Files changed (43) hide show
  1. package/README.md +68 -8
  2. package/bin/agentctl.mjs +327 -24
  3. package/bin/init.js +87 -74
  4. package/index.mjs +10 -0
  5. package/package.json +5 -2
  6. package/scripts/jules-create.mjs +5 -16
  7. package/scripts/jules-patch.mjs +18 -10
  8. package/scripts/stale-base-check.mjs +1 -1
  9. package/src/ci-templates.mjs +199 -0
  10. package/src/config-edit.mjs +138 -0
  11. package/src/config.mjs +39 -11
  12. package/src/engine.mjs +2 -21
  13. package/src/env-aliases.mjs +80 -0
  14. package/src/git.mjs +79 -0
  15. package/src/mcp.mjs +19 -1
  16. package/src/ops/cli-intent.mjs +29 -0
  17. package/src/ops/command-registry.mjs +19 -10
  18. package/src/ops/doctor-registry.mjs +75 -32
  19. package/src/ops/ide-scaffold.mjs +22 -16
  20. package/src/ops/next-step.mjs +38 -7
  21. package/src/ops/verify-output.mjs +38 -0
  22. package/src/profiles.mjs +188 -0
  23. package/src/provider-readiness.mjs +284 -0
  24. package/src/security.mjs +42 -1
  25. package/src/tui.mjs +14 -3
  26. package/src/wizard-init.mjs +88 -12
  27. package/src/wizard-task.mjs +22 -5
  28. package/src/ops/doctor-planner.mjs +0 -220
  29. package/src/ops/receipts.mjs +0 -171
  30. package/src/ops/swarm-actions.mjs +0 -156
  31. package/src/ops/task-actions.mjs +0 -214
  32. package/src/ops/transaction.mjs +0 -278
  33. package/src/ux/capabilities.mjs +0 -180
  34. package/src/ux/diff-viewer.mjs +0 -289
  35. package/src/ux/layout.mjs +0 -195
  36. package/src/ux/log-viewer.mjs +0 -110
  37. package/src/ux/palette.mjs +0 -179
  38. package/src/ux/queue-model.mjs +0 -206
  39. package/src/ux/renderer.mjs +0 -188
  40. package/src/ux/swarm-model.mjs +0 -146
  41. package/src/ux/terminal-session.mjs +0 -215
  42. package/src/ux/widgets.mjs +0 -323
  43. /package/src/{ux/key-decoder.mjs → key-decoder.mjs} +0 -0
@@ -1,220 +0,0 @@
1
- import { join, resolve } from "node:path";
2
- import { existsSync, readdirSync, readFileSync } from "node:fs";
3
- import { createHash, randomUUID } from "node:crypto";
4
- import { detectStack } from "../config.mjs";
5
-
6
- /**
7
- * @typedef {import("../ux/capabilities.mjs").ActionPlan} ActionPlan
8
- * @typedef {import("./doctor-registry.mjs").DoctorReport} DoctorReport
9
- *
10
- * @typedef {Object} FixPlanningContext
11
- * @property {string} root
12
- * @property {DoctorReport} report
13
- * @property {string[]} selectedFixIds
14
- * @property {Record<string, unknown>} [answers]
15
- */
16
-
17
- /**
18
- * Generate unified diff preview string for file creation/replacement.
19
- * @param {string} path
20
- * @param {string} oldContent
21
- * @param {string} newContent
22
- * @returns {string}
23
- */
24
- function createDiffPreview(path, oldContent, newContent) {
25
- const oldLines = oldContent ? oldContent.split("\n") : [];
26
- const newLines = newContent ? newContent.split("\n") : [];
27
- const lines = [];
28
-
29
- lines.push(`--- a/${path}`);
30
- lines.push(`+++ b/${path}`);
31
- lines.push(`@@ -1,${oldLines.length || 0} +1,${newLines.length || 0} @@`);
32
-
33
- for (const line of oldLines) {
34
- lines.push(`-${line}`);
35
- }
36
- for (const line of newLines) {
37
- lines.push(`+${line}`);
38
- }
39
-
40
- return lines.join("\n");
41
- }
42
-
43
- function deepFreeze(value) {
44
- if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
45
- for (const child of Object.values(value)) deepFreeze(child);
46
- return Object.freeze(value);
47
- }
48
-
49
- function finalizePlan(plan) {
50
- const { planHash: _oldHash, ...unsigned } = plan;
51
- const planHash = "sha256:" + createHash("sha256").update(JSON.stringify(unsigned)).digest("hex");
52
- return deepFreeze({ ...unsigned, planHash });
53
- }
54
-
55
- /**
56
- * Plan diagnostic fixes into immutable ActionPlan objects.
57
- * @param {FixPlanningContext} context
58
- * @returns {Promise<ActionPlan[]>}
59
- */
60
- export async function planDiagnosticFixes(context = {}) {
61
- const root = resolve(context.root || process.cwd());
62
- const report = context.report || { results: [] };
63
- const selectedFixIds = Array.isArray(context.selectedFixIds) ? context.selectedFixIds : ["safe"];
64
-
65
- const wantSafeOnly = selectedFixIds.includes("safe");
66
- /** @type {ActionPlan[]} */
67
- const plans = [];
68
-
69
- for (const result of report.results || []) {
70
- for (const fix of result.fixes || []) {
71
- const isSelected = selectedFixIds.includes(fix.id) || (wantSafeOnly && fix.automatic);
72
- if (!isSelected) continue;
73
-
74
- if (fix.id === "config.create-default") {
75
- const configPath = ".agent/config.yml";
76
- // Detected, not assumed. Writing `npm test` into a Rust or Python
77
- // repository's config produced a manifest whose first verification run
78
- // could only fail, and the stack detector already has the answer.
79
- const detected = detectStack(root);
80
- const newContent = `# jules-orchestrator-kit configuration manifest
81
- version: 1
82
- # Free-tier limits until you state your plan (free | pro | ultra).
83
- tier: free
84
- verify:
85
- test: ${JSON.stringify(detected.testCmd || "")}
86
- build: ${JSON.stringify(detected.buildCmd || "")}
87
- `;
88
- const diff = createDiffPreview(configPath, "", newContent);
89
- const planId = `PLAN-${Date.now()}-${randomUUID().slice(0, 8)}`;
90
-
91
- plans.push({
92
- schema: "agentctl/action-plan-v1",
93
- id: planId,
94
- kind: "config.create-default",
95
- title: "Create default .agent/config.yml",
96
- summary: "Generate initial configuration manifest for jules-orchestrator-kit",
97
- risk: "low",
98
- repository: root,
99
- createdAt: new Date().toISOString(),
100
- preconditions: [],
101
- fileMutations: [
102
- {
103
- operation: "create",
104
- path: configPath,
105
- newContent,
106
- },
107
- ],
108
- commandEffects: [],
109
- stateTransitions: [],
110
- preview: {
111
- unifiedDiff: diff,
112
- warnings: [],
113
- estimatedImpact: ["Creates .agent/config.yml"],
114
- },
115
- confirmation: {
116
- mode: "none",
117
- prompt: "Apply default configuration?",
118
- },
119
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
120
- });
121
- } else if (fix.id === "state.rebuild-head") {
122
- const dateStr = new Date().toISOString().split("T")[0];
123
- const stateDir = join(root, ".agent", "state");
124
- const headPath = `.agent/state/telemetry-${dateStr}.head`;
125
- let tailHash = "0".repeat(64);
126
- let segment = 0;
127
- try {
128
- const telemetryFiles = readdirSync(stateDir)
129
- .filter((name) => name.startsWith(`telemetry-${dateStr}`) && name.endsWith(".jsonl"))
130
- .sort((a, b) => {
131
- const index = (name) => Number(name.match(/-(\d+)\.jsonl$/)?.[1] || 0);
132
- return index(a) - index(b);
133
- });
134
- const lastFile = telemetryFiles.at(-1);
135
- if (lastFile) {
136
- const lines = readFileSync(join(stateDir, lastFile), "utf-8").split("\n").filter(Boolean);
137
- const lastEntry = JSON.parse(lines.at(-1));
138
- if (typeof lastEntry?.hash === "string") tailHash = lastEntry.hash;
139
- const segmentMatch = lastFile.match(/-(\d+)\.jsonl$/);
140
- segment = segmentMatch ? Number(segmentMatch[1]) : 0;
141
- }
142
- } catch (_) {}
143
- const newContent = JSON.stringify({ v: 1, hash: tailHash, segment, ts: new Date().toISOString() });
144
- const planId = `PLAN-${Date.now()}-${randomUUID().slice(0, 8)}`;
145
-
146
- plans.push({
147
- schema: "agentctl/action-plan-v1",
148
- id: planId,
149
- kind: "state.rebuild-head",
150
- title: "Rebuild telemetry spine head pointer",
151
- summary: "Re-initialize .head pointer for telemetry spine ledger",
152
- risk: "low",
153
- repository: root,
154
- createdAt: new Date().toISOString(),
155
- preconditions: [],
156
- fileMutations: [
157
- {
158
- operation: "create",
159
- path: headPath,
160
- newContent,
161
- },
162
- ],
163
- commandEffects: [],
164
- stateTransitions: [],
165
- preview: {
166
- unifiedDiff: createDiffPreview(headPath, "", newContent),
167
- warnings: [],
168
- estimatedImpact: [`Creates ${headPath} pointer`],
169
- },
170
- confirmation: {
171
- mode: "none",
172
- prompt: "Rebuild telemetry head pointer?",
173
- },
174
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
175
- });
176
- } else if (fix.id === "locks.prune-stale") {
177
- const locksDir = join(root, ".agent", "state", "locks");
178
- if (existsSync(locksDir)) {
179
- const lockFiles = readdirSync(locksDir).filter((f) => f.endsWith(".json"));
180
- for (const lockFile of lockFiles) {
181
- const lockRelPath = `.agent/state/locks/${lockFile}`;
182
- const planId = `PLAN-${Date.now()}-${randomUUID().slice(0, 8)}`;
183
-
184
- plans.push({
185
- schema: "agentctl/action-plan-v1",
186
- id: planId,
187
- kind: "locks.prune-stale",
188
- title: `Remove stale lock ${lockFile}`,
189
- summary: `Prune inactive VFS lock file ${lockRelPath}`,
190
- risk: "low",
191
- repository: root,
192
- createdAt: new Date().toISOString(),
193
- preconditions: [],
194
- fileMutations: [
195
- {
196
- operation: "delete",
197
- path: lockRelPath,
198
- },
199
- ],
200
- commandEffects: [],
201
- stateTransitions: [],
202
- preview: {
203
- unifiedDiff: `--- a/${lockRelPath}\n+++ /dev/null\n`,
204
- warnings: [],
205
- estimatedImpact: [`Removes ${lockRelPath}`],
206
- },
207
- confirmation: {
208
- mode: "none",
209
- prompt: `Remove lock ${lockFile}?`,
210
- },
211
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
212
- });
213
- }
214
- }
215
- }
216
- }
217
- }
218
-
219
- return plans.map(finalizePlan);
220
- }
@@ -1,171 +0,0 @@
1
- import {
2
- readFileSync,
3
- mkdirSync,
4
- existsSync,
5
- openSync,
6
- writeSync,
7
- fsyncSync,
8
- closeSync,
9
- renameSync,
10
- unlinkSync,
11
- } from "node:fs";
12
- import { join, resolve, relative, isAbsolute } from "node:path";
13
- import { createHash, randomUUID } from "node:crypto";
14
- import { redactSecrets } from "../security.mjs";
15
-
16
- /**
17
- * @typedef {Object} OperationReceipt
18
- * @property {"agentctl/operation-receipt-v1"} schema
19
- * @property {string} receiptId
20
- * @property {string} actionPlanId
21
- * @property {string} kind
22
- * @property {string} title
23
- * @property {string} repository
24
- * @property {string} appliedAt
25
- * @property {"success" | "rolled-back" | "failed"} status
26
- * @property {number} durationMs
27
- * @property {Array<{ path: string, operation: string, hashBefore?: string, hashAfter?: string }>} mutationsApplied
28
- * @property {Array<{ executable: string, args: string[], exitCode: number }>} effectsExecuted
29
- * @property {string} receiptHash
30
- */
31
-
32
- /**
33
- * Compute SHA-256 hash of operation receipt metadata.
34
- * @param {Omit<OperationReceipt, "receiptHash">} receipt
35
- * @returns {string}
36
- */
37
- export function computeReceiptHash(receipt) {
38
- const payload = JSON.stringify({
39
- schema: receipt.schema,
40
- receiptId: receipt.receiptId,
41
- actionPlanId: receipt.actionPlanId,
42
- kind: receipt.kind,
43
- title: receipt.title,
44
- repository: receipt.repository,
45
- status: receipt.status,
46
- appliedAt: receipt.appliedAt,
47
- durationMs: receipt.durationMs,
48
- mutationsApplied: receipt.mutationsApplied,
49
- effectsExecuted: receipt.effectsExecuted,
50
- });
51
- return "sha256:" + createHash("sha256").update(payload).digest("hex");
52
- }
53
-
54
- function redactReceiptValue(value) {
55
- if (typeof value === "string") return redactSecrets(value);
56
- if (Array.isArray(value)) return value.map(redactReceiptValue);
57
- if (value && typeof value === "object") {
58
- return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, redactReceiptValue(child)]));
59
- }
60
- return value;
61
- }
62
-
63
- function assertReceiptId(receiptId) {
64
- if (typeof receiptId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(receiptId)) {
65
- throw new Error("Invalid operation receipt id");
66
- }
67
- }
68
-
69
- function writeFileAtomically(filePath, content) {
70
- const tmpPath = `${filePath}.${randomUUID()}.tmp`;
71
- let fd;
72
- try {
73
- fd = openSync(tmpPath, "wx", 0o600);
74
- writeSync(fd, content, "utf-8");
75
- fsyncSync(fd);
76
- closeSync(fd);
77
- fd = undefined;
78
- renameSync(tmpPath, filePath);
79
- } catch (err) {
80
- if (fd !== undefined) {
81
- try { closeSync(fd); } catch (_) {}
82
- }
83
- try { unlinkSync(tmpPath); } catch (_) {}
84
- throw err;
85
- }
86
- }
87
-
88
- /**
89
- * Create and persist an OperationReceipt atomically.
90
- * @param {string} root
91
- * @param {Omit<OperationReceipt, "schema" | "receiptId" | "appliedAt" | "receiptHash"> & { receiptId?: string }} data
92
- * @returns {OperationReceipt}
93
- */
94
- export function createOperationReceipt(root, data) {
95
- const receiptsDir = join(root, ".agent", "receipts");
96
- if (!existsSync(receiptsDir)) {
97
- mkdirSync(receiptsDir, { recursive: true });
98
- }
99
-
100
- const receiptId = data.receiptId || `REC-${Date.now()}-${randomUUID().slice(0, 8)}`;
101
- assertReceiptId(receiptId);
102
- const appliedAt = new Date().toISOString();
103
-
104
- const baseReceipt = redactReceiptValue({
105
- schema: /** @type {const} */ ("agentctl/operation-receipt-v1"),
106
- receiptId,
107
- actionPlanId: data.actionPlanId,
108
- kind: data.kind,
109
- title: data.title,
110
- repository: data.repository || "local",
111
- appliedAt,
112
- status: data.status,
113
- durationMs: data.durationMs || 0,
114
- mutationsApplied: data.mutationsApplied || [],
115
- effectsExecuted: data.effectsExecuted || [],
116
- });
117
-
118
- const receiptHash = computeReceiptHash(baseReceipt);
119
- /** @type {OperationReceipt} */
120
- const fullReceipt = {
121
- ...baseReceipt,
122
- receiptHash,
123
- };
124
-
125
- const targetPath = join(receiptsDir, `${receiptId}.json`);
126
- writeFileAtomically(targetPath, JSON.stringify(fullReceipt, null, 2));
127
-
128
- return fullReceipt;
129
- }
130
-
131
- /**
132
- * Load and validate an OperationReceipt from disk.
133
- * @param {string} root
134
- * @param {string} receiptIdOrPath
135
- * @returns {OperationReceipt}
136
- */
137
- export function loadOperationReceipt(root, receiptIdOrPath) {
138
- const receiptsDir = resolve(root, ".agent", "receipts");
139
- let targetPath;
140
- if (typeof receiptIdOrPath !== "string" || !receiptIdOrPath) {
141
- throw new Error("Operation receipt id or path is required");
142
- }
143
-
144
- if (!receiptIdOrPath.endsWith(".json")) {
145
- assertReceiptId(receiptIdOrPath);
146
- targetPath = join(receiptsDir, `${receiptIdOrPath}.json`);
147
- } else {
148
- targetPath = isAbsolute(receiptIdOrPath) ? resolve(receiptIdOrPath) : resolve(root, receiptIdOrPath);
149
- const rel = relative(receiptsDir, targetPath);
150
- if (rel.startsWith("..") || isAbsolute(rel)) {
151
- throw new Error("Operation receipt path escapes the receipts directory");
152
- }
153
- }
154
-
155
- if (!existsSync(targetPath)) {
156
- throw new Error(`Operation receipt not found: ${targetPath}`);
157
- }
158
-
159
- const content = readFileSync(targetPath, "utf-8");
160
- /** @type {OperationReceipt} */
161
- const receipt = JSON.parse(content);
162
-
163
- if (receipt.schema !== "agentctl/operation-receipt-v1") {
164
- throw new Error(`Invalid receipt schema version: ${receipt.schema}`);
165
- }
166
- if (receipt.receiptHash !== computeReceiptHash(receipt)) {
167
- throw new Error(`Invalid operation receipt hash: ${targetPath}`);
168
- }
169
-
170
- return receipt;
171
- }
@@ -1,156 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
-
3
- /**
4
- * @typedef {import("../ux/capabilities.mjs").ActionPlan} ActionPlan
5
- * @typedef {import("../ux/swarm-model.mjs").SwarmSlot} SwarmSlot
6
- */
7
-
8
- /**
9
- * Plan swarm concurrency action into an immutable ActionPlan.
10
- * @param {{ slots: SwarmSlot[], revision: string }} snapshot
11
- * @param {Object} intent
12
- * @param {string} intent.kind
13
- * @param {string} [intent.slotId]
14
- * @param {string} [intent.taskId]
15
- * @param {Record<string, any>} [intent.answers]
16
- * @returns {Promise<ActionPlan>}
17
- */
18
- export async function planSwarmAction(snapshot, intent) {
19
- const planId = `PLAN-SWARM-${Date.now()}-${randomUUID().slice(0, 8)}`;
20
-
21
- if (intent.kind === "pause-scheduler" || intent.kind === "resume-scheduler") {
22
- const isPause = intent.kind === "pause-scheduler";
23
- const schedulerRelPath = ".agent/state/swarm/scheduler.json";
24
- const newContent = JSON.stringify(
25
- {
26
- schema: "agentctl/swarm-scheduler-v1",
27
- paused: isPause,
28
- updatedAt: new Date().toISOString(),
29
- },
30
- null,
31
- 2
32
- );
33
-
34
- return {
35
- schema: "agentctl/action-plan-v1",
36
- id: planId,
37
- kind: intent.kind,
38
- title: `${isPause ? "Pause" : "Resume"} Swarm Scheduler`,
39
- summary: `${isPause ? "Halt dispatching new tasks to swarm slots" : "Resume task dispatching to available slots"}`,
40
- risk: "moderate",
41
- repository: "local",
42
- createdAt: new Date().toISOString(),
43
- preconditions: [],
44
- fileMutations: [
45
- {
46
- operation: "replace",
47
- path: schedulerRelPath,
48
- newContent,
49
- },
50
- ],
51
- commandEffects: [],
52
- stateTransitions: [],
53
- preview: {
54
- unifiedDiff: `--- a/${schedulerRelPath}\n+++ b/${schedulerRelPath}\n@@ -paused @@\n- paused: ${!isPause}\n+ paused: ${isPause}\n`,
55
- warnings: isPause ? ["Running tasks will complete, but no new tasks will be scheduled."] : [],
56
- estimatedImpact: [`Swarm scheduler state set to ${isPause ? "PAUSED" : "RUNNING"}`],
57
- },
58
- confirmation: {
59
- mode: "keypress",
60
- prompt: `Confirm ${isPause ? "pause" : "resume"} of swarm scheduler?`,
61
- },
62
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
63
- };
64
- }
65
-
66
- if (intent.kind === "cancel-slot-task") {
67
- const slotId = intent.slotId;
68
- const slot = (snapshot.slots || []).find((s) => s.id === slotId || s.taskId === intent.taskId);
69
- if (!slot) {
70
- throw new Error(`Target slot ${slotId || intent.taskId} not found.`);
71
- }
72
-
73
- const slotRelPath = `.agent/state/swarm/${slot.id}.json`;
74
- const newContent = JSON.stringify(
75
- {
76
- id: slot.id,
77
- state: "draining",
78
- taskId: slot.taskId,
79
- cancelledAt: new Date().toISOString(),
80
- },
81
- null,
82
- 2
83
- );
84
-
85
- return {
86
- schema: "agentctl/action-plan-v1",
87
- id: planId,
88
- kind: "swarm.cancel-slot-task",
89
- title: `Cancel active slot task ${slot.taskId || slot.id}`,
90
- summary: `Send cancellation signal to slot ${slot.id} (Task ${slot.taskId || "N/A"})`,
91
- risk: "high",
92
- repository: "local",
93
- createdAt: new Date().toISOString(),
94
- preconditions: [],
95
- fileMutations: [
96
- {
97
- operation: "replace",
98
- path: slotRelPath,
99
- newContent,
100
- },
101
- ],
102
- commandEffects: [],
103
- stateTransitions: [{ taskId: slot.taskId, from: slot.state, to: "cancelled" }],
104
- preview: {
105
- unifiedDiff: `--- a/${slotRelPath}\n+++ b/${slotRelPath}\n@@ -state @@\n- state: ${slot.state}\n+ state: draining\n`,
106
- warnings: ["Cancelling a running worker process will terminate its active attempt."],
107
- estimatedImpact: [`Cancels execution in slot ${slot.id}`],
108
- },
109
- confirmation: {
110
- mode: "typed-task-id",
111
- prompt: `Type '${slot.taskId || slot.id}' to confirm cancellation:`,
112
- expected: slot.taskId || slot.id,
113
- },
114
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
115
- };
116
- }
117
-
118
- if (intent.kind === "prune-stale-slots") {
119
- const staleSlots = (snapshot.slots || []).filter((s) => s.state === "stale");
120
- if (staleSlots.length === 0) {
121
- throw new Error("No stale slots found to prune.");
122
- }
123
-
124
- const fileMutations = staleSlots.map((s) => ({
125
- operation: /** @type {const} */ ("delete"),
126
- path: `.agent/state/swarm/${s.id}.json`,
127
- }));
128
-
129
- return {
130
- schema: "agentctl/action-plan-v1",
131
- id: planId,
132
- kind: "swarm.prune-stale-slots",
133
- title: `Prune ${staleSlots.length} stale swarm slot(s)`,
134
- summary: `Remove stale slot state files for dead worker process PIDs`,
135
- risk: "low",
136
- repository: "local",
137
- createdAt: new Date().toISOString(),
138
- preconditions: [],
139
- fileMutations,
140
- commandEffects: [],
141
- stateTransitions: [],
142
- preview: {
143
- unifiedDiff: staleSlots.map((s) => `--- a/.agent/state/swarm/${s.id}.json\n+++ /dev/null\n`).join("\n"),
144
- warnings: [],
145
- estimatedImpact: [`Prunes ${staleSlots.length} stale slot file(s)`],
146
- },
147
- confirmation: {
148
- mode: "keypress",
149
- prompt: `Prune ${staleSlots.length} stale slot(s)?`,
150
- },
151
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
152
- };
153
- }
154
-
155
- throw new Error(`Unsupported swarm action intent: ${intent.kind}`);
156
- }