@kenkaiiii/ggcoder 5.6.3 → 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 +291 -7
- 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/progress/engine.d.ts +23 -0
- package/dist/core/progress/engine.d.ts.map +1 -0
- package/dist/core/progress/engine.js +131 -0
- package/dist/core/progress/engine.js.map +1 -0
- package/dist/core/progress/engine.test.d.ts +2 -0
- package/dist/core/progress/engine.test.d.ts.map +1 -0
- package/dist/core/progress/engine.test.js +136 -0
- package/dist/core/progress/engine.test.js.map +1 -0
- package/dist/core/progress/git-xp.d.ts +16 -0
- package/dist/core/progress/git-xp.d.ts.map +1 -0
- package/dist/core/progress/git-xp.js +106 -0
- package/dist/core/progress/git-xp.js.map +1 -0
- package/dist/core/progress/git-xp.test.d.ts +2 -0
- package/dist/core/progress/git-xp.test.d.ts.map +1 -0
- package/dist/core/progress/git-xp.test.js +88 -0
- package/dist/core/progress/git-xp.test.js.map +1 -0
- package/dist/core/progress/ranks.d.ts +21 -0
- package/dist/core/progress/ranks.d.ts.map +1 -0
- package/dist/core/progress/ranks.js +141 -0
- package/dist/core/progress/ranks.js.map +1 -0
- package/dist/core/progress/ranks.test.d.ts +2 -0
- package/dist/core/progress/ranks.test.d.ts.map +1 -0
- package/dist/core/progress/ranks.test.js +59 -0
- package/dist/core/progress/ranks.test.js.map +1 -0
- package/dist/core/progress/rebuild.d.ts +7 -0
- package/dist/core/progress/rebuild.d.ts.map +1 -0
- package/dist/core/progress/rebuild.js +106 -0
- package/dist/core/progress/rebuild.js.map +1 -0
- package/dist/core/progress/rebuild.test.d.ts +2 -0
- package/dist/core/progress/rebuild.test.d.ts.map +1 -0
- package/dist/core/progress/rebuild.test.js +72 -0
- package/dist/core/progress/rebuild.test.js.map +1 -0
- package/dist/core/progress/store.d.ts +35 -0
- package/dist/core/progress/store.d.ts.map +1 -0
- package/dist/core/progress/store.js +200 -0
- package/dist/core/progress/store.js.map +1 -0
- package/dist/core/progress/store.test.d.ts +2 -0
- package/dist/core/progress/store.test.d.ts.map +1 -0
- package/dist/core/progress/store.test.js +108 -0
- package/dist/core/progress/store.test.js.map +1 -0
- package/dist/core/progress/types.d.ts +108 -0
- package/dist/core/progress/types.d.ts.map +1 -0
- package/dist/core/progress/types.js +3 -0
- package/dist/core/progress/types.js.map +1 -0
- 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
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import http from "node:http";
|
|
15
15
|
import fs from "node:fs/promises";
|
|
16
|
+
import { watch as fsWatch } from "node:fs";
|
|
16
17
|
import os from "node:os";
|
|
17
18
|
import path from "node:path";
|
|
18
19
|
import { randomUUID } from "node:crypto";
|
|
@@ -21,7 +22,7 @@ import { formatError } from "@kenkaiiii/gg-ai";
|
|
|
21
22
|
import { runJsonMode } from "./modes/json-mode.js";
|
|
22
23
|
import { AgentSession } from "./core/agent-session.js";
|
|
23
24
|
import { buildKenSystemPrompt, buildKenAutopilotSystemPrompt } from "./core/ken-prompt.js";
|
|
24
|
-
import { buildKenDigest, buildKenAutopilotContext } from "./core/ken-context.js";
|
|
25
|
+
import { buildKenDigest, buildKenAutopilotContext, buildKenAutopilotPlanContext, } from "./core/ken-context.js";
|
|
25
26
|
import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
|
|
26
27
|
import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
|
|
27
28
|
import { driveAutopilotCycle } from "./core/autopilot-cycle.js";
|
|
@@ -50,6 +51,11 @@ import { downscaleForPreview, validateVisionImage } from "./utils/image.js";
|
|
|
50
51
|
import { startServeMode } from "./modes/serve-mode.js";
|
|
51
52
|
import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
|
|
52
53
|
import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
|
|
54
|
+
import { buildSnapshot, levelForXp, rankForLevel } from "./core/progress/ranks.js";
|
|
55
|
+
import { loadProgress, peekProgress, updateProgress } from "./core/progress/store.js";
|
|
56
|
+
import { awardPrompt, awardCommits } from "./core/progress/engine.js";
|
|
57
|
+
import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
|
|
58
|
+
import { rebuildFromSessions } from "./core/progress/rebuild.js";
|
|
53
59
|
const ALL_PROVIDERS = [
|
|
54
60
|
"anthropic",
|
|
55
61
|
"xiaomi",
|
|
@@ -495,6 +501,13 @@ async function main() {
|
|
|
495
501
|
// request to its window's session via the `x-gg-session` header (and the
|
|
496
502
|
// `?session=` query for the SSE /events stream).
|
|
497
503
|
const sessions = new Map();
|
|
504
|
+
// XP/rank progress — loaded once per daemon; awards fan out to every window.
|
|
505
|
+
// Each frame is tagged `origin: true` only for the session that earned the
|
|
506
|
+
// XP, so that window alone plays sounds/chips while the rest just re-render.
|
|
507
|
+
const progress = await createProgressManager(paths.agentDir, (snapshot, originId) => {
|
|
508
|
+
for (const ctx of sessions.values())
|
|
509
|
+
ctx.broadcast("progress", { ...snapshot, origin: ctx.id === originId });
|
|
510
|
+
});
|
|
498
511
|
/** Resolve the target session id: the `x-gg-session` header, else a
|
|
499
512
|
* `?session=` query param (used by the SSE /events connection). */
|
|
500
513
|
function sessionIdFromReq(req, url) {
|
|
@@ -538,7 +551,7 @@ async function main() {
|
|
|
538
551
|
const sessionPath = typeof body.sessionPath === "string" && body.sessionPath ? body.sessionPath : undefined;
|
|
539
552
|
const id = randomUUID();
|
|
540
553
|
try {
|
|
541
|
-
const ctx = await createSession({ auth, paths }, { id, cwd: sessionCwd, sessionPath });
|
|
554
|
+
const ctx = await createSession({ auth, paths, progress }, { id, cwd: sessionCwd, sessionPath });
|
|
542
555
|
sessions.set(id, ctx);
|
|
543
556
|
log("INFO", "app-sidecar", "session created", { id, cwd: sessionCwd });
|
|
544
557
|
daemonJson(res, 200, { sessionId: id });
|
|
@@ -563,6 +576,12 @@ async function main() {
|
|
|
563
576
|
daemonJson(res, 200, { ok: true });
|
|
564
577
|
return;
|
|
565
578
|
}
|
|
579
|
+
// Progress is daemon-level so the Home screen can paint before a project
|
|
580
|
+
// session exists; per-session callers still work through the same endpoint.
|
|
581
|
+
if (method === "GET" && url === "/progress") {
|
|
582
|
+
daemonJson(res, 200, progress.snapshot());
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
566
585
|
// ── Per-session delegation ───────────────────────────────────────────
|
|
567
586
|
const id = sessionIdFromReq(req, url);
|
|
568
587
|
const ctx = id ? sessions.get(id) : undefined;
|
|
@@ -641,6 +660,88 @@ function buildKenContext(buildSession, cwd, gitBranch, question, workflowCommand
|
|
|
641
660
|
injectedPrompts,
|
|
642
661
|
});
|
|
643
662
|
}
|
|
663
|
+
async function createProgressManager(agentDir, broadcastAll) {
|
|
664
|
+
// Boot: recovery chain main → backup → one-time session-history rebuild → empty.
|
|
665
|
+
let file = await loadProgress({ rebuild: () => rebuildFromSessions() });
|
|
666
|
+
// Don't re-celebrate an old levelUp event on boot.
|
|
667
|
+
let lastSeenNonce = file.lastEvent?.nonce ?? null;
|
|
668
|
+
log("INFO", "app-sidecar", "progress loaded", {
|
|
669
|
+
xp: String(file.xp),
|
|
670
|
+
level: String(levelForXp(file.xp)),
|
|
671
|
+
});
|
|
672
|
+
function snapshot() {
|
|
673
|
+
return buildSnapshot(file);
|
|
674
|
+
}
|
|
675
|
+
async function awardRun(cwd, runStartedAt, originId) {
|
|
676
|
+
try {
|
|
677
|
+
const now = Date.now();
|
|
678
|
+
const updated = await updateProgress(async (f) => {
|
|
679
|
+
const levelBefore = levelForXp(f.xp);
|
|
680
|
+
awardPrompt(f, now, cwd);
|
|
681
|
+
// Commit XP: probe repo root + HEAD, then score lastHead..HEAD bounded
|
|
682
|
+
// by the run window. First sight of a repo records HEAD, scores nothing.
|
|
683
|
+
const probe = await detectNewCommits(cwd, undefined, runStartedAt);
|
|
684
|
+
if (probe) {
|
|
685
|
+
const key = repoKey(probe.repoRoot);
|
|
686
|
+
const lastHead = f.repos[key]?.lastHead;
|
|
687
|
+
if (lastHead && lastHead !== probe.head) {
|
|
688
|
+
const detected = await detectNewCommits(cwd, lastHead, runStartedAt);
|
|
689
|
+
if (detected && detected.commits.length > 0) {
|
|
690
|
+
awardCommits(f, detected.commits, now, cwd);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
f.repos[key] = { lastHead: probe.head };
|
|
694
|
+
}
|
|
695
|
+
// One combined lastEvent per run so other windows celebrate exactly once.
|
|
696
|
+
const levelAfter = levelForXp(f.xp);
|
|
697
|
+
const levelUp = levelAfter > levelBefore
|
|
698
|
+
? { from: levelBefore, to: levelAfter, rankName: rankForLevel(levelAfter).name }
|
|
699
|
+
: null;
|
|
700
|
+
f.lastEvent = { nonce: randomUUID(), levelUp };
|
|
701
|
+
return { file: f, levelledUp: levelUp !== null };
|
|
702
|
+
});
|
|
703
|
+
file = updated;
|
|
704
|
+
lastSeenNonce = updated.lastEvent?.nonce ?? null;
|
|
705
|
+
broadcastAll(buildSnapshot(updated), originId);
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
log("DEBUG", "app-sidecar", "progress award failed", {
|
|
709
|
+
message: err instanceof Error ? err.message : String(err),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// Watch ~/.gg (dir watch survives the atomic tmp+rename) for progress.json
|
|
714
|
+
// writes from other daemon processes; debounce, reload read-only, dedupe by nonce.
|
|
715
|
+
let watchDebounce = null;
|
|
716
|
+
try {
|
|
717
|
+
const watcher = fsWatch(agentDir, (_event, filename) => {
|
|
718
|
+
if (filename !== "progress.json")
|
|
719
|
+
return;
|
|
720
|
+
if (watchDebounce)
|
|
721
|
+
clearTimeout(watchDebounce);
|
|
722
|
+
watchDebounce = setTimeout(() => {
|
|
723
|
+
void (async () => {
|
|
724
|
+
const reloaded = await peekProgress();
|
|
725
|
+
if (!reloaded)
|
|
726
|
+
return;
|
|
727
|
+
const nonce = reloaded.lastEvent?.nonce ?? null;
|
|
728
|
+
if (nonce === lastSeenNonce)
|
|
729
|
+
return;
|
|
730
|
+
file = reloaded;
|
|
731
|
+
lastSeenNonce = nonce;
|
|
732
|
+
broadcastAll(buildSnapshot(reloaded));
|
|
733
|
+
})();
|
|
734
|
+
}, 150);
|
|
735
|
+
});
|
|
736
|
+
watcher.unref();
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
log("DEBUG", "app-sidecar", "progress watch unavailable", {
|
|
740
|
+
message: err instanceof Error ? err.message : String(err),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
return { snapshot, awardRun };
|
|
744
|
+
}
|
|
644
745
|
/**
|
|
645
746
|
* Build one in-process agent session: its AgentSession, SSE client set, event
|
|
646
747
|
* bridge, task runner, auth/login bridge, and the full HTTP route table exposed
|
|
@@ -649,7 +750,7 @@ function buildKenContext(buildSession, cwd, gitBranch, question, workflowCommand
|
|
|
649
750
|
* logger, PATH, shared auth file, and radio live at the daemon level.
|
|
650
751
|
*/
|
|
651
752
|
async function createSession(deps, opts) {
|
|
652
|
-
const { auth } = deps;
|
|
753
|
+
const { auth, progress } = deps;
|
|
653
754
|
const paths = deps.paths;
|
|
654
755
|
const cwd = opts.cwd;
|
|
655
756
|
// Base host for parsing request-URL query params (value is irrelevant to
|
|
@@ -746,6 +847,12 @@ async function createSession(deps, opts) {
|
|
|
746
847
|
catch {
|
|
747
848
|
content = "";
|
|
748
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);
|
|
749
856
|
broadcast("plan_exit", { planPath, content });
|
|
750
857
|
return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
|
|
751
858
|
},
|
|
@@ -812,6 +919,9 @@ async function createSession(deps, opts) {
|
|
|
812
919
|
session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
|
|
813
920
|
let running = false;
|
|
814
921
|
let titleGenerated = false;
|
|
922
|
+
// Bumped by /cancel — a run whose cancel generation changed mid-flight was
|
|
923
|
+
// canceled and earns no XP.
|
|
924
|
+
let cancelGeneration = 0;
|
|
815
925
|
// Autopilot (auto-review) toggle for THIS window's project. Loaded from
|
|
816
926
|
// gg-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
|
|
817
927
|
// runAutopilotCycle after the user's turn settles — Ken auto-reviews the work
|
|
@@ -836,6 +946,28 @@ async function createSession(deps, opts) {
|
|
|
836
946
|
// cycles drift into Ken reviewing against his own last prompt. Cleared
|
|
837
947
|
// whenever the conversation resets (new session / plan accept / task run).
|
|
838
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
|
+
}
|
|
839
971
|
// Workflow (prompt-template) commands: built-in + the project's custom
|
|
840
972
|
// `.gg/commands/*.md`. Used to gate autopilot off command turns and to label
|
|
841
973
|
// expanded templates in Ken's digests. Loaded fresh so a newly added custom
|
|
@@ -1020,15 +1152,28 @@ async function createSession(deps, opts) {
|
|
|
1020
1152
|
// run_start frame.
|
|
1021
1153
|
async function runAgent(label, run) {
|
|
1022
1154
|
running = true;
|
|
1155
|
+
// Progress (Ranks): completed, non-canceled runs with ≥1 assistant turn earn
|
|
1156
|
+
// XP — prompt + any commits authored during the run window.
|
|
1157
|
+
const runStartedAt = Date.now();
|
|
1158
|
+
const cancelGenAtStart = cancelGeneration;
|
|
1159
|
+
const assistantsBeforeRun = countAssistantMessages(session.getMessages());
|
|
1160
|
+
let runSucceeded = false;
|
|
1023
1161
|
broadcast("run_start", { text: label });
|
|
1024
1162
|
try {
|
|
1025
1163
|
await run();
|
|
1164
|
+
runSucceeded = true;
|
|
1026
1165
|
}
|
|
1027
1166
|
catch (err) {
|
|
1028
1167
|
broadcastError("error", "run failed", err);
|
|
1029
1168
|
}
|
|
1030
1169
|
finally {
|
|
1031
1170
|
running = false;
|
|
1171
|
+
if (runSucceeded &&
|
|
1172
|
+
cancelGeneration === cancelGenAtStart &&
|
|
1173
|
+
countAssistantMessages(session.getMessages()) > assistantsBeforeRun) {
|
|
1174
|
+
// Fire-and-forget — XP must never delay or break run teardown.
|
|
1175
|
+
void progress.awardRun(cwd, runStartedAt, opts.id);
|
|
1176
|
+
}
|
|
1032
1177
|
// A run may have switched branches (git checkout) or spawned/finished
|
|
1033
1178
|
// background tasks — refresh the footer extras once it settles.
|
|
1034
1179
|
gitBranch = await getGitBranch(cwd).catch(() => gitBranch);
|
|
@@ -1092,6 +1237,60 @@ async function createSession(deps, opts) {
|
|
|
1092
1237
|
await syncKenAutoModel(pending.provider, pending.model);
|
|
1093
1238
|
}
|
|
1094
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.";
|
|
1095
1294
|
// Drive the review→prompt→review loop for one finished user turn. Only ever
|
|
1096
1295
|
// called after shouldStartAutopilotCycle approves the turn (POST /prompt or
|
|
1097
1296
|
// the stranded-queue drain) — never from the task runner, resume, /ken, or
|
|
@@ -1102,14 +1301,53 @@ async function createSession(deps, opts) {
|
|
|
1102
1301
|
if (!autopilot || autopilotCancelled)
|
|
1103
1302
|
return;
|
|
1104
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;
|
|
1105
1307
|
try {
|
|
1106
1308
|
await driveAutopilotCycle({
|
|
1107
|
-
|
|
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,
|
|
1108
1313
|
isCancelled: () => autopilotCancelled,
|
|
1109
|
-
// An injected run entering plan mode
|
|
1110
|
-
//
|
|
1111
|
-
// 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.
|
|
1112
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)),
|
|
1113
1351
|
// Lean context per user turn: wipe prior review history so each new
|
|
1114
1352
|
// turn starts cheap, while within this cycle the few review messages
|
|
1115
1353
|
// persist so Ken remembers what he already asked GG Coder to fix.
|
|
@@ -1123,6 +1361,10 @@ async function createSession(deps, opts) {
|
|
|
1123
1361
|
// streams normally; the shared finally never re-triggers autopilot,
|
|
1124
1362
|
// so this can't recurse.
|
|
1125
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();
|
|
1126
1368
|
injectedAutopilotPrompts.push(body);
|
|
1127
1369
|
broadcast("autopilot_prompted", { round, body });
|
|
1128
1370
|
void session.persistAutopilotMarker("prompted", { body });
|
|
@@ -1174,6 +1416,9 @@ async function createSession(deps, opts) {
|
|
|
1174
1416
|
broadcast("queued", { count: session.getQueuedCount() });
|
|
1175
1417
|
if (!next.text.trim() && next.attachments.length === 0)
|
|
1176
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();
|
|
1177
1422
|
const workflowCommand = next.attachments.length === 0 &&
|
|
1178
1423
|
isWorkflowCommandText(next.text, await loadWorkflowCommandSpecs());
|
|
1179
1424
|
const assistantsBefore = countAssistantMessages(session.getMessages());
|
|
@@ -1190,6 +1435,9 @@ async function createSession(deps, opts) {
|
|
|
1190
1435
|
enabled: autopilot,
|
|
1191
1436
|
cancelled: autopilotCancelled,
|
|
1192
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,
|
|
1193
1441
|
workflowCommand,
|
|
1194
1442
|
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1195
1443
|
// Skip the review API call outright for turns that only started a
|
|
@@ -1199,6 +1447,9 @@ async function createSession(deps, opts) {
|
|
|
1199
1447
|
mechanicalOnly: isMechanicalOnlyTurn(extractTurnToolCalls(session.getMessages(), messagesBefore)),
|
|
1200
1448
|
});
|
|
1201
1449
|
if (decision.start) {
|
|
1450
|
+
log("INFO", "app-sidecar", "autopilot cycle starting (queued turn)", {
|
|
1451
|
+
kind: decision.kind,
|
|
1452
|
+
});
|
|
1202
1453
|
await runAutopilotCycle(next.text);
|
|
1203
1454
|
}
|
|
1204
1455
|
else if (autopilot) {
|
|
@@ -1224,6 +1475,7 @@ async function createSession(deps, opts) {
|
|
|
1224
1475
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1225
1476
|
await session.newSession();
|
|
1226
1477
|
injectedAutopilotPrompts = [];
|
|
1478
|
+
clearPendingPlan();
|
|
1227
1479
|
titleGenerated = false;
|
|
1228
1480
|
broadcast("session_reset", {});
|
|
1229
1481
|
markTaskInProgress(cwd, task.id);
|
|
@@ -1336,6 +1588,10 @@ async function createSession(deps, opts) {
|
|
|
1336
1588
|
});
|
|
1337
1589
|
return;
|
|
1338
1590
|
}
|
|
1591
|
+
if (method === "GET" && url === "/progress") {
|
|
1592
|
+
json(res, 200, progress.snapshot());
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1339
1595
|
if (method === "GET" && (url === "/events" || url.startsWith("/events?"))) {
|
|
1340
1596
|
res.writeHead(200, {
|
|
1341
1597
|
"content-type": "text/event-stream",
|
|
@@ -1730,6 +1986,10 @@ async function createSession(deps, opts) {
|
|
|
1730
1986
|
// Fresh user turn: clear any cancel flag left from a prior cycle so this
|
|
1731
1987
|
// turn's autopilot review can run.
|
|
1732
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();
|
|
1733
1993
|
// Gate inputs captured around the run: whether this turn is a workflow
|
|
1734
1994
|
// slash command (attachment prompts skip slash expansion entirely), and
|
|
1735
1995
|
// how many assistant messages the run actually adds. Computed even when
|
|
@@ -1766,6 +2026,9 @@ async function createSession(deps, opts) {
|
|
|
1766
2026
|
enabled: autopilot,
|
|
1767
2027
|
cancelled: autopilotCancelled,
|
|
1768
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,
|
|
1769
2032
|
workflowCommand,
|
|
1770
2033
|
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1771
2034
|
// Skip the review API call outright for turns that only started a
|
|
@@ -1775,6 +2038,7 @@ async function createSession(deps, opts) {
|
|
|
1775
2038
|
mechanicalOnly: isMechanicalOnlyTurn(extractTurnToolCalls(session.getMessages(), messagesBefore)),
|
|
1776
2039
|
});
|
|
1777
2040
|
if (decision.start) {
|
|
2041
|
+
log("INFO", "app-sidecar", "autopilot cycle starting", { kind: decision.kind });
|
|
1778
2042
|
await runAutopilotCycle(text);
|
|
1779
2043
|
}
|
|
1780
2044
|
else if (autopilot) {
|
|
@@ -2146,6 +2410,7 @@ async function createSession(deps, opts) {
|
|
|
2146
2410
|
return;
|
|
2147
2411
|
}
|
|
2148
2412
|
if (method === "POST" && url === "/cancel") {
|
|
2413
|
+
cancelGeneration++;
|
|
2149
2414
|
abort.abort();
|
|
2150
2415
|
abort = new AbortController();
|
|
2151
2416
|
session.setSignal(abort.signal);
|
|
@@ -2176,6 +2441,7 @@ async function createSession(deps, opts) {
|
|
|
2176
2441
|
.newSession()
|
|
2177
2442
|
.then(() => {
|
|
2178
2443
|
injectedAutopilotPrompts = [];
|
|
2444
|
+
clearPendingPlan();
|
|
2179
2445
|
broadcast("session_reset", {});
|
|
2180
2446
|
json(res, 200, { ok: true });
|
|
2181
2447
|
})
|
|
@@ -2207,6 +2473,24 @@ async function createSession(deps, opts) {
|
|
|
2207
2473
|
json(res, 409, { error: "cannot accept a plan while the agent is running" });
|
|
2208
2474
|
return;
|
|
2209
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
|
+
}
|
|
2210
2494
|
try {
|
|
2211
2495
|
await session.newSession();
|
|
2212
2496
|
injectedAutopilotPrompts = [];
|