jules-orchestrator-kit 0.51.0 → 0.52.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/README.md +4 -1
- package/bin/agentctl.mjs +144 -0
- package/index.mjs +10 -0
- package/package.json +1 -1
- package/src/mcp.mjs +277 -0
- package/src/provider.mjs +134 -5
- package/src/session-ops.mjs +312 -0
package/README.md
CHANGED
|
@@ -143,7 +143,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
143
143
|
* **Fail-Closed Security & Secret Redaction:** Evaluates explicit Deny rules before Allow rules against canonicalized, case-folded paths. Redacts high-entropy keys and base64-encoded credentials (such as Kubernetes `Secret` manifests).
|
|
144
144
|
* **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors, with a `node --check` syntax-verification gate that transparently escalates a FAST-tier result to the primary provider if it left broken JS on disk.
|
|
145
145
|
* **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
|
|
146
|
-
* **Verified Test Suite:** Tested with **
|
|
146
|
+
* **Verified Test Suite:** Tested with **797 unit tests across 91 suites passing in < 10.0s**.
|
|
147
147
|
|
|
148
148
|
<br/>
|
|
149
149
|
|
|
@@ -165,6 +165,9 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
165
165
|
| `dispatch` | `agentctl dispatch [<prompt>] [-p <prompt>] [-f <file>] [-r <role>] [-t <tier>] [--author <name>] [--check-premise] [--auto-pr] [--repoless] [--dry-run]` | Dispatches autonomous task to the active provider with pre-flight idempotency checks, payload limits, and role prompt resolution. `--dry-run` stops short of the provider call and reports itself as a rehearsal rather than a dispatch. | `0` (Dispatched), `1` (Error) |
|
|
166
166
|
| `plan approve` | `agentctl plan approve <sessionId> [--dry-run] [--json]` | Approves pending execution plan for an active Jules session (`:approvePlan`) with automatic 404/503 retry backoff. | `0` (Approved), `1` (Error) |
|
|
167
167
|
| `session get` | `agentctl session get <sessionId> [--dry-run] [--json]` | Retrieves live session lifecycle state from provider REST API with token rotation. | `0` (Fetched), `1` (Error) |
|
|
168
|
+
| `patch` | `agentctl patch <sessionId> [--apply] [--save <path>] [--json]` | Extracts raw git diff patch from a completed Jules session and tests or applies it locally with `git apply --check` safety. | `0` (Clean/Applied), `1` (Conflict/Error) |
|
|
169
|
+
| `retry` | `agentctl retry <sessionId> [--role <role>] [--with-failure] [--json]` | Fetches error traces and activity logs from a failed session and synthesizes a targeted OODA retry dispatch. | `0` (Dispatched), `1` (Error) |
|
|
170
|
+
| `prune` | `agentctl prune [--age 7d] [--state <state>] [--delete] [--yes] [--json]` | Queries and batch-archives or deletes stale/completed sessions via Jules v1alpha API to keep workspaces clean. | `0` (Cleaned) |
|
|
168
171
|
| `pr harvest` | `agentctl pr harvest [--tier r0,r1] [--limit <n>] [--auto] [--allow-no-checks] [--dry-run]` | Discovers open agent PRs, evaluates CI checks & risk tiers, and auto-squashes green low-risk changes autonomously. A PR reporting **no** CI checks is skipped unless `--allow-no-checks` is passed, and an unavailable changed-file list blocks rather than classifying as low risk. | `0` (Triaged/Merged), `1` (Error) |
|
|
169
172
|
| `doctor` | `agentctl doctor [--json]` | Diagnostic DAG check runner & automated transactional self-repair engine. | `0` (Healthy), `1` (Failures) |
|
|
170
173
|
| `queue` | `agentctl queue [--dag] [--concurrency <n>] [--dry-run] [--json]` | Consumes and executes task envelopes in `.agent/jules-queue/` with Kahn's DAG dependency resolution. Non-task files (manifests, `README.md`) are skipped, and `--dry-run` previews without moving anything. | `0` (Complete) |
|
package/bin/agentctl.mjs
CHANGED
|
@@ -69,6 +69,9 @@ Commands:
|
|
|
69
69
|
resume Resume warm session with human response (--response "<text>")
|
|
70
70
|
plan approve <id> Approve pending plan for a Jules session (:approvePlan)
|
|
71
71
|
session get <id> Retrieve remote session status from provider API
|
|
72
|
+
patch <id> Extract and test/apply git patch from a Jules session (--apply, --save)
|
|
73
|
+
retry <id> Retry failed session with automated failure-trace injection (--role)
|
|
74
|
+
prune Batch-archive or delete stale sessions via Jules API (--age 7d, --yes)
|
|
72
75
|
pr harvest Triage, verify CI, and auto-merge low-risk agent PRs (--auto, --tier r0,r1)
|
|
73
76
|
escalate Dispatch or manage webhook escalation incidents (--flush, --status, --clear)
|
|
74
77
|
flaky Manage Wilson-quarantined tests and dispatch healing swarm (status | heal | reset)
|
|
@@ -720,6 +723,147 @@ async function main() {
|
|
|
720
723
|
break;
|
|
721
724
|
}
|
|
722
725
|
|
|
726
|
+
case "patch": {
|
|
727
|
+
const { applySessionPatch, resolveRoot } = await import("../index.mjs");
|
|
728
|
+
const { values, positionals } = parseArgs({
|
|
729
|
+
args: args.slice(1),
|
|
730
|
+
options: {
|
|
731
|
+
session: { type: "string", short: "s" },
|
|
732
|
+
apply: { type: "boolean", short: "a" },
|
|
733
|
+
save: { type: "string" },
|
|
734
|
+
json: { type: "boolean", short: "j" },
|
|
735
|
+
},
|
|
736
|
+
allowPositionals: true,
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const sessionId = values.session || positionals[0] || positionals[1];
|
|
740
|
+
if (!sessionId) {
|
|
741
|
+
console.error("❌ Error: Missing required session ID. Usage: agentctl patch <session_id> [--apply] [--save <path>]");
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const root = resolveRoot();
|
|
746
|
+
const res = await applySessionPatch(sessionId, {
|
|
747
|
+
root,
|
|
748
|
+
apply: Boolean(values.apply),
|
|
749
|
+
save: values.save,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
if (values.json) {
|
|
753
|
+
console.log(JSON.stringify(res, null, 2));
|
|
754
|
+
} else if (!res.ok) {
|
|
755
|
+
console.error(`\n❌ Failed to extract or apply patch for session ${sessionId}:`);
|
|
756
|
+
console.error(` ${res.error || "Unknown error"}\n`);
|
|
757
|
+
} else {
|
|
758
|
+
console.log(`\n📦 Session Git Patch (${sessionId})`);
|
|
759
|
+
console.log(`------------------------------------------------------------------`);
|
|
760
|
+
console.log(` Modified Files : ${res.files && res.files.length > 0 ? res.files.join(", ") : "None"}`);
|
|
761
|
+
console.log(` git apply --check : ${res.checkPassed ? "✅ CLEAN PASS" : "❌ CONFLICT"}`);
|
|
762
|
+
console.log(` Applied to Disk : ${res.patchApplied ? "✅ APPLIED" : "DRY-RUN ONLY (--apply to write)"}`);
|
|
763
|
+
if (values.save) {
|
|
764
|
+
console.log(` Saved to File : ${values.save}`);
|
|
765
|
+
}
|
|
766
|
+
if (!values.apply && !values.save && res.patch) {
|
|
767
|
+
console.log(`\n--- Patch Content ---\n${res.patch}\n---------------------\n`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
process.exit(res.ok ? 0 : 1);
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
case "retry": {
|
|
776
|
+
const { retrySession, resolveRoot } = await import("../index.mjs");
|
|
777
|
+
const { values, positionals } = parseArgs({
|
|
778
|
+
args: args.slice(1),
|
|
779
|
+
options: {
|
|
780
|
+
session: { type: "string", short: "s" },
|
|
781
|
+
role: { type: "string", short: "r" },
|
|
782
|
+
title: { type: "string", short: "t" },
|
|
783
|
+
"without-failure": { type: "boolean" },
|
|
784
|
+
"dry-run": { type: "boolean", short: "d" },
|
|
785
|
+
json: { type: "boolean", short: "j" },
|
|
786
|
+
},
|
|
787
|
+
allowPositionals: true,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const sessionId = values.session || positionals[0] || positionals[1];
|
|
791
|
+
if (!sessionId) {
|
|
792
|
+
console.error("❌ Error: Missing required session ID. Usage: agentctl retry <session_id> [--role <role>]");
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const root = resolveRoot();
|
|
797
|
+
const res = await retrySession(sessionId, {
|
|
798
|
+
root,
|
|
799
|
+
role: values.role,
|
|
800
|
+
title: values.title,
|
|
801
|
+
withFailure: values["without-failure"] ? false : true,
|
|
802
|
+
dryRun: Boolean(values["dry-run"]),
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
if (values.json) {
|
|
806
|
+
console.log(JSON.stringify(res, null, 2));
|
|
807
|
+
} else {
|
|
808
|
+
console.log(`\n🔄 Session Retry Dispatched!`);
|
|
809
|
+
console.log(`------------------------------------------------------------------`);
|
|
810
|
+
console.log(` Original Session : ${res.originalSessionId}`);
|
|
811
|
+
console.log(` New Session ID : ${res.newSession?.id || "N/A"}`);
|
|
812
|
+
console.log(` Failure Trace Added : ${res.failureReason ? "YES" : "NO"}`);
|
|
813
|
+
console.log(`------------------------------------------------------------------\n`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
process.exit(res.ok ? 0 : 1);
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
case "prune": {
|
|
821
|
+
const { pruneSessions, resolveRoot } = await import("../index.mjs");
|
|
822
|
+
const { values } = parseArgs({
|
|
823
|
+
args: args.slice(1),
|
|
824
|
+
options: {
|
|
825
|
+
age: { type: "string", short: "a" },
|
|
826
|
+
state: { type: "string", short: "s" },
|
|
827
|
+
delete: { type: "boolean" },
|
|
828
|
+
"dry-run": { type: "boolean", short: "d" },
|
|
829
|
+
yes: { type: "boolean", short: "y" },
|
|
830
|
+
json: { type: "boolean", short: "j" },
|
|
831
|
+
},
|
|
832
|
+
allowPositionals: true,
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
const root = resolveRoot();
|
|
836
|
+
const dryRun = values["dry-run"] || (!values.yes && !values.delete);
|
|
837
|
+
const res = await pruneSessions({
|
|
838
|
+
root,
|
|
839
|
+
age: values.age || "7d",
|
|
840
|
+
state: values.state,
|
|
841
|
+
delete: Boolean(values.delete),
|
|
842
|
+
dryRun,
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
if (values.json) {
|
|
846
|
+
console.log(JSON.stringify(res, null, 2));
|
|
847
|
+
} else {
|
|
848
|
+
console.log(`\n🧹 Session Pruning Report (${dryRun ? "DRY RUN" : "EXECUTED"})`);
|
|
849
|
+
console.log(`------------------------------------------------------------------`);
|
|
850
|
+
console.log(` Filter Age Cutoff : ${values.age || "7d"}`);
|
|
851
|
+
console.log(` Filter Target State : ${values.state || "ALL"}`);
|
|
852
|
+
console.log(` Matched Sessions : ${res.matchedCount}`);
|
|
853
|
+
console.log(` Archived / Deleted : ${res.archivedCount}`);
|
|
854
|
+
console.log(`------------------------------------------------------------------`);
|
|
855
|
+
for (const s of res.sessions) {
|
|
856
|
+
console.log(` - [${s.action}] ${s.id} (State: ${s.state || "UNKNOWN"})`);
|
|
857
|
+
}
|
|
858
|
+
if (dryRun && res.matchedCount > 0) {
|
|
859
|
+
console.log(`\n💡 Re-run with --yes to archive matched sessions.\n`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
process.exit(0);
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
|
|
723
867
|
case "assert": {
|
|
724
868
|
const { runAssertion, parseYaml } = await import("../index.mjs");
|
|
725
869
|
const { values } = parseArgs({
|
package/index.mjs
CHANGED
|
@@ -229,3 +229,13 @@ export {
|
|
|
229
229
|
getHandoverDir,
|
|
230
230
|
HandoverError,
|
|
231
231
|
} from "./src/ops/handover.mjs";
|
|
232
|
+
|
|
233
|
+
// Jules Power-User Session Operations & Lifecycle Engine
|
|
234
|
+
export {
|
|
235
|
+
extractSessionPatch,
|
|
236
|
+
applySessionPatch,
|
|
237
|
+
retrySession,
|
|
238
|
+
pruneSessions,
|
|
239
|
+
parseAgeDuration,
|
|
240
|
+
} from "./src/session-ops.mjs";
|
|
241
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for Google Jules (jules) autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/src/mcp.mjs
CHANGED
|
@@ -8,6 +8,13 @@ import { reapOrphanedIntents, reapStaleMutexDirs } from "./journal.mjs";
|
|
|
8
8
|
import { readTelemetry } from "./telemetry.mjs";
|
|
9
9
|
import { ProgressBus } from "./mcp-progress.mjs";
|
|
10
10
|
import { KIT_VERSION } from "./version.mjs";
|
|
11
|
+
import { createProvider } from "./provider.mjs";
|
|
12
|
+
import {
|
|
13
|
+
extractSessionPatch,
|
|
14
|
+
applySessionPatch,
|
|
15
|
+
retrySession,
|
|
16
|
+
pruneSessions,
|
|
17
|
+
} from "./session-ops.mjs";
|
|
11
18
|
|
|
12
19
|
export const MCP_SERVER_INFO = {
|
|
13
20
|
name: "jules-orchestrator-kit",
|
|
@@ -196,6 +203,113 @@ export const MCP_TOOLS = [
|
|
|
196
203
|
required: ["trigger", "solution"],
|
|
197
204
|
},
|
|
198
205
|
},
|
|
206
|
+
{
|
|
207
|
+
name: "jules_list_sessions",
|
|
208
|
+
description: "List remote Jules coding sessions with optional pagination and state filtering.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
pageSize: { type: "number", description: "Maximum number of sessions to return (default 50)" },
|
|
213
|
+
pageToken: { type: "string", description: "Page token for cursor pagination" },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "jules_list_activities",
|
|
219
|
+
description: "List execution activities, logs, and error outputs for a specific Jules session.",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
sessionId: { type: "string", description: "Jules session ID" },
|
|
224
|
+
pageSize: { type: "number", description: "Maximum number of activities to return" },
|
|
225
|
+
pageToken: { type: "string", description: "Page token for pagination" },
|
|
226
|
+
},
|
|
227
|
+
required: ["sessionId"],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "jules_get_session_output",
|
|
232
|
+
description: "Extract the git diff patch, pull request metadata, and changed files from a Jules session.",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
sessionId: { type: "string", description: "Jules session ID" },
|
|
237
|
+
},
|
|
238
|
+
required: ["sessionId"],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "jules_archive_session",
|
|
243
|
+
description: "Archive a Jules coding session to clean up the dashboard.",
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: "object",
|
|
246
|
+
properties: {
|
|
247
|
+
sessionId: { type: "string", description: "Jules session ID to archive" },
|
|
248
|
+
},
|
|
249
|
+
required: ["sessionId"],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "jules_delete_session",
|
|
254
|
+
description: "Permanently delete a Jules coding session.",
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
sessionId: { type: "string", description: "Jules session ID to delete" },
|
|
259
|
+
},
|
|
260
|
+
required: ["sessionId"],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "jules_retry_session",
|
|
265
|
+
description: "Retry a failed Jules session by extracting failure diagnostics and dispatching a new OODA repair session.",
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {
|
|
269
|
+
sessionId: { type: "string", description: "Original failed Jules session ID" },
|
|
270
|
+
role: { type: "string", description: "Optional specialist role override" },
|
|
271
|
+
withFailure: { type: "boolean", description: "Include previous failure diagnostic in retry prompt (default true)" },
|
|
272
|
+
},
|
|
273
|
+
required: ["sessionId"],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: "jules_apply_patch",
|
|
278
|
+
description: "Extract and apply a Jules session's git patch to the local workspace with git apply safety verification.",
|
|
279
|
+
inputSchema: {
|
|
280
|
+
type: "object",
|
|
281
|
+
properties: {
|
|
282
|
+
sessionId: { type: "string", description: "Jules session ID to extract patch from" },
|
|
283
|
+
apply: { type: "boolean", description: "If true, applies the patch; if false, runs git apply --check dry-run" },
|
|
284
|
+
save: { type: "string", description: "Optional file path to save raw patch diff to" },
|
|
285
|
+
},
|
|
286
|
+
required: ["sessionId"],
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "jules_list_sources",
|
|
291
|
+
description: "List connected GitHub repository sources available to the Jules account.",
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: "object",
|
|
294
|
+
properties: {
|
|
295
|
+
pageSize: { type: "number", description: "Maximum number of sources to return" },
|
|
296
|
+
pageToken: { type: "string", description: "Page token for pagination" },
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "jules_prune_sessions",
|
|
302
|
+
description: "Batch-archive or prune old/completed Jules sessions matching age and state filters.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
age: { type: "string", description: "Age cutoff string (e.g. '7d', '24h', '30m')" },
|
|
307
|
+
state: { type: "string", description: "Target session state to filter (e.g. 'FAILED', 'COMPLETED')" },
|
|
308
|
+
dryRun: { type: "boolean", description: "If true, previews sessions without modifying (default true)" },
|
|
309
|
+
delete: { type: "boolean", description: "If true, permanently deletes instead of archiving" },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
199
313
|
];
|
|
200
314
|
|
|
201
315
|
export async function handleMcpRequest(request, opts = {}) {
|
|
@@ -431,6 +545,169 @@ export async function handleMcpRequest(request, opts = {}) {
|
|
|
431
545
|
};
|
|
432
546
|
}
|
|
433
547
|
|
|
548
|
+
if (toolName === "jules_list_sessions") {
|
|
549
|
+
const provider = createProvider(config.provider || "jules", config);
|
|
550
|
+
const res = await provider.listSessions({ root, pageSize: args.pageSize, pageToken: args.pageToken });
|
|
551
|
+
return {
|
|
552
|
+
jsonrpc: "2.0",
|
|
553
|
+
id,
|
|
554
|
+
result: {
|
|
555
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, ...res }, null, 2) }],
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (toolName === "jules_list_activities") {
|
|
561
|
+
if (!args.sessionId) {
|
|
562
|
+
return {
|
|
563
|
+
jsonrpc: "2.0",
|
|
564
|
+
id,
|
|
565
|
+
error: { code: -32602, message: "Missing required parameter 'sessionId'" },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const provider = createProvider(config.provider || "jules", config);
|
|
569
|
+
const res = await provider.listActivities(args.sessionId, { root, pageSize: args.pageSize, pageToken: args.pageToken });
|
|
570
|
+
return {
|
|
571
|
+
jsonrpc: "2.0",
|
|
572
|
+
id,
|
|
573
|
+
result: {
|
|
574
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, ...res }, null, 2) }],
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (toolName === "jules_get_session_output") {
|
|
580
|
+
if (!args.sessionId) {
|
|
581
|
+
return {
|
|
582
|
+
jsonrpc: "2.0",
|
|
583
|
+
id,
|
|
584
|
+
error: { code: -32602, message: "Missing required parameter 'sessionId'" },
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const res = await extractSessionPatch(args.sessionId, { root, config });
|
|
588
|
+
return {
|
|
589
|
+
jsonrpc: "2.0",
|
|
590
|
+
id,
|
|
591
|
+
result: {
|
|
592
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (toolName === "jules_archive_session") {
|
|
598
|
+
if (!args.sessionId) {
|
|
599
|
+
return {
|
|
600
|
+
jsonrpc: "2.0",
|
|
601
|
+
id,
|
|
602
|
+
error: { code: -32602, message: "Missing required parameter 'sessionId'" },
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const provider = createProvider(config.provider || "jules", config);
|
|
606
|
+
const res = await provider.archiveSession(args.sessionId, { root });
|
|
607
|
+
return {
|
|
608
|
+
jsonrpc: "2.0",
|
|
609
|
+
id,
|
|
610
|
+
result: {
|
|
611
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, ...res }, null, 2) }],
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (toolName === "jules_delete_session") {
|
|
617
|
+
if (!args.sessionId) {
|
|
618
|
+
return {
|
|
619
|
+
jsonrpc: "2.0",
|
|
620
|
+
id,
|
|
621
|
+
error: { code: -32602, message: "Missing required parameter 'sessionId'" },
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const provider = createProvider(config.provider || "jules", config);
|
|
625
|
+
const res = await provider.deleteSession(args.sessionId, { root });
|
|
626
|
+
return {
|
|
627
|
+
jsonrpc: "2.0",
|
|
628
|
+
id,
|
|
629
|
+
result: {
|
|
630
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, ...res }, null, 2) }],
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (toolName === "jules_retry_session") {
|
|
636
|
+
if (!args.sessionId) {
|
|
637
|
+
return {
|
|
638
|
+
jsonrpc: "2.0",
|
|
639
|
+
id,
|
|
640
|
+
error: { code: -32602, message: "Missing required parameter 'sessionId'" },
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const res = await retrySession(args.sessionId, {
|
|
644
|
+
root,
|
|
645
|
+
config,
|
|
646
|
+
role: args.role,
|
|
647
|
+
withFailure: args.withFailure !== false,
|
|
648
|
+
});
|
|
649
|
+
return {
|
|
650
|
+
jsonrpc: "2.0",
|
|
651
|
+
id,
|
|
652
|
+
result: {
|
|
653
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (toolName === "jules_apply_patch") {
|
|
659
|
+
if (!args.sessionId) {
|
|
660
|
+
return {
|
|
661
|
+
jsonrpc: "2.0",
|
|
662
|
+
id,
|
|
663
|
+
error: { code: -32602, message: "Missing required parameter 'sessionId'" },
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const res = await applySessionPatch(args.sessionId, {
|
|
667
|
+
root,
|
|
668
|
+
config,
|
|
669
|
+
apply: Boolean(args.apply),
|
|
670
|
+
save: args.save,
|
|
671
|
+
});
|
|
672
|
+
return {
|
|
673
|
+
jsonrpc: "2.0",
|
|
674
|
+
id,
|
|
675
|
+
result: {
|
|
676
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (toolName === "jules_list_sources") {
|
|
682
|
+
const provider = createProvider(config.provider || "jules", config);
|
|
683
|
+
const res = await provider.listSources({ root, pageSize: args.pageSize, pageToken: args.pageToken });
|
|
684
|
+
return {
|
|
685
|
+
jsonrpc: "2.0",
|
|
686
|
+
id,
|
|
687
|
+
result: {
|
|
688
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, ...res }, null, 2) }],
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (toolName === "jules_prune_sessions") {
|
|
694
|
+
const res = await pruneSessions({
|
|
695
|
+
root,
|
|
696
|
+
config,
|
|
697
|
+
age: args.age || "7d",
|
|
698
|
+
state: args.state,
|
|
699
|
+
delete: Boolean(args.delete),
|
|
700
|
+
dryRun: args.dryRun !== false,
|
|
701
|
+
});
|
|
702
|
+
return {
|
|
703
|
+
jsonrpc: "2.0",
|
|
704
|
+
id,
|
|
705
|
+
result: {
|
|
706
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
434
711
|
return {
|
|
435
712
|
jsonrpc: "2.0",
|
|
436
713
|
id,
|
package/src/provider.mjs
CHANGED
|
@@ -691,9 +691,9 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
691
691
|
}
|
|
692
692
|
|
|
693
693
|
if (providerSpec.type === "http") {
|
|
694
|
-
const getSessionUrlTemplate = providerSpec.getSessionUrl || `${providerSpec.url}/${sessionId}`;
|
|
694
|
+
const getSessionUrlTemplate = ctx.customUrl || providerSpec.getSessionUrl || `${providerSpec.url}/${sessionId}`;
|
|
695
695
|
const urlData = { sessionId, ...ctx };
|
|
696
|
-
const url = interpolateString(getSessionUrlTemplate, urlData);
|
|
696
|
+
const url = ctx.customUrl || interpolateString(getSessionUrlTemplate, urlData);
|
|
697
697
|
|
|
698
698
|
const headerData = { ...urlData, token: rawToken };
|
|
699
699
|
const headers = {};
|
|
@@ -720,8 +720,9 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
720
720
|
let res;
|
|
721
721
|
try {
|
|
722
722
|
res = await fetch(url, {
|
|
723
|
-
method: "GET",
|
|
723
|
+
method: ctx.method || "GET",
|
|
724
724
|
headers,
|
|
725
|
+
body: ctx.body ? JSON.stringify(ctx.body) : undefined,
|
|
725
726
|
signal: AbortSignal.timeout(timeoutMs),
|
|
726
727
|
});
|
|
727
728
|
} catch (err) {
|
|
@@ -816,9 +817,9 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
816
817
|
}
|
|
817
818
|
|
|
818
819
|
if (providerSpec.type === "http") {
|
|
819
|
-
const approvePlanUrlTemplate = providerSpec.approvePlanUrl || `${providerSpec.url}/${sessionId}:approvePlan`;
|
|
820
|
+
const approvePlanUrlTemplate = ctx.customUrl || providerSpec.approvePlanUrl || `${providerSpec.url}/${sessionId}:approvePlan`;
|
|
820
821
|
const urlData = { sessionId, ...ctx };
|
|
821
|
-
const url = interpolateString(approvePlanUrlTemplate, urlData);
|
|
822
|
+
const url = ctx.customUrl || interpolateString(approvePlanUrlTemplate, urlData);
|
|
822
823
|
|
|
823
824
|
const headerData = { ...urlData, token: rawToken };
|
|
824
825
|
const headers = {};
|
|
@@ -918,6 +919,94 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
918
919
|
|
|
919
920
|
throw new Error(`Unsupported provider type: ${providerSpec.type}`);
|
|
920
921
|
},
|
|
922
|
+
|
|
923
|
+
async listSessions(ctx = {}) {
|
|
924
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
925
|
+
if (ctx.dryRun) return { sessions: [], raw: {} };
|
|
926
|
+
if (providerSpec.type === "http") {
|
|
927
|
+
const baseUrl = providerSpec.url || "https://jules.googleapis.com/v1alpha/sessions";
|
|
928
|
+
const params = new URLSearchParams();
|
|
929
|
+
if (ctx.pageSize) params.set("pageSize", String(ctx.pageSize));
|
|
930
|
+
if (ctx.pageToken) params.set("pageToken", String(ctx.pageToken));
|
|
931
|
+
const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
|
932
|
+
const res = await this.getSession("", { ...ctx, customUrl: url });
|
|
933
|
+
return {
|
|
934
|
+
sessions: res?.raw?.sessions || [],
|
|
935
|
+
nextPageToken: res?.raw?.nextPageToken,
|
|
936
|
+
raw: res?.raw || {},
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
return { sessions: [], raw: {} };
|
|
940
|
+
},
|
|
941
|
+
|
|
942
|
+
async listActivities(sessionId, ctx = {}) {
|
|
943
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
944
|
+
throw new TypeError("listActivities() requires a valid sessionId string");
|
|
945
|
+
}
|
|
946
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
947
|
+
if (ctx.dryRun) return { activities: [], raw: {} };
|
|
948
|
+
if (providerSpec.type === "http") {
|
|
949
|
+
const baseUrl = `${providerSpec.url || "https://jules.googleapis.com/v1alpha/sessions"}/${sessionId}/activities`;
|
|
950
|
+
const params = new URLSearchParams();
|
|
951
|
+
if (ctx.pageSize) params.set("pageSize", String(ctx.pageSize));
|
|
952
|
+
if (ctx.pageToken) params.set("pageToken", String(ctx.pageToken));
|
|
953
|
+
const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
|
954
|
+
const res = await this.getSession("", { ...ctx, customUrl: url });
|
|
955
|
+
return {
|
|
956
|
+
activities: res?.raw?.activities || [],
|
|
957
|
+
nextPageToken: res?.raw?.nextPageToken,
|
|
958
|
+
raw: res?.raw || {},
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
return { activities: [], raw: {} };
|
|
962
|
+
},
|
|
963
|
+
|
|
964
|
+
async archiveSession(sessionId, ctx = {}) {
|
|
965
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
966
|
+
throw new TypeError("archiveSession() requires a valid sessionId string");
|
|
967
|
+
}
|
|
968
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
969
|
+
if (ctx.dryRun) return { id: sessionId, archived: true, raw: {} };
|
|
970
|
+
if (providerSpec.type === "http") {
|
|
971
|
+
const url = `${providerSpec.url || "https://jules.googleapis.com/v1alpha/sessions"}/${sessionId}:archive`;
|
|
972
|
+
const res = await this.approvePlan(sessionId, { ...ctx, customUrl: url });
|
|
973
|
+
return { id: sessionId, archived: true, raw: res?.raw || {} };
|
|
974
|
+
}
|
|
975
|
+
return { id: sessionId, archived: true, raw: {} };
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
async deleteSession(sessionId, ctx = {}) {
|
|
979
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
980
|
+
throw new TypeError("deleteSession() requires a valid sessionId string");
|
|
981
|
+
}
|
|
982
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
983
|
+
if (ctx.dryRun) return { id: sessionId, deleted: true, raw: {} };
|
|
984
|
+
if (providerSpec.type === "http") {
|
|
985
|
+
const url = `${providerSpec.url || "https://jules.googleapis.com/v1alpha/sessions"}/${sessionId}`;
|
|
986
|
+
const res = await this.getSession("", { ...ctx, customUrl: url, method: "DELETE" });
|
|
987
|
+
return { id: sessionId, deleted: true, raw: res?.raw || {} };
|
|
988
|
+
}
|
|
989
|
+
return { id: sessionId, deleted: true, raw: {} };
|
|
990
|
+
},
|
|
991
|
+
|
|
992
|
+
async listSources(ctx = {}) {
|
|
993
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
994
|
+
if (ctx.dryRun) return { sources: [], raw: {} };
|
|
995
|
+
if (providerSpec.type === "http") {
|
|
996
|
+
const baseUrl = "https://jules.googleapis.com/v1alpha/sources";
|
|
997
|
+
const params = new URLSearchParams();
|
|
998
|
+
if (ctx.pageSize) params.set("pageSize", String(ctx.pageSize));
|
|
999
|
+
if (ctx.pageToken) params.set("pageToken", String(ctx.pageToken));
|
|
1000
|
+
const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
|
1001
|
+
const res = await this.getSession("", { ...ctx, customUrl: url });
|
|
1002
|
+
return {
|
|
1003
|
+
sources: res?.raw?.sources || [],
|
|
1004
|
+
nextPageToken: res?.raw?.nextPageToken,
|
|
1005
|
+
raw: res?.raw || {},
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
return { sources: [], raw: {} };
|
|
1009
|
+
},
|
|
921
1010
|
};
|
|
922
1011
|
}
|
|
923
1012
|
|
|
@@ -1038,6 +1127,26 @@ export function createFailoverProvider(providers = ["jules"], config = {}) {
|
|
|
1038
1127
|
}
|
|
1039
1128
|
},
|
|
1040
1129
|
|
|
1130
|
+
async listSessions(ctx = {}) {
|
|
1131
|
+
return providerList[0].listSessions(ctx);
|
|
1132
|
+
},
|
|
1133
|
+
|
|
1134
|
+
async listActivities(sessionId, ctx = {}) {
|
|
1135
|
+
return providerList[0].listActivities(sessionId, ctx);
|
|
1136
|
+
},
|
|
1137
|
+
|
|
1138
|
+
async archiveSession(sessionId, ctx = {}) {
|
|
1139
|
+
return providerList[0].archiveSession(sessionId, ctx);
|
|
1140
|
+
},
|
|
1141
|
+
|
|
1142
|
+
async deleteSession(sessionId, ctx = {}) {
|
|
1143
|
+
return providerList[0].deleteSession(sessionId, ctx);
|
|
1144
|
+
},
|
|
1145
|
+
|
|
1146
|
+
async listSources(ctx = {}) {
|
|
1147
|
+
return providerList[0].listSources(ctx);
|
|
1148
|
+
},
|
|
1149
|
+
|
|
1041
1150
|
validate() {
|
|
1042
1151
|
return providerList.every((p) => typeof p.validate !== "function" || p.validate());
|
|
1043
1152
|
},
|
|
@@ -1108,5 +1217,25 @@ export function createSyntaxVerifiedProvider(fastProvider, complexProvider, conf
|
|
|
1108
1217
|
approvePlan(...args) {
|
|
1109
1218
|
return fastProvider.approvePlan(...args);
|
|
1110
1219
|
},
|
|
1220
|
+
|
|
1221
|
+
listSessions(...args) {
|
|
1222
|
+
return typeof fastProvider.listSessions === "function" ? fastProvider.listSessions(...args) : { sessions: [] };
|
|
1223
|
+
},
|
|
1224
|
+
|
|
1225
|
+
listActivities(...args) {
|
|
1226
|
+
return typeof fastProvider.listActivities === "function" ? fastProvider.listActivities(...args) : { activities: [] };
|
|
1227
|
+
},
|
|
1228
|
+
|
|
1229
|
+
archiveSession(...args) {
|
|
1230
|
+
return typeof fastProvider.archiveSession === "function" ? fastProvider.archiveSession(...args) : { id: args[0], archived: true };
|
|
1231
|
+
},
|
|
1232
|
+
|
|
1233
|
+
deleteSession(...args) {
|
|
1234
|
+
return typeof fastProvider.deleteSession === "function" ? fastProvider.deleteSession(...args) : { id: args[0], deleted: true };
|
|
1235
|
+
},
|
|
1236
|
+
|
|
1237
|
+
listSources(...args) {
|
|
1238
|
+
return typeof fastProvider.listSources === "function" ? fastProvider.listSources(...args) : { sources: [] };
|
|
1239
|
+
},
|
|
1111
1240
|
};
|
|
1112
1241
|
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { createProvider } from "./provider.mjs";
|
|
5
|
+
import { loadConfig, resolveRoot } from "./config.mjs";
|
|
6
|
+
import { redactSecrets } from "./security.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parses human-readable duration strings (e.g. "7d", "24h", "30m", "60s") into milliseconds.
|
|
10
|
+
*
|
|
11
|
+
* @param {string|number} duration
|
|
12
|
+
* @returns {number} Milliseconds
|
|
13
|
+
*/
|
|
14
|
+
export function parseAgeDuration(duration) {
|
|
15
|
+
if (typeof duration === "number" && Number.isFinite(duration)) return duration;
|
|
16
|
+
if (!duration || typeof duration !== "string") return 0;
|
|
17
|
+
const match = duration.trim().match(/^(\d+(?:\.\d+)?)\s*([smhdw])$/i);
|
|
18
|
+
if (!match) {
|
|
19
|
+
const num = Number(duration);
|
|
20
|
+
return Number.isFinite(num) ? num : 0;
|
|
21
|
+
}
|
|
22
|
+
const val = parseFloat(match[1]);
|
|
23
|
+
const unit = match[2].toLowerCase();
|
|
24
|
+
switch (unit) {
|
|
25
|
+
case "s": return val * 1000;
|
|
26
|
+
case "m": return val * 60 * 1000;
|
|
27
|
+
case "h": return val * 60 * 60 * 1000;
|
|
28
|
+
case "d": return val * 24 * 60 * 60 * 1000;
|
|
29
|
+
case "w": return val * 7 * 24 * 60 * 60 * 1000;
|
|
30
|
+
default: return 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extracts git diff patch, PR details, and modified files from a Jules session.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} sessionId
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @returns {Promise<{ ok: boolean, id: string, patch: string, pr: object|null, files: string[], error?: string }>}
|
|
40
|
+
*/
|
|
41
|
+
export async function extractSessionPatch(sessionId, opts = {}) {
|
|
42
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
43
|
+
throw new TypeError("extractSessionPatch requires a valid sessionId string");
|
|
44
|
+
}
|
|
45
|
+
const root = opts.root || resolveRoot();
|
|
46
|
+
const config = opts.config || loadConfig(root);
|
|
47
|
+
const provider = opts.provider || createProvider(config.provider || "jules", config);
|
|
48
|
+
|
|
49
|
+
const session = await provider.getSession(sessionId, opts);
|
|
50
|
+
const activitiesRes = await provider.listActivities(sessionId, opts).catch(() => ({ activities: [] }));
|
|
51
|
+
const activities = activitiesRes.activities || [];
|
|
52
|
+
|
|
53
|
+
let patch = "";
|
|
54
|
+
let pr = null;
|
|
55
|
+
const files = new Set();
|
|
56
|
+
|
|
57
|
+
// 1. Inspect session outputs
|
|
58
|
+
const outputs = session?.raw?.outputs || session?.outputs || [];
|
|
59
|
+
for (const out of outputs) {
|
|
60
|
+
if (out.pullRequest) {
|
|
61
|
+
pr = out.pullRequest;
|
|
62
|
+
}
|
|
63
|
+
if (out.gitPatch || out.patch || out.diff) {
|
|
64
|
+
patch = out.gitPatch || out.patch || out.diff;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Inspect activity artifacts and logs if patch wasn't in top-level output
|
|
69
|
+
for (const act of activities) {
|
|
70
|
+
if (act.pullRequest) pr = act.pullRequest;
|
|
71
|
+
if (act.gitPatch || act.patch || act.diff) {
|
|
72
|
+
patch = patch || act.gitPatch || act.patch || act.diff;
|
|
73
|
+
}
|
|
74
|
+
if (act.artifacts && Array.isArray(act.artifacts)) {
|
|
75
|
+
for (const art of act.artifacts) {
|
|
76
|
+
if (art.patch || art.diff || (typeof art.content === "string" && art.content.startsWith("diff --git"))) {
|
|
77
|
+
patch = patch || art.patch || art.diff || art.content;
|
|
78
|
+
}
|
|
79
|
+
if (art.path || art.filename) {
|
|
80
|
+
files.add(art.path || art.filename);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (act.changedFiles && Array.isArray(act.changedFiles)) {
|
|
85
|
+
act.changedFiles.forEach((f) => files.add(f));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Extract changed files from patch headers if files set is empty
|
|
90
|
+
if (patch && files.size === 0) {
|
|
91
|
+
const fileMatches = patch.matchAll(/^diff --git a\/(.+?) b\/(.+?)$/gm);
|
|
92
|
+
for (const m of fileMatches) {
|
|
93
|
+
files.add(m[2]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ok: Boolean(patch || pr),
|
|
99
|
+
id: sessionId,
|
|
100
|
+
patch: patch ? (patch.endsWith("\n") ? patch : patch + "\n") : "",
|
|
101
|
+
pr,
|
|
102
|
+
files: Array.from(files),
|
|
103
|
+
rawSession: session.raw,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Applies a Jules session's patch to the local working tree with git apply safety checks.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} sessionId
|
|
111
|
+
* @param {object} opts
|
|
112
|
+
* @returns {Promise<{ ok: boolean, patchApplied: boolean, checkPassed: boolean, patch: string, files: string[], error?: string }>}
|
|
113
|
+
*/
|
|
114
|
+
export async function applySessionPatch(sessionId, opts = {}) {
|
|
115
|
+
const root = opts.root || resolveRoot();
|
|
116
|
+
const res = await extractSessionPatch(sessionId, { ...opts, root });
|
|
117
|
+
|
|
118
|
+
if (!res.patch) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
patchApplied: false,
|
|
122
|
+
checkPassed: false,
|
|
123
|
+
patch: "",
|
|
124
|
+
files: res.files || [],
|
|
125
|
+
error: "No git patch found in session output or activities.",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (opts.save) {
|
|
130
|
+
const savePath = resolve(root, opts.save);
|
|
131
|
+
writeFileSync(savePath, res.patch, "utf-8");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 1. Dry run: git apply --check
|
|
135
|
+
const checkRes = spawnSync("git", ["apply", "--check", "-"], {
|
|
136
|
+
cwd: root,
|
|
137
|
+
input: res.patch,
|
|
138
|
+
encoding: "utf-8",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (checkRes.status !== 0) {
|
|
142
|
+
const stderr = (checkRes.stderr || "").trim();
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
patchApplied: false,
|
|
146
|
+
checkPassed: false,
|
|
147
|
+
patch: res.patch,
|
|
148
|
+
files: res.files,
|
|
149
|
+
error: `git apply --check failed: ${stderr || "Patch does not cleanly apply to working directory"}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 2. If apply requested, execute git apply
|
|
154
|
+
if (opts.apply) {
|
|
155
|
+
const applyRes = spawnSync("git", ["apply", "-"], {
|
|
156
|
+
cwd: root,
|
|
157
|
+
input: res.patch,
|
|
158
|
+
encoding: "utf-8",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (applyRes.status !== 0) {
|
|
162
|
+
const stderr = (applyRes.stderr || "").trim();
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
patchApplied: false,
|
|
166
|
+
checkPassed: true,
|
|
167
|
+
patch: res.patch,
|
|
168
|
+
files: res.files,
|
|
169
|
+
error: `git apply failed: ${stderr}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
patchApplied: true,
|
|
176
|
+
checkPassed: true,
|
|
177
|
+
patch: res.patch,
|
|
178
|
+
files: res.files,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
patchApplied: false,
|
|
185
|
+
checkPassed: true,
|
|
186
|
+
patch: res.patch,
|
|
187
|
+
files: res.files,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Gathers failure context and error output from a failed session and synthesizes a new OODA retry session.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} sessionId
|
|
195
|
+
* @param {object} opts
|
|
196
|
+
* @returns {Promise<{ ok: boolean, originalSessionId: string, newSession: object, failureReason: string }>}
|
|
197
|
+
*/
|
|
198
|
+
export async function retrySession(sessionId, opts = {}) {
|
|
199
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
200
|
+
throw new TypeError("retrySession requires a valid sessionId string");
|
|
201
|
+
}
|
|
202
|
+
const root = opts.root || resolveRoot();
|
|
203
|
+
const config = opts.config || loadConfig(root);
|
|
204
|
+
const provider = opts.provider || createProvider(config.provider || "jules", config);
|
|
205
|
+
|
|
206
|
+
const session = await provider.getSession(sessionId, opts);
|
|
207
|
+
const activitiesRes = await provider.listActivities(sessionId, opts).catch(() => ({ activities: [] }));
|
|
208
|
+
const activities = activitiesRes.activities || [];
|
|
209
|
+
|
|
210
|
+
// Extract failure diagnostics
|
|
211
|
+
const errors = [];
|
|
212
|
+
for (const act of activities) {
|
|
213
|
+
if (act.error) errors.push(typeof act.error === "string" ? act.error : JSON.stringify(act.error));
|
|
214
|
+
if (act.executionOutput && (act.exitCode !== 0 || act.status === "FAILED")) {
|
|
215
|
+
errors.push(act.executionOutput);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const rawPrompt = session?.raw?.prompt || session?.prompt || "";
|
|
220
|
+
const title = session?.raw?.title || `Retry of Session ${sessionId}`;
|
|
221
|
+
const failureReason = errors.join("\n").slice(0, 4000) || "Previous session did not complete cleanly.";
|
|
222
|
+
|
|
223
|
+
let synthesizedPrompt = rawPrompt;
|
|
224
|
+
if (opts.withFailure !== false && failureReason) {
|
|
225
|
+
synthesizedPrompt = `${rawPrompt}\n\n[PREVIOUS_ATTEMPT_FAILURE_DIAGNOSTIC]\nThe previous automated session failed with the following error output:\n\`\`\`\n${redactSecrets(failureReason)}\n\`\`\`\nPlease analyze this failure, fix the root cause, and ensure all tests pass.\n`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const source = session?.raw?.sourceContext?.source || opts.source;
|
|
229
|
+
const branch = session?.raw?.sourceContext?.githubRepoContext?.startingBranch || opts.branch || "main";
|
|
230
|
+
|
|
231
|
+
const dispatchRes = await provider.dispatch(
|
|
232
|
+
{
|
|
233
|
+
title: opts.title || title,
|
|
234
|
+
prompt: synthesizedPrompt,
|
|
235
|
+
role: opts.role,
|
|
236
|
+
source,
|
|
237
|
+
branch,
|
|
238
|
+
},
|
|
239
|
+
opts
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
originalSessionId: sessionId,
|
|
245
|
+
newSession: dispatchRes,
|
|
246
|
+
failureReason,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Queries and batch-archives old or completed Jules sessions based on age and state filters.
|
|
252
|
+
*
|
|
253
|
+
* @param {object} opts
|
|
254
|
+
* @returns {Promise<{ ok: boolean, matchedCount: number, archivedCount: number, sessions: Array<{ id: string, state: string, createTime: string, action: string }> }>}
|
|
255
|
+
*/
|
|
256
|
+
export async function pruneSessions(opts = {}) {
|
|
257
|
+
const root = opts.root || resolveRoot();
|
|
258
|
+
const config = opts.config || loadConfig(root);
|
|
259
|
+
const provider = opts.provider || createProvider(config.provider || "jules", config);
|
|
260
|
+
|
|
261
|
+
const listRes = await provider.listSessions({ ...opts, pageSize: opts.pageSize || 100 });
|
|
262
|
+
const allSessions = listRes.sessions || [];
|
|
263
|
+
|
|
264
|
+
const maxAgeMs = opts.age ? parseAgeDuration(opts.age) : 0;
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
const targetState = opts.state ? String(opts.state).toUpperCase() : null;
|
|
267
|
+
|
|
268
|
+
const matched = [];
|
|
269
|
+
for (const s of allSessions) {
|
|
270
|
+
const state = (s.state || s.status || "UNKNOWN").toUpperCase();
|
|
271
|
+
if (targetState && state !== targetState) continue;
|
|
272
|
+
|
|
273
|
+
if (maxAgeMs > 0) {
|
|
274
|
+
const timeStr = s.createTime || s.updateTime;
|
|
275
|
+
const sessionTime = timeStr ? new Date(timeStr).getTime() : 0;
|
|
276
|
+
if (sessionTime > 0 && now - sessionTime < maxAgeMs) {
|
|
277
|
+
continue; // Session is newer than the cutoff age
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
matched.push(s);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const results = [];
|
|
285
|
+
for (const s of matched) {
|
|
286
|
+
const id = s.id || (s.name ? s.name.split("/").pop() : "");
|
|
287
|
+
if (!id) continue;
|
|
288
|
+
|
|
289
|
+
if (opts.dryRun) {
|
|
290
|
+
results.push({ id, state: s.state || s.status, createTime: s.createTime, action: "DRY_RUN_SKIP" });
|
|
291
|
+
} else {
|
|
292
|
+
try {
|
|
293
|
+
if (opts.delete) {
|
|
294
|
+
await provider.deleteSession(id, opts);
|
|
295
|
+
results.push({ id, state: s.state || s.status, createTime: s.createTime, action: "DELETED" });
|
|
296
|
+
} else {
|
|
297
|
+
await provider.archiveSession(id, opts);
|
|
298
|
+
results.push({ id, state: s.state || s.status, createTime: s.createTime, action: "ARCHIVED" });
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
results.push({ id, state: s.state || s.status, createTime: s.createTime, action: `ERROR: ${err.message}` });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
ok: true,
|
|
308
|
+
matchedCount: matched.length,
|
|
309
|
+
archivedCount: results.filter((r) => r.action === "ARCHIVED" || r.action === "DELETED").length,
|
|
310
|
+
sessions: results,
|
|
311
|
+
};
|
|
312
|
+
}
|