jules-orchestrator-kit 0.29.0 → 0.31.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.
@@ -0,0 +1,442 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { execFileSync } from "node:child_process";
5
+
6
+ /**
7
+ * @typedef {"pass" | "warn" | "fail" | "skip" | "unknown"} DiagnosticStatus
8
+ * @typedef {"info" | "low" | "medium" | "high" | "critical"} DiagnosticSeverity
9
+ *
10
+ * @typedef {Object} DiagnosticEvidence
11
+ * @property {string} label
12
+ * @property {string | number | boolean} value
13
+ * @property {boolean} sensitive
14
+ *
15
+ * @typedef {Object} FixDescriptor
16
+ * @property {string} id
17
+ * @property {string} title
18
+ * @property {string} summary
19
+ * @property {"low" | "moderate" | "high" | "destructive"} risk
20
+ * @property {boolean} automatic
21
+ * @property {boolean} requiresProbe
22
+ *
23
+ * @typedef {Object} DiagnosticResult
24
+ * @property {"agentctl/diagnostic-result-v1"} schema
25
+ * @property {string} id
26
+ * @property {string} category
27
+ * @property {string} title
28
+ * @property {DiagnosticStatus} status
29
+ * @property {DiagnosticSeverity} severity
30
+ * @property {string} summary
31
+ * @property {number} durationMs
32
+ * @property {boolean} passive
33
+ * @property {DiagnosticEvidence[]} evidence
34
+ * @property {FixDescriptor[]} fixes
35
+ * @property {{ code: string, message: string }} [error]
36
+ *
37
+ * @typedef {Object} DoctorReport
38
+ * @property {"agentctl/doctor-report-v1"} schema
39
+ * @property {string} repository
40
+ * @property {string} [headSha]
41
+ * @property {string} generatedAt
42
+ * @property {boolean} activeProbe
43
+ * @property {Record<DiagnosticStatus, number>} summary
44
+ * @property {DiagnosticResult[]} results
45
+ * @property {string} reportHash
46
+ */
47
+
48
+ /**
49
+ * Compute SHA-256 report hash for DoctorReport.
50
+ * @param {Omit<DoctorReport, "reportHash">} report
51
+ * @returns {string}
52
+ */
53
+ export function computeReportHash(report) {
54
+ const payload = JSON.stringify({
55
+ schema: report.schema,
56
+ repository: report.repository,
57
+ generatedAt: report.generatedAt,
58
+ summary: report.summary,
59
+ results: report.results.map((r) => ({ id: r.id, status: r.status, summary: r.summary })),
60
+ });
61
+ return "sha256:" + createHash("sha256").update(payload).digest("hex");
62
+ }
63
+
64
+ /**
65
+ * Run diagnostic check suite across DAG.
66
+ * @param {Object} options
67
+ * @param {string} options.root
68
+ * @param {boolean} [options.activeProbe=false]
69
+ * @param {string[]} [options.selectedChecks]
70
+ * @returns {Promise<DoctorReport>}
71
+ */
72
+ export async function runDoctorChecks(options = {}) {
73
+ const root = resolve(options.root || process.cwd());
74
+ const activeProbe = Boolean(options.activeProbe);
75
+ const _startTime = Date.now();
76
+
77
+ /** @type {DiagnosticResult[]} */
78
+ const results = [];
79
+
80
+ // Helper to append result
81
+ const addResult = (res) => {
82
+ results.push({
83
+ schema: "agentctl/diagnostic-result-v1",
84
+ durationMs: 0,
85
+ passive: !activeProbe,
86
+ evidence: [],
87
+ fixes: [],
88
+ ...res,
89
+ });
90
+ };
91
+
92
+ // 1. System Checks
93
+ // runtime.node
94
+ const nodeVersion = process.version;
95
+ const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
96
+ if (nodeMajor >= 20) {
97
+ addResult({
98
+ id: "runtime.node",
99
+ category: "System",
100
+ title: "Node.js Engine Version",
101
+ status: "pass",
102
+ severity: "info",
103
+ summary: `Node.js ${nodeVersion} satisfies runtime requirements (v20+)`,
104
+ evidence: [{ label: "nodeVersion", value: nodeVersion, sensitive: false }],
105
+ });
106
+ } else {
107
+ addResult({
108
+ id: "runtime.node",
109
+ category: "System",
110
+ title: "Node.js Engine Version",
111
+ status: "fail",
112
+ severity: "critical",
113
+ summary: `Node.js ${nodeVersion} is below required minimum v20.0.0`,
114
+ evidence: [{ label: "nodeVersion", value: nodeVersion, sensitive: false }],
115
+ });
116
+ }
117
+
118
+ // runtime.git
119
+ let gitPassed = false;
120
+ try {
121
+ const gitOut = execFileSync("git", ["--version"], { encoding: "utf-8", timeout: 5000 });
122
+ gitPassed = true;
123
+ addResult({
124
+ id: "runtime.git",
125
+ category: "System",
126
+ title: "Git Command Available",
127
+ status: "pass",
128
+ severity: "info",
129
+ summary: gitOut.trim(),
130
+ evidence: [{ label: "gitVersion", value: gitOut.trim(), sensitive: false }],
131
+ });
132
+ } catch (err) {
133
+ addResult({
134
+ id: "runtime.git",
135
+ category: "System",
136
+ title: "Git Command Available",
137
+ status: "fail",
138
+ severity: "critical",
139
+ summary: "Git executable not found or failed to run",
140
+ error: { code: "ENOENT", message: err.message },
141
+ });
142
+ }
143
+
144
+ // 2. Repository Checks
145
+
146
+ if (!gitPassed) {
147
+ addResult({
148
+ id: "repo.root",
149
+ category: "Repository",
150
+ title: "Repository Root Verification",
151
+ status: "skip",
152
+ severity: "info",
153
+ summary: "Skipped due to failed runtime.git check",
154
+ });
155
+ addResult({
156
+ id: "repo.dirty",
157
+ category: "Repository",
158
+ title: "Working Directory Cleanliness",
159
+ status: "skip",
160
+ severity: "info",
161
+ summary: "Skipped due to failed runtime.git check",
162
+ });
163
+ } else {
164
+ let isRepo = false;
165
+ let headSha = "";
166
+ try {
167
+ headSha = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf-8" }).trim();
168
+ isRepo = true;
169
+ addResult({
170
+ id: "repo.root",
171
+ category: "Repository",
172
+ title: "Repository Root Verification",
173
+ status: "pass",
174
+ severity: "info",
175
+ summary: `Valid repository at ${root} (HEAD: ${headSha.slice(0, 8)})`,
176
+ evidence: [{ label: "headSha", value: headSha, sensitive: false }],
177
+ });
178
+ } catch {
179
+ addResult({
180
+ id: "repo.root",
181
+ category: "Repository",
182
+ title: "Repository Root Verification",
183
+ status: "fail",
184
+ severity: "high",
185
+ summary: `Directory ${root} is not a valid Git repository`,
186
+ });
187
+ }
188
+
189
+ if (isRepo) {
190
+ try {
191
+ const statusOut = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf-8" }).trim();
192
+ const modifiedCount = statusOut ? statusOut.split("\n").length : 0;
193
+ if (modifiedCount === 0) {
194
+ addResult({
195
+ id: "repo.dirty",
196
+ category: "Repository",
197
+ title: "Working Directory Cleanliness",
198
+ status: "pass",
199
+ severity: "info",
200
+ summary: "Working tree is clean",
201
+ evidence: [{ label: "uncommittedFiles", value: 0, sensitive: false }],
202
+ });
203
+ } else {
204
+ addResult({
205
+ id: "repo.dirty",
206
+ category: "Repository",
207
+ title: "Working Directory Cleanliness",
208
+ status: "warn",
209
+ severity: "low",
210
+ summary: `Working tree has ${modifiedCount} uncommitted changes`,
211
+ evidence: [{ label: "uncommittedFiles", value: modifiedCount, sensitive: false }],
212
+ });
213
+ }
214
+ } catch {
215
+ addResult({
216
+ id: "repo.dirty",
217
+ category: "Repository",
218
+ title: "Working Directory Cleanliness",
219
+ status: "warn",
220
+ severity: "low",
221
+ summary: "Could not query git status",
222
+ });
223
+ }
224
+ }
225
+ }
226
+
227
+ // 3. Config Checks
228
+ const configCandidates = [
229
+ [".agent/config.yml", join(root, ".agent", "config.yml")],
230
+ [".agent/jules.yml", join(root, ".agent", "jules.yml")],
231
+ ];
232
+ const existingConfig = configCandidates.find(([, path]) => existsSync(path));
233
+ if (existingConfig) {
234
+ addResult({
235
+ id: "config.present",
236
+ category: "Config",
237
+ title: "Agent Configuration File",
238
+ status: "pass",
239
+ severity: "info",
240
+ summary: `${existingConfig[0]} exists`,
241
+ evidence: [{ label: "configPath", value: existingConfig[0], sensitive: false }],
242
+ });
243
+ } else {
244
+ addResult({
245
+ id: "config.present",
246
+ category: "Config",
247
+ title: "Agent Configuration File",
248
+ status: "warn",
249
+ severity: "medium",
250
+ summary: "Neither .agent/config.yml nor .agent/jules.yml exists",
251
+ fixes: [
252
+ {
253
+ id: "config.create-default",
254
+ title: "Create default config",
255
+ summary: "Initialize standard .agent/config.yml manifest",
256
+ risk: "low",
257
+ automatic: true,
258
+ requiresProbe: false,
259
+ },
260
+ ],
261
+ });
262
+ }
263
+
264
+ // 4. Verification Oracle Checks
265
+ const pkgPath = join(root, "package.json");
266
+ if (existsSync(pkgPath)) {
267
+ try {
268
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
269
+ const testScript = pkg.scripts && pkg.scripts.test;
270
+ if (testScript) {
271
+ addResult({
272
+ id: "oracle.test",
273
+ category: "Verification",
274
+ title: "Test Oracle Configuration",
275
+ status: "pass",
276
+ severity: "info",
277
+ summary: `Test oracle script found: "npm test" -> "${testScript}"`,
278
+ evidence: [{ label: "testScript", value: testScript, sensitive: false }],
279
+ });
280
+ } else {
281
+ addResult({
282
+ id: "oracle.test",
283
+ category: "Verification",
284
+ title: "Test Oracle Configuration",
285
+ status: "warn",
286
+ severity: "medium",
287
+ summary: "package.json missing test script entry",
288
+ });
289
+ }
290
+ } catch {
291
+ addResult({
292
+ id: "oracle.test",
293
+ category: "Verification",
294
+ title: "Test Oracle Configuration",
295
+ status: "warn",
296
+ severity: "medium",
297
+ summary: "Malformed package.json file",
298
+ });
299
+ }
300
+ } else {
301
+ addResult({
302
+ id: "oracle.test",
303
+ category: "Verification",
304
+ title: "Test Oracle Configuration",
305
+ status: "pass",
306
+ severity: "info",
307
+ summary: "Non-Node workspace verified",
308
+ });
309
+ }
310
+
311
+ // 5. State & Telemetry Checks
312
+ const telemetryDate = new Date().toISOString().split("T")[0];
313
+ const telemetryHeadPath = join(root, ".agent", "state", `telemetry-${telemetryDate}.head`);
314
+ if (existsSync(telemetryHeadPath)) {
315
+ addResult({
316
+ id: "state.telemetry",
317
+ category: "State",
318
+ title: "Telemetry Spine Head Integrity",
319
+ status: "pass",
320
+ severity: "info",
321
+ summary: "Telemetry .head file present"
322
+ });
323
+ } else {
324
+ addResult({
325
+ id: "state.telemetry",
326
+ category: "State",
327
+ title: "Telemetry Spine Head Integrity",
328
+ status: "warn",
329
+ severity: "low",
330
+ summary: "Telemetry .head file missing; can be rebuilt from tail log",
331
+ fixes: [
332
+ {
333
+ id: "state.rebuild-head",
334
+ title: "Rebuild telemetry head",
335
+ summary: "Recompute .head SHA-256 pointer from tail log",
336
+ risk: "low",
337
+ automatic: true,
338
+ requiresProbe: false,
339
+ },
340
+ ],
341
+ });
342
+ }
343
+
344
+ // 6. VFS Locks Check
345
+ const locksDir = join(root, ".agent", "state", "locks");
346
+ let activeLockCount = 0;
347
+ let lockInspectionFailed = false;
348
+ try {
349
+ if (existsSync(locksDir)) {
350
+ const lockFiles = readdirSync(locksDir).filter((f) => f.endsWith(".json"));
351
+ activeLockCount = lockFiles.length;
352
+ }
353
+ } catch (_) {
354
+ lockInspectionFailed = true;
355
+ addResult({
356
+ id: "locks.active",
357
+ category: "State",
358
+ title: "VFS Active Locks",
359
+ status: "unknown",
360
+ severity: "medium",
361
+ summary: "Could not inspect VFS lock directory",
362
+ });
363
+ }
364
+ if (!lockInspectionFailed && activeLockCount === 0) {
365
+ addResult({
366
+ id: "locks.active",
367
+ category: "State",
368
+ title: "VFS Active Locks",
369
+ status: "pass",
370
+ severity: "info",
371
+ summary: "No active VFS locks present",
372
+ });
373
+ } else if (!lockInspectionFailed) {
374
+ addResult({
375
+ id: "locks.active",
376
+ category: "State",
377
+ title: "VFS Active Locks",
378
+ status: "warn",
379
+ severity: "medium",
380
+ summary: `${activeLockCount} active VFS lock(s) held`,
381
+ evidence: [{ label: "lockCount", value: activeLockCount, sensitive: false }],
382
+ fixes: [
383
+ {
384
+ id: "locks.prune-stale",
385
+ title: "Prune stale locks",
386
+ summary: "Remove locks owned by dead processes",
387
+ risk: "low",
388
+ automatic: true,
389
+ requiresProbe: false,
390
+ },
391
+ ],
392
+ });
393
+ }
394
+
395
+ // 7. Jules Provider Key Check
396
+ const hasApiKey = Boolean(process.env.JULES_API_KEY || process.env.GEMINI_API_KEY);
397
+ if (hasApiKey) {
398
+ addResult({
399
+ id: "provider.key",
400
+ category: "Jules",
401
+ title: "Jules Provider API Key",
402
+ status: "pass",
403
+ severity: "info",
404
+ summary: "API key environment variable detected",
405
+ evidence: [{ label: "keyConfigured", value: true, sensitive: false }],
406
+ });
407
+ } else {
408
+ addResult({
409
+ id: "provider.key",
410
+ category: "Jules",
411
+ title: "Jules Provider API Key",
412
+ status: "warn",
413
+ severity: "high",
414
+ summary: "Neither JULES_API_KEY nor GEMINI_API_KEY environment variable is set",
415
+ evidence: [{ label: "keyConfigured", value: false, sensitive: false }],
416
+ });
417
+ }
418
+
419
+ // Summarize count by status
420
+ const summary = { pass: 0, warn: 0, fail: 0, skip: 0, unknown: 0 };
421
+ for (const r of results) {
422
+ summary[r.status] = (summary[r.status] || 0) + 1;
423
+ }
424
+
425
+ const baseReport = {
426
+ schema: /** @type {const} */ ("agentctl/doctor-report-v1"),
427
+ repository: root,
428
+ generatedAt: new Date().toISOString(),
429
+ activeProbe,
430
+ summary,
431
+ results,
432
+ };
433
+
434
+ const reportHash = computeReportHash(baseReport);
435
+ /** @type {DoctorReport} */
436
+ const report = {
437
+ ...baseReport,
438
+ reportHash,
439
+ };
440
+
441
+ return report;
442
+ }
@@ -0,0 +1,139 @@
1
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { resolveRoot } from "../config.mjs";
4
+
5
+ export class IdeScaffoldError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "IdeScaffoldError";
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Scaffolds IDE integration configuration files for Cursor, VS Code, and Claude Desktop.
14
+ * @param {string} target - 'cursor' | 'vscode' | 'claude' | 'all'
15
+ * @param {object} [options]
16
+ * @returns {object} Summary of scaffolded files
17
+ */
18
+ export function scaffoldIdeConfig(target = "all", options = {}) {
19
+ const root = options.root || resolveRoot();
20
+ const validTargets = new Set(["cursor", "vscode", "claude", "all"]);
21
+
22
+ const normTarget = (target || "all").toLowerCase();
23
+ if (!validTargets.has(normTarget)) {
24
+ throw new IdeScaffoldError(`Invalid target '${target}'. Allowed targets: cursor, vscode, claude, all`);
25
+ }
26
+
27
+ const results = [];
28
+
29
+ // Target: Cursor (.cursor/mcp.json)
30
+ if (normTarget === "cursor" || normTarget === "all") {
31
+ const cursorDir = join(root, ".cursor");
32
+ try {
33
+ mkdirSync(cursorDir, { recursive: true });
34
+ } catch (_) {}
35
+
36
+ const cursorConfigPath = join(cursorDir, "mcp.json");
37
+ let existingConfig = {};
38
+ if (existsSync(cursorConfigPath)) {
39
+ try {
40
+ existingConfig = JSON.parse(readFileSync(cursorConfigPath, "utf-8"));
41
+ } catch (_) {}
42
+ }
43
+
44
+ const updatedCursorConfig = {
45
+ ...existingConfig,
46
+ mcpServers: {
47
+ ...(existingConfig.mcpServers || {}),
48
+ "jules-orchestrator-kit": {
49
+ command: "npx",
50
+ args: ["-y", "jules-orchestrator-kit@latest", "mcp"],
51
+ },
52
+ },
53
+ };
54
+
55
+ writeFileSync(cursorConfigPath, JSON.stringify(updatedCursorConfig, null, 2), "utf-8");
56
+ results.push({ target: "cursor", file: ".cursor/mcp.json", ok: true });
57
+ }
58
+
59
+ // Target: VS Code (.vscode/tasks.json)
60
+ if (normTarget === "vscode" || normTarget === "all") {
61
+ const vscodeDir = join(root, ".vscode");
62
+ try {
63
+ mkdirSync(vscodeDir, { recursive: true });
64
+ } catch (_) {}
65
+
66
+ const vscodeTasksPath = join(vscodeDir, "tasks.json");
67
+ let existingConfig = { version: "2.0.0", tasks: [] };
68
+ if (existsSync(vscodeTasksPath)) {
69
+ try {
70
+ existingConfig = JSON.parse(readFileSync(vscodeTasksPath, "utf-8"));
71
+ } catch (_) {}
72
+ }
73
+
74
+ const defaultTasks = [
75
+ {
76
+ label: "Jules: Run System Doctor",
77
+ type: "shell",
78
+ command: "npx agentctl doctor",
79
+ group: "test",
80
+ problemMatcher: [],
81
+ },
82
+ {
83
+ label: "Jules: Create Task Envelope",
84
+ type: "shell",
85
+ command: "npx agentctl task create",
86
+ group: "build",
87
+ problemMatcher: [],
88
+ },
89
+ {
90
+ label: "Jules: 1-Click Rollback Checkpoint",
91
+ type: "shell",
92
+ command: "npx agentctl rollback --latest",
93
+ group: "none",
94
+ problemMatcher: [],
95
+ },
96
+ ];
97
+
98
+ const currentTasks = existingConfig.tasks || [];
99
+ const updatedTasks = [...currentTasks];
100
+
101
+ for (const newTask of defaultTasks) {
102
+ if (!updatedTasks.some((t) => t.label === newTask.label)) {
103
+ updatedTasks.push(newTask);
104
+ }
105
+ }
106
+
107
+ const updatedVsCodeConfig = {
108
+ ...existingConfig,
109
+ version: existingConfig.version || "2.0.0",
110
+ tasks: updatedTasks,
111
+ };
112
+
113
+ writeFileSync(vscodeTasksPath, JSON.stringify(updatedVsCodeConfig, null, 2), "utf-8");
114
+ results.push({ target: "vscode", file: ".vscode/tasks.json", ok: true });
115
+ }
116
+
117
+ // Target: Claude Desktop (claude_desktop_config.json snippet helper)
118
+ if (normTarget === "claude" || normTarget === "all") {
119
+ const agentDir = join(root, ".agent");
120
+ try {
121
+ mkdirSync(agentDir, { recursive: true });
122
+ } catch (_) {}
123
+
124
+ const snippetPath = join(agentDir, "claude_desktop_config.snippet.json");
125
+ const claudeSnippet = {
126
+ mcpServers: {
127
+ "jules-orchestrator-kit": {
128
+ command: "npx",
129
+ args: ["-y", "jules-orchestrator-kit@latest", "mcp"],
130
+ },
131
+ },
132
+ };
133
+
134
+ writeFileSync(snippetPath, JSON.stringify(claudeSnippet, null, 2), "utf-8");
135
+ results.push({ target: "claude", file: ".agent/claude_desktop_config.snippet.json", ok: true });
136
+ }
137
+
138
+ return { ok: true, target: normTarget, results };
139
+ }