@kenkaiiii/ggcoder 5.7.0 → 5.8.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/dist/app-sidecar.js +167 -5
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/autopilot-cycle.d.ts +52 -17
- package/dist/core/autopilot-cycle.d.ts.map +1 -1
- package/dist/core/autopilot-cycle.js +53 -13
- package/dist/core/autopilot-cycle.js.map +1 -1
- package/dist/core/autopilot-cycle.test.js +195 -10
- package/dist/core/autopilot-cycle.test.js.map +1 -1
- package/dist/core/autopilot-gate.d.ts +12 -0
- package/dist/core/autopilot-gate.d.ts.map +1 -1
- package/dist/core/autopilot-gate.js +10 -1
- package/dist/core/autopilot-gate.js.map +1 -1
- package/dist/core/autopilot-gate.test.js +27 -2
- package/dist/core/autopilot-gate.test.js.map +1 -1
- package/dist/core/ken-context.d.ts +20 -0
- package/dist/core/ken-context.d.ts.map +1 -1
- package/dist/core/ken-context.js +46 -2
- package/dist/core/ken-context.js.map +1 -1
- package/dist/core/ken-context.test.js +52 -5
- package/dist/core/ken-context.test.js.map +1 -1
- package/dist/core/ken-prompt.js +14 -4
- package/dist/core/ken-prompt.js.map +1 -1
- package/dist/core/ken-prompt.test.js +20 -3
- package/dist/core/ken-prompt.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +5 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +9 -0
- package/dist/core/session-manager.test.js.map +1 -1
- package/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -22,7 +22,7 @@ import { formatError } from "@kenkaiiii/gg-ai";
|
|
|
22
22
|
import { runJsonMode } from "./modes/json-mode.js";
|
|
23
23
|
import { AgentSession } from "./core/agent-session.js";
|
|
24
24
|
import { buildKenSystemPrompt, buildKenAutopilotSystemPrompt } from "./core/ken-prompt.js";
|
|
25
|
-
import { buildKenDigest, buildKenAutopilotContext } from "./core/ken-context.js";
|
|
25
|
+
import { buildKenDigest, buildKenAutopilotContext, buildKenAutopilotPlanContext, } from "./core/ken-context.js";
|
|
26
26
|
import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
|
|
27
27
|
import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
|
|
28
28
|
import { driveAutopilotCycle } from "./core/autopilot-cycle.js";
|
|
@@ -847,6 +847,12 @@ async function createSession(deps, opts) {
|
|
|
847
847
|
catch {
|
|
848
848
|
content = "";
|
|
849
849
|
}
|
|
850
|
+
// Record the submitted plan so the autopilot gate can route this turn
|
|
851
|
+
// into a PLAN review instead of a stale work review (plan mode is
|
|
852
|
+
// already false here, so the gate's planMode check alone never catches
|
|
853
|
+
// a submission). setPendingPlan bumps planGeneration, which invalidates
|
|
854
|
+
// any in-flight Ken plan review racing a user action.
|
|
855
|
+
setPendingPlan(planPath, content);
|
|
850
856
|
broadcast("plan_exit", { planPath, content });
|
|
851
857
|
return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
|
|
852
858
|
},
|
|
@@ -940,6 +946,28 @@ async function createSession(deps, opts) {
|
|
|
940
946
|
// cycles drift into Ken reviewing against his own last prompt. Cleared
|
|
941
947
|
// whenever the conversation resets (new session / plan accept / task run).
|
|
942
948
|
let injectedAutopilotPrompts = [];
|
|
949
|
+
// The plan GG Coder submitted via exit_plan that still awaits a decision
|
|
950
|
+
// (Ken's auto-review in autopilot, or the user's modal). Path + the content
|
|
951
|
+
// read at submission time (fallback if the file becomes unreadable).
|
|
952
|
+
let pendingPlanPath = null;
|
|
953
|
+
let pendingPlanContent = "";
|
|
954
|
+
// Bumped on EVERY pending-plan set/clear. Ken's plan review captures it
|
|
955
|
+
// before reviewing and re-checks it before acting on the verdict, so a user
|
|
956
|
+
// Accept/Reject racing an in-flight review always wins — the stale verdict
|
|
957
|
+
// is discarded silently.
|
|
958
|
+
let planGeneration = 0;
|
|
959
|
+
function setPendingPlan(planPath, content) {
|
|
960
|
+
pendingPlanPath = planPath;
|
|
961
|
+
pendingPlanContent = content;
|
|
962
|
+
planGeneration++;
|
|
963
|
+
}
|
|
964
|
+
function clearPendingPlan() {
|
|
965
|
+
if (pendingPlanPath === null)
|
|
966
|
+
return;
|
|
967
|
+
pendingPlanPath = null;
|
|
968
|
+
pendingPlanContent = "";
|
|
969
|
+
planGeneration++;
|
|
970
|
+
}
|
|
943
971
|
// Workflow (prompt-template) commands: built-in + the project's custom
|
|
944
972
|
// `.gg/commands/*.md`. Used to gate autopilot off command turns and to label
|
|
945
973
|
// expanded templates in Ken's digests. Loaded fresh so a newly added custom
|
|
@@ -1209,6 +1237,60 @@ async function createSession(deps, opts) {
|
|
|
1209
1237
|
await syncKenAutoModel(pending.provider, pending.model);
|
|
1210
1238
|
}
|
|
1211
1239
|
}
|
|
1240
|
+
// One PLAN review: like runAutopilotReview but the digest carries the
|
|
1241
|
+
// submitted plan's markdown (`## Plan under review`) and the plan-review
|
|
1242
|
+
// instruction — Ken judges the plan itself, not finished work. Returns null
|
|
1243
|
+
// on failure; a failure caused by the user's own action racing the review
|
|
1244
|
+
// (cancel or a manual Accept/Reject that bumped planGeneration) stays
|
|
1245
|
+
// SILENT — no autopilot_error — because the user's decision already won.
|
|
1246
|
+
async function runAutopilotPlanReview(originalRequest) {
|
|
1247
|
+
const planPath = pendingPlanPath;
|
|
1248
|
+
if (planPath === null)
|
|
1249
|
+
return null;
|
|
1250
|
+
const genAtStart = planGeneration;
|
|
1251
|
+
autopilotReviewing = true;
|
|
1252
|
+
broadcast("autopilot_review_start", {});
|
|
1253
|
+
try {
|
|
1254
|
+
const ken = await ensureKenAutoSession();
|
|
1255
|
+
// Re-read the plan file (the run may have revised it in place); fall
|
|
1256
|
+
// back to the content captured at exit_plan time.
|
|
1257
|
+
const planContent = await fs.readFile(planPath, "utf-8").catch(() => pendingPlanContent);
|
|
1258
|
+
const digest = buildKenAutopilotPlanContext({
|
|
1259
|
+
cwd,
|
|
1260
|
+
gitBranch,
|
|
1261
|
+
messages: session.getMessages(),
|
|
1262
|
+
originalRequest,
|
|
1263
|
+
injectedPrompts: [...injectedAutopilotPrompts],
|
|
1264
|
+
workflowCommands: await loadWorkflowCommandSpecs(),
|
|
1265
|
+
planContent,
|
|
1266
|
+
});
|
|
1267
|
+
await ken.prompt(digest);
|
|
1268
|
+
if (autopilotCancelled || planGeneration !== genAtStart)
|
|
1269
|
+
return null;
|
|
1270
|
+
return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
// User action mid-review (manual Accept aborts the kenAuto run): drop
|
|
1274
|
+
// the review silently — the user's decision supersedes Ken's.
|
|
1275
|
+
if (autopilotCancelled || planGeneration !== genAtStart)
|
|
1276
|
+
return null;
|
|
1277
|
+
broadcastError("autopilot_error", "autopilot plan review failed", err);
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
finally {
|
|
1281
|
+
autopilotReviewing = false;
|
|
1282
|
+
// Apply any model switch that landed mid-review.
|
|
1283
|
+
const pending = pendingKenAutoModel;
|
|
1284
|
+
pendingKenAutoModel = null;
|
|
1285
|
+
if (pending)
|
|
1286
|
+
await syncKenAutoModel(pending.provider, pending.model);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
// The prompt fed to the fresh session after a plan is accepted — the SAME
|
|
1290
|
+
// string the webview sends on a manual Accept (see PlanReviewModal's accept
|
|
1291
|
+
// handler in gg-app/src/App.tsx). Keep the two in lockstep so auto- and
|
|
1292
|
+
// manual approval produce identical implementation turns.
|
|
1293
|
+
const IMPLEMENT_PLAN_PROMPT = "The plan has been approved. Implement it now, following each step in order.";
|
|
1212
1294
|
// Drive the review→prompt→review loop for one finished user turn. Only ever
|
|
1213
1295
|
// called after shouldStartAutopilotCycle approves the turn (POST /prompt or
|
|
1214
1296
|
// the stranded-queue drain) — never from the task runner, resume, /ken, or
|
|
@@ -1219,14 +1301,53 @@ async function createSession(deps, opts) {
|
|
|
1219
1301
|
if (!autopilot || autopilotCancelled)
|
|
1220
1302
|
return;
|
|
1221
1303
|
autopilotActive = true;
|
|
1304
|
+
// Generation captured by the last plan review; acceptPlan re-checks it so
|
|
1305
|
+
// a user Accept/Reject landing mid-review always wins.
|
|
1306
|
+
let planGenAtReview = -1;
|
|
1222
1307
|
try {
|
|
1223
1308
|
await driveAutopilotCycle({
|
|
1224
|
-
|
|
1309
|
+
// A plan-pending cycle needs extra rounds: approve+implement and the
|
|
1310
|
+
// post-implement work review each consume one, so +2 keeps a real fix
|
|
1311
|
+
// round available.
|
|
1312
|
+
maxRounds: pendingPlanPath !== null ? MAX_AUTOPILOT_ROUNDS + 2 : MAX_AUTOPILOT_ROUNDS,
|
|
1225
1313
|
isCancelled: () => autopilotCancelled,
|
|
1226
|
-
// An injected run entering plan mode
|
|
1227
|
-
//
|
|
1228
|
-
// plan-mode session
|
|
1314
|
+
// An injected run entering plan mode WITHOUT submitting (enter_plan,
|
|
1315
|
+
// no exit_plan) halts the cycle — Ken never prompts into a read-only
|
|
1316
|
+
// plan-mode session. A submitted plan takes the planPending branch.
|
|
1229
1317
|
isPlanMode: () => session.getPlanMode(),
|
|
1318
|
+
planPending: () => pendingPlanPath !== null,
|
|
1319
|
+
reviewPlan: async () => {
|
|
1320
|
+
planGenAtReview = planGeneration;
|
|
1321
|
+
return runAutopilotPlanReview(originalRequest);
|
|
1322
|
+
},
|
|
1323
|
+
// Auto-accept: the inlined POST /plan/accept body. Returns false when
|
|
1324
|
+
// the plan generation moved since the review (user acted) — the cycle
|
|
1325
|
+
// exits silently and the user's action stands.
|
|
1326
|
+
acceptPlan: async () => {
|
|
1327
|
+
if (pendingPlanPath === null || planGeneration !== planGenAtReview)
|
|
1328
|
+
return false;
|
|
1329
|
+
const planPath = pendingPlanPath;
|
|
1330
|
+
try {
|
|
1331
|
+
await session.newSession();
|
|
1332
|
+
injectedAutopilotPrompts = [];
|
|
1333
|
+
titleGenerated = false;
|
|
1334
|
+
await session.setApprovedPlan(planPath);
|
|
1335
|
+
}
|
|
1336
|
+
catch (err) {
|
|
1337
|
+
broadcastError("autopilot_error", "autopilot plan accept failed", err);
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
clearPendingPlan();
|
|
1341
|
+
// Ordering is load-bearing: the webview reads its still-open plan
|
|
1342
|
+
// modal state (step count) on autopilot_plan_accepted, and
|
|
1343
|
+
// session_reset clears it — accepted must land first.
|
|
1344
|
+
broadcast("autopilot_plan_accepted", {});
|
|
1345
|
+
broadcast("session_reset", {});
|
|
1346
|
+
// Persisted into the NEW session so a resume shows the marker.
|
|
1347
|
+
void session.persistAutopilotMarker("plan_approved");
|
|
1348
|
+
return true;
|
|
1349
|
+
},
|
|
1350
|
+
runImplement: () => runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(IMPLEMENT_PLAN_PROMPT)),
|
|
1230
1351
|
// Lean context per user turn: wipe prior review history so each new
|
|
1231
1352
|
// turn starts cheap, while within this cycle the few review messages
|
|
1232
1353
|
// persist so Ken remembers what he already asked GG Coder to fix.
|
|
@@ -1240,6 +1361,10 @@ async function createSession(deps, opts) {
|
|
|
1240
1361
|
// streams normally; the shared finally never re-triggers autopilot,
|
|
1241
1362
|
// so this can't recurse.
|
|
1242
1363
|
onInjected: (body, round) => {
|
|
1364
|
+
// A revision injection supersedes the pending plan — if the run
|
|
1365
|
+
// resubmits via exit_plan, onExitPlan re-sets it (no-op for work-
|
|
1366
|
+
// branch injections, where nothing is pending).
|
|
1367
|
+
clearPendingPlan();
|
|
1243
1368
|
injectedAutopilotPrompts.push(body);
|
|
1244
1369
|
broadcast("autopilot_prompted", { round, body });
|
|
1245
1370
|
void session.persistAutopilotMarker("prompted", { body });
|
|
@@ -1291,6 +1416,9 @@ async function createSession(deps, opts) {
|
|
|
1291
1416
|
broadcast("queued", { count: session.getQueuedCount() });
|
|
1292
1417
|
if (!next.text.trim() && next.attachments.length === 0)
|
|
1293
1418
|
continue;
|
|
1419
|
+
// A queued message draining as a fresh turn supersedes any pending
|
|
1420
|
+
// plan, exactly like a direct POST /prompt turn.
|
|
1421
|
+
clearPendingPlan();
|
|
1294
1422
|
const workflowCommand = next.attachments.length === 0 &&
|
|
1295
1423
|
isWorkflowCommandText(next.text, await loadWorkflowCommandSpecs());
|
|
1296
1424
|
const assistantsBefore = countAssistantMessages(session.getMessages());
|
|
@@ -1307,6 +1435,9 @@ async function createSession(deps, opts) {
|
|
|
1307
1435
|
enabled: autopilot,
|
|
1308
1436
|
cancelled: autopilotCancelled,
|
|
1309
1437
|
planMode: session.getPlanMode(),
|
|
1438
|
+
// A submitted plan (exit_plan fired) routes into the PLAN review
|
|
1439
|
+
// branch — the cycle reviews the plan itself instead of skipping.
|
|
1440
|
+
planPending: pendingPlanPath !== null,
|
|
1310
1441
|
workflowCommand,
|
|
1311
1442
|
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1312
1443
|
// Skip the review API call outright for turns that only started a
|
|
@@ -1316,6 +1447,9 @@ async function createSession(deps, opts) {
|
|
|
1316
1447
|
mechanicalOnly: isMechanicalOnlyTurn(extractTurnToolCalls(session.getMessages(), messagesBefore)),
|
|
1317
1448
|
});
|
|
1318
1449
|
if (decision.start) {
|
|
1450
|
+
log("INFO", "app-sidecar", "autopilot cycle starting (queued turn)", {
|
|
1451
|
+
kind: decision.kind,
|
|
1452
|
+
});
|
|
1319
1453
|
await runAutopilotCycle(next.text);
|
|
1320
1454
|
}
|
|
1321
1455
|
else if (autopilot) {
|
|
@@ -1341,6 +1475,7 @@ async function createSession(deps, opts) {
|
|
|
1341
1475
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1342
1476
|
await session.newSession();
|
|
1343
1477
|
injectedAutopilotPrompts = [];
|
|
1478
|
+
clearPendingPlan();
|
|
1344
1479
|
titleGenerated = false;
|
|
1345
1480
|
broadcast("session_reset", {});
|
|
1346
1481
|
markTaskInProgress(cwd, task.id);
|
|
@@ -1851,6 +1986,10 @@ async function createSession(deps, opts) {
|
|
|
1851
1986
|
// Fresh user turn: clear any cancel flag left from a prior cycle so this
|
|
1852
1987
|
// turn's autopilot review can run.
|
|
1853
1988
|
autopilotCancelled = false;
|
|
1989
|
+
// A typed message while a plan modal/review is pending (reject,
|
|
1990
|
+
// feedback, anything) supersedes the pending plan — the bump also
|
|
1991
|
+
// invalidates any in-flight Ken plan review.
|
|
1992
|
+
clearPendingPlan();
|
|
1854
1993
|
// Gate inputs captured around the run: whether this turn is a workflow
|
|
1855
1994
|
// slash command (attachment prompts skip slash expansion entirely), and
|
|
1856
1995
|
// how many assistant messages the run actually adds. Computed even when
|
|
@@ -1887,6 +2026,9 @@ async function createSession(deps, opts) {
|
|
|
1887
2026
|
enabled: autopilot,
|
|
1888
2027
|
cancelled: autopilotCancelled,
|
|
1889
2028
|
planMode: session.getPlanMode(),
|
|
2029
|
+
// A submitted plan (exit_plan fired) routes into the PLAN review
|
|
2030
|
+
// branch — the cycle reviews the plan itself instead of skipping.
|
|
2031
|
+
planPending: pendingPlanPath !== null,
|
|
1890
2032
|
workflowCommand,
|
|
1891
2033
|
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1892
2034
|
// Skip the review API call outright for turns that only started a
|
|
@@ -1896,6 +2038,7 @@ async function createSession(deps, opts) {
|
|
|
1896
2038
|
mechanicalOnly: isMechanicalOnlyTurn(extractTurnToolCalls(session.getMessages(), messagesBefore)),
|
|
1897
2039
|
});
|
|
1898
2040
|
if (decision.start) {
|
|
2041
|
+
log("INFO", "app-sidecar", "autopilot cycle starting", { kind: decision.kind });
|
|
1899
2042
|
await runAutopilotCycle(text);
|
|
1900
2043
|
}
|
|
1901
2044
|
else if (autopilot) {
|
|
@@ -2298,6 +2441,7 @@ async function createSession(deps, opts) {
|
|
|
2298
2441
|
.newSession()
|
|
2299
2442
|
.then(() => {
|
|
2300
2443
|
injectedAutopilotPrompts = [];
|
|
2444
|
+
clearPendingPlan();
|
|
2301
2445
|
broadcast("session_reset", {});
|
|
2302
2446
|
json(res, 200, { ok: true });
|
|
2303
2447
|
})
|
|
@@ -2329,6 +2473,24 @@ async function createSession(deps, opts) {
|
|
|
2329
2473
|
json(res, 409, { error: "cannot accept a plan while the agent is running" });
|
|
2330
2474
|
return;
|
|
2331
2475
|
}
|
|
2476
|
+
// Manual accept, possibly racing Ken's autopilot plan review: the user
|
|
2477
|
+
// always wins. Bump the plan generation (invalidates any in-flight
|
|
2478
|
+
// review's verdict), stop the cycle, abort a mid-prompt review on the
|
|
2479
|
+
// kenAuto session, and clear the spinner — autopilot_ignored renders
|
|
2480
|
+
// nothing, so no stale "approve or reject" bubble ever lands. The
|
|
2481
|
+
// webview's follow-up "implement" /prompt arrives as a fresh turn
|
|
2482
|
+
// (resetting autopilotCancelled), so the implementation still gets its
|
|
2483
|
+
// normal post-run review; if it lands while the cycle is winding down
|
|
2484
|
+
// it queues and runStrandedQueue drains it as a fresh turn.
|
|
2485
|
+
clearPendingPlan();
|
|
2486
|
+
autopilotCancelled = true;
|
|
2487
|
+
kenAutoAbort.abort();
|
|
2488
|
+
kenAutoAbort = new AbortController();
|
|
2489
|
+
kenAutoSession?.setSignal(kenAutoAbort.signal);
|
|
2490
|
+
if (autopilotReviewing) {
|
|
2491
|
+
autopilotReviewing = false;
|
|
2492
|
+
broadcast("autopilot_ignored", {});
|
|
2493
|
+
}
|
|
2332
2494
|
try {
|
|
2333
2495
|
await session.newSession();
|
|
2334
2496
|
injectedAutopilotPrompts = [];
|