@kenkaiiii/ggcoder 5.4.2 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/app-sidecar.js +212 -10
  2. package/dist/app-sidecar.js.map +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +13 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config.js +32 -30
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/agent-tools-allowlist.test.d.ts +2 -0
  9. package/dist/core/agent-tools-allowlist.test.d.ts.map +1 -0
  10. package/dist/core/agent-tools-allowlist.test.js +72 -0
  11. package/dist/core/agent-tools-allowlist.test.js.map +1 -0
  12. package/dist/core/autopilot-verdict.d.ts +32 -0
  13. package/dist/core/autopilot-verdict.d.ts.map +1 -0
  14. package/dist/core/autopilot-verdict.js +93 -0
  15. package/dist/core/autopilot-verdict.js.map +1 -0
  16. package/dist/core/autopilot-verdict.test.d.ts +2 -0
  17. package/dist/core/autopilot-verdict.test.d.ts.map +1 -0
  18. package/dist/core/autopilot-verdict.test.js +80 -0
  19. package/dist/core/autopilot-verdict.test.js.map +1 -0
  20. package/dist/core/event-bus.d.ts +4 -0
  21. package/dist/core/event-bus.d.ts.map +1 -1
  22. package/dist/core/event-bus.js +6 -0
  23. package/dist/core/event-bus.js.map +1 -1
  24. package/dist/core/ken-context.d.ts +18 -0
  25. package/dist/core/ken-context.d.ts.map +1 -1
  26. package/dist/core/ken-context.js +19 -0
  27. package/dist/core/ken-context.js.map +1 -1
  28. package/dist/core/ken-context.test.js +23 -1
  29. package/dist/core/ken-context.test.js.map +1 -1
  30. package/dist/core/ken-prompt.d.ts +10 -0
  31. package/dist/core/ken-prompt.d.ts.map +1 -1
  32. package/dist/core/ken-prompt.js +48 -0
  33. package/dist/core/ken-prompt.js.map +1 -1
  34. package/dist/core/prompt-commands.d.ts.map +1 -1
  35. package/dist/core/prompt-commands.js +17 -3
  36. package/dist/core/prompt-commands.js.map +1 -1
  37. package/dist/core/prompt-commands.test.js +27 -1
  38. package/dist/core/prompt-commands.test.js.map +1 -1
  39. package/dist/core/tasks-store.d.ts +7 -0
  40. package/dist/core/tasks-store.d.ts.map +1 -1
  41. package/dist/core/tasks-store.js +13 -0
  42. package/dist/core/tasks-store.js.map +1 -1
  43. package/dist/modes/json-mode.d.ts +7 -0
  44. package/dist/modes/json-mode.d.ts.map +1 -1
  45. package/dist/modes/json-mode.js +4 -0
  46. package/dist/modes/json-mode.js.map +1 -1
  47. package/dist/tools/subagent.d.ts.map +1 -1
  48. package/dist/tools/subagent.js +24 -2
  49. package/dist/tools/subagent.js.map +1 -1
  50. package/package.json +4 -4
@@ -20,8 +20,9 @@ import { parseArgs } from "node:util";
20
20
  import { formatError } from "@kenkaiiii/gg-ai";
21
21
  import { runJsonMode } from "./modes/json-mode.js";
22
22
  import { AgentSession } from "./core/agent-session.js";
23
- import { buildKenSystemPrompt } from "./core/ken-prompt.js";
24
- import { buildKenDigest } from "./core/ken-context.js";
23
+ import { buildKenSystemPrompt, buildKenAutopilotSystemPrompt } from "./core/ken-prompt.js";
24
+ import { buildKenDigest, buildKenAutopilotContext } from "./core/ken-context.js";
25
+ import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
25
26
  import { collectProjectContext } from "./system-prompt.js";
26
27
  import { AuthStorage } from "./core/auth-storage.js";
27
28
  import { MOONSHOT_OAUTH_KEY, XIAOMI_CREDITS_KEY } from "@kenkaiiii/gg-core";
@@ -39,7 +40,7 @@ import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSuppor
39
40
  import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
40
41
  import { loadCustomCommands } from "./core/custom-commands.js";
41
42
  import { discoverProjects, listRecentSessions } from "./core/project-discovery.js";
42
- import { loadTasksSync, saveTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/tasks-store.js";
43
+ import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/tasks-store.js";
43
44
  import { initLogger, log } from "./core/logger.js";
44
45
  import { RADIO_STATIONS, getCurrentStation, playRadio, stopRadio } from "./core/radio.js";
45
46
  import { enrichProcessPath } from "./core/shell-path.js";
@@ -80,6 +81,7 @@ async function loadAppSettings() {
80
81
  // Preserve the per-project map verbatim (validated + written by the
81
82
  // model/thinking handlers below).
82
83
  projectModels: raw.projectModels && typeof raw.projectModels === "object" ? raw.projectModels : undefined,
84
+ autopilot: raw.autopilot && typeof raw.autopilot === "object" ? raw.autopilot : undefined,
83
85
  };
84
86
  }
85
87
  catch {
@@ -103,6 +105,19 @@ async function saveProjectModelPrefs(cwd, prefs) {
103
105
  s.projectModels = { ...(s.projectModels ?? {}), [key]: prefs };
104
106
  await saveAppSettings(s);
105
107
  }
108
+ /** Read this project's persisted autopilot flag (default off). */
109
+ async function loadAutopilot(cwd) {
110
+ const s = await loadAppSettings();
111
+ return s.autopilot?.[projectModelKey(cwd)] ?? false;
112
+ }
113
+ /** Persist this project's autopilot flag via read-modify-write so the rest of
114
+ * the settings file (projectsRoot, model map, other projects) is preserved. */
115
+ async function saveAutopilot(cwd, enabled) {
116
+ const s = await loadAppSettings();
117
+ const key = projectModelKey(cwd);
118
+ s.autopilot = { ...(s.autopilot ?? {}), [key]: enabled };
119
+ await saveAppSettings(s);
120
+ }
106
121
  /**
107
122
  * Persist the active model selection to ~/.gg/settings.json so it survives app
108
123
  * restarts. Mirrors the CLI's handleModelSelect persistence (App.tsx).
@@ -759,6 +774,24 @@ async function createSession(deps, opts) {
759
774
  session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
760
775
  let running = false;
761
776
  let titleGenerated = false;
777
+ // Autopilot (auto-review) toggle for THIS window's project. Loaded from
778
+ // gg-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
779
+ // runAutopilotCycle after the user's turn settles — Ken auto-reviews the work
780
+ // and drives the review→prompt→review loop.
781
+ let autopilot = await loadAutopilot(cwd);
782
+ // True while an autopilot review is in flight (used to defer kenAuto model
783
+ // switches, like kenRunning does for chat Ken, and to drive the spinner).
784
+ let autopilotReviewing = false;
785
+ // True for the WHOLE autopilot cycle (reviews + injected runs). The build
786
+ // `running` flag is false during the review windows between injected runs, so
787
+ // this is the extra guard that makes a user /prompt queue as steering instead
788
+ // of starting a run that would collide with an injected one on the same
789
+ // session (AgentSession.prompt has no concurrency guard).
790
+ let autopilotActive = false;
791
+ // Set by /cancel to break out of an in-flight autopilot cycle between steps.
792
+ let autopilotCancelled = false;
793
+ // Hard cap on review→prompt→review rounds per user turn (loop safety).
794
+ const MAX_AUTOPILOT_ROUNDS = 3;
762
795
  // ── Telegram serve (remote control via Telegram) ───────────
763
796
  // A single embedded serve session lives in this sidecar process. Only the main
764
797
  // window's home screen exposes the controls, so there's one bot per app.
@@ -827,6 +860,53 @@ async function createSession(deps, opts) {
827
860
  log("INFO", "app-sidecar", "ken session ready", { provider: st.provider, model: st.model });
828
861
  return ken;
829
862
  }
863
+ // ── Autopilot Ken (auto-reviewer) ──────────────────────────
864
+ // A THIRD read-only AgentSession, separate from chat Ken. In autopilot mode
865
+ // Ken silently reviews each finished GG Coder turn and returns a verdict
866
+ // (PROMPT / ALL_CLEAR / HUMAN). Its bus is intentionally NOT bridged to the
867
+ // ken_* chat bubbles — the review is silent; we read its final assistant text
868
+ // and parse it. Uses the lean autopilot system prompt + the same read-only
869
+ // tools. Created lazily on the first autopilot cycle.
870
+ let kenAutoSession = null;
871
+ let kenAutoAbort = new AbortController();
872
+ let pendingKenAutoModel = null;
873
+ async function syncKenAutoModel(provider, model) {
874
+ if (autopilotReviewing) {
875
+ pendingKenAutoModel = { provider, model };
876
+ return;
877
+ }
878
+ if (!kenAutoSession)
879
+ return;
880
+ const st = kenAutoSession.getState();
881
+ if (st.provider === provider && st.model === model)
882
+ return;
883
+ await kenAutoSession.switchModel(provider, model);
884
+ log("INFO", "app-sidecar", "ken autopilot session model synced", { provider, model });
885
+ }
886
+ async function ensureKenAutoSession() {
887
+ if (kenAutoSession)
888
+ return kenAutoSession;
889
+ const st = session.getState();
890
+ const ken = new AgentSession({
891
+ provider: st.provider,
892
+ model: st.model,
893
+ cwd,
894
+ systemPrompt: buildKenAutopilotSystemPrompt(),
895
+ allowedTools: KEN_ALLOWED_TOOLS,
896
+ allowedMcpServers: KEN_ALLOWED_MCP_SERVERS,
897
+ transient: true,
898
+ signal: kenAutoAbort.signal,
899
+ });
900
+ await ken.initialize();
901
+ // Deliberately no bus bridge: the review is silent. Errors surface via the
902
+ // runAutopilotReview try/catch as autopilot_error frames.
903
+ kenAutoSession = ken;
904
+ log("INFO", "app-sidecar", "ken autopilot session ready", {
905
+ provider: st.provider,
906
+ model: st.model,
907
+ });
908
+ return ken;
909
+ }
830
910
  // Resumed session: if it already has a conversation, generate its title now so
831
911
  // the title bar shows it immediately on load (not just after the next prompt).
832
912
  {
@@ -861,6 +941,14 @@ async function createSession(deps, opts) {
861
941
  gitBranch = await getGitBranch(cwd).catch(() => gitBranch);
862
942
  gitIsRepo = await isGitRepo(cwd).catch(() => gitIsRepo);
863
943
  broadcast("run_end", {});
944
+ // Autopilot's review loop is driven explicitly from POST /prompt (see
945
+ // runAutopilotCycle), NOT from this shared finally — that keeps the
946
+ // injected GG Coder runs this cycle triggers from recursively re-entering
947
+ // the loop through the same bracket.
948
+ // The agent may have marked project tasks done during the run — prune the
949
+ // completed ones so they drop out of the Tasks modal automatically (users
950
+ // never have to delete finished tasks by hand).
951
+ broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
864
952
  // Queue drains into the run as steering, so it's empty by run_end —
865
953
  // sync the webview indicator.
866
954
  broadcast("queued", { count: session.getQueuedCount() });
@@ -876,6 +964,79 @@ async function createSession(deps, opts) {
876
964
  }
877
965
  }
878
966
  }
967
+ // ── Autopilot orchestration ─────────────────────────────────
968
+ // One review = prompt the kenAuto session with the review digest, read its
969
+ // final assistant text, parse a verdict. Returns null on failure (surfaced as
970
+ // an autopilot_error frame) so the cycle stops rather than looping blind.
971
+ async function runAutopilotReview() {
972
+ autopilotReviewing = true;
973
+ broadcast("autopilot_review_start", {});
974
+ try {
975
+ const ken = await ensureKenAutoSession();
976
+ const projectContext = await collectProjectContext(cwd).catch(() => []);
977
+ const digest = buildKenAutopilotContext({
978
+ projectContext,
979
+ cwd,
980
+ gitBranch,
981
+ messages: session.getMessages(),
982
+ });
983
+ await ken.prompt(digest);
984
+ return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
985
+ }
986
+ catch (err) {
987
+ broadcastError("autopilot_error", "autopilot review failed", err);
988
+ return null;
989
+ }
990
+ finally {
991
+ autopilotReviewing = false;
992
+ // Apply any model switch that landed mid-review.
993
+ const pending = pendingKenAutoModel;
994
+ pendingKenAutoModel = null;
995
+ if (pending)
996
+ await syncKenAutoModel(pending.provider, pending.model);
997
+ }
998
+ }
999
+ // Drive the review→prompt→review loop for one finished user turn. Only ever
1000
+ // called from POST /prompt after the user's own run resolves — never from the
1001
+ // task runner, resume, /ken, or error paths, so there's no recursion and no
1002
+ // guard tangle. Bounded by MAX_AUTOPILOT_ROUNDS and cancellable between steps.
1003
+ async function runAutopilotCycle() {
1004
+ if (!autopilot || autopilotCancelled)
1005
+ return;
1006
+ autopilotActive = true;
1007
+ try {
1008
+ // Lean context per user turn: wipe prior review history so each new turn
1009
+ // starts cheap, while within this cycle the few review messages persist so
1010
+ // Ken remembers what he already asked GG Coder to fix.
1011
+ await kenAutoSession?.newSession().catch(() => { });
1012
+ for (let round = 1; round <= MAX_AUTOPILOT_ROUNDS; round++) {
1013
+ if (autopilotCancelled)
1014
+ return;
1015
+ const verdict = await runAutopilotReview();
1016
+ if (!verdict || autopilotCancelled)
1017
+ return;
1018
+ if (verdict.kind === "all_clear") {
1019
+ broadcast("autopilot_done", {});
1020
+ return;
1021
+ }
1022
+ if (verdict.kind === "human") {
1023
+ broadcast("autopilot_human", { reason: verdict.reason });
1024
+ return;
1025
+ }
1026
+ // prompt → show a compact Ken-tinted marker (not the prompt body), then
1027
+ // feed GG Coder. Bracketed by runAgent so the run streams normally; the
1028
+ // shared finally no longer re-triggers autopilot, so this can't recurse.
1029
+ broadcast("autopilot_prompted", { round, body: verdict.body });
1030
+ await runAgent(verdict.body, () => session.prompt(verdict.body));
1031
+ if (autopilotCancelled)
1032
+ return;
1033
+ }
1034
+ broadcast("autopilot_capped", { rounds: MAX_AUTOPILOT_ROUNDS });
1035
+ }
1036
+ finally {
1037
+ autopilotActive = false;
1038
+ }
1039
+ }
879
1040
  // ── Task runner (project task list → sessions) ──────────────
880
1041
  // Mirrors the CLI's task flow: each task runs in its OWN fresh session, with a
881
1042
  // completion hint instructing the agent to mark the task done via the tasks
@@ -897,8 +1058,8 @@ async function createSession(deps, opts) {
897
1058
  `tasks({ action: "done", id: "${shortId}" })`;
898
1059
  await runAgent(task.title, () => session.prompt(task.prompt + completionHint));
899
1060
  // The agent typically marks the task done via the tasks tool during the run;
900
- // push the refreshed list so the webview's task modal reflects it.
901
- broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
1061
+ // prune completed tasks and push the refreshed list so the modal drops them.
1062
+ broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
902
1063
  return true;
903
1064
  }
904
1065
  async function runTasks(startId, all) {
@@ -993,6 +1154,7 @@ async function createSession(deps, opts) {
993
1154
  thinkingLevel: session.getThinkingLevel() ?? null,
994
1155
  supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
995
1156
  supportsVideo: getModel(st.model)?.supportsVideo ?? false,
1157
+ autopilot,
996
1158
  ...footerExtras(),
997
1159
  });
998
1160
  return;
@@ -1016,6 +1178,7 @@ async function createSession(deps, opts) {
1016
1178
  thinkingLevel: session.getThinkingLevel() ?? null,
1017
1179
  supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
1018
1180
  supportsVideo: getModel(st.model)?.supportsVideo ?? false,
1181
+ autopilot,
1019
1182
  ...footerExtras(),
1020
1183
  },
1021
1184
  })}\n\n`);
@@ -1339,10 +1502,13 @@ async function createSession(deps, opts) {
1339
1502
  json(res, 400, { error: "empty prompt" });
1340
1503
  return;
1341
1504
  }
1342
- if (running) {
1343
- // Queue prompts as mid-run steering (mirrors the CLI). Attachments are
1344
- // persisted to .gg/uploads first so the queued media rides the same
1345
- // native-block path as a non-queued attachment prompt when it drains.
1505
+ if (running || autopilotActive) {
1506
+ // Queue prompts as mid-run steering (mirrors the CLI). Also queue while
1507
+ // an autopilot cycle is active but between injected runs (build idle,
1508
+ // Ken reviewing) so the message never starts a run that collides with
1509
+ // an injected one on the same session. Attachments are persisted to
1510
+ // .gg/uploads first so the queued media rides the same native-block
1511
+ // path as a non-queued attachment prompt when it drains.
1346
1512
  const prepared = attachments.length > 0 ? await prepareAttachments(cwd, attachments) : [];
1347
1513
  const count = session.queueMessage(text, prepared);
1348
1514
  broadcast("queued", { count });
@@ -1350,6 +1516,9 @@ async function createSession(deps, opts) {
1350
1516
  return;
1351
1517
  }
1352
1518
  json(res, 202, { accepted: true });
1519
+ // Fresh user turn: clear any cancel flag left from a prior cycle so this
1520
+ // turn's autopilot review can run.
1521
+ autopilotCancelled = false;
1353
1522
  await runAgent(text, async () => {
1354
1523
  if (attachments.length > 0) {
1355
1524
  // Persist each attachment under .gg/uploads so files are inspectable
@@ -1365,6 +1534,11 @@ async function createSession(deps, opts) {
1365
1534
  await session.prompt(text);
1366
1535
  }
1367
1536
  });
1537
+ // After the user's run settles, kick off Ken's auto-review loop. This is
1538
+ // the ONLY entry point into the cycle — it drives any follow-up GG Coder
1539
+ // runs itself, so the shared runAgent finally never recurses.
1540
+ if (autopilot && !autopilotCancelled)
1541
+ await runAutopilotCycle();
1368
1542
  });
1369
1543
  return;
1370
1544
  }
@@ -1427,6 +1601,24 @@ async function createSession(deps, opts) {
1427
1601
  json(res, 200, { cancelled: true });
1428
1602
  return;
1429
1603
  }
1604
+ if (method === "POST" && url === "/autopilot") {
1605
+ void readBody(req).then(async (raw) => {
1606
+ let enabled;
1607
+ try {
1608
+ enabled = Boolean(JSON.parse(raw).enabled);
1609
+ }
1610
+ catch {
1611
+ json(res, 400, { error: "invalid JSON body" });
1612
+ return;
1613
+ }
1614
+ autopilot = enabled;
1615
+ await saveAutopilot(cwd, enabled);
1616
+ log("INFO", "app-sidecar", "autopilot toggled", { enabled: String(enabled) });
1617
+ broadcast("autopilot", { autopilot: enabled });
1618
+ json(res, 200, { autopilot: enabled });
1619
+ });
1620
+ return;
1621
+ }
1430
1622
  if (method === "POST" && url === "/enhance") {
1431
1623
  void readBody(req).then(async (raw) => {
1432
1624
  let text;
@@ -1456,7 +1648,7 @@ async function createSession(deps, opts) {
1456
1648
  return;
1457
1649
  }
1458
1650
  if (method === "GET" && url === "/tasks") {
1459
- json(res, 200, { tasks: loadTasksSync(cwd) });
1651
+ json(res, 200, { tasks: pruneDoneTasksSync(cwd) });
1460
1652
  return;
1461
1653
  }
1462
1654
  // ── Radio (app-wide) ──────────────────────────────────────
@@ -1576,6 +1768,7 @@ async function createSession(deps, opts) {
1576
1768
  }
1577
1769
  await session.switchModel(target.provider, target.id);
1578
1770
  await syncKenModel(target.provider, target.id);
1771
+ await syncKenAutoModel(target.provider, target.id);
1579
1772
  // Clamp the reasoning level to what the new model supports (mirrors the
1580
1773
  // CLI): keep thinking on at the first supported tier if it was on but
1581
1774
  // the prior level is unsupported here; leave it off if it was off.
@@ -1657,6 +1850,13 @@ async function createSession(deps, opts) {
1657
1850
  running = false;
1658
1851
  // Stop a run-all sweep so the next pending task isn't auto-started.
1659
1852
  taskRunAll = false;
1853
+ // Stop any in-flight autopilot cycle: flag it so the loop bails between
1854
+ // steps, and abort a review that's mid-prompt on the kenAuto session.
1855
+ autopilotCancelled = true;
1856
+ kenAutoAbort.abort();
1857
+ kenAutoAbort = new AbortController();
1858
+ kenAutoSession?.setSignal(kenAutoAbort.signal);
1859
+ autopilotReviewing = false;
1660
1860
  // Drop any queued steering and return it so the webview can restore it to
1661
1861
  // the composer.
1662
1862
  const drained = session.drainQueue();
@@ -2143,7 +2343,9 @@ async function createSession(deps, opts) {
2143
2343
  for (const c of clients)
2144
2344
  c.res.end();
2145
2345
  kenAbort.abort();
2346
+ kenAutoAbort.abort();
2146
2347
  await kenSession?.dispose().catch(() => { });
2348
+ await kenAutoSession?.dispose().catch(() => { });
2147
2349
  await session.dispose().catch(() => { });
2148
2350
  }
2149
2351
  return {